init
This commit is contained in:
parent
06f61d8ce8
commit
f301cb744d
2271 changed files with 103162 additions and 0 deletions
94
02-ui/05-widgets/05-custom-events/01-voter-events/index.html
Executable file
94
02-ui/05-widgets/05-custom-events/01-voter-events/index.html
Executable file
|
@ -0,0 +1,94 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<style>
|
||||
.voter {
|
||||
font-family: Consolas, "Lucida Console", monospace;
|
||||
font-size: 18px;
|
||||
}
|
||||
.up, .down {
|
||||
cursor: pointer;
|
||||
color: blue;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="voter" class="voter">
|
||||
<span class="down">—</span>
|
||||
<span class="vote">0</span>
|
||||
<span class="up">+</span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function Voter(options) {
|
||||
var self = this;
|
||||
var elem = options.elem;
|
||||
|
||||
var voteElem = elem.find('.vote');
|
||||
|
||||
// вынесем текущее значение в отдельную переменную
|
||||
var vote = options.value || 0;
|
||||
|
||||
// установить значение без генерации события
|
||||
setVote(vote, true);
|
||||
|
||||
elem.on('click', '.down', onDownClick)
|
||||
.on('click', '.up', onUpClick)
|
||||
.on('mousedown selectstart', false);
|
||||
|
||||
// ----------- методы -------------
|
||||
|
||||
function onDownClick() {
|
||||
// сам обработчик не меняет голос, он вызывает для этого функцию
|
||||
voteDecrease();
|
||||
}
|
||||
|
||||
function onUpClick() {
|
||||
voteIncrease();
|
||||
}
|
||||
|
||||
function voteDecrease() {
|
||||
self.setVote( vote - 1 );
|
||||
}
|
||||
|
||||
function voteIncrease() {
|
||||
self.setVote( vote + 1 );
|
||||
}
|
||||
|
||||
// сделали функцию приватной, чтобы её можно было вызывать до объявления
|
||||
// в коде инициализации
|
||||
function setVote(newVote, quiet) {
|
||||
vote = newVote;
|
||||
|
||||
voteElem.html( vote );
|
||||
|
||||
if (!quiet) {
|
||||
$(self).triggerHandler({
|
||||
type: 'change',
|
||||
value: vote
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.setVote = setVote;
|
||||
|
||||
}
|
||||
|
||||
var voter = new Voter({
|
||||
elem: $('#voter'),
|
||||
value: 5
|
||||
});
|
||||
|
||||
$(voter).on('change', function(e) {
|
||||
alert(e.value);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1 @@
|
|||
[edit src="solution"/]
|
1
02-ui/05-widgets/05-custom-events/01-voter-events/solution/.plnkr
Executable file
1
02-ui/05-widgets/05-custom-events/01-voter-events/solution/.plnkr
Executable file
|
@ -0,0 +1 @@
|
|||
{"name":"voter-event","plunk":"uRILPlo0yZNmQoC93SlM"}
|
94
02-ui/05-widgets/05-custom-events/01-voter-events/solution/index.html
Executable file
94
02-ui/05-widgets/05-custom-events/01-voter-events/solution/index.html
Executable file
|
@ -0,0 +1,94 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<style>
|
||||
.voter {
|
||||
font-family: Consolas, "Lucida Console", monospace;
|
||||
font-size: 18px;
|
||||
}
|
||||
.up, .down {
|
||||
cursor: pointer;
|
||||
color: blue;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="voter" class="voter">
|
||||
<span class="down">—</span>
|
||||
<span class="vote">0</span>
|
||||
<span class="up">+</span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function Voter(options) {
|
||||
var self = this;
|
||||
var elem = options.elem;
|
||||
|
||||
var voteElem = elem.find('.vote');
|
||||
|
||||
// вынесем текущее значение в отдельную переменную
|
||||
var vote = options.value || 0;
|
||||
|
||||
// установить значение без генерации события
|
||||
setVote(vote, true);
|
||||
|
||||
elem.on('click', '.down', onDownClick)
|
||||
.on('click', '.up', onUpClick)
|
||||
.on('mousedown selectstart', false);
|
||||
|
||||
// ----------- методы -------------
|
||||
|
||||
function onDownClick() {
|
||||
// сам обработчик не меняет голос, он вызывает для этого функцию
|
||||
voteDecrease();
|
||||
}
|
||||
|
||||
function onUpClick() {
|
||||
voteIncrease();
|
||||
}
|
||||
|
||||
function voteDecrease() {
|
||||
self.setVote( vote - 1 );
|
||||
}
|
||||
|
||||
function voteIncrease() {
|
||||
self.setVote( vote + 1 );
|
||||
}
|
||||
|
||||
// сделали функцию приватной, чтобы её можно было вызывать до объявления
|
||||
// в коде инициализации
|
||||
function setVote(newVote, quiet) {
|
||||
vote = newVote;
|
||||
|
||||
voteElem.html( vote );
|
||||
|
||||
if (!quiet) {
|
||||
$(self).triggerHandler({
|
||||
type: 'change',
|
||||
value: vote
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.setVote = setVote;
|
||||
|
||||
}
|
||||
|
||||
var voter = new Voter({
|
||||
elem: $('#voter'),
|
||||
value: 5
|
||||
});
|
||||
|
||||
$(voter).on('change', function(e) {
|
||||
alert(e.value);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
25
02-ui/05-widgets/05-custom-events/01-voter-events/task.md
Normal file
25
02-ui/05-widgets/05-custom-events/01-voter-events/task.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Голосовалка "на событиях"
|
||||
|
||||
[importance 5]
|
||||
|
||||
Добавьте событие в голосовалку, созданную в задаче [](/task/voter), используя jQuery-механизм генерации событий на объекте.
|
||||
|
||||
Пусть каждое изменение голоса сопровождается событием `change`:
|
||||
|
||||
```js
|
||||
var voter = new Voter({
|
||||
elem: $('#voter'),
|
||||
value: 5
|
||||
});
|
||||
|
||||
$(voter).on('change', function(e) {
|
||||
alert(e.value);
|
||||
});
|
||||
```
|
||||
|
||||
Все изменения голоса должны производиться централизованно, через метод `setVote`, который и генерирует событие.
|
||||
|
||||
Результат использования кода выше (планируемый):
|
||||
[iframe border=1 height=60 src="index.html"].
|
||||
|
||||
Исходный документ возьмите из решения задачи [](/task/voter).
|
|
@ -0,0 +1,8 @@
|
|||
[edit src="solution"/]
|
||||
|
||||
Обратите внимание:
|
||||
<ul>
|
||||
<li>`onLiClick` не генерирует событие `select`. Это обработчик, его роль -- разобраться, что происходит, и передать работу нужным методам.</li>
|
||||
<li>В событие передаётся массив значений `value`. Код виджета производит всю работу по подготовке этого значения. Было бы совершенно недопустимо, хотя это проще, передавать массив выбранных `LI`. Вообще, элементы -- это внутреннее дело компонента, они могут измениться в любой момент, доступ к ним снаружи крайне нежелателен.</li>
|
||||
<li>Код получения значений вынесен в отдельную функцию `getValues` -- для чистоты (каждая функция делает свою работу), и потому что скорее всего она ещё где-то понадобится.</li>
|
||||
</ul>
|
|
@ -0,0 +1 @@
|
|||
{"name":"selectable-list-events","plunk":"GqD0Tve2xqcgr9cfFBJO"}
|
|
@ -0,0 +1,79 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<style>
|
||||
.selected {
|
||||
background: #0f0;
|
||||
}
|
||||
li {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Клик на элементе выделяет только его.<br>
|
||||
Shift+Клик добавляет/убирает элемент из выделенных.<br>
|
||||
|
||||
<ul>
|
||||
<li>Винни-Пух</li>
|
||||
<li>Ослик Иа</li>
|
||||
<li>Мудрая Сова</li>
|
||||
<li>Кролик. Просто кролик.</li>
|
||||
</ul>
|
||||
|
||||
<script>
|
||||
|
||||
function ListSelect(options) {
|
||||
var elem = options.elem;
|
||||
|
||||
var self = this;
|
||||
|
||||
elem.on('click', 'li', onLiClick);
|
||||
elem.on('selectstart mousedown', false);
|
||||
|
||||
function onLiClick(e) {
|
||||
if (!e.shiftKey) {
|
||||
deselectAllItems();
|
||||
}
|
||||
|
||||
toggleSelectItem( $(this) );
|
||||
}
|
||||
|
||||
function deselectAllItems() {
|
||||
elem.children().removeClass('selected');
|
||||
}
|
||||
|
||||
function getValue() {
|
||||
var value = [];
|
||||
|
||||
elem.children('.selected').each(function() {
|
||||
value.push( $(this).html() );
|
||||
});
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function toggleSelectItem(li) {
|
||||
li.toggleClass('selected');
|
||||
|
||||
$(self).triggerHandler({
|
||||
type: 'change',
|
||||
value: getValue()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var select = new ListSelect({
|
||||
elem: $('ul')
|
||||
});
|
||||
|
||||
$(select).on('change', function(e) {
|
||||
alert(e.value);
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
{"name":"selectable-list-events","plunk":"GqD0Tve2xqcgr9cfFBJO"}
|
|
@ -0,0 +1,79 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<style>
|
||||
.selected {
|
||||
background: #0f0;
|
||||
}
|
||||
li {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Клик на элементе выделяет только его.<br>
|
||||
Shift+Клик добавляет/убирает элемент из выделенных.<br>
|
||||
|
||||
<ul>
|
||||
<li>Винни-Пух</li>
|
||||
<li>Ослик Иа</li>
|
||||
<li>Мудрая Сова</li>
|
||||
<li>Кролик. Просто кролик.</li>
|
||||
</ul>
|
||||
|
||||
<script>
|
||||
|
||||
function ListSelect(options) {
|
||||
var elem = options.elem;
|
||||
|
||||
var self = this;
|
||||
|
||||
elem.on('click', 'li', onLiClick);
|
||||
elem.on('selectstart mousedown', false);
|
||||
|
||||
function onLiClick(e) {
|
||||
if (!e.shiftKey) {
|
||||
deselectAllItems();
|
||||
}
|
||||
|
||||
toggleSelectItem( $(this) );
|
||||
}
|
||||
|
||||
function deselectAllItems() {
|
||||
elem.children().removeClass('selected');
|
||||
}
|
||||
|
||||
function getValue() {
|
||||
var value = [];
|
||||
|
||||
elem.children('.selected').each(function() {
|
||||
value.push( $(this).html() );
|
||||
});
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function toggleSelectItem(li) {
|
||||
li.toggleClass('selected');
|
||||
|
||||
$(self).triggerHandler({
|
||||
type: 'change',
|
||||
value: getValue()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var select = new ListSelect({
|
||||
elem: $('ul')
|
||||
});
|
||||
|
||||
$(select).on('change', function(e) {
|
||||
alert(e.value);
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,12 @@
|
|||
# Список с выделением и событием
|
||||
|
||||
[importance 5]
|
||||
|
||||
Добавьте в решение задачи [](/task/selectable-list-component) событие `select`.
|
||||
|
||||
Оно должно срабатывать при каждом изменении выбора и содержать список выбранных строк.
|
||||
|
||||
Во внешнем коде добавьте обработчик к списку, который при изменениях выводит список значений.
|
||||
[iframe border="1" src="solution"]
|
||||
|
||||
В качестве исходного кода возьмите решение задачи [](/task/selectable-list-component).
|
|
@ -0,0 +1,9 @@
|
|||
В этом решении для закрытия селекта по клику вне него используется отслеживание произвольных кликов вне документа.
|
||||
|
||||
Альтернатива -- события `focusin/focusout`, т.е. считаем, что пока фокус в селекте -- он открыт. С одной стороны, это более мощный способ, он позволяет перемещаться по элементам управления при помощи [key Tab] и корректно обрабатывать уход при помощи клавиатуры.
|
||||
|
||||
С другой стороны, это решение не универсально: если выводится `alert`, то фокус "прыгает" в него, уходя с элемента, а потом возвращается обратно. При этом JavaScript зарегистрирует уход фокуса `focusout` и возвращение `focusin`, хотя по смыслу фокус с элемента никуда не уходил, просто был `alert`.
|
||||
|
||||
Побочный эффект -- к закрытию и раскрытию (лишнему) элемента управления при таких "ненамеренных" потерях фокуса. Поэтому был выбран `onclick`.
|
||||
|
||||
Решение: [edit src="solution"]Открыть в песочнице[/edit]
|
1
02-ui/05-widgets/05-custom-events/03-custom-select/solution/.plnkr
Executable file
1
02-ui/05-widgets/05-custom-events/03-custom-select/solution/.plnkr
Executable file
|
@ -0,0 +1 @@
|
|||
{"name":"customselect","plunk":"NSMq0hilrebQaa5zU0w6"}
|
47
02-ui/05-widgets/05-custom-events/03-custom-select/solution/customselect.css
Executable file
47
02-ui/05-widgets/05-custom-events/03-custom-select/solution/customselect.css
Executable file
|
@ -0,0 +1,47 @@
|
|||
.customselect {
|
||||
width: 200px;
|
||||
font-size: 14px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.customselect-title {
|
||||
height: 17px;
|
||||
border: 1px solid #ADD8E6;
|
||||
background-position: right;
|
||||
background-image: url(http://js.cx/clipart/select-button.gif);
|
||||
background-repeat: no-repeat;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.customselect-options li {
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.customselect-options li:nth-child(even) {
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
.customselect-options li:hover {
|
||||
background-color: #7fffd4;
|
||||
}
|
||||
|
||||
.customselect-options {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: none;
|
||||
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
width: 200px;
|
||||
border-bottom: 1px solid #add8e6;
|
||||
border-left: 1px solid #add8e6;
|
||||
border-right: 1px solid #add8e6;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.customselect-open .customselect-options {
|
||||
display: block;
|
||||
}
|
61
02-ui/05-widgets/05-custom-events/03-custom-select/solution/customselect.js
Executable file
61
02-ui/05-widgets/05-custom-events/03-custom-select/solution/customselect.js
Executable file
|
@ -0,0 +1,61 @@
|
|||
function CustomSelect(options) {
|
||||
var self = this;
|
||||
|
||||
var elem = options.elem;
|
||||
|
||||
elem.on('click', '.customselect-title', onTitleClick);
|
||||
elem.on('click', 'li', onOptionClick);
|
||||
|
||||
var isOpen = false;
|
||||
|
||||
// ------ обработчики ------
|
||||
|
||||
function onTitleClick(event) {
|
||||
toggle();
|
||||
}
|
||||
|
||||
// закрыть селект, если клик вне его
|
||||
function onDocumentClick(event) {
|
||||
var isInside = $(event.target).closest(elem).length;
|
||||
if (!isInside) close();
|
||||
}
|
||||
|
||||
function onOptionClick(event) {
|
||||
close();
|
||||
|
||||
var name = $(event.target).html();
|
||||
var value = $(event.target).data('value');
|
||||
|
||||
setValue(name, value);
|
||||
}
|
||||
|
||||
// ------------------------
|
||||
|
||||
function setValue(name, value) {
|
||||
elem.find('.customselect-title').html(name);
|
||||
|
||||
$(self).triggerHandler({
|
||||
type: 'select',
|
||||
name: name,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (isOpen) close()
|
||||
else open();
|
||||
}
|
||||
|
||||
function open() {
|
||||
elem.addClass('customselect-open');
|
||||
$(document).on('click', onDocumentClick);
|
||||
isOpen = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
elem.removeClass('customselect-open');
|
||||
$(document).off('click', onDocumentClick);
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{"name":"customselect","plunk":"NSMq0hilrebQaa5zU0w6"}
|
|
@ -0,0 +1,47 @@
|
|||
.customselect {
|
||||
width: 200px;
|
||||
font-size: 14px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.customselect-title {
|
||||
height: 17px;
|
||||
border: 1px solid #ADD8E6;
|
||||
background-position: right;
|
||||
background-image: url(http://js.cx/clipart/select-button.gif);
|
||||
background-repeat: no-repeat;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.customselect-options li {
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.customselect-options li:nth-child(even) {
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
.customselect-options li:hover {
|
||||
background-color: #7fffd4;
|
||||
}
|
||||
|
||||
.customselect-options {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: none;
|
||||
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
width: 200px;
|
||||
border-bottom: 1px solid #add8e6;
|
||||
border-left: 1px solid #add8e6;
|
||||
border-right: 1px solid #add8e6;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.customselect-open .customselect-options {
|
||||
display: block;
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
function CustomSelect(options) {
|
||||
var self = this;
|
||||
|
||||
var elem = options.elem;
|
||||
|
||||
elem.on('click', '.customselect-title', onTitleClick);
|
||||
elem.on('click', 'li', onOptionClick);
|
||||
|
||||
var isOpen = false;
|
||||
|
||||
// ------ обработчики ------
|
||||
|
||||
function onTitleClick(event) {
|
||||
toggle();
|
||||
}
|
||||
|
||||
// закрыть селект, если клик вне его
|
||||
function onDocumentClick(event) {
|
||||
var isInside = $(event.target).closest(elem).length;
|
||||
if (!isInside) close();
|
||||
}
|
||||
|
||||
function onOptionClick(event) {
|
||||
close();
|
||||
|
||||
var name = $(event.target).html();
|
||||
var value = $(event.target).data('value');
|
||||
|
||||
setValue(name, value);
|
||||
}
|
||||
|
||||
// ------------------------
|
||||
|
||||
function setValue(name, value) {
|
||||
elem.find('.customselect-title').html(name);
|
||||
|
||||
$(self).triggerHandler({
|
||||
type: 'select',
|
||||
name: name,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (isOpen) close()
|
||||
else open();
|
||||
}
|
||||
|
||||
function open() {
|
||||
elem.addClass('customselect-open');
|
||||
$(document).on('click', onDocumentClick);
|
||||
isOpen = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
elem.removeClass('customselect-open');
|
||||
$(document).off('click', onDocumentClick);
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Селект</title>
|
||||
<script src='http://code.jquery.com/jquery.min.js'></script>
|
||||
<link rel="stylesheet" href="customselect.css"/>
|
||||
<script src="customselect.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div>Последний результат: <span id="result">...</span></div>
|
||||
|
||||
<div id="animal-select" class="customselect">
|
||||
<div class="customselect-title">Выберите</div>
|
||||
<ol class="customselect-options">
|
||||
<!-- значение хранится в свойстве data-value -->
|
||||
<li data-value="bird">Птицы</li>
|
||||
<li data-value="fish">Рыбы</li>
|
||||
<li data-value="animal">Звери</li>
|
||||
<li data-value="dino">Динозавры</li>
|
||||
<li data-value="simplest">Одноклеточные</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div id="food-select" class="customselect">
|
||||
<div class="customselect-title">Выберите</div>
|
||||
<ol class="customselect-options">
|
||||
<li data-value="carnivore">Плотоядные</li>
|
||||
<li data-value="herbivore">Травоядные</li>
|
||||
<li data-value="omnivore">Всеядные</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
var animalSelect = new CustomSelect({
|
||||
elem: $('#animal-select')
|
||||
});
|
||||
|
||||
var foodSelect = new CustomSelect({
|
||||
elem: $('#food-select')
|
||||
});
|
||||
|
||||
$([animalSelect, foodSelect]).on('select', function (event) {
|
||||
$('#result').html(event.value)
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
51
02-ui/05-widgets/05-custom-events/03-custom-select/solution/index.html
Executable file
51
02-ui/05-widgets/05-custom-events/03-custom-select/solution/index.html
Executable file
|
@ -0,0 +1,51 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Селект</title>
|
||||
<script src='http://code.jquery.com/jquery.min.js'></script>
|
||||
<link rel="stylesheet" href="customselect.css"/>
|
||||
<script src="customselect.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div>Последний результат: <span id="result">...</span></div>
|
||||
|
||||
<div id="animal-select" class="customselect">
|
||||
<div class="customselect-title">Выберите</div>
|
||||
<ol class="customselect-options">
|
||||
<!-- значение хранится в свойстве data-value -->
|
||||
<li data-value="bird">Птицы</li>
|
||||
<li data-value="fish">Рыбы</li>
|
||||
<li data-value="animal">Звери</li>
|
||||
<li data-value="dino">Динозавры</li>
|
||||
<li data-value="simplest">Одноклеточные</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div id="food-select" class="customselect">
|
||||
<div class="customselect-title">Выберите</div>
|
||||
<ol class="customselect-options">
|
||||
<li data-value="carnivore">Плотоядные</li>
|
||||
<li data-value="herbivore">Травоядные</li>
|
||||
<li data-value="omnivore">Всеядные</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
var animalSelect = new CustomSelect({
|
||||
elem: $('#animal-select')
|
||||
});
|
||||
|
||||
var foodSelect = new CustomSelect({
|
||||
elem: $('#food-select')
|
||||
});
|
||||
|
||||
$([animalSelect, foodSelect]).on('select', function (event) {
|
||||
$('#result').html(event.value)
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
20
02-ui/05-widgets/05-custom-events/03-custom-select/task.md
Normal file
20
02-ui/05-widgets/05-custom-events/03-custom-select/task.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Свой селект
|
||||
|
||||
[importance 5]
|
||||
|
||||
Напишите свой, самостоятельно оформленный, селект.
|
||||
|
||||
Требования:
|
||||
<ul>
|
||||
<li>Открытие и закрытие по клику на заголовок.</li>
|
||||
<li>Закрытие селекта происходит при выборе или клике на любое другое место документа, в том числе на другой аналогичный селект.</li>
|
||||
<li>Событие `"select"` при выборе опции.</li>
|
||||
<li>Значение опции хранится в атрибуте `data-value` (HTML-структура есть в исходном документе).
|
||||
</ul>
|
||||
Например:
|
||||
|
||||
[iframe src="solution" border="1" height="200" newwin]
|
||||
|
||||
В примере выше два селекта, чтобы можно было проверить процесс открытия-закрытия.
|
||||
|
||||
[edit src="task" task/]
|
1
02-ui/05-widgets/05-custom-events/03-custom-select/task/.plnkr
Executable file
1
02-ui/05-widgets/05-custom-events/03-custom-select/task/.plnkr
Executable file
|
@ -0,0 +1 @@
|
|||
{"name":"customselect-src","plunk":"cCX1mrKxjKiSibESyEaZ"}
|
55
02-ui/05-widgets/05-custom-events/03-custom-select/task/index.html
Executable file
55
02-ui/05-widgets/05-custom-events/03-custom-select/task/index.html
Executable file
|
@ -0,0 +1,55 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Селект</title>
|
||||
<script src='http://code.jquery.com/jquery.min.js'></script>
|
||||
<link rel="stylesheet" href="customselect.css"/>
|
||||
<script src="customselect.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div>Последний результат: <span id="result">...</span></div>
|
||||
|
||||
<img src="http://js.cx/clipart/select-button.gif">
|
||||
|
||||
<div id="animal-select" class="customselect">
|
||||
<div class="customselect-title">Выберите</div>
|
||||
<ol class="customselect-options">
|
||||
<!-- значение хранится в свойстве data-value -->
|
||||
<li data-value="bird">Птицы</li>
|
||||
<li data-value="fish">Рыбы</li>
|
||||
<li data-value="animal">Звери</li>
|
||||
<li data-value="dino">Динозавры</li>
|
||||
<li data-value="simplest">Одноклеточные</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div id="food-select" class="customselect">
|
||||
<div class="customselect-title">Выберите</div>
|
||||
<ol class="customselect-options">
|
||||
<li data-value="carnivore">Плотоядные</li>
|
||||
<li data-value="herbivore">Травоядные</li>
|
||||
<li data-value="omnivore">Всеядные</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
// создаём два селекта
|
||||
var animalSelect = new CustomSelect({
|
||||
elem: $('#animal-select')
|
||||
});
|
||||
|
||||
var foodSelect = new CustomSelect({
|
||||
elem: $('#food-select')
|
||||
});
|
||||
|
||||
// выводим выбранное значение
|
||||
$([animalSelect, foodSelect]).on('select', function (event) {
|
||||
$('#result').html(event.value)
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
354
02-ui/05-widgets/05-custom-events/article.md
Normal file
354
02-ui/05-widgets/05-custom-events/article.md
Normal file
|
@ -0,0 +1,354 @@
|
|||
# Коллбэки и события на компонентах
|
||||
|
||||
Компоненты, хоть и каждый сам по себе, обычно интегрированы друг с другом.
|
||||
|
||||
Есть несколько способов, при помощи которых компоненты сообщают друг другу о важных событиях, которые в них произошли.
|
||||
|
||||
[cut]
|
||||
|
||||
## Коллбэки
|
||||
|
||||
Коллбэк (от англ. callback) -- это функция, которую мы передаём куда-либо и ожидаем, что она будет вызвана при наступлении события.
|
||||
|
||||
Например, мы можем добавить в `options` для `Menu` новый параметр -- функцию `onselect`, которая будет вызываться при выборе пункта меню:
|
||||
|
||||
```js
|
||||
var menu = new Menu({
|
||||
title: "Сладости",
|
||||
template: _.template($('#menu-template').html()),
|
||||
listTemplate: _.template($('#menu-list-template').html()),
|
||||
items: {
|
||||
"donut": "Пончик",
|
||||
"cake": "Пирожное",
|
||||
"chocolate": "Шоколадка"
|
||||
},
|
||||
*!*
|
||||
onselect: showSelected
|
||||
*/!*
|
||||
});
|
||||
|
||||
*!*
|
||||
function showSelected(href) {
|
||||
alert(href);
|
||||
}
|
||||
*/!*
|
||||
```
|
||||
|
||||
В коде меню нужно будет вызывать её, как-то так:
|
||||
|
||||
```js
|
||||
...
|
||||
function onItemClick(e) {
|
||||
e.preventDefault();
|
||||
*!*
|
||||
var onselect = options.onselect;
|
||||
if (onselect) {
|
||||
onselect(e.currentTarget.getAttribute('href').slice(1));
|
||||
}
|
||||
*/!*
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
Демо:
|
||||
|
||||
[example src="menu-callback" height="180"]
|
||||
|
||||
## Свои события
|
||||
|
||||
Оповещение через коллбэки -- это примерный аналог назначения обработчика через `onсвойство` для DOM-элементов.
|
||||
|
||||
Да, он работает, но чтобы можно было назначать сколько угодно обработчиков в любой момент, нужна полноценная поддержка событий, то есть аналог `addEventListener`.
|
||||
|
||||
Поддержка событий для компонентов будет выглядеть так:
|
||||
|
||||
```js
|
||||
var menu = new Menu(...);
|
||||
|
||||
// любой код может подписаться на событие "select"
|
||||
menu.on("select", function(item) {
|
||||
// при выборе пункта меню сработает этот обработчик
|
||||
alert("Выбран элемент " + item);
|
||||
});
|
||||
```
|
||||
|
||||
Код, который заинтересован в том, чтобы узнавать, что происходит с меню, подписывается на нужные события вызовом `menu.on(имя события, обработчик)`.
|
||||
|
||||
Далее `Menu` при наступлении события вызывает обработчик методом `trigger`, при необходимости передавая ему важные данные в качестве аргументов, вот так:
|
||||
|
||||
```js
|
||||
function Menu(options) {
|
||||
var self = this;
|
||||
|
||||
*!*
|
||||
// ... при клике на item - сгенерировать событие (trigger)
|
||||
function onItemClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
self.trigger("select", e.currentTarget.getAttribute('href').slice(1));
|
||||
}
|
||||
*/!*
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Обратим внимание -- события будут генерироваться не на элементе `<div class="menu">`, а на объекте `new Menu`. Сейчас мы разберём методы, которые для этого нужны.
|
||||
|
||||
## Методы on, off и trigger
|
||||
|
||||
Для поддержки событий в любой объект достаточно добавить три метода: `on`, `off` и `trigger`, которые перечислены ниже в свойствах `EventMixin`:
|
||||
|
||||
```js
|
||||
var EventMixin = {
|
||||
|
||||
/**
|
||||
* Подписка на событие
|
||||
* Использование:
|
||||
* menu.on('select', function(item) { ... }
|
||||
*/
|
||||
on: function(eventName, handler) {
|
||||
if (!this._eventHandlers) this._eventHandlers = {};
|
||||
if (!this._eventHandlers[eventName]) {
|
||||
this._eventHandlers[eventName] = [];
|
||||
}
|
||||
this._eventHandlers[eventName].push(handler);
|
||||
},
|
||||
|
||||
/**
|
||||
* Прекращение подписки
|
||||
* menu.off('select', handler)
|
||||
*/
|
||||
off: function(eventName, handler) {
|
||||
var handlers = this._eventHandlers && this._eventHandlers[eventName];
|
||||
if (!handlers) return;
|
||||
for(var i=0; i<handlers.length; i++) {
|
||||
if (handlers[i] == handler) {
|
||||
handlers.splice(i--, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Генерация события с передачей данных
|
||||
* this.trigger('select', item);
|
||||
*/
|
||||
trigger: function(eventName) {
|
||||
|
||||
if (!this._eventHandlers || !this._eventHandlers[eventName]) {
|
||||
return; // обработчиков для события нет
|
||||
}
|
||||
|
||||
// вызвать обработчики
|
||||
var handlers = this._eventHandlers[eventName];
|
||||
for (var i = 0; i < handlers.length; i++) {
|
||||
handlers[i].apply(this, [].slice.call(arguments, 1));
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Объект `EventMixin` -- всего лишь хранилище для функций, которые нужно скопировать в тот объект, которому нужны события.
|
||||
|
||||
Пример использования:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var obj = {}; // произвольный объект
|
||||
|
||||
*!*
|
||||
// скопируем в него методы для работы с событиями
|
||||
*/!*
|
||||
for(var m in EventMixin) obj[m] = EventMixin[m];
|
||||
|
||||
*!*
|
||||
// поставить обработчик на событие hello
|
||||
*/!*
|
||||
obj.on("hello", function(a, b, c) {
|
||||
alert(a + ", " + b + ", " + c);
|
||||
});
|
||||
|
||||
// и ещё один обработчик
|
||||
obj.on("hello", function() {
|
||||
alert("привет!");
|
||||
});
|
||||
|
||||
*!*
|
||||
// генерация события с дополнительными данными
|
||||
// обычно происходит внутри методов объекта
|
||||
*/!*
|
||||
obj.trigger("hello", "data1", "data2", "data3");
|
||||
```
|
||||
|
||||
При запуске этого кода будет поставлено два обработчика на событие `hello`, которые (по очереди) будут вызваны при `obj.trigger(...)`.
|
||||
|
||||
[smart header="Внутренний алгоритм `on/off/trigger`"]
|
||||
Большинство фреймворков, предоставляющих события на произвольных объектах, работают примерно так же, как `on/off/trigger` в примере выше.
|
||||
|
||||
**Все обработчики событий `obj` хранятся в "скрытом" свойстве самого `obj`.**
|
||||
<ul>
|
||||
<li>Вызов `obj.on(event, handler)` создаёт свойство `obj._eventHandlers`, если его ещё нет, и сохраняет в нём обработчик: `obj._eventHandlers[event].push(handler)`.</li>
|
||||
<li>Далее вызов `obj.trigger(event, ...)` получает обработчики из `obj._eventHandlers[event]` и выполняет их один за другим.</li>
|
||||
<li>Вызов `obj.off(event, handler)` удаляет обработчик из `obj._eventHandlers`.</li>
|
||||
</ul>
|
||||
[/smart]
|
||||
|
||||
## Меню с событиями
|
||||
|
||||
Для перехода на события в меню, нужно:
|
||||
<ol>
|
||||
<li>Добавить методы из `EventMixin` в объект меню. Их можно скопировать либо в объект, либо в прототип.</li>
|
||||
<li>Вместо вызова коллбэка `onselect` -- генерируем событие вызовом `trigger('select', значение)`.</li>
|
||||
</ol>
|
||||
|
||||
Код:
|
||||
|
||||
```js
|
||||
function Menu(options) {
|
||||
var elem;
|
||||
var self = this;
|
||||
|
||||
*!*
|
||||
for(var method in EventMixin) {
|
||||
this[method] = EventMixin[method]
|
||||
}
|
||||
*/!*
|
||||
|
||||
function onItemClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
*!*
|
||||
self.trigger("select", currentTarget.getAttribute('href').slice(1));
|
||||
*/!*
|
||||
}
|
||||
|
||||
// другие методы без изменений
|
||||
}
|
||||
```
|
||||
|
||||
Во внешнем коде:
|
||||
|
||||
```js
|
||||
var menu = new Menu(...)
|
||||
menu.on('select', function(value) {
|
||||
alert('выбрано значение ' + value);
|
||||
});
|
||||
```
|
||||
|
||||
Результат:
|
||||
[example src="menu-event"]
|
||||
|
||||
## События при помощи jQuery
|
||||
|
||||
Фреймворк jQuery предоставляет свои средства для генерации произвольных событий.
|
||||
|
||||
Метод [trigger](http://api.jquery.com/trigger/) позволяет генерировать любое событие на элементе и он же -- работает на любом объекте, "обёрнутом" в jQuery: `$(...)`.
|
||||
|
||||
Синтаксис для генерации события на элементе:
|
||||
|
||||
```js
|
||||
elem.trigger(event);
|
||||
```
|
||||
|
||||
Объект `event` должен содержать свойство `type` -- тип события (произвольный) и любые другие свойства, которые будут переданы обработчикам.
|
||||
|
||||
Например:
|
||||
|
||||
```js
|
||||
$('body').on('hello', function(e) {
|
||||
alert(e.user)
|
||||
});
|
||||
|
||||
*!*
|
||||
$('body').trigger({
|
||||
type: 'hello', // тип события
|
||||
user: 'Вася', // любые данные
|
||||
});
|
||||
*/!*
|
||||
|
||||
$('body').off('hello');
|
||||
```
|
||||
|
||||
Есть также альтернативный синтаксис:
|
||||
|
||||
```js
|
||||
elem.trigger(eventType, args);
|
||||
```
|
||||
|
||||
<ul>
|
||||
<li>`eventType` -- тип события (строка)</li>
|
||||
<li>`args` -- массив аргументов, которые будут переданы обработчику.</li>
|
||||
</ul>
|
||||
|
||||
Пример использования:
|
||||
|
||||
```js
|
||||
$('body').on('hello', function(e, user) {
|
||||
alert(user)
|
||||
});
|
||||
|
||||
*!*
|
||||
// аргументы - в виде массива
|
||||
$('body').trigger("hello", ["Вася"]);
|
||||
*/!*
|
||||
|
||||
$('body').off('hello');
|
||||
```
|
||||
|
||||
Как правильно, предпочтителен первый синтаксис: `elem.trigger(event)`, поскольку он гораздо гибче, можно указывать любые свойства в любом порядке.
|
||||
|
||||
События, сгенерированные таким образом на элементах, всплывают и обрабатываются jQuery наравне с обычными браузерными событиями.
|
||||
|
||||
**Для объекта всё точно так же, как и для элемента:**
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var obj = {}; // произвольный объект
|
||||
|
||||
$(obj).on('hello', function(e) {
|
||||
alert(e.user)
|
||||
});
|
||||
|
||||
$(obj).trigger({
|
||||
type: 'hello',
|
||||
user: 'Вася'
|
||||
});
|
||||
```
|
||||
|
||||
Конечно, события на объектах не всплывают, всё-таки "всплытие" -- это привилегия DOM.
|
||||
|
||||
Внутри jQuery делает в точности то же самое, что `EventMixin` -- вызов `on(...)` добавляет обработчик добавляются в "скрытое" свойство объекта, из которого они потом читаются и выполняются вызовом `trigger(...)`.
|
||||
|
||||
Это свойство можно легко увидеть:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var obj = {};
|
||||
|
||||
$(obj).on('hello', function(e) { /* ... */ });
|
||||
|
||||
// свойство называется примерно так: jQuery19105266267273109406
|
||||
// оно имеет такое псевдослучайное название,
|
||||
// чтобы не перезаписать "настоящие", важные свойства объекта
|
||||
for(var prop in obj) alert(prop);
|
||||
```
|
||||
|
||||
Технически, можно генерировать событие и на корневом элементе меню `<div class="menu">` и на JavaScript-объекте `new Menu` (`this` внутри конструктора). Однако, обычно используют именно объект: считается, что DOM компонента -- это его сугубо личное дело. Он может в любой момент поменяться и быть пересоздан "с нуля", если потребуется. Залезать в него снаружи и ставить какие-то обработчики никто не имеет права.
|
||||
|
||||
|
||||
## Итого
|
||||
|
||||
Для того, чтобы внешний код мог узнавать о важных событиях, произошедших внутри компоненты, используются:
|
||||
<ul>
|
||||
<li>Коллбэки -- функции, которые передаются "снаружи" при создании компонента, и которые он обязуется вызвать при наступлении событий.</li>
|
||||
<li>События -- компонент генерирует их при помощи вызова `trigger` (`EventMixin`, jQuery или другой фреймворк), а внешний код ставит обработчики при помощи `on`.</li>
|
||||
</ul>
|
||||
|
||||
Можно использовать и то и другое одновременно. Например, виджеты фреймворка jQuery UI позволяют передать при создании коллбэк `onselect`, и вместе с тем генерируют событие.
|
||||
|
||||
Далее, для простоты, в примерах и задачах мы будем использовать для событий методы `on/off/trigger` из jQuery. Если jQuery не нужен -- всегда можно использовать `EventMixin` или какой-либо другой фреймворк.
|
||||
|
||||
|
||||
[libs]
|
||||
event-mixin.js
|
||||
[/libs]
|
1
02-ui/05-widgets/05-custom-events/menu-callback/.plnkr
Executable file
1
02-ui/05-widgets/05-custom-events/menu-callback/.plnkr
Executable file
|
@ -0,0 +1 @@
|
|||
{"name":"menu-callback","plunk":"vucl0s95kLWICLKw59gR"}
|
48
02-ui/05-widgets/05-custom-events/menu-callback/index.html
Executable file
48
02-ui/05-widgets/05-custom-events/menu-callback/index.html
Executable file
|
@ -0,0 +1,48 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="http://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.js"></script>
|
||||
<script src="menu.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script type="text/template" id="menu-template">
|
||||
<div class="menu">
|
||||
<span class="title"><%-title%></span>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="menu-list-template">
|
||||
<ul>
|
||||
<% for(var key in items) { %>
|
||||
<li><a href="#<%-key%>"><%-items[key]%></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</script>
|
||||
|
||||
<script>
|
||||
var menu = new Menu({
|
||||
title: "Сладости",
|
||||
template: _.template($('#menu-template').html()),
|
||||
listTemplate: _.template($('#menu-list-template').html()),
|
||||
items: {
|
||||
"donut": "Пончик",
|
||||
"cake": "Пирожное",
|
||||
"chocolate": "Шоколадка"
|
||||
},
|
||||
onselect: showSelected
|
||||
});
|
||||
|
||||
function showSelected(href) {
|
||||
alert(href);
|
||||
}
|
||||
|
||||
$(document.body).append(menu.getElem());
|
||||
menu.open();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
59
02-ui/05-widgets/05-custom-events/menu-callback/menu.js
Executable file
59
02-ui/05-widgets/05-custom-events/menu-callback/menu.js
Executable file
|
@ -0,0 +1,59 @@
|
|||
function Menu(options) {
|
||||
var elem;
|
||||
|
||||
function getElem() {
|
||||
if (!elem) render();
|
||||
return elem;
|
||||
}
|
||||
|
||||
function render() {
|
||||
var elemHtml = options.template({title: options.title});
|
||||
|
||||
elem = $(elemHtml);
|
||||
|
||||
elem.on('mousedown selectstart', false);
|
||||
|
||||
elem.on('click', '.title', onTitleClick);
|
||||
elem.on('click', 'a', onItemClick)
|
||||
}
|
||||
|
||||
|
||||
function renderItems() {
|
||||
if (elem.find('ul').length) return;
|
||||
|
||||
var listHtml = options.listTemplate({items: options.items});
|
||||
elem.append(listHtml);
|
||||
}
|
||||
|
||||
function onItemClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var onselect = options.onselect;
|
||||
if (onselect) {
|
||||
onselect(e.currentTarget.getAttribute('href').slice(1));
|
||||
}
|
||||
}
|
||||
|
||||
function onTitleClick(e) {
|
||||
toggle();
|
||||
}
|
||||
|
||||
function open() {
|
||||
renderItems();
|
||||
elem.addClass('open');
|
||||
};
|
||||
|
||||
function close() {
|
||||
elem.removeClass('open');
|
||||
};
|
||||
|
||||
function toggle() {
|
||||
if (elem.hasClass('open')) close();
|
||||
else open();
|
||||
};
|
||||
|
||||
this.getElem = getElem;
|
||||
this.toggle = toggle;
|
||||
this.close = close;
|
||||
this.open = open;
|
||||
}
|
20
02-ui/05-widgets/05-custom-events/menu-callback/style.css
Executable file
20
02-ui/05-widgets/05-custom-events/menu-callback/style.css
Executable file
|
@ -0,0 +1,20 @@
|
|||
|
||||
.menu ul {
|
||||
display: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu .title {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
background: url(http://js.cx/clipart/arrow-right.png) left center no-repeat;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.menu.open ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.menu.open .title {
|
||||
background-image: url(http://js.cx/clipart/arrow-down.png);
|
||||
}
|
1
02-ui/05-widgets/05-custom-events/menu-event/.plnkr
Executable file
1
02-ui/05-widgets/05-custom-events/menu-event/.plnkr
Executable file
|
@ -0,0 +1 @@
|
|||
{"name":"menu-event","plunk":"JHiJOzvcuurjetQqUvDS"}
|
48
02-ui/05-widgets/05-custom-events/menu-event/eventMixin.js
Executable file
48
02-ui/05-widgets/05-custom-events/menu-event/eventMixin.js
Executable file
|
@ -0,0 +1,48 @@
|
|||
var EventMixin = {
|
||||
|
||||
/**
|
||||
* Подписка на событие
|
||||
* Использование:
|
||||
* menu.on('select', function(item) { ... }
|
||||
*/
|
||||
on: function(eventName, handler) {
|
||||
if (!this._eventHandlers) this._eventHandlers = {};
|
||||
if (!this._eventHandlers[eventName]) {
|
||||
this._eventHandlers[eventName] = [];
|
||||
}
|
||||
this._eventHandlers[eventName].push(handler);
|
||||
},
|
||||
|
||||
/**
|
||||
* Прекращение подписки
|
||||
* menu.off('select', handler)
|
||||
*/
|
||||
off: function(eventName, handler) {
|
||||
var handlers = this._eventHandlers && this._eventHandlers[eventName];
|
||||
if (!handlers) return;
|
||||
for(var i=0; i<handlers.length; i++) {
|
||||
if (handlers[i] == handler) {
|
||||
handlers.splice(i--, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Генерация события с передачей данных
|
||||
* this.trigger('select', item);
|
||||
*/
|
||||
trigger: function(eventName) {
|
||||
|
||||
if (!this._eventHandlers || !this._eventHandlers[eventName]) {
|
||||
return; // обработчиков для события нет
|
||||
}
|
||||
|
||||
// вызвать обработчики
|
||||
var handlers = this._eventHandlers[eventName];
|
||||
for (var i = 0; i < handlers.length; i++) {
|
||||
handlers[i].apply(this, [].slice.call(arguments, 1));
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
48
02-ui/05-widgets/05-custom-events/menu-event/index.html
Executable file
48
02-ui/05-widgets/05-custom-events/menu-event/index.html
Executable file
|
@ -0,0 +1,48 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="http://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.js"></script>
|
||||
<script src="eventMixin.js"></script>
|
||||
<script src="menu.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script type="text/template" id="menu-template">
|
||||
<div class="menu">
|
||||
<span class="title"><%-title%></span>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="menu-list-template">
|
||||
<ul>
|
||||
<% for(var key in items) { %>
|
||||
<li><a href="#<%-key%>"><%-items[key]%></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</script>
|
||||
|
||||
<script>
|
||||
var menu = new Menu({
|
||||
title: "Сладости",
|
||||
template: _.template($('#menu-template').html()),
|
||||
listTemplate: _.template($('#menu-list-template').html()),
|
||||
items: {
|
||||
"donut": "Пончик",
|
||||
"cake": "Пирожное",
|
||||
"chocolate": "Шоколадка"
|
||||
}
|
||||
});
|
||||
|
||||
$(document.body).append(menu.getElem());
|
||||
menu.open();
|
||||
|
||||
menu.on('select', function(value) {
|
||||
alert('выбрано значение ' + value);
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
61
02-ui/05-widgets/05-custom-events/menu-event/menu.js
Executable file
61
02-ui/05-widgets/05-custom-events/menu-event/menu.js
Executable file
|
@ -0,0 +1,61 @@
|
|||
function Menu(options) {
|
||||
var elem;
|
||||
var self = this;
|
||||
|
||||
for(var method in EventMixin) {
|
||||
this[method] = EventMixin[method]
|
||||
}
|
||||
|
||||
function getElem() {
|
||||
if (!elem) render();
|
||||
return elem;
|
||||
}
|
||||
|
||||
function render() {
|
||||
var elemHtml = options.template({title: options.title});
|
||||
|
||||
elem = $(elemHtml);
|
||||
|
||||
elem.on('mousedown selectstart', false);
|
||||
|
||||
elem.on('click', '.title', onTitleClick);
|
||||
elem.on('click', 'a', onItemClick)
|
||||
}
|
||||
|
||||
|
||||
function renderItems() {
|
||||
if (elem.find('ul').length) return;
|
||||
|
||||
var listHtml = options.listTemplate({items: options.items});
|
||||
elem.append(listHtml);
|
||||
}
|
||||
|
||||
function onItemClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
self.trigger('select', e.currentTarget.getAttribute('href').slice(1));
|
||||
}
|
||||
|
||||
function onTitleClick(e) {
|
||||
toggle();
|
||||
}
|
||||
|
||||
function open() {
|
||||
renderItems();
|
||||
elem.addClass('open');
|
||||
};
|
||||
|
||||
function close() {
|
||||
elem.removeClass('open');
|
||||
};
|
||||
|
||||
function toggle() {
|
||||
if (elem.hasClass('open')) close();
|
||||
else open();
|
||||
};
|
||||
|
||||
this.getElem = getElem;
|
||||
this.toggle = toggle;
|
||||
this.close = close;
|
||||
this.open = open;
|
||||
}
|
20
02-ui/05-widgets/05-custom-events/menu-event/style.css
Executable file
20
02-ui/05-widgets/05-custom-events/menu-event/style.css
Executable file
|
@ -0,0 +1,20 @@
|
|||
|
||||
.menu ul {
|
||||
display: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu .title {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
background: url(http://js.cx/clipart/arrow-right.png) left center no-repeat;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.menu.open ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.menu.open .title {
|
||||
background-image: url(http://js.cx/clipart/arrow-down.png);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue