renovations
This commit is contained in:
parent
24171550ae
commit
a62682e188
49 changed files with 620 additions and 894 deletions
|
@ -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);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue