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"?>
|
||||
<svg width="723px" height="562px" viewBox="0 0 723 562" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
|
||||
<!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->
|
||||
<!-- Generator: bin/sketchtool 1.3 (252) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>code-style.svg</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<desc>Created with bin/sketchtool.</desc>
|
||||
<defs></defs>
|
||||
<g id="combined" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
||||
<g id="code-style.svg" sketch:type="MSArtboardGroup">
|
||||
|
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 126 KiB |
|
@ -2,8 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
|
||||
<style>
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
|
@ -44,14 +43,13 @@ var users = [
|
|||
{name: "Даша", age: 30}
|
||||
];
|
||||
|
||||
$('#grid-holder').html(
|
||||
_.template( $('#grid-template').html().trim(), {list: users})
|
||||
);
|
||||
var tmpl = document.getElementById('grid-template').innerHTML.trim();
|
||||
tmpl = _.template(tmpl);
|
||||
|
||||
|
||||
document.getElementById('grid-holder').innerHTML = tmpl({list: users});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
|
@ -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>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="menu.css">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
|
||||
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
|
||||
<script src="menu.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -15,10 +15,15 @@
|
|||
</div>
|
||||
</script>
|
||||
|
||||
<!--
|
||||
встроенная браузерная функция encodeURIComponent кодирует спец-символы для URL,
|
||||
например русские буквы и пробелы
|
||||
в этом примере русских букв в ключах items нет, но потенциально они возможны
|
||||
-->
|
||||
<script type="text/template" id="menu-list-template">
|
||||
<ul>
|
||||
<% for(var key in items) { %>
|
||||
<li><a href="#<%-key%>"><%-items[key]%></li>
|
||||
<% for(var name in items) { %>
|
||||
<li><a href="#<%=encodeURIComponent(name)%>"><%-items[name]%></a></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</script>
|
||||
|
@ -26,19 +31,17 @@
|
|||
<script>
|
||||
var menu = new Menu({
|
||||
title: "Сладости",
|
||||
template: _.template($('#menu-template').html()),
|
||||
listTemplate: _.template($('#menu-list-template').html()),
|
||||
template: _.template( document.getElementById('menu-template').innerHTML.trim()),
|
||||
listTemplate: _.template( document.getElementById('menu-list-template').innerHTML.trim()),
|
||||
items: {
|
||||
"donut": "Пончик",
|
||||
"cake": "Пирожное",
|
||||
"chocolate": "Шоколадка"
|
||||
cake: "Торт", // cake <a href="#cake">Торт</a>
|
||||
donut: "Пончик", // donut
|
||||
chokolate: "Шоколадка" // chokolate
|
||||
}
|
||||
});
|
||||
|
||||
$(document.body).append(menu.getElem());
|
||||
menu.open();
|
||||
document.body.appendChild(menu.getElem());
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
|
@ -7,14 +7,18 @@
|
|||
.menu .title {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
background: url(https://js.cx/clipart/arrow-right.png) left center no-repeat;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.menu .title:before {
|
||||
content: '▶';
|
||||
padding-right: 6px;
|
||||
color: green;
|
||||
}
|
||||
|
||||
.menu.open ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.menu.open .title {
|
||||
background-image: url(https://js.cx/clipart/arrow-down.png);
|
||||
.menu.open .title:before {
|
||||
content: '▼';
|
||||
}
|
|
@ -7,44 +7,51 @@ function Menu(options) {
|
|||
}
|
||||
|
||||
function render() {
|
||||
var elemHtml = options.template({title: options.title});
|
||||
var html = options.template({title: options.title});
|
||||
|
||||
elem = $(elemHtml);
|
||||
elem = document.createElement('div');
|
||||
elem.innerHTML = html;
|
||||
elem = elem.firstElementChild;
|
||||
|
||||
elem.on('mousedown selectstart', false);
|
||||
elem.onmousedown = function() {
|
||||
return false;
|
||||
}
|
||||
|
||||
elem.on('click', '.title', onTitleClick);
|
||||
elem.on('click', 'a', onItemClick)
|
||||
elem.onclick = function(event) {
|
||||
if (event.target.closest('.title')) {
|
||||
toggle();
|
||||
}
|
||||
|
||||
if (event.target.closest('a')) {
|
||||
event.preventDefault();
|
||||
select(event.target.closest('a'));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function renderItems() {
|
||||
if (elem.find('ul').length) return;
|
||||
if (elem.querySelector('ul')) return;
|
||||
|
||||
var listHtml = options.listTemplate({items: options.items});
|
||||
elem.append(listHtml);
|
||||
elem.insertAdjacentHTML("beforeEnd", listHtml);
|
||||
}
|
||||
|
||||
function onItemClick(e) {
|
||||
alert(e.currentTarget.getAttribute('href').slice(1));
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function onTitleClick(e) {
|
||||
toggle();
|
||||
function select(link) {
|
||||
alert(link.getAttribute('href').slice(1));
|
||||
}
|
||||
|
||||
function open() {
|
||||
renderItems();
|
||||
elem.addClass('open');
|
||||
elem.classList.add('open');
|
||||
};
|
||||
|
||||
function close() {
|
||||
elem.removeClass('open');
|
||||
elem.classList.remove('open');
|
||||
};
|
||||
|
||||
function toggle() {
|
||||
if (elem.hasClass('open')) close();
|
||||
if (elem.classList.contains('open')) close();
|
||||
else open();
|
||||
};
|
||||
|
||||
|
@ -52,4 +59,4 @@ function Menu(options) {
|
|||
this.toggle = toggle;
|
||||
this.close = close;
|
||||
this.open = open;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="menu.css">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
|
||||
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
|
||||
<script src="menu.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -26,18 +26,17 @@
|
|||
<script>
|
||||
var menu = new Menu({
|
||||
title: "Сладости",
|
||||
template: _.template($('#menu-template').html()),
|
||||
listTemplate: _.template($('#menu-list-template').html()),
|
||||
template: _.template( document.getElementById('menu-template').innerHTML.trim()),
|
||||
listTemplate: _.template( document.getElementById('menu-list-template').innerHTML.trim()),
|
||||
items: [
|
||||
"Торт", // cake <a href="#cake">Торт</a>
|
||||
"Пончик", // donut
|
||||
"Пирожное", // cake
|
||||
"Шоколадка", // chockolate
|
||||
"Шоколадка" // chokolate
|
||||
]
|
||||
});
|
||||
|
||||
$(document.body).append(menu.getElem());
|
||||
document.body.appendChild(menu.getElem());
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
|
@ -7,14 +7,18 @@
|
|||
.menu .title {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
background: url(https://js.cx/clipart/arrow-right.png) left center no-repeat;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.menu .title:before {
|
||||
content: '▶';
|
||||
padding-right: 6px;
|
||||
color: green;
|
||||
}
|
||||
|
||||
.menu.open ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.menu.open .title {
|
||||
background-image: url(https://js.cx/clipart/arrow-down.png);
|
||||
.menu.open .title:before {
|
||||
content: '▼';
|
||||
}
|
|
@ -7,37 +7,41 @@ function Menu(options) {
|
|||
}
|
||||
|
||||
function render() {
|
||||
var elemHtml = options.template({title: options.title});
|
||||
var html = options.template({title: options.title});
|
||||
|
||||
elem = $(elemHtml);
|
||||
elem = document.createElement('div');
|
||||
elem.innerHTML = html;
|
||||
elem = elem.firstElementChild;
|
||||
|
||||
elem.on('mousedown selectstart', false);
|
||||
elem.onmousedown = function() {
|
||||
return false;
|
||||
}
|
||||
|
||||
elem.on('click', '.title', onTitleClick);
|
||||
elem.onclick = function(event) {
|
||||
if (event.target.closest('.title')) {
|
||||
toggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderItems() {
|
||||
if (elem.find('ul').length) return;
|
||||
if (elem.querySelector('ul')) return;
|
||||
|
||||
var listHtml = options.listTemplate({items: options.items});
|
||||
elem.append(listHtml);
|
||||
}
|
||||
|
||||
function onTitleClick(e) {
|
||||
toggle();
|
||||
elem.insertAdjacentHTML("beforeEnd", listHtml);
|
||||
}
|
||||
|
||||
function open() {
|
||||
renderItems();
|
||||
elem.addClass('open');
|
||||
elem.classList.add('open');
|
||||
};
|
||||
|
||||
function close() {
|
||||
elem.removeClass('open');
|
||||
elem.classList.remove('open');
|
||||
};
|
||||
|
||||
function toggle() {
|
||||
if (elem.hasClass('open')) close();
|
||||
if (elem.classList.contains('open')) close();
|
||||
else open();
|
||||
};
|
||||
|
||||
|
@ -45,4 +49,4 @@ function Menu(options) {
|
|||
this.toggle = toggle;
|
||||
this.close = close;
|
||||
this.open = open;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,13 +2,16 @@
|
|||
|
||||
[importance 5]
|
||||
|
||||
Возьмите в качестве исходного кода меню на шаблонах и модифицируйте его, чтобы вместо массива `items` оно принимало *объект* `items`, вот так:
|
||||
Возьмите в качестве исходного кода меню на шаблонах и модифицируйте его, чтобы оно выводило не просто список, а список ссылок.
|
||||
|
||||
<ol>
|
||||
<li>Вместо массива `items` меню должно принимать *объект* `items`, вот так:
|
||||
|
||||
```js
|
||||
var menu = new Menu({
|
||||
title: "Сладости",
|
||||
template: _.template($('#menu-template').html()),
|
||||
listTemplate: _.template($('#menu-list-template').html()),
|
||||
template: _.template(document.getElementById('menu-template').innerHTML),
|
||||
listTemplate: _.template(document.getElementById('menu-list-template').innerHTML),
|
||||
*!*
|
||||
items: {
|
||||
"donut": "Пончик",
|
||||
|
@ -18,11 +21,10 @@ var menu = new Menu({
|
|||
*/!*
|
||||
});
|
||||
```
|
||||
</ol>
|
||||
<li>Вывод в шаблоне пусть будет не просто `<li>Пончик</li>`, а через ссылку: `<a href="#donut">Пончик</a>`. При клике на ссылку должно выводиться название из её `href`.</li>
|
||||
</ol>
|
||||
|
||||
Вывод в шаблоне пусть будет не просто `<li>Пончик</li>`, а через ссылку: `<a href="#donut">Пончик</a>`.
|
||||
|
||||
При клике на ссылку должно выводиться название из её `href`. Демо:
|
||||
Демо:
|
||||
|
||||
[iframe src="solution" height="130" border="1"]
|
||||
|
||||
[edit src="source"]Исходное меню[/edit]
|
||||
|
|
|
@ -4,27 +4,9 @@
|
|||
|
||||
[cut]
|
||||
|
||||
Все виджеты можно условно разделить на три группы по генерации DOM:
|
||||
<ol>
|
||||
<li>**Получают готовый HTML/DOM и "оживляют" его.**
|
||||
Большинство виджетов, которые мы видели ранее, получают готовый HTML/DOM и "оживляют" его. Это типичный случай в сайтах, где JavaScript -- на ролях "второго помощника". Разметка, CSS уже есть, от JavaScript, условно говоря, требуются лишь обработчики, чтобы менюшки заработали.
|
||||
|
||||
Это типичный случай в сайтах, где JavaScript -- на ролях "второго помощника". Разметка, CSS уже есть, от JavaScript, условно говоря, требуются лишь обработчики, чтобы менюшки заработали.</li>
|
||||
<li>**Создают DOM для компоненты самостоятельно.**
|
||||
|
||||
В ряде случаев даже на простых страницах нужно генерировать DOM динамически. Например, виджет "подсказка" при наведении на элемент создаёт красивую подсказку и показывает её. Элемент генерируется в JavaScript-коде.
|
||||
|
||||
В более сложных интерфейсах компоненты генерируют свой DOM на основе данных, полученных с сервера или из других источников.</li>
|
||||
<li>**=1+2: должны уметь как "оживить" уже готовый DOM, так и создать свой.**
|
||||
|
||||
Бывает и так, что виджет должен уметь и то и другое. Например, так работает сервис сообщений Twitter.
|
||||
|
||||
При начальной загрузки сервер генерирует HTML с текущими сообщениями, а JavaScript лишь добавляет им "живости". Далее, когда страница уже загружена, он получает с сервера данные для новых сообщений и генерирует HTML уже динамически.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
С первой группой виджетов -- вопросов нет. Добавляем обработчики, и дело сделано.
|
||||
|
||||
Здесь нам интересна вторая группа. Третья -- это по существу оптимизация с едиными шаблонами на клиенте и сервере, рассмотрим её позже.
|
||||
Но в сложных интерфейсах разметка изначально отсутствует на странице. Компоненты генерируют свой DOM сами, динамически, на основе данных, полученных с сервера или из других источников.
|
||||
|
||||
## Зачем нужны шаблоны?
|
||||
|
||||
|
@ -105,7 +87,7 @@ function Menu(options) {
|
|||
// сгенерировать HTML, используя шаблон tmpl (см. выше)
|
||||
// с данными title и items
|
||||
*/!*
|
||||
var html = _.template(tmpl, {
|
||||
var html = _.template(tmpl)({
|
||||
title: "Сладости",
|
||||
items: [
|
||||
"Торт",
|
||||
|
@ -163,13 +145,11 @@ var html = _.template(tmpl, {
|
|||
<dl>
|
||||
<dt>`tmpl`</dt>
|
||||
<dd>Шаблон.</dd>
|
||||
<dt>`data`</dt>
|
||||
<dd>Объект с данными.</dd>
|
||||
<dt>`options`</dt>
|
||||
<dd>Необязательные настройки, например можно поменять разделители.</dd>
|
||||
</dl>
|
||||
|
||||
Эта функция запускает "сборку" шаблона `tmpl` с объектом `data` и возвращает результат в виде строки.
|
||||
Эта функция запускает "компиляцию" шаблона `tmpl` и возвращает результат в виде функции, которую далее можно запустить с данными и получить строку-результат.
|
||||
|
||||
Вот так:
|
||||
|
||||
|
@ -178,7 +158,7 @@ var html = _.template(tmpl, {
|
|||
*!*
|
||||
// Шаблон
|
||||
*/!*
|
||||
var tmpl = '<span class="title"><%=title%></span>';
|
||||
var tmpl = _.template('<span class="title"><%=title%></span>');
|
||||
|
||||
*!*
|
||||
// Данные
|
||||
|
@ -187,15 +167,13 @@ var data = {
|
|||
title: "Заголовок"
|
||||
};
|
||||
|
||||
var result = _.template(tmpl, data);
|
||||
|
||||
*!*
|
||||
// Результат подстановки
|
||||
*/!*
|
||||
alert(result); // <span class="title">Заголовок</span>
|
||||
alert( tmpl(data) ); // <span class="title">Заголовок</span>
|
||||
```
|
||||
|
||||
Пример выше похож на операцию "поиск-и-замена": функция `_.template` просто заменила `<%=title%>` в шаблоне `tmpl` на значение свойства `data.title`.
|
||||
Пример выше похож на операцию "поиск-и-замена": шаблон просто заменил `<%=title%>` на значение свойства `data.title`.
|
||||
|
||||
Но возможность вставки JS-кода делает шаблоны сильно мощнее.
|
||||
|
||||
|
@ -209,16 +187,16 @@ var tmpl = '<ul>\
|
|||
<li><%=i%></li> \
|
||||
<% } %>\
|
||||
</ul>';
|
||||
alert( _.template(tmpl, {count: 5}) );
|
||||
alert( _.template(tmpl)({count: 5}) ); // <ul><li>1</li><li>2</li>...</ul>
|
||||
```
|
||||
|
||||
Здесь в результат попал сначала текст `<ul>`, потом выполнился код `for`, который последовательно сгенерировал элементы списка, и затем список был закрыт `</ul>`.
|
||||
|
||||
## Хранение шаблона в документе
|
||||
|
||||
Шаблон -- это многострочный HTML-текст. Объявлять его в скрипте, как сделано выше -- неудобно и некрасиво.
|
||||
Шаблон -- это многострочный HTML-текст. Записывать его прямо в скрипте -- неудобно.
|
||||
|
||||
Один из способов объявления шаблона -- записать его в HTML, в тег <code><script></code> с нестандартным `type`, например `"text/template"`:
|
||||
Один из альтернативных способов объявления шаблона -- записать его в HTML, в тег <code><script></code> с нестандартным `type`, например `"text/template"`:
|
||||
|
||||
```html
|
||||
<script type="*!*text/template*/!*" id="menu-template">
|
||||
|
@ -253,11 +231,11 @@ var template = document.getElementById('menu-template').innerHTML;
|
|||
</script>
|
||||
|
||||
<script>
|
||||
var tmpl = document.getElementById('list-template').innerHTML;
|
||||
var tmpl = _.template(document.getElementById('list-template').innerHTML);
|
||||
|
||||
*!*
|
||||
// ..а вот и результат
|
||||
var result = _.template(tmpl, {count: 5});
|
||||
var result = tmpl({count: 5});
|
||||
document.write( result );
|
||||
*/!*
|
||||
</script>
|
||||
|
@ -271,15 +249,9 @@ var template = document.getElementById('menu-template').innerHTML;
|
|||
|
||||
Оказывается, очень просто.
|
||||
|
||||
Вызов `_.template(tmpl, data)` выполняется в два этапа:
|
||||
<ol>
|
||||
<li>Разбивает строку `tmpl` по разделителям и, при помощи `new Function` создаёт на её основе JavaScript-функцию, которая в специальную переменную-буфер записывает текст из шаблона и выполняет код.</li>
|
||||
<li>Запускает эту функцию с данными `data`, так что она уже генерирует результат.</li>
|
||||
</ol>
|
||||
Вызов `_.template(str)` разбивает строку `str` по разделителям и, при помощи `new Function` создаёт на её основе JavaScript-функцию. Тело этой функции создаётся таким образом, что код, который в шаблоне оформлен как `<% ... %>` -- попадает в неё "как есть", а переменные и текст прибавляются к специальному временному "буферу", который в итоге возвращается.
|
||||
|
||||
Эти два процесса можно разделить. Функцию из строки-шаблона можно получить в явном виде вызовом `_.template(tmpl)`, без второго аргумента.
|
||||
|
||||
Пример:
|
||||
Взглянем на пример:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
|
@ -303,7 +275,7 @@ function(obj) {
|
|||
}
|
||||
```
|
||||
|
||||
Она является результатом вызова `new Function("obj", "код")`, где код динамическим образом генерируется внутри `_.template` на основе шаблона:
|
||||
Она является результатом вызова `new Function("obj", "код")`, где `код` динамическим образом генерируется на основе шаблона:
|
||||
<ol>
|
||||
<li>Вначале в коде идёт "шапка" -- стандартное начало функции, в котором объявляется переменная `__p`. В неё будет записываться результат.</li>
|
||||
<li>Затем добавляется блок `with(obj) { ... }`, внутри которого в `__p` добавляются фрагменты HTML из шаблона, а также переменные из выражений `<%=...%>`. Код из `<%...%>` копируется в функцию "как есть".</li>
|
||||
|
@ -320,13 +292,13 @@ function(obj) {
|
|||
<li>Она работает в глобальной области видимости, не имеет доступа к внешним локальным переменным.</li>
|
||||
<li>Внешний `use strict` на такую функцию не влияет, то есть даже в строгом режиме шаблон продолжит работать.</li>
|
||||
</ul>
|
||||
Если мы всё же не хотим использовать `with` -- нужно поставить третий параметр -- `options`, указав параметр `variable` (название переменной с данными).
|
||||
Если мы всё же не хотим использовать `with` -- нужно поставить второй параметр -- `options`, указав параметр `variable` (название переменной с данными).
|
||||
|
||||
Например:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
alert( _.template("<h1><%=menu.title%></h1>", null, {variable: "menu"}) );
|
||||
alert( _.template("<h1><%=menu.title%></h1>", {variable: "menu"}) );
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
@ -335,7 +307,7 @@ alert( _.template("<h1><%=menu.title%></h1>", null, {variable: "menu"}) );
|
|||
*!*
|
||||
function(*!*menu*/!*) {
|
||||
*/!*
|
||||
var __t, __p = '', __e = _.escape;
|
||||
var __t, __p = '';
|
||||
__p += '<h1>' +
|
||||
((__t = (menu.title)) == null ? '' : __t) +
|
||||
'</h1>';
|
||||
|
@ -528,10 +500,9 @@ function(obj) {
|
|||
</script>
|
||||
|
||||
<script>
|
||||
var tmpl = document.getElementById('menu-template').innerHTML;
|
||||
var tmpl = _.template(document.getElementById('menu-template').innerHTML);
|
||||
|
||||
var compiled = _.template(tmpl);
|
||||
var result = compiled({ title: "Заголовок" });
|
||||
var result = tmpl({ title: "Заголовок" });
|
||||
|
||||
document.write(result);
|
||||
</script>
|
||||
|
@ -553,7 +524,7 @@ function(obj) {
|
|||
|
||||
```js
|
||||
...
|
||||
var compiled = _.template(tmpl, null, {sourceURL: '/template/menu-template'});
|
||||
var compiled = _.template(tmpl, {sourceURL: '/template/menu-template'});
|
||||
...
|
||||
```
|
||||
|
||||
|
@ -571,10 +542,32 @@ var compiled = _.template(tmpl, null, {sourceURL: '/template/menu-template'});
|
|||
|
||||
Шаблоны полезны для того, чтобы отделить HTML от кода. Это упрощает разработку и поддержку.
|
||||
|
||||
В этой главе подробно разобрана система шаблонизации из библиотеки [LoDash](https://lodash.com).
|
||||
В этой главе подробно разобрана система шаблонизации из библиотеки [LoDash](https://lodash.com):
|
||||
|
||||
Теперь, когда мы с ней знакомы, мы можем как использовать её в своих проектах, так и перейти к более глобальному рассмотрению подходов к шаблонизации.
|
||||
<ul>
|
||||
<li>Шаблон -- это строка со специальными вставками кода `<% ... %>` или переменных `<%- expr ->`, `<%= expr ->`.</li>
|
||||
<li>Вызов `_.template(tmpl)` превращает шаблон `tmpl` в функцию, которой в дальнейшем передаются данные --
|
||||
и она генерирует HTML с ними. Можно вызвать передать данные сразу в `_.template` вторым аргументом.</li>
|
||||
</ul>
|
||||
|
||||
В этой главе мы рассмотрели хранение шаблонов в документе, при помощи `<script>` с нестандартным `type`. Конечно, есть и другие способы, можно хранить шаблоны и в отдельном файле, если шаблонная система или система сборки проектов это позволяют.
|
||||
|
||||
Шаблонных систем много. Многие основаны на схожем принципе -- генерации функции из строки, например:
|
||||
|
||||
<ul>
|
||||
<li>[EJS](http://www.embeddedjs.com/)</li>
|
||||
<li>[Jade](http://jade-lang.com/)</li>
|
||||
<li>[Handlebars](http://handlebarsjs.com/)</li>
|
||||
</ul>
|
||||
|
||||
Есть и альтернативный подход -- шаблонная система получает "образец" DOM-узела и клонирует его вызовом `cloneNode(true)`, каждый раз изменяя что-то внутри. В отличие от подхода, описанного выше, это будет работать не с произвольной строкой текста, а только и именно с DOM-узлами. Но в некоторых ситуациях у него есть преимущество.
|
||||
|
||||
Такой подход используется во фреймворках:
|
||||
<ul>
|
||||
<li>[AngularJS](http://angularjs.org)</li>
|
||||
<li>[Knockout.JS](http://knockoutjs.com/)</li>
|
||||
</ul>
|
||||
|
||||
[libs]
|
||||
lodash.js
|
||||
lodash
|
||||
[/libs]
|
|
@ -16,10 +16,12 @@
|
|||
</script>
|
||||
|
||||
<script>
|
||||
var tmpl = document.getElementById('menu-template').innerHTML;
|
||||
var tmpl = _.template(
|
||||
document.getElementById('menu-template').innerHTML,
|
||||
{sourceURL: '/template/menu-template'}
|
||||
);
|
||||
|
||||
var compiled = _.template(tmpl, null, {sourceURL: '/template/menu-template'});
|
||||
var result = compiled({ title: "Заголовок" });
|
||||
var result = tmpl({ title: "Заголовок" });
|
||||
|
||||
document.write(result);
|
||||
</script>
|
||||
|
|
|
@ -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>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<style>
|
||||
.voter {
|
||||
font-family: Consolas, "Lucida Console", monospace;
|
||||
|
@ -14,6 +13,8 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=CustomEvent,Element.prototype.closest"></script>
|
||||
<script src="voter.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
@ -24,67 +25,14 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
function Voter(options) {
|
||||
var self = this;
|
||||
var elem = options.elem;
|
||||
|
||||
var voteElem = elem.find('.vote');
|
||||
|
||||
// вынесем текущее значение в отдельную переменную
|
||||
var vote = options.value || 0;
|
||||
|
||||
// установить значение без генерации события
|
||||
setVote(vote, true);
|
||||
|
||||
elem.on('click', '.down', onDownClick)
|
||||
.on('click', '.up', onUpClick)
|
||||
.on('mousedown selectstart', false);
|
||||
|
||||
// ----------- методы -------------
|
||||
|
||||
function onDownClick() {
|
||||
// сам обработчик не меняет голос, он вызывает для этого функцию
|
||||
voteDecrease();
|
||||
}
|
||||
|
||||
function onUpClick() {
|
||||
voteIncrease();
|
||||
}
|
||||
|
||||
function voteDecrease() {
|
||||
self.setVote( vote - 1 );
|
||||
}
|
||||
|
||||
function voteIncrease() {
|
||||
self.setVote( vote + 1 );
|
||||
}
|
||||
|
||||
// сделали функцию приватной, чтобы её можно было вызывать до объявления
|
||||
// в коде инициализации
|
||||
function setVote(newVote, quiet) {
|
||||
vote = newVote;
|
||||
|
||||
voteElem.html( vote );
|
||||
|
||||
if (!quiet) {
|
||||
$(self).triggerHandler({
|
||||
type: 'change',
|
||||
value: vote
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.setVote = setVote;
|
||||
|
||||
}
|
||||
|
||||
var voter = new Voter({
|
||||
elem: $('#voter'),
|
||||
value: 5
|
||||
elem: document.getElementById('voter')
|
||||
});
|
||||
|
||||
$(voter).on('change', function(e) {
|
||||
alert(e.value);
|
||||
voter.setVote(5);
|
||||
|
||||
document.getElementById('voter').addEventListener('change', function(e) {
|
||||
alert(e.detail);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
|
|
@ -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]
|
||||
|
||||
Добавьте событие в голосовалку, созданную в задаче [](/task/voter), используя jQuery-механизм генерации событий на объекте.
|
||||
Добавьте событие в голосовалку, созданную в задаче [](/task/voter), используя механизм генерации событий на объекте.
|
||||
|
||||
Пусть каждое изменение голоса сопровождается событием `change`:
|
||||
Пусть каждое изменение голоса сопровождается событием `change` со свойством `detail`, содержащим обновлённое значение:
|
||||
|
||||
```js
|
||||
var voter = new Voter({
|
||||
elem: $('#voter'),
|
||||
value: 5
|
||||
elem: document.getElementById('voter')
|
||||
});
|
||||
|
||||
$(voter).on('change', function(e) {
|
||||
alert(e.value);
|
||||
voter.setVote(5);
|
||||
|
||||
document.getElementById('voter').addEventListener('change', function(e) {
|
||||
alert(e.detail); // новое значение голоса
|
||||
});
|
||||
```
|
||||
|
||||
Все изменения голоса должны производиться централизованно, через метод `setVote`, который и генерирует событие.
|
||||
|
||||
Результат использования кода выше (планируемый):
|
||||
[iframe border=1 height=60 src="index.html"].
|
||||
[iframe border=1 height=60 src="solution"]
|
||||
|
||||
Исходный документ возьмите из решения задачи [](/task/voter).
|
|
@ -1,8 +0,0 @@
|
|||
|
||||
|
||||
Обратите внимание:
|
||||
<ul>
|
||||
<li>`onLiClick` не генерирует событие `select`. Это обработчик, его роль -- разобраться, что происходит, и передать работу нужным методам.</li>
|
||||
<li>В событие передаётся массив значений `value`. Код виджета производит всю работу по подготовке этого значения. Было бы совершенно недопустимо, хотя это проще, передавать массив выбранных `LI`. Вообще, элементы -- это внутреннее дело компонента, они могут измениться в любой момент, доступ к ним снаружи крайне нежелателен.</li>
|
||||
<li>Код получения значений вынесен в отдельную функцию `getValues` -- для чистоты (каждая функция делает свою работу), и потому что скорее всего она ещё где-то понадобится.</li>
|
||||
</ul>
|
|
@ -2,22 +2,18 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<style>
|
||||
.selected {
|
||||
background: #0f0;
|
||||
}
|
||||
li {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=CustomEvent,Element.prototype.closest"></script>
|
||||
<script src="listSelect.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Клик на элементе выделяет только его.<br>
|
||||
Shift+Клик добавляет/убирает элемент из выделенных.<br>
|
||||
Ctrl(Cmd)+Клик добавляет/убирает элемент из выделенных.<br>
|
||||
Shift+Клик добавляет промежуток от последнего кликнутого к выделению.<br>
|
||||
|
||||
<ul>
|
||||
<ul id="heroes">
|
||||
<li>Кристофер Робин</li>
|
||||
<li>Винни-Пух</li>
|
||||
<li>Ослик Иа</li>
|
||||
<li>Мудрая Сова</li>
|
||||
|
@ -25,55 +21,16 @@ Shift+Клик добавляет/убирает элемент из выдел
|
|||
</ul>
|
||||
|
||||
<script>
|
||||
|
||||
function ListSelect(options) {
|
||||
var elem = options.elem;
|
||||
|
||||
var self = this;
|
||||
|
||||
elem.on('click', 'li', onLiClick);
|
||||
elem.on('selectstart mousedown', false);
|
||||
|
||||
function onLiClick(e) {
|
||||
if (!e.shiftKey) {
|
||||
deselectAllItems();
|
||||
}
|
||||
|
||||
toggleSelectItem( $(this) );
|
||||
}
|
||||
|
||||
function deselectAllItems() {
|
||||
elem.children().removeClass('selected');
|
||||
}
|
||||
|
||||
function getValue() {
|
||||
var value = [];
|
||||
|
||||
elem.children('.selected').each(function() {
|
||||
value.push( $(this).html() );
|
||||
});
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function toggleSelectItem(li) {
|
||||
li.toggleClass('selected');
|
||||
|
||||
$(self).triggerHandler({
|
||||
type: 'change',
|
||||
value: getValue()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var select = new ListSelect({
|
||||
elem: $('ul')
|
||||
var listSelect = new ListSelect({
|
||||
elem: document.querySelector('#heroes')
|
||||
});
|
||||
|
||||
$(select).on('change', function(e) {
|
||||
alert(e.value);
|
||||
document.querySelector('#heroes').addEventListener('select', function(event) {
|
||||
alert(event.value);
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -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`.
|
||||
|
||||
Оно должно срабатывать при каждом изменении выбора и содержать список выбранных строк.
|
||||
Оно должно срабатывать при каждом изменении выбора и в свойстве `detail` содержать список выбранных строк.
|
||||
|
||||
Во внешнем коде добавьте обработчик к списку, который при изменениях выводит список значений.
|
||||
[iframe border="1" src="solution"]
|
||||
[iframe border="1" src="solution" height=180]
|
||||
|
||||
В качестве исходного кода возьмите решение задачи [](/task/selectable-list-component).
|
||||
|
|
|
@ -4,30 +4,32 @@
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
.customselect-title {
|
||||
height: 17px;
|
||||
border: 1px solid #ADD8E6;
|
||||
background-position: right;
|
||||
background-image: url(https://js.cx/clipart/select-button.gif);
|
||||
background-repeat: no-repeat;
|
||||
.customselect .title {
|
||||
height: 20px;
|
||||
border: 2px groove #ADD8E6;
|
||||
background: white;
|
||||
width: 200px;
|
||||
box-sizing: border-box;
|
||||
padding: 2px;
|
||||
line-height: 14px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.customselect li {
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.customselect-options li {
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.customselect-options li:nth-child(even) {
|
||||
.customselect li:nth-child(even) {
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
.customselect-options li:hover {
|
||||
.customselect li:hover {
|
||||
background-color: #7fffd4;
|
||||
}
|
||||
|
||||
.customselect-options {
|
||||
.customselect ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
@ -42,6 +44,7 @@
|
|||
border-right: 1px solid #add8e6;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.customselect-open .customselect-options {
|
||||
|
||||
.customselect.open ul {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
@ -1,44 +1,39 @@
|
|||
function CustomSelect(options) {
|
||||
var self = this;
|
||||
|
||||
var elem = options.elem;
|
||||
|
||||
elem.on('click', '.customselect-title', onTitleClick);
|
||||
elem.on('click', 'li', onOptionClick);
|
||||
elem.onclick = function(event) {
|
||||
if (event.target.className == 'title') {
|
||||
toggle();
|
||||
} else if (event.target.tagName == 'LI') {
|
||||
setValue(event.target.innerHTML, event.target.dataset.value);
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
var isOpen = false;
|
||||
|
||||
// ------ обработчики ------
|
||||
|
||||
function onTitleClick(event) {
|
||||
toggle();
|
||||
}
|
||||
|
||||
// закрыть селект, если клик вне его
|
||||
function onDocumentClick(event) {
|
||||
var isInside = $(event.target).closest(elem).length;
|
||||
if (!isInside) close();
|
||||
}
|
||||
|
||||
function onOptionClick(event) {
|
||||
close();
|
||||
|
||||
var name = $(event.target).html();
|
||||
var value = $(event.target).data('value');
|
||||
|
||||
setValue(name, value);
|
||||
if (!elem.contains(event.target)) close();
|
||||
}
|
||||
|
||||
// ------------------------
|
||||
|
||||
function setValue(name, value) {
|
||||
elem.find('.customselect-title').html(name);
|
||||
function setValue(title, value) {
|
||||
elem.querySelector('.title').innerHTML = title;
|
||||
|
||||
$(self).triggerHandler({
|
||||
type: 'select',
|
||||
name: name,
|
||||
value: value
|
||||
var widgetEvent = new CustomEvent('select', {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
title: title,
|
||||
value: value
|
||||
}
|
||||
});
|
||||
|
||||
elem.dispatchEvent(widgetEvent);
|
||||
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
|
@ -47,14 +42,14 @@ function CustomSelect(options) {
|
|||
}
|
||||
|
||||
function open() {
|
||||
elem.addClass('customselect-open');
|
||||
$(document).on('click', onDocumentClick);
|
||||
elem.classList.add('open');
|
||||
document.addEventListener('click', onDocumentClick);
|
||||
isOpen = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
elem.removeClass('customselect-open');
|
||||
$(document).off('click', onDocumentClick);
|
||||
elem.classList.remove('open');
|
||||
document.removeEventListener('click', onDocumentClick);
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Селект</title>
|
||||
<script src='http://code.jquery.com/jquery.min.js'></script>
|
||||
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=CustomEvent,Element.prototype.closest"></script>
|
||||
<link rel="stylesheet" href="customselect.css"/>
|
||||
<script src="customselect.js"></script>
|
||||
</head>
|
||||
|
@ -11,38 +11,38 @@
|
|||
<div>Последний результат: <span id="result">...</span></div>
|
||||
|
||||
<div id="animal-select" class="customselect">
|
||||
<div class="customselect-title">Выберите</div>
|
||||
<ol class="customselect-options">
|
||||
<button class="title">Выберите</button>
|
||||
<ul>
|
||||
<!-- значение хранится в свойстве data-value -->
|
||||
<li data-value="bird">Птицы</li>
|
||||
<li data-value="fish">Рыбы</li>
|
||||
<li data-value="animal">Звери</li>
|
||||
<li data-value="dino">Динозавры</li>
|
||||
<li data-value="simplest">Одноклеточные</li>
|
||||
</ol>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="food-select" class="customselect">
|
||||
<div class="customselect-title">Выберите</div>
|
||||
<ol class="customselect-options">
|
||||
<button class="title">Выберите</button>
|
||||
<ul>
|
||||
<li data-value="carnivore">Плотоядные</li>
|
||||
<li data-value="herbivore">Травоядные</li>
|
||||
<li data-value="omnivore">Всеядные</li>
|
||||
</ol>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
var animalSelect = new CustomSelect({
|
||||
elem: $('#animal-select')
|
||||
elem: document.getElementById('animal-select')
|
||||
});
|
||||
|
||||
var foodSelect = new CustomSelect({
|
||||
elem: $('#food-select')
|
||||
elem: document.getElementById('food-select')
|
||||
});
|
||||
|
||||
$([animalSelect, foodSelect]).on('select', function (event) {
|
||||
$('#result').html(event.value)
|
||||
document.addEventListener('select', function (event) {
|
||||
document.getElementById('result').innerHTML = event.detail.value;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
|
|
@ -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,54 +2,51 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Селект</title>
|
||||
<script src='http://code.jquery.com/jquery.min.js'></script>
|
||||
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=CustomEvent,Element.prototype.closest"></script>
|
||||
<link rel="stylesheet" href="customselect.css"/>
|
||||
<!-- для вашего кода -->
|
||||
<script src="customselect.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div>Последний результат: <span id="result">...</span></div>
|
||||
|
||||
<img src="https://js.cx/clipart/select-button.gif">
|
||||
|
||||
<div id="animal-select" class="customselect">
|
||||
<div class="customselect-title">Выберите</div>
|
||||
<ol class="customselect-options">
|
||||
<button class="title">Выберите</button>
|
||||
<ul>
|
||||
<!-- значение хранится в свойстве data-value -->
|
||||
<li data-value="bird">Птицы</li>
|
||||
<li data-value="fish">Рыбы</li>
|
||||
<li data-value="animal">Звери</li>
|
||||
<li data-value="dino">Динозавры</li>
|
||||
<li data-value="simplest">Одноклеточные</li>
|
||||
</ol>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="food-select" class="customselect">
|
||||
<div class="customselect-title">Выберите</div>
|
||||
<ol class="customselect-options">
|
||||
<button class="title">Выберите</button>
|
||||
<ul>
|
||||
<li data-value="carnivore">Плотоядные</li>
|
||||
<li data-value="herbivore">Травоядные</li>
|
||||
<li data-value="omnivore">Всеядные</li>
|
||||
</ol>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
// создаём два селекта
|
||||
var animalSelect = new CustomSelect({
|
||||
elem: $('#animal-select')
|
||||
elem: document.getElementById('animal-select')
|
||||
});
|
||||
|
||||
var foodSelect = new CustomSelect({
|
||||
elem: $('#food-select')
|
||||
elem: document.getElementById('food-select')
|
||||
});
|
||||
|
||||
// выводим выбранное значение
|
||||
$([animalSelect, foodSelect]).on('select', function (event) {
|
||||
$('#result').html(event.value)
|
||||
document.addEventListener('select', function (event) {
|
||||
document.getElementById('result').innerHTML = event.detail.value;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
|
@ -8,12 +8,12 @@
|
|||
<ul>
|
||||
<li>Открытие и закрытие по клику на заголовок.</li>
|
||||
<li>Закрытие селекта происходит при выборе или клике на любое другое место документа, в том числе на другой аналогичный селект.</li>
|
||||
<li>Событие `"select"` при выборе опции.</li>
|
||||
<li>Событие `"select"` при выборе опции возникает на элементе селекта и всплывает.</li>
|
||||
<li>Значение опции хранится в атрибуте `data-value` (HTML-структура есть в исходном документе).
|
||||
</ul>
|
||||
Например:
|
||||
|
||||
[iframe src="solution" border="1" height="200" newwin]
|
||||
[iframe src="solution" height="200"]
|
||||
|
||||
В примере выше два селекта, чтобы можно было проверить процесс открытия-закрытия.
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Коллбэки и события на компонентах
|
||||
|
||||
Компоненты, хоть и каждый сам по себе, обычно интегрированы друг с другом.
|
||||
Компоненты, хоть и каждый сам по себе, обычно как-то общаются с остальной частью страницы
|
||||
|
||||
Есть несколько способов, при помощи которых компоненты сообщают друг другу о важных событиях, которые в них произошли.
|
||||
|
||||
|
@ -15,8 +15,8 @@
|
|||
```js
|
||||
var menu = new Menu({
|
||||
title: "Сладости",
|
||||
template: _.template($('#menu-template').html()),
|
||||
listTemplate: _.template($('#menu-list-template').html()),
|
||||
template: _.template(document.getElementById('menu-template').innerHTML),
|
||||
listTemplate: _.template(document.getElementById('menu-list-template').innerHTML,
|
||||
items: {
|
||||
"donut": "Пончик",
|
||||
"cake": "Пирожное",
|
||||
|
@ -34,321 +34,84 @@ function showSelected(href) {
|
|||
*/!*
|
||||
```
|
||||
|
||||
В коде меню нужно будет вызывать её, как-то так:
|
||||
В коде меню нужно будет вызывать её, например так:
|
||||
|
||||
```js
|
||||
...
|
||||
function onItemClick(e) {
|
||||
e.preventDefault();
|
||||
*!*
|
||||
var onselect = options.onselect;
|
||||
if (onselect) {
|
||||
onselect(e.currentTarget.getAttribute('href').slice(1));
|
||||
}
|
||||
*/!*
|
||||
function select(link) {
|
||||
options.onselect(link.getAttribute('href').slice(1));
|
||||
...
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
Демо:
|
||||
Полный пример:
|
||||
|
||||
[codetabs src="menu-callback" height="180"]
|
||||
|
||||
## Свои события
|
||||
|
||||
Оповещение через коллбэки -- это примерный аналог назначения обработчика через `onсвойство` для DOM-элементов.
|
||||
Как мы уже знаем, в современных браузерах DOM-элементы могут [генерировать поизвольные события](/dispatch-events) при помощи встроенных методов, а в IE8- это возможно с использованием фреймворка, к примеру, jQuery.
|
||||
|
||||
Да, он работает, но чтобы можно было назначать сколько угодно обработчиков в любой момент, нужна полноценная поддержка событий, то есть аналог `addEventListener`.
|
||||
Воспользуемся ими, чтобы корневой элемент меню генерировал событие, которое мы назовём `select`, при выборе элемента, и передавал в объект события выбранное значение.
|
||||
|
||||
Поддержка событий для компонентов будет выглядеть так:
|
||||
Для этого модифицируем функцию `select`:
|
||||
|
||||
```js
|
||||
function Menu(options) {
|
||||
...
|
||||
|
||||
function select(link) {
|
||||
*!*
|
||||
var widgetEvent = new CustomEvent("select", {
|
||||
bubbles: true,
|
||||
// detail - стандартное свойство CustomEvent для произвольных данных
|
||||
detail: link.getAttribute('href').slice(1)
|
||||
});
|
||||
elem.dispatchEvent(widgetEvent);
|
||||
*/!*
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Код, который заинтересован в том, чтобы узнавать, что выбрано в меню, подписывается на событие `select` его корневого элемента:
|
||||
|
||||
```js
|
||||
var menu = new Menu(...);
|
||||
|
||||
// любой код может подписаться на событие "select"
|
||||
menu.on("select", function(item) {
|
||||
// при выборе пункта меню сработает этот обработчик
|
||||
alert("Выбран элемент " + item);
|
||||
var elem = menu.getElem();
|
||||
|
||||
elem.addEventListener('select', function(event) {
|
||||
alert(event.detail);
|
||||
});
|
||||
```
|
||||
|
||||
Код, который заинтересован в том, чтобы узнавать, что происходит с меню, подписывается на нужные события вызовом `menu.on(имя события, обработчик)`.
|
||||
Вместо `detail` можно было бы выбрать и другое название свойства, но тогда нужно позаботиться о том, чтобы оно не конфликтовало со стандартными. Кроме того, в конструкторе `CustomEvent` разрешено только `detail`, другое свойство понадобилось бы присваивать в отдельной строке.
|
||||
|
||||
Далее `Menu` при наступлении события вызывает обработчик методом `trigger`, при необходимости передавая ему важные данные в качестве аргументов, вот так:
|
||||
Полный пример:
|
||||
|
||||
```js
|
||||
function Menu(options) {
|
||||
var self = this;
|
||||
|
||||
*!*
|
||||
// ... при клике на item - сгенерировать событие (trigger)
|
||||
function onItemClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
self.trigger("select", e.currentTarget.getAttribute('href').slice(1));
|
||||
}
|
||||
*/!*
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Обратим внимание -- события будут генерироваться не на элементе `<div class="menu">`, а на объекте `new Menu`. Сейчас мы разберём методы, которые для этого нужны.
|
||||
|
||||
## Методы on, off и trigger
|
||||
|
||||
Для поддержки событий в любой объект достаточно добавить три метода: `on`, `off` и `trigger`, которые перечислены ниже в свойствах `EventMixin`:
|
||||
|
||||
```js
|
||||
var EventMixin = {
|
||||
|
||||
/**
|
||||
* Подписка на событие
|
||||
* Использование:
|
||||
* menu.on('select', function(item) { ... }
|
||||
*/
|
||||
on: function(eventName, handler) {
|
||||
if (!this._eventHandlers) this._eventHandlers = {};
|
||||
if (!this._eventHandlers[eventName]) {
|
||||
this._eventHandlers[eventName] = [];
|
||||
}
|
||||
this._eventHandlers[eventName].push(handler);
|
||||
},
|
||||
|
||||
/**
|
||||
* Прекращение подписки
|
||||
* menu.off('select', handler)
|
||||
*/
|
||||
off: function(eventName, handler) {
|
||||
var handlers = this._eventHandlers && this._eventHandlers[eventName];
|
||||
if (!handlers) return;
|
||||
for(var i=0; i<handlers.length; i++) {
|
||||
if (handlers[i] == handler) {
|
||||
handlers.splice(i--, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Генерация события с передачей данных
|
||||
* this.trigger('select', item);
|
||||
*/
|
||||
trigger: function(eventName) {
|
||||
|
||||
if (!this._eventHandlers || !this._eventHandlers[eventName]) {
|
||||
return; // обработчиков для события нет
|
||||
}
|
||||
|
||||
// вызвать обработчики
|
||||
var handlers = this._eventHandlers[eventName];
|
||||
for (var i = 0; i < handlers.length; i++) {
|
||||
handlers[i].apply(this, [].slice.call(arguments, 1));
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Объект `EventMixin` -- всего лишь хранилище для функций, которые нужно скопировать в тот объект, которому нужны события.
|
||||
|
||||
Пример использования:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var obj = {}; // произвольный объект
|
||||
|
||||
*!*
|
||||
// скопируем в него методы для работы с событиями
|
||||
*/!*
|
||||
for(var m in EventMixin) obj[m] = EventMixin[m];
|
||||
|
||||
*!*
|
||||
// поставить обработчик на событие hello
|
||||
*/!*
|
||||
obj.on("hello", function(a, b, c) {
|
||||
alert(a + ", " + b + ", " + c);
|
||||
});
|
||||
|
||||
// и ещё один обработчик
|
||||
obj.on("hello", function() {
|
||||
alert("привет!");
|
||||
});
|
||||
|
||||
*!*
|
||||
// генерация события с дополнительными данными
|
||||
// обычно происходит внутри методов объекта
|
||||
*/!*
|
||||
obj.trigger("hello", "data1", "data2", "data3");
|
||||
```
|
||||
|
||||
При запуске этого кода будет поставлено два обработчика на событие `hello`, которые (по очереди) будут вызваны при `obj.trigger(...)`.
|
||||
|
||||
[smart header="Внутренний алгоритм `on/off/trigger`"]
|
||||
Большинство фреймворков, предоставляющих события на произвольных объектах, работают примерно так же, как `on/off/trigger` в примере выше.
|
||||
|
||||
**Все обработчики событий `obj` хранятся в "скрытом" свойстве самого `obj`.**
|
||||
<ul>
|
||||
<li>Вызов `obj.on(event, handler)` создаёт свойство `obj._eventHandlers`, если его ещё нет, и сохраняет в нём обработчик: `obj._eventHandlers[event].push(handler)`.</li>
|
||||
<li>Далее вызов `obj.trigger(event, ...)` получает обработчики из `obj._eventHandlers[event]` и выполняет их один за другим.</li>
|
||||
<li>Вызов `obj.off(event, handler)` удаляет обработчик из `obj._eventHandlers`.</li>
|
||||
</ul>
|
||||
[/smart]
|
||||
|
||||
## Меню с событиями
|
||||
|
||||
Для перехода на события в меню, нужно:
|
||||
<ol>
|
||||
<li>Добавить методы из `EventMixin` в объект меню. Их можно скопировать либо в объект, либо в прототип.</li>
|
||||
<li>Вместо вызова коллбэка `onselect` -- генерируем событие вызовом `trigger('select', значение)`.</li>
|
||||
</ol>
|
||||
|
||||
Код:
|
||||
|
||||
```js
|
||||
function Menu(options) {
|
||||
var elem;
|
||||
var self = this;
|
||||
|
||||
*!*
|
||||
for(var method in EventMixin) {
|
||||
this[method] = EventMixin[method]
|
||||
}
|
||||
*/!*
|
||||
|
||||
function onItemClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
*!*
|
||||
self.trigger("select", currentTarget.getAttribute('href').slice(1));
|
||||
*/!*
|
||||
}
|
||||
|
||||
// другие методы без изменений
|
||||
}
|
||||
```
|
||||
|
||||
Во внешнем коде:
|
||||
|
||||
```js
|
||||
var menu = new Menu(...)
|
||||
menu.on('select', function(value) {
|
||||
alert('выбрано значение ' + value);
|
||||
});
|
||||
```
|
||||
|
||||
Результат:
|
||||
[codetabs src="menu-event"]
|
||||
|
||||
## События при помощи jQuery
|
||||
[warn header="Внимание, инкапсуляция!"]
|
||||
Очень важно, что внешний код ставит обработчик на корневой элемент, но не на внутренние элементы меню.
|
||||
|
||||
Фреймворк jQuery предоставляет свои средства для генерации произвольных событий.
|
||||
Строго говоря, он вообще не знает про то, как устроено меню, есть ли там ссылки и какие, или там вообще всё реализовано через кнопки.
|
||||
|
||||
Метод [trigger](http://api.jquery.com/trigger/) позволяет генерировать любое событие на элементе и он же -- работает на любом объекте, "обёрнутом" в jQuery: `$(...)`.
|
||||
Меню для него -- "чёрный ящик". Корневой элемент -- точка доступа к его функционалу. Событие -- не то, которое произошло на ссылке, а "переработанный вариант", интерпретация действия со стороны меню.
|
||||
|
||||
Синтаксис для генерации события на элементе:
|
||||
|
||||
```js
|
||||
elem.trigger(event);
|
||||
```
|
||||
|
||||
Объект `event` должен содержать свойство `type` -- тип события (произвольный) и любые другие свойства, которые будут переданы обработчикам.
|
||||
|
||||
Например:
|
||||
|
||||
```js
|
||||
$('body').on('hello', function(e) {
|
||||
alert(e.user)
|
||||
});
|
||||
|
||||
*!*
|
||||
$('body').trigger({
|
||||
type: 'hello', // тип события
|
||||
user: 'Вася', // любые данные
|
||||
});
|
||||
*/!*
|
||||
|
||||
$('body').off('hello');
|
||||
```
|
||||
|
||||
Есть также альтернативный синтаксис:
|
||||
|
||||
```js
|
||||
elem.trigger(eventType, args);
|
||||
```
|
||||
|
||||
<ul>
|
||||
<li>`eventType` -- тип события (строка)</li>
|
||||
<li>`args` -- массив аргументов, которые будут переданы обработчику.</li>
|
||||
</ul>
|
||||
|
||||
Пример использования:
|
||||
|
||||
```js
|
||||
$('body').on('hello', function(e, user) {
|
||||
alert(user)
|
||||
});
|
||||
|
||||
*!*
|
||||
// аргументы - в виде массива
|
||||
$('body').trigger("hello", ["Вася"]);
|
||||
*/!*
|
||||
|
||||
$('body').off('hello');
|
||||
```
|
||||
|
||||
Как правильно, предпочтителен первый синтаксис: `elem.trigger(event)`, поскольку он гораздо гибче, можно указывать любые свойства в любом порядке.
|
||||
|
||||
События, сгенерированные таким образом на элементах, всплывают и обрабатываются jQuery наравне с обычными браузерными событиями.
|
||||
|
||||
**Для объекта всё точно так же, как и для элемента:**
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var obj = {}; // произвольный объект
|
||||
|
||||
$(obj).on('hello', function(e) {
|
||||
alert(e.user)
|
||||
});
|
||||
|
||||
$(obj).trigger({
|
||||
type: 'hello',
|
||||
user: 'Вася'
|
||||
});
|
||||
```
|
||||
|
||||
Конечно, события на объектах не всплывают, всё-таки "всплытие" -- это привилегия DOM.
|
||||
|
||||
Внутри jQuery делает в точности то же самое, что `EventMixin` -- вызов `on(...)` добавляет обработчик добавляются в "скрытое" свойство объекта, из которого они потом читаются и выполняются вызовом `trigger(...)`.
|
||||
|
||||
Это свойство можно легко увидеть:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var obj = {};
|
||||
|
||||
$(obj).on('hello', function(e) { /* ... */ });
|
||||
|
||||
// свойство называется примерно так: jQuery19105266267273109406
|
||||
// оно имеет такое псевдослучайное название,
|
||||
// чтобы не перезаписать "настоящие", важные свойства объекта
|
||||
for(var prop in obj) alert(prop);
|
||||
```
|
||||
|
||||
Технически, можно генерировать событие и на корневом элементе меню `<div class="menu">` и на JavaScript-объекте `new Menu` (`this` внутри конструктора). Однако, обычно используют именно объект: считается, что DOM компонента -- это его сугубо личное дело. Он может в любой момент поменяться и быть пересоздан "с нуля", если потребуется. Залезать в него снаружи и ставить какие-то обработчики никто не имеет права.
|
||||
Такое правило позволяет нам не опасаться проблем при оптимизации, расширении и даже полной переделке DOM-структуры меню. Коль скоро события и методы сохраняются, внешний код будет работать как прежде.
|
||||
|
||||
Ещё раз -- внешний код не имеет права залезать внутрь DOM-структуры меню, ставить там обработчики и так далее.
|
||||
[/warn]
|
||||
|
||||
## Итого
|
||||
|
||||
Для того, чтобы внешний код мог узнавать о важных событиях, произошедших внутри компоненты, используются:
|
||||
<ul>
|
||||
<li>Коллбэки -- функции, которые передаются "снаружи" при создании компонента, и которые он обязуется вызвать при наступлении событий.</li>
|
||||
<li>События -- компонент генерирует их при помощи вызова `trigger` (`EventMixin`, jQuery или другой фреймворк), а внешний код ставит обработчики при помощи `on`.</li>
|
||||
<li>События -- компонент генерирует их на корневом элементе при помощи `dispatchEvent`, а внешний код ставит обработчики при помощи `addEventListener`. Такие события всплывают, если указан флаг `bubbles`, поэтому можно использовать с ними делегирование.</li>
|
||||
</ul>
|
||||
|
||||
Можно использовать и то и другое одновременно. Например, виджеты фреймворка jQuery UI позволяют передать при создании коллбэк `onselect`, и вместе с тем генерируют событие.
|
||||
|
||||
Далее, для простоты, в примерах и задачах мы будем использовать для событий методы `on/off/trigger` из jQuery. Если jQuery не нужен -- всегда можно использовать `EventMixin` или какой-либо другой фреймворк.
|
||||
|
||||
|
||||
[libs]
|
||||
event-mixin.js
|
||||
[/libs]
|
|
@ -2,9 +2,9 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<link rel="stylesheet" href="menu.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
|
||||
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
|
||||
<script src="menu.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -15,10 +15,15 @@
|
|||
</div>
|
||||
</script>
|
||||
|
||||
<!--
|
||||
встроенная браузерная функция encodeURIComponent кодирует спец-символы для URL,
|
||||
например русские буквы и пробелы
|
||||
в этом примере русских букв в ключах items нет, но потенциально они возможны
|
||||
-->
|
||||
<script type="text/template" id="menu-list-template">
|
||||
<ul>
|
||||
<% for(var key in items) { %>
|
||||
<li><a href="#<%-key%>"><%-items[key]%></li>
|
||||
<% for(var name in items) { %>
|
||||
<li><a href="#<%=encodeURIComponent(name)%>"><%-items[name]%></a></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</script>
|
||||
|
@ -26,22 +31,21 @@
|
|||
<script>
|
||||
var menu = new Menu({
|
||||
title: "Сладости",
|
||||
template: _.template($('#menu-template').html()),
|
||||
listTemplate: _.template($('#menu-list-template').html()),
|
||||
template: _.template( document.getElementById('menu-template').innerHTML.trim()),
|
||||
listTemplate: _.template( document.getElementById('menu-list-template').innerHTML.trim()),
|
||||
items: {
|
||||
"donut": "Пончик",
|
||||
"cake": "Пирожное",
|
||||
"chocolate": "Шоколадка"
|
||||
cake: "Торт", // cake <a href="#cake">Торт</a>
|
||||
donut: "Пончик", // donut
|
||||
chokolate: "Шоколадка" // chokolate
|
||||
},
|
||||
onselect: showSelected
|
||||
});
|
||||
|
||||
function showSelected(href) {
|
||||
alert(href);
|
||||
function showSelected(itemName) {
|
||||
alert(itemName);
|
||||
}
|
||||
|
||||
$(document.body).append(menu.getElem());
|
||||
menu.open();
|
||||
document.body.appendChild(menu.getElem());
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
|
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() {
|
||||
var elemHtml = options.template({title: options.title});
|
||||
var html = options.template({title: options.title});
|
||||
|
||||
elem = $(elemHtml);
|
||||
elem = document.createElement('div');
|
||||
elem.innerHTML = html;
|
||||
elem = elem.firstElementChild;
|
||||
|
||||
elem.on('mousedown selectstart', false);
|
||||
elem.onmousedown = function() {
|
||||
return false;
|
||||
}
|
||||
|
||||
elem.on('click', '.title', onTitleClick);
|
||||
elem.on('click', 'a', onItemClick)
|
||||
}
|
||||
elem.onclick = function(event) {
|
||||
if (event.target.closest('.title')) {
|
||||
toggle();
|
||||
}
|
||||
|
||||
if (event.target.closest('a')) {
|
||||
event.preventDefault();
|
||||
select(event.target.closest('a'));
|
||||
}
|
||||
|
||||
function renderItems() {
|
||||
if (elem.find('ul').length) return;
|
||||
|
||||
var listHtml = options.listTemplate({items: options.items});
|
||||
elem.append(listHtml);
|
||||
}
|
||||
|
||||
function onItemClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var onselect = options.onselect;
|
||||
if (onselect) {
|
||||
onselect(e.currentTarget.getAttribute('href').slice(1));
|
||||
}
|
||||
}
|
||||
|
||||
function onTitleClick(e) {
|
||||
toggle();
|
||||
function renderItems() {
|
||||
if (elem.querySelector('ul')) return;
|
||||
|
||||
var listHtml = options.listTemplate({items: options.items});
|
||||
elem.insertAdjacentHTML("beforeEnd", listHtml);
|
||||
}
|
||||
|
||||
function select(link) {
|
||||
options.onselect(link.getAttribute('href').slice(1));
|
||||
}
|
||||
|
||||
function open() {
|
||||
renderItems();
|
||||
elem.addClass('open');
|
||||
elem.classList.add('open');
|
||||
};
|
||||
|
||||
function close() {
|
||||
elem.removeClass('open');
|
||||
elem.classList.remove('open');
|
||||
};
|
||||
|
||||
function toggle() {
|
||||
if (elem.hasClass('open')) close();
|
||||
if (elem.classList.contains('open')) close();
|
||||
else open();
|
||||
};
|
||||
|
||||
|
@ -56,4 +59,4 @@ function Menu(options) {
|
|||
this.toggle = toggle;
|
||||
this.close = close;
|
||||
this.open = open;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<link rel="stylesheet" href="menu.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
|
||||
<script src="eventMixin.js"></script>
|
||||
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=CustomEvent,Element.prototype.closest"></script>
|
||||
<script src="menu.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -18,8 +17,8 @@
|
|||
|
||||
<script type="text/template" id="menu-list-template">
|
||||
<ul>
|
||||
<% for(var key in items) { %>
|
||||
<li><a href="#<%-key%>"><%-items[key]%></li>
|
||||
<% for(var name in items) { %>
|
||||
<li><a href="#<%=encodeURIComponent(name)%>"><%-items[name]%></a></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</script>
|
||||
|
@ -27,21 +26,21 @@
|
|||
<script>
|
||||
var menu = new Menu({
|
||||
title: "Сладости",
|
||||
template: _.template($('#menu-template').html()),
|
||||
listTemplate: _.template($('#menu-list-template').html()),
|
||||
template: _.template( document.getElementById('menu-template').innerHTML.trim()),
|
||||
listTemplate: _.template( document.getElementById('menu-list-template').innerHTML.trim()),
|
||||
items: {
|
||||
"donut": "Пончик",
|
||||
"cake": "Пирожное",
|
||||
"chocolate": "Шоколадка"
|
||||
cake: "Торт", // cake <a href="#cake">Торт</a>
|
||||
donut: "Пончик", // donut
|
||||
chokolate: "Шоколадка" // chokolate
|
||||
}
|
||||
});
|
||||
|
||||
$(document.body).append(menu.getElem());
|
||||
menu.open();
|
||||
|
||||
menu.on('select', function(value) {
|
||||
alert('выбрано значение ' + value);
|
||||
var elem = menu.getElem();
|
||||
document.body.appendChild(elem);
|
||||
elem.addEventListener('select', function(event) {
|
||||
alert(event.detail);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
|
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) {
|
||||
var elem;
|
||||
var self = this;
|
||||
|
||||
for(var method in EventMixin) {
|
||||
this[method] = EventMixin[method]
|
||||
}
|
||||
|
||||
function getElem() {
|
||||
if (!elem) render();
|
||||
|
@ -12,45 +7,55 @@ function Menu(options) {
|
|||
}
|
||||
|
||||
function render() {
|
||||
var elemHtml = options.template({title: options.title});
|
||||
var html = options.template({title: options.title});
|
||||
|
||||
elem = $(elemHtml);
|
||||
elem = document.createElement('div');
|
||||
elem.innerHTML = html;
|
||||
elem = elem.firstElementChild;
|
||||
|
||||
elem.on('mousedown selectstart', false);
|
||||
elem.onmousedown = function() {
|
||||
return false;
|
||||
}
|
||||
|
||||
elem.on('click', '.title', onTitleClick);
|
||||
elem.on('click', 'a', onItemClick)
|
||||
elem.onclick = function(event) {
|
||||
if (event.target.closest('.title')) {
|
||||
toggle();
|
||||
}
|
||||
|
||||
if (event.target.closest('a')) {
|
||||
event.preventDefault();
|
||||
select(event.target.closest('a'));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function renderItems() {
|
||||
if (elem.find('ul').length) return;
|
||||
if (elem.querySelector('ul')) return;
|
||||
|
||||
var listHtml = options.listTemplate({items: options.items});
|
||||
elem.append(listHtml);
|
||||
elem.insertAdjacentHTML("beforeEnd", listHtml);
|
||||
}
|
||||
|
||||
function onItemClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
self.trigger('select', e.currentTarget.getAttribute('href').slice(1));
|
||||
}
|
||||
|
||||
function onTitleClick(e) {
|
||||
toggle();
|
||||
function select(link) {
|
||||
var widgetEvent = new CustomEvent("select", {
|
||||
bubbles: true,
|
||||
detail: link.getAttribute('href').slice(1)
|
||||
});
|
||||
elem.dispatchEvent(widgetEvent);
|
||||
}
|
||||
|
||||
function open() {
|
||||
renderItems();
|
||||
elem.addClass('open');
|
||||
elem.classList.add('open');
|
||||
};
|
||||
|
||||
function close() {
|
||||
elem.removeClass('open');
|
||||
elem.classList.remove('open');
|
||||
};
|
||||
|
||||
function toggle() {
|
||||
if (elem.hasClass('open')) close();
|
||||
if (elem.classList.contains('open')) close();
|
||||
else open();
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
<meta charset="utf-8">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
|
||||
<script src="dateselector.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
|
||||
<script src="dateselector.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
|
||||
<link type="text/css" rel="stylesheet" href="window.css" />
|
||||
<style>
|
||||
html, body {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
|
||||
<link type="text/css" rel="stylesheet" href="window.css" />
|
||||
<style>
|
||||
html, body {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
|
||||
<style>
|
||||
html, body {
|
||||
padding: 0;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
|
||||
<link type="text/css" rel="stylesheet" href="window.css" />
|
||||
<style>
|
||||
html, body {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<link type="text/css" rel="stylesheet" href="datepicker.css">
|
||||
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
|
||||
|
||||
<script src="calendar.js"></script>
|
||||
<script src="datepicker.js"></script>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<link type="text/css" rel="stylesheet" href="datepicker.css">
|
||||
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
|
||||
|
||||
<script src="calendar.js"></script>
|
||||
<script src="datepicker.js"></script>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<link href="tree.css" rel="stylesheet">
|
||||
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
|
||||
<script src="regions.js"></script>
|
||||
<script src="tree.js"></script>
|
||||
</head>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue