This commit is contained in:
Ilya Kantor 2014-11-16 01:40:20 +03:00
parent 962caebbb7
commit 87bf53d076
1825 changed files with 94929 additions and 0 deletions

View file

@ -0,0 +1 @@
[edit src="solution"]Решение задачи[/edit]

View file

@ -0,0 +1,18 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<input type="button" id="hider" value="Нажмите, чтобы спрятать текст"/>
<div id="hide">Текст</div>
<script>
document.getElementById('hider').onclick = function() {
document.getElementById('hide').style.display = 'none';
}
</script>
</body>
</html>

View file

@ -0,0 +1,16 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<input type="button" id="hider" value="Нажмите, чтобы спрятать текст"/>
<div id="hide">Текст</div>
<script>
/* ваш код */
</script>
</body>
</html>

View file

@ -0,0 +1,10 @@
# Спрятать при клике
[importance 5]
Используя JavaScript, сделайте так, чтобы при клике на кнопку исчезал элемент с `id="hide"`.
Демо:
[iframe border=1 src="solution"]
[edit src="source" task/]

View file

@ -0,0 +1,7 @@
Решение задачи заключается в использовании `this` в обработчике.
```html
<!--+ run height=50 -->
<input type="button" onclick="this.style.display='none'" value="Нажми, чтобы меня спрятать"/>
```

View file

@ -0,0 +1,8 @@
# Спрятаться
[importance 5]
Создайте кнопку, при клике на которую, она будет скрывать сама себя.
Как эта:
<input type="button" onclick="this.style.display='none'" value="Нажми, чтобы меня спрятать"/>

View file

@ -0,0 +1,17 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script>
function countRabbits() {
for(var i=1; i<=3; i++) {
alert("Кролик номер " + i);
}
}
</script>
</head>
<body>
<input type="button" onclick="countRabbits()" value="Считать кроликов!"/>
</body>
</html>

View file

@ -0,0 +1,16 @@
Ответ: будет выведено `1` и `2`.
Первый обработчик сработает, так как он не убран вызовом `removeEventListener`. Для удаления обработчика нужно передать в точности ту же функцию (ссылку на нее), что была назначена, а в коде передается такая же с виду функция, но, тем не менее, это другой объект.
Для того, чтобы удалить функцию-обработчик, нужно где-то сохранить ссылку на неё, например так:
```js
function handler() {
alert("1");
}
button.addEventListener("click", handler, false);
button.removeEventListener("click", handler, false);
```
Обработчик `button.onclick` сработает независимо и в дополнение к назначенному в `addEventListener`.

View file

@ -0,0 +1,18 @@
# Какие обработчики сработают?
[importance 5]
В переменной `button` находится кнопка.
Изначально обработчиков на ней нет.
Что будет выведено при клике после выполнения кода?
```js
button.addEventListener("click", function() { alert("1"); }, false);
button.removeEventListener("click", function() { alert("1"); }, false);
button.onclick = function() { alert(2); };
```

View file

@ -0,0 +1,74 @@
# Структура HTML/CSS
Для начала, зададим структуру HTML/CSS.
Меню является отдельным графическим компонентом, его лучше поместить в единый DOM-элемент.
Элементы меню с точки зрения семантики являются списком `UL/LI`. Заголовок должен быть отдельным кликабельным элементом.
Получаем структуру:
```html
<div class="menu">
<span class="title">Сладости (нажми меня)!</span>
<ul>
<li>Пирог</li>
<li>Пончик</li>
<li>Мед</li>
</ul>
</div>
```
Для заголовка лучше использовать именно `SPAN`, а не `DIV`, так как `DIV` постарается занять 100% ширины, и мы не сможем ловить `click` только на тексте:
```html
<!--+ autorun height=50 -->
<div style="border: solid red 1px">[Сладости (нажми меня)!]</div>
```
...А `SPAN` -- это элемент с `display: inline`, поэтому он занимает ровно столько места, сколько занимает текст внутри него:
```html
<!--+ autorun height=50 -->
<span style="border: solid red 1px">[Сладости (нажми меня)!]</span>
```
Раскрытие/закрытие делайте путём добавления/удаления класса `.menu-open` к меню, которые отвечает за стрелочку и отображение `UL`.
# CSS
CSS для меню:
```css
.menu ul {
margin: 0;
list-style: none;
padding-left: 20px;
display: none;
}
.menu .title {
padding-left: 16px;
font-size: 18px;
cursor: pointer;
background: url(...arrow-right.png) left center no-repeat;
}
```
Если же меню раскрыто, то есть имеет класс `.menu-open`, то стрелочка слева заголовка меняется и список детей показывается:
```css
.menu-open .title {
background: url(...arrow-down.png) left center no-repeat;
}
.menu-open ul {
display: block;
}
```
Теперь сделайте JavaScript.
[edit src="solution"]Полное решение в песочнице[/edit]

View file

@ -0,0 +1,57 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<style>
.menu ul {
margin: 0;
list-style: none;
padding-left: 20px;
display: none;
}
.menu .title {
padding-left: 16px;
font-size: 18px;
cursor: pointer;
background: url(http://js.cx/clipart/arrow-right.png) left center no-repeat;
}
.menu-open .title {
background: url(http://js.cx/clipart/arrow-down.png) left center no-repeat;
}
.menu-open ul {
display: block;
}
</style>
</head>
<body>
<div id="sweeties" class="menu">
<span id="sweeties-title" class="title">Сладости (нажми меня)!</span>
<ul>
<li>Торт</li>
<li>Пончик</li>
<li>Пирожное</li>
</ul>
</div>
<script>
var titleElem = document.getElementById('sweeties-title');
titleElem.onclick = function() {
var menu = this.parentNode;
menu.classList.toggle('menu-open');
};
</script>
</body>
</html>

View file

@ -0,0 +1,21 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<img src="http://js.cx/clipart/arrow-down.png">
<img src="http://js.cx/clipart/arrow-right.png">
Сладости (нажми меня)!
<ul>
<li>Торт</li>
<li>Пончик</li>
<li>Пирожное</li>
</ul>
</body>
</html>

View file

@ -0,0 +1,11 @@
# Раскрывающееся меню
[importance 5]
Создайте меню, которое раскрывается/сворачивается при клике:
[iframe border=1 height=100 src="solution"]
HTML/CSS исходного документа, возможно, понадобится изменить.
[edit src="source" task/]

View file

@ -0,0 +1,26 @@
# Алгоритм решения
<ol>
<li>Разработать структуру HTML/CSS. Позиционировать кнопку внутри сообщения.</li>
<li>Найти все кнопки</li>
<li>Присвоить им обработчики</li>
<li>Обработчик будет ловить событие на кнопке и удалять соответствующий элемент.</li>
<ol>
# Вёрстка
Исправьте HTML/CSS, чтобы кнопка была в нужном месте сообщения. Кнопку лучше сделать как `div`, а картинка --- будет его `background`. Это более правильно, чем `img`, т.к. в данном случае картинка является *оформлением кнопки*, а оформление должно быть в CSS.
Расположить кнопку справа можно при помощи `position: relative` для `pane`, а для кнопки `position: absolute + right/top`. Так как `position: absolute` вынимает элемент из потока, то кнопка может перекрыть текст заголовка. Чтобы этого не произошло, можно добавить `padding-right` к заголовку.
Потенциальным преимуществом способа с `position` по сравнению с `float` в данном случае является возможность поместить элемент кнопки в HTML *после текста*, а не до него.
# Обработчики
Для того, чтобы получить кнопку из контейнера, можно найти все `IMG` в нём и выбрать из них кнопку по `className`. На каждую кнопку можно повесить обработчик.
# Решение
[edit src="solution"]Решение в песочнице[/edit]
Для поиска элементов `span` с нужным классом в нём используется `getElementsByTagName` с фильтрацией. К сожалению, это единственный способ, доступный в IE 6,7. Если же эти браузеры вам не нужны, то гораздо лучше -- искать элементы при помощи `querySelector` или `getElementsByClassName`.

View file

@ -0,0 +1,67 @@
<!DOCTYPE HTML>
<html>
<head>
<link rel="stylesheet" href="messages.css">
<meta charset="utf-8">
<style>
.pane {
position: relative;
}
.remove-button {
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
display: block;
background: url(http://js.cx/clipart/delete.gif) no-repeat;
width: 16px;
height: 16px;
}
h3 {
padding-right: 20px;
}
</style>
</head>
<body>
<div id="messages-container">
<div class="pane">
<h3>Лошадь</h3>
<p>Домашняя лошадь — животное семейства непарнокопытных, одомашненный и единственный сохранившийся подвид дикой лошади, вымершей в дикой природе, за исключением небольшой популяции лошади Пржевальского.</p>
<span class="remove-button"></span>
</div>
<div class="pane">
<h3>Осёл</h3>
<p>Домашний осёл или ишак — одомашненный подвид дикого осла, сыгравший важную историческую роль в развитии хозяйства и культуры человека. Все одомашненные ослы относятся к африканским ослам.</p>
<span class="remove-button"></span>
</div>
<div class="pane">
<h3>Корова, а также пара слов о диком быке, о волах и о тёлках. </h3>
<p>Коро́ва — самка домашнего быка, одомашненного подвида дикого быка, парнокопытного жвачного животного семейства полорогих. Самцы вида называются быками, молодняк — телятами, кастрированные самцы — волами. Молодых (до первой стельности) самок называют тёлками.</p>
<span class="remove-button"></span>
</div>
</div>
<script>
var spans = document.getElementById('messages-container').getElementsByTagName('span');
for(var i=0; i<spans.length; i++) {
var span = spans[i];
if (span.className != 'remove-button') continue;
span.onclick = function() {
var el = this.parentNode;
el.parentNode.removeChild(el);
};
}
</script>
</body>
</html>

View file

@ -0,0 +1,32 @@
body {
margin: 10px auto;
width: 470px;
}
h3 {
margin: 0;
padding-bottom: .3em;
padding-right: 20px;
font-size: 1.1em;
}
p {
margin: 0;
padding: 0 0 .5em;
}
.pane {
background: #edf5e1;
padding: 10px 20px 10px;
border-top: solid 2px #c4df9b;
position: relative;
}
.remove-button {
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
display: block;
background: url(delete.gif) no-repeat;
width: 16px;
height: 16px;
}

View file

@ -0,0 +1,28 @@
<!DOCTYPE HTML>
<html>
<head>
<link rel="stylesheet" type="text/css" href="messages.css">
<meta charset="utf-8">
</head>
<body>
Картинка для кнопки удаления: <img src="http://js.cx/clipart/delete.gif">
<div>
<div class="pane">
<h3>Лошадь</h3>
<p>Домашняя лошадь — животное семейства непарнокопытных, одомашненный и единственный сохранившийся подвид дикой лошади, вымершей в дикой природе, за исключением небольшой популяции лошади Пржевальского.</p>
</div>
<div class="pane">
<h3>Осёл</h3>
<p>Домашний осёл или ишак — одомашненный подвид дикого осла, сыгравший важную историческую роль в развитии хозяйства и культуры человека. Все одомашненные ослы относятся к африканским ослам.</p>
</div>
<div class="pane">
<h3>Корова, а также пара слов о диком быке, о волах и о тёлках. </h3>
<p>Коро́ва — самка домашнего быка, одомашненного подвида дикого быка, парнокопытного жвачного животного семейства полорогих. Самцы вида называются быками, молодняк — телятами, кастрированные самцы — волами. Молодых (до первой стельности) самок называют тёлками.</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,18 @@
body {
margin: 10px auto;
width: 470px;
}
h3 {
margin: 0;
padding-bottom: .3em;
font-size: 1.1em;
}
p {
margin: 0;
padding: 0 0 .5em;
}
.pane {
background: #edf5e1;
padding: 10px 20px 10px;
border-top: solid 2px #c4df9b;
}

View file

@ -0,0 +1,12 @@
# Спрятать сообщение
[importance 5]
Есть список сообщений. Добавьте каждому сообщению по кнопке для его скрытия.
Результат:
[iframe src="solution"]
Как лучше отобразить кнопку справа-сверху: через `position:absolute` или `float`?
[edit src="source" task/]

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

View file

@ -0,0 +1,28 @@
# HTML/CSS
Лента изображений должна быть оформлена как список, согласно принципам семантической вёрстки.
Нужно стилизовать его так, чтобы он был длинной лентой, из которой внешний `DIV` вырезает нужную часть для просмотра:
<img src="carousel1.png">
Чтобы список был длинный и элементы не переходили вниз, ему ставится `width: 9999px`, а элементам, соответственно, `float:left`.
[warn header="Не используйте display:inline"]
Элементы с `display:inline` имеют дополнительные отступы для возможных "хвостов букв".
В частности, для `img` нужно поставить в стилях явно `display:block`, чтобы пространства под ними не оставалось.
[/warn]
При прокрутке UL сдвигается назначением `margin-left`:
<img src="carousel2.png">
У внешнего `DIV` фиксированная ширина, поэтому "лишние" изображения обрезаются.
Снаружи окошка находятся стрелки и внешний контейнер.
Реализуйте эту структуру, и к ней прикручивайте обработчики, которые меняют `ul.style.marginLeft`.
# Полное решение
[edit src="solution"]Открыть в песочнице[/edit]

View file

@ -0,0 +1,71 @@
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div class="carousel">
<a href="#" class="arrow left-arrow" id="prev">&#8249; </a>
<div class="gallery">
<ul id="images">
<li><img src="http://js.cx/carousel/1.png"></li>
<li><img src="http://js.cx/carousel/2.png"></li>
<li><img src="http://js.cx/carousel/3.png"></li>
<li><img src="http://js.cx/carousel/4.png"></li>
<li><img src="http://js.cx/carousel/5.png"></li>
<li><img src="http://js.cx/carousel/6.png"></li>
<li><img src="http://js.cx/carousel/7.png"></li>
<li><img src="http://js.cx/carousel/8.png"></li>
<li><img src="http://js.cx/carousel/9.png"></li>
<li><img src="http://js.cx/carousel/10.png"></li>
</ul>
</div>
<a href="#" class="arrow right-arrow" id="next">&#8250; </a>
</div>
<script>
/* этот код помечает картинки, для удобства разработки */
var lis = document.getElementsByTagName('li');
for(var i=0; i<lis.length; i++) {
lis[i].style.position='relative';
var span = document.createElement('span');
// обычно лучше использовать CSS-классы,
// но этот код - для удобства разработки, так что не будем трогать стили
span.style.cssText='position:absolute;left:0;top:0';
span.innerHTML = i+1;
lis[i].appendChild(span);
}
/* конфигурация */
var width = 130; // ширина изображения
var count = 3; // количество изображений
var ul = document.getElementById('images');
var imgs = ul.getElementsByTagName('li');
var position = 0; // текущий сдвиг влево
document.getElementById('prev').onclick = function() {
if (position >= 0) return false; // уже до упора
// последнее передвижение влево может быть не на 3, а на 2 или 1 элемент
position = Math.min(position + width*count, 0)
ul.style.marginLeft = position + 'px';
return false;
}
document.getElementById('next').onclick = function() {
if (position <= -width*(imgs.length-count)) return false; // уже до упора
// последнее передвижение вправо может быть не на 3, а на 2 или 1 элемент
position = Math.max(position-width*count, -width*(imgs.length-count));
ul.style.marginLeft = position + 'px';
return false;
};
</script>
</body>
</html>

View file

@ -0,0 +1,52 @@
body {
padding: 10px
}
.carousel {
position: relative;
width: 398px;
padding: 10px 40px;
border: 1px solid #CCC;
border-radius: 15px;
background: #eee;
}
.carousel img {
width: 130px;
height: 130px;
display: block; /* если не поставить block, в ряде браузеров будет inline -> лишнее пространтсво вокруг элементов */
}
.carousel .arrow {
position: absolute;
top: 57px;
padding: 3px 8px 8px 9px;
background: #ddd;
border-radius: 15px;
font-size: 24px;
color: #444;
text-decoration: none;
}
.carousel .arrow:hover {
background: #ccc;
}
.carousel .left-arrow {
left: 7px;
}
.carousel .right-arrow {
right: 7px;
}
.gallery {
width: 390px;
overflow: hidden;
}
.gallery ul {
height: 130px;
width: 9999px;
margin: 0;
padding: 0;
list-style: none;
}
.gallery ul li {
float: left;
}

View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
<head>
<meta charset="utf-8">
</head>
<body>
<!-- ваша верстка виджета, ваши стили -->
<ul id="images">
<li><img src="http://js.cx/carousel/1.png"></li>
<li><img src="http://js.cx/carousel/2.png"></li>
<li><img src="http://js.cx/carousel/3.png"></li>
<li><img src="http://js.cx/carousel/4.png"></li>
<li><img src="http://js.cx/carousel/5.png"></li>
<li><img src="http://js.cx/carousel/6.png"></li>
<li><img src="http://js.cx/carousel/7.png"></li>
<li><img src="http://js.cx/carousel/8.png"></li>
<li><img src="http://js.cx/carousel/9.png"></li>
<li><img src="http://js.cx/carousel/10.png"></li>
</ul>
<script>
/* этот код помечает картинки цифрами, для удобства разработки
его можно убрать, если не нужен */
var lis = document.getElementsByTagName('li');
for(var i=0; i<lis.length; i++) {
lis[i].style.position='relative';
var span = document.createElement('span');
span.style.cssText='position:absolute;left:0;top:0';
span.innerHTML = i+1;
lis[i].appendChild(span);
}
</script>
<script>
// ваш код..
</script>
</body>
</html>

View file

@ -0,0 +1,13 @@
# Карусель
[importance 5]
Напишите "Карусель" -- ленту изображений, которую можно листать влево-вправо нажатием на стрелочки.
[iframe height=200 src="solution"]
В дальнейшем к ней можно легко добавить анимацию, динамическую подгрузку и другие возможности.
В этой задаче разработка HTML/CSS-структуры составляет 90% решения.
[edit src="source" task/]

View file

@ -0,0 +1,526 @@
# Введение в браузерные события
Для реакции на действия посетителя и внутреннего взаимодействия скриптов существуют *события*.
*Событие* - это сигнал от браузера о том, что что-то произошло.
[cut]
Существует много видов событий.
Посмотрим список самых часто используемых, пока просто для ознакомления:
<dl>
<dt>События мыши</dt>
<dd>
<ul>
<li>`click` -- происходит, когда кликнули на элемент левой кнопкой мыши</li>
<li>`contextmenu` -- происходит, когда кликнули на элемент правой кнопкой мыши</li>
<li>`mouseover` -- возникает, когда на элемент наводится мышь</li>
<li>`mousedown` и `mouseup` -- когда кнопку мыши нажали или отжали</li>
<li>`mousemove` -- при движении мыши</li>
</ul>
</dd>
<dt>События на элементах управления</dt>
<dd>
<ul>
<li>`submit` -- посетитель отправил форму `<form>`</li>
<li>`focus` -- посетитель фокусируется на элементе, например нажимает на `<input>`</li>
</ul>
<dt>Клавиатурные события</dt>
<dd>
<ul>
<li>`keydown` -- когда посетитель нажимает клавишу</li>
<li>`keyup` -- когда посетитель отпускает клавишу</li>
</ul>
</dd>
<dt>События документа</dt>
<dd>
<ul>
<li>`DOMContentLoaded` -- когда HTML загружен и обработан, DOM документа полностью построен и доступен.</li>
</ul></dd>
<dt>События CSS</dt>
<dd>
<ul>
<li>`transitionend` -- когда CSS-анимация завершена.</li>
</ul></dd>
</dl>
Также есть и много других событий.
## Назначение обработчиков событий
Событию можно назначить обработчик, то есть функцию, которая сработает, как только событие произошло.
Именно благодаря событиям JavaScript-код может реагировать на действия посетителя.
Есть несколько способов назначить событию обработчик. Сейчас мы их рассмотрим, начиная от самого простого.
### Использование атрибута HTML
Обработчик может быть назначен прямо в разметке, в атрибуте, который называется `on<событие>`.
Например, чтобы прикрепить `click`-событие к `input` кнопке, можно присвоить обработчик `onclick`, вот так:
```html
<input value="Нажми меня" *!*onclick="alert('Клик!')"*/!* type="button">
```
При клике мышкой на кнопке выполнится код, указанный в атрибуте `onclick`.
В действии:
<input value="Нажми меня" onclick="alert('Клик!');" type="button">
Обратите внимание, для строки *внутри* `alert('Клик!')` используются *одиночные кавычки*, так как сам атрибут находится в двойных.
Частая ошибка новичков в том, что они забывают, что код находится внутри атрибута. Запись вида `onclick="alert("Клик!")"` не будет работать. Если вам действительно нужно использовать именно двойные кавычки, то это можно сделать, заменив их на `&quot;`: <code>onclick="alert(&amp;quot;Клик!&amp;quot;)"</code>.
Однако, обычно этого не требуется, так как в разметке пишутся только очень простые обработчики. Если нужно сделать что-то сложное, то имеет смысл описать это в функции, и в обработчике вызвать уже её.
Следующий пример по клику запускает функцию `countRabbits()`.
```html
<!--+ src="2.html" run height=80 -->
```
Как мы помним, атрибут HTML-тега не чувствителен к регистру, поэтому `ONCLICK` будет работать так же, как `onClick` или `onclick`... Но, как правило, атрибуты пишут в нижнем регистре: `onclick`.
### Использование свойства DOM-объекта
Можно назначать обработчик, используя свойство DOM-элемента `on<событие>`.
Пример установки обработчика `click`:
```html
<input id="elem" type="button" value="Нажми меня"/>
<script>
*!*
elem.onclick = function() {
alert('Спасибо');
};
*/!*
</script>
```
В действии:
<input id="elem" type="button" value="Нажми меня"/>
<script>
elem.onclick = function() {
alert('Спасибо');
};
</script>
Если обработчик задан через атрибут, то браузер читает HTML-разметку, создаёт новую функцию из содержимого атрибута и записывает в свойство `onclick`.
**Обработчик хранится именно в свойстве, а атрибут -- лишь один из способов его инициализации.**
Эти два примера кода работают одинаково:
<ol>
<li>Только HTML:
```html
<!--+ run height=50 -->
<input type="button" *!*onclick="alert('Клик!')"*/!* value="Кнопка"/>
```
</li>
<li>HTML + JS:
```html
<!--+ run height=50 -->
<input type="button" id="button" value="Кнопка"/>
<script>
*!*
button.onclick = function() {
alert('Клик!');
};
*/!*
</script>
```
</li>
</ol>
**Так как свойство, в итоге, одно, то назначить более одного обработчика так нельзя.**
В примере ниже назначение через JavaScript перезапишет обработчик из атрибута:
```html
<!--+ run height=50 autorun -->
<input type="button" id="elem" onclick="alert('До')" value="Нажми меня"/>
<script>
*!*
elem.onclick = function() { // перезапишет существующий обработчик
alert('После');
};
*/!*
</script>
```
Кстати, обработчиком можно назначить и уже существующую функцию:
```js
function sayThanks() {
alert('Спасибо!');
}
elem.onclick = sayThanks;
```
Если обработчик надоел -- его всегда можно убрать назначением `elem.onclick = null`.
### Доступ к элементу через this
Внутри обработчика события `this` ссылается на текущий элемент, то есть на тот, на котором он сработал.
Это можно использовать, чтобы получить свойства или изменить элемент.
В коде ниже `button` выводит свое содержимое, используя `this.innerHTML`:
```html
<button onclick="alert(this.innerHTML)">Нажми меня</button>
```
В действии:
<button onclick="alert(this.innerHTML)">Нажми меня</button>
### Частые ошибки
Если вы только начинаете работать с событиями -- обратите внимание на следующие особенности.
<dl>
<dt>Функция должна быть присвоена как `sayThanks`, а не `sayThanks()`.</dt>
<dd>
```js
button.onclick = sayThanks;
```
Если добавить скобки, то `sayThanks()` -- будет уже *результат* выполнения функции (а так как в ней нет `return`, то в `onclick` попадёт `undefined`). Нам же нужна именно функция.
...А вот в разметке как раз скобки нужны:
```html
<input type="button" id="button" onclick="sayThanks()"/>
```
Это различие просто объяснить. При создании обработчика браузером по разметке, он автоматически создает функцию из его содержимого. Поэтому последний пример -- фактически то же самое, что:
```js
button.onclick = function() {
*!*
sayThanks(); // содержимое атрибута
*/!*
};
```
</dd>
<dt>Используйте именно функции, а не строки.</dt>
<dd>
Назначение обработчика строкой `elem.onclick = 'alert(1)'` будет работать, но не рекомендуется, могут быть проблемы при сжатии JavaScript.
Передавать код в виде строки по меньшей мере странно в языке, который поддерживает Function Expressions, оно здесь доступно только по соображениям совместимости с древними временами.
</dd>
<dt>Не используйте `setAttribute`.</dt>
<dd>
Такой вызов работать не будет:
```js
//+ run
// при нажатии на body будут ошибки
// потому что при назначении в атрибут функция будет преобразована в строку
document.body.setAttribute('onclick', function() { alert(1) });
```
</dd>
<dt>Регистр свойства имеет значение.</dt>
<dd>Свойство называется `onclick`, а не `ONCLICK`.</dd>
</dl>
## Специальные методы
Фундаментальный недостаток описанных выше способов назначения обработчика -- невозможность повесить *несколько* обработчиков на одно событие.
Например, одна часть кода хочет при клике на кнопку делать ее подсвеченной, а другая -- выдавать сообщение. Нужно в разных местах два обработчика повесить.
При этом новый обработчик будет затирать предыдущий. Например, следующий код на самом деле назначает один обработчик -- последний:
```js
input.onclick = function() { alert(1); }
// ...
input.onclick = function() { alert(2); } // заменит предыдущий обработчик
```
Разработчики стандартов достаточно давно это поняли и предложили альтернативный способ назначения обработчиков при помощи специальных методов, который свободен от указанного недостатка.
### addEventListener и removeEventListener
Методы `addEventListener` и `removeEventListener` являются современным способом назначить или удалить обработчик, и при этом позволяют использовать сколько угодно любых обработчиков.
Назначение обработчика осуществляется вызовом `addEventListener` с тремя аргументами:
```js
element.addEventListener( event, handler, phase );
```
<dl>
<dt>`event`</dt>
<dd>Имя события, например `click`</dd>
<dt>`handler`</dt>
<dd>Ссылка на функцию, которую надо поставить обработчиком.</dd>
<dt>`phase`</dt>
<dd>Фаза, на которой обработчик должен сработать. Этот аргумент мы рассмотрим далее в учебнике. Пока что будем использовать значение `phase = false`, которое нужно в 99% случаев.</dd>
</dl>
Удаление обработчика осуществляется вызовом `removeEventListener`:
```js
element.removeEventListener( event, handler, phase );
```
[warn header="Удаление требует ту же функцию"]
Для удаления нужно передать именно ту функцию-обработчик которая была назначена.
Вот так `removeEventListener` не сработает:
```js
input.addEventListener( "click" , function() {alert('Спасибо!')}, false);
// ....
input.removeEventListener( "click", function() {alert('Спасибо!')}, false);
```
Это не одна и та же функция, а две независимо созданные (с одинаковым кодом, но это не важно).
Вот так правильно:
```js
function handler() {
alert('Спасибо!');
}
input.addEventListener( "click" , handler, false);
// ....
input.removeEventListener( "click", handler, false);
```
[/warn]
**Использование `addEventListener` позволяет добавлять несколько обработчиков на одно событие одного элемента:**
```html
<!--+ run -->
<input id="elem" type="button" value="Нажми меня"/>
<script>
function handler1() {
alert('Спасибо!');
};
function handler2() {
alert('Спасибо ещё раз!');
}
*!*
elem.onclick = function() { alert("Привет"); };
elem.addEventListener("click", handler1, false); // Спасибо!
elem.addEventListener("click", handler2, false); // Спасибо ещё раз!
*/!*
</script>
```
Как видно из примера выше, можно одновременно назначать обработчики и через `onсвойство` (только один) и через `addEventListener`. Однако, во избежание путаницы обычно рекомендуется выбрать один способ.
[warn header="`addEventListener` работает всегда, а `onсвойство` -- нет"]
У специальных методов есть ещё одно перимущество перед "старой школой".
Есть некоторые события, которые нельзя назначить через `onсвойство`, но можно через `addEventListener`.
Например, таково событие `transitionend`, то есть окончание CSS-анимации. В большинстве браузеров оно требует назначения через `addEventListener`.
При нажатии на кнопку в примере ниже сработает второй обработчик, но не первый.
```html
<!--+ autorun run -->
<style>
button {
transition: width 3s;
width: 100px;
}
.wide {
width: 300px;
}
</style>
<button id="elem" onclick="this.classList.toggle('wide');">
Нажми меня
</button>
<script>
elem.ontransitionend = function() {
alert("ontransitionend"); // не сработает
};
*!*
elem.addEventListener("transitionend", function() {
alert("addEventListener"); // сработает по окончании анимации
}, false);
*/!*
</script>
```
[/warn]
## Отличия IE8-
При работе с событиями в IE8- есть много отличий. Как правило, они формальны -- некое свойство или метод называются по-другому. Начиная с версии 9, также работают и стандартные свойства и методы, которые предпочтительны.
**В IE8- вместо `addEventListener/removeEventListener` используются свои методы.**
Назначение обработчика осуществляется вызовом `attachEvent`:
```js
element.attachEvent( "on"+event, handler);
```
Удаление обработчика -- вызовом `detachEvent`:
```js
element.detachEvent( "on"+event, handler);
```
Например:
```js
function handler() {
alert('Спасибо!');
}
button.attachEvent( "onclick" , handler) // Назначение обработчика
// ....
button.detachEvent( "onclick", handler) // Удаление обработчика
```
Как видите, почти то же самое, только событие должно включать префикс `on` и нет третьего аргумента, который нам пока не нужен.
[warn header="У обработчиков, назначенных с `attachEvent`, нет `this`"]
Обработчики, назначенные с `attachEvent` не получают `this`!
Это важная особенность и подводный камень старых IE.
[/warn]
### Кроссбраузерный способ назначения обработчиков
Можно объединить способы для IE<9 и современных браузеров, создав свои методы `addEvent(elem, type, handler)` и `removeEvent(elem, type, handler)`:
```js
var addEvent, removeEvent;
if (document.addEventListener) { // проверка существования метода
addEvent = function(elem, type, handler) {
elem.addEventListener(type, handler, false);
};
removeEvent = function(elem, type, handler) {
elem.removeEventListener(type, handler, false);
};
} else {
addEvent = function(elem, type, handler) {
elem.attachEvent("on" + type, handler);
};
removeEvent = function(elem, type, handler) {
elem.detachEvent("on" + type, handler);
};
}
...
// использование:
addEvent(elem, "click", function() { alert("Привет"); });
```
Это хорошо работает в большинстве случаев, но у обработчика не будет `this` в IE, потому что `attachEvent` не поддерживает `this`.
Кроме того, в IE7- есть проблемы с утечками памяти... Но если вам не нужно `this`, и вы не боитесь утечек (как вариант -- не поддерживаете IE7-), то это решение может подойти.
## Итого
Есть три способа назначения обработчиков событий:
<ol>
<li>Атрибут HTML: `onclick="..."`.</li>
<li>Свойство: <code>elem.onclick = function</code>.</li>
<li>Специальные методы:
<ul>
<li>Для IE8-: `elem.attachEvent( on+событие, handler )` (удаление через `detachEvent`).</li>
<li>Для остальных: `elem.addEventListener( событие, handler, false )` (удаление через `removeEventListener`).</li>
</ul>
</li>
</ol>
Сравнение `addEventListener` и `onclick`:
[compare]
+Некоторые события можно назначить только через `addEventListener`.
+Метод `addEventListener` позволяет назначить много обработчиков на одно событие.
-Обработчик, назначенный через `onclick`, проще удалить или заменить.
-Метод `onclick` кросс-браузерный.
[/compare]
**Этим введением мы только начинаем работу с событиями, но вы уже можете решать разнообразные задачи с их использованием.**
[head]
<style type="text/css">
.d0 { text-align:center;margin:auto; }
.d1 p { margin: 0 }
.d1 {
margin:2em;
background-color:green;
width:13em;
height:13em;
text-align:center;
}
.d1 .number {
line-height: 2em;
}
.d2 {
text-align:center;
margin:auto;
background-color:blue;
width:9em;
height:9em;
}
.d1 .d2 ,number {
line-height: 2em;
}
.d3 {
text-align:center;
margin:auto;
background-color:red;
width:5em;
height:5em;
}
.d1 .d2 .d3 .number {
line-height: 5em;
}
.d1 .d2 .d2a {
color:white;
line-height: 2em;
}
</style>
<script type="text/javascript">
function highlightMe(elem) {
elem.style.backgroundColor='yellow'
alert(elem.className)
elem.style.backgroundColor = ''
}
function highlightMe2(e) {
highlightMe(e.currentTarget);
}
</script>
[/head]

View file

@ -0,0 +1,175 @@
# Порядок обработки событий
События могут возникать не только по очереди, но и пачкой, по многу сразу. Возможно и такое, что во время обработки одного события возникают другие.
Здесь и далее, очень важно понимать, как браузер обычно работает с событиями и важные исключения из этого правила. Это мы и разберём в этой главе.
[cut]
## Главный поток
В каждом окне выполняется только один *главный* поток, который занимается выполнением JavaScript, отрисовкой и работой с DOM.
Он выполняет команды последовательно и блокируется при выводе модальных окон, таких как `alert`.
[smart header="Дополнительные потоки тоже есть"]
Есть и другие, служебные потоки, например, для сетевых коммуникаций.
Поэтому скачивание файлов может продолжаться пока главный поток ждёт реакции на `alert`. Но управлять служебными потоками мы не можем.
[/smart]
[smart header="Web Workers"]
Существует спецификация <a href="http://www.w3.org/TR/workers/">Web Workers</a>, которая позволяет запускать дополнительные JavaScript-процессы(workers).
Они могут обмениваться сообщениями с главным процессом, но их переменные полностью независимы.
В частности, дополнительные процессы не имеют доступа к DOM, поэтому они полезны, преимущественно, при вычислениях, чтобы загрузить несколько ядер/процессоров одновременно.
[/smart]
## Очередь событий
Произошло одновременно несколько событий или во время работы одного случилось другое -- как главному потоку обработать это?
Если главный поток прямо сейчас занят, то он не может срочно выйти из середины одной функции и прыгнуть в другую. А потом третью. Отладка при этом могла бы превратиться в кошмар, потому что пришлось бы разбираться с совместным состоянием нескольких функций сразу.
Поэтому используется альтернативный подход.
**Когда происходит событие, оно попадает в очередь.**
Внутри браузера существует главный внутренний цикл, который проверяет очередь и обрабатывает события, запускает соответствующие обработчики и т.п.
**Иногда события добавляются в очередь сразу пачкой.**
Например, при клике на элементе генерируется несколько событий:
<ol>
<li>Сначала `mousedown` -- нажата кнопка мыши.</li>
<li>Затем `mouseup` -- кнопка мыши отпущена.</li>
<li>Так как это было над одним элементом, то дополнительно генерируется `click`</li>
</ol>
В действии:
```html
<!--+ autorun -->
<textarea rows="6" cols="40" id="area">Кликни меня
</textarea>
<script>
area.onmousedown = function(e) { this.value += "mousedown\n"; this.scrollTop = 1e9; };
area.onmouseup = function(e) { this.value += "mouseup\n"; this.scrollTop = 1e9; };
area.onclick = function(e) { this.value += "click\n"; this.scrollTop = 1e9; };
</script>
```
Таким образом, при нажатии кнопки мыши в очередь попадёт событие `mousedown`, а при отпускании -- сразу два события: `mouseup` и `click`. Браузер сначала обработает первое, а потом -- второе.
**При этом каждое событие из очереди обрабатывается полностью отдельно от других.**
## Вложенные (синхронные) события
В тех случаях, когда событие инициируется не посетителем, а кодом, то оно, как правило, обрабатывается синхронно, то есть прямо сейчас.
Рассмотрим в качестве примера событие `onfocus`.
### Пример: событие onfocus
Когда посетитель фокусируется на элементе, возникает событие `onfocus`. Обычно оно происходит, когда посетитель кликает на поле ввода, например:
```html
<!--+ run autorun -->
<p>При фокусе на поле оно изменит значение.</p>
<input type="text" onfocus="this.value = 'Фокус!'" value="Кликни меня">
```
Но ту же фокусировку можно вызвать и явно, вызовом метода `elem.focus()`:
```html
<!--+ run -->
<input type="text" id="elem" onfocus="this.value = 'Фокус!'">
<script>
*!*
// сфокусируется на input и вызовет обработчик onfocus
elem.focus();
*/!*
</script>
```
В главе [](/focus-blur) мы познакомимся с этим событием подробнее, а пока -- нажмите на кнопку в примере ниже. При этом обработчик `onclick` вызовет метод `focus()` на текстовом поле `text`.
**Событие `onfocus`, инициированное вызовом `text.focus()`, будет обработано синхронно, прямо сейчас, до завершения `onclick`.**
```html
<!--+ autorun -->
<input type="button" id="button" value="Нажми меня">
<input type="text" id="text" size="60">
<script>
button.onclick = function() {
text.value += ' ->в onclick ';
text.focus(); // вызов инициирует событие onfocus
text.value += ' из onclick-> ';
};
text.onfocus = function() {
text.value += ' !focus! ';
};
</script>
```
При клике на кнопке в примере выше будет видно, что управление вошло в `onclick`, затем перешло в `onfocus`, затем вышло из `onclick`.
**Так ведут себя все браузеры, кроме IE.**
В нём событие `onfocus` -- всегда асинхронное, так что будет сначала полностью обработан клик, а потом -- фокус. В остальных -- фокус вызовется посередине клика. Попробуйте кликнуть в IE и в другом браузере, чтобы увидеть разницу.
## Делаем события асинхронными через setTimeout(...,0)
А что, если мы хотим, чтобы *сначала* закончилась обработка `onclick`, а потом уже произошла обработка `onfocus` и связанные с ней действия?
Можно добиться и этого.
Один вариант -- просто переместить строку `text.focus()` вниз кода обработчика.
Если это неудобно, можно запланировать `text.focus()` чуть позже через `setTimeout(..., 0)`, вот так
```html
<!--+ autorun -->
<input type="button" id="button" value="Нажми меня">
<input type="text" id="text" size="60">
<script>
button.onclick = function() {
text.value += ' ->в onclick ';
*!*
setTimeout(function() {
text.focus(); // сработает после onclick
}, 0);
*/!*
text.value += ' из onclick-> ';
};
text.onfocus = function() {
text.value += ' !focus! ';
};
</script>
```
Такой вызов обеспечит фокусировку через минимальный "тик" таймера, по стандарту равный 4мс. Обычно такая задержка не играет роли, а необходимую асинхронность мы получили.
## Итого
<ul>
<li>JavaScript выполняется в едином потоке. Современные браузеры позволяют порождать подпроцессы <a href="http://www.w3.org/TR/workers/">Web Workers</a>, они выполняются параллельно и могут отправлять/принимать сообщения, но не имеют доступа к DOM.</li>
<li>Обычно события становятся в очередь и обрабатываются в порядке поступления, асинхронно, независимо друг от друга.</li>
<li>Синхронными являются вложенные события, инициированные из кода.</li>
<li>Чтобы сделать событие гарантированно асинхронным, используется вызов через `setTimeout(func, 0)`.</li>
</ul>

View file

@ -0,0 +1,35 @@
# Мяч под курсор мыши
Основная сложность первого этапа -- сдвинуть мяч под курсор, т.к. координаты клика `e.clientX/Y` -- относительно окна, а мяч позиционирован абсолютно внутри поля, его координаты `left/top` нужно ставить относительно левого-верхнего внутреннего (внутри рамки!) угла поля.
Чтобы правильно вычислить координаты мяча, нужно получить координаты угла поля и вычесть их из `clientX/Y`:
```js
var field = document.getElementById('field');
var ball = document.getElementById('ball');
field.onclick = function(e) {
*!*
var fieldCoords = field.getBoundingClientRect();
var fieldInnerCoords = {
top: fieldCoords.top + field.clientTop,
left: fieldCoords.left + field.clientLeft
};
ball.style.left = e.clientX - fieldInnerCoords.left + 'px';
ball.style.top = e.clientY - fieldInnerCoords.top + 'px';
*/!*
};
```
Далее мяч нужно сдвинуть на половину его ширины и высоты `ball.clientWidth/clientHeight`, чтобы он оказался центром под курсором.
Здесь есть важный "подводный камень" -- размеры мяча в исходном документе не прописаны. Там просто стоит `<img>`. Но на момент выполнения JavaScript картинка, возможно, ещё не загрузилась, так что высота и ширина мяча будут неизвестны (а они необходимы для центрирования).
Нужно добавить `width/height` в тег `<img>` или задать размеры в CSS, тогда на момент выполнения JavaScript будет знать их и передвинет мяч правильно.
Код, который полностью центрирует мяч, вы найдете в полном решении:
[iframe border="1" src="solution" height="260" link edit]

View file

@ -0,0 +1,93 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<style>
#field {
width: 200px;
height: 150px;
border: 10px groove black;
background-color: #00FF00;
position: relative;
overflow: hidden;
cursor: pointer;
}
#ball {
position: absolute;
left: 0;
top: 0;
width: 40px;
height: 40px;
-webkit-transition: all 1s;
-moz-transition: all 1s;
-o-transition: all 1s;
-ms-transition: all 1s;
transition: all 1s;
}
</style>
</head>
<body style="height:2000px">
Кликните на любое место поля, чтобы мяч перелетел туда.<br>
<div id="field">
<img src="http://js.cx/clipart/ball.gif" id="ball">
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
</div>
<script>
var field = document.getElementById('field');
var ball = document.getElementById('ball');
field.onclick = function(event) {
// координаты поля относительно окна
var fieldCoords = this.getBoundingClientRect();
// координаты левого-верхнего внутреннего угла поля
var fieldInnerCoords = {
top: fieldCoords.top + field.clientTop,
left: fieldCoords.left + field.clientLeft
};
// разместить по клику,
// но сдвинув относительно поля (т.к. position:relative)
// и сдвинув на половину ширины/высоты
// (!) используются координаты относительно окна clientX/Y, как и в fieldCoords
var ballCoords = {
top: event.clientY - fieldInnerCoords.top - ball.clientHeight / 2,
left: event.clientX - fieldInnerCoords.left - ball.clientWidth / 2
};
// вылезает за верхнюю границу - разместить по ней
if(ballCoords.top < 0) ballCoords.top = 0;
// вылезает за левую границу - разместить по ней
if(ballCoords.left < 0) ballCoords.left = 0;
// вылезает за правую границу - разместить по ней
if(ballCoords.left + ball.clientWidth > field.clientWidth) {
ballCoords.left = field.clientWidth - ball.clientWidth;
}
// вылезает за нижнюю границу - разместить по ней
if(ballCoords.top + ball.clientHeight > field.clientHeight) {
ballCoords.top = field.clientHeight - ball.clientHeight;
}
ball.style.left = ballCoords.left + 'px';
ball.style.top = ballCoords.top + 'px';
}
</script>
</body>
</html>

View file

@ -0,0 +1,37 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<style>
#field {
width: 200px;
height: 150px;
border: 10px groove black;
background-color: #00FF00;
position: relative;
overflow: hidden;
}
#ball {
position: absolute;
top: 50%;
left: 50%;
margin-left: -20px;
margin-top: -20px;
}
</style>
</head>
<body style="height:2000px">
Кликните на любое место поля, чтобы мяч перелетел туда.<br>
Мяч никогда не вылетит за границы поля.
<div id="field">
<img src="http://js.cx/clipart/ball.gif" id="ball">
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
</div>
</body>
</html>

View file

@ -0,0 +1,26 @@
# Передвигать мяч по полю
[importance 5]
Сделайте так, что при клике по полю мяч перемещался на место клика.
[iframe src="solution" height="260" link]
Требования:
<ul>
<li>Мяч после перелёта должен становиться центром ровно под курсор мыши, если это возможно без вылета за край поля.</li>
<li>CSS-анимация не обязательна, но желательна.</li>
<li>Мяч должен останавливаться у границ поля, ни в коем случае не вылетать за них.</li>
<li>При прокрутке страницы ничего не должно ломаться.</li>
</ul>
Замечания:
<ul>
<li>Код не должен зависеть от конкретных размеров мяча и поля.</li>
<li>Текущий HTML/CSS нельзя менять, можно лишь "украшать" (анимация).</li>
<li>Вам пригодятся свойства `event.clientX/event.clientY`</li>
</ul>
[edit src="source" task/]
P.S. Центрировать мяч можно и при помощи CSS, но JavaScript в перспективе позволит делать это гибче и определять позицию более динамически.

View file

@ -0,0 +1,109 @@
# Объект события
Чтобы хорошо обработать событие, недостаточно знать о том, что это -- "клик" или "нажатие клавиши". Могут понадобиться детали: координаты курсора, введённый символ и другие, в зависимости от события.
**Детали произошедшего браузер записывает в "объект события", который передаётся первым аргументом в обработчик.**
[cut]
## Получение объекта события
Пример ниже демонстрирует использования объекта события:
```html
<!--+ run -->
<input type="button" value="Нажми меня" id="elem">
<script>
elem.onclick = function(*!*event*/!*) {
alert(event.type + " на " + event.currentTarget);
alert(event.clientX + ":" + event.clientY);
}
</script>
```
Свойства объекта `event`:
<dl>
<dt>`event.type`</dt>
<dd>Тип события, в данном случае `click`</dd>
<dt>`event.currentTarget`</dt>
<dd>Элемент, на котором сработал обработчик -- то же, что и `this`, но бывают ситуации, когда обработчик является методом объекта и его `this` при помощи `bind` привязан к объекту, тогда `event.currentTarget` полезен.</dd>
<dt>`event.clientX / event.clientY`</dt>
<dd>Координаты курсора в момент клика (относительно окна)</dd>
</dl>
Есть также и ряд других свойств, в зависимости от событий, которые мы разберём в дальнейших главах, когда будем подробно знакомиться с событиями мыши, клавиатуры и так далее.
[smart header="Объект события доступен и в HTML"]
При назначении обработчика в HTML, тоже можно использовать переменную `event`, это будет работать кросс-браузерно:
```html
<!--+ autorun height=auto -->
<input type="button" onclick="*!*alert(event.type)*/!*" value="Тип события">
```
Это возможно потому, что когда браузер из атрибута создаёт функцию-обработчик, то она выглядит так: `function(event) { alert(event.type) }`. То есть, её первый аргумент называется `"event"`.
[/smart]
## Особенности IE8-
IE8- вместо передачи параметра обработчику создаёт глобальный объект `window.event`. Обработчик может обратиться к нему.
Работает это так:
```js
elem.onclick = function() {
// window.event - объект события
alert( window.event.clientX );
};
```
### Кроссбраузерное решение
Универсальное решение для получения объекта события:
```js
element.onclick = function(event) {
event = event || window.event; // (*)
// Теперь event - объект события во всех браузерах.
};
```
Строка `(*)`, в случае, если функция не получила `event` (IE8-), использует `window.event`.-событие `event`.
Можно написать и иначе, если мы сами не используем переменную `event` в замыкании:
```js
element.onclick = function(e) {
e = e || event;
// Теперь e - объект события во всех браузерах.
};
```
## Итого
<ul>
<li>Объект события содержит ценную информацию о деталях события.</li>
<li>Он передается первым аргументом `event` в обработчик для всех браузеров, кроме IE8-, в которых используется глобальная переменная `window.event`.</li>
</ul>
Кросс-браузерно для JavaScript-обработчика получаем объект события так:
```js
element.onclick = function(event) {
event = event || window.event;
// Теперь event - объект события во всех браузерах.
};
```
Еще вариант:
```js
element.onclick = function(e) {
e = e || event; // если нет другой внешней переменной event
...
};
```

View file

@ -0,0 +1,255 @@
# Всплытие и перехват
Давайте сразу начнём с примера.
Этот обработчик для `<div>` сработает, если вы кликните по вложенному тегу `<em>` или `<code>`:
```html
<!--+ autorun height=auto -->
<div onclick="alert('Обработчик для Div сработал!')">
<em>Кликните на <code>EM</code>, сработает обработчик на <code>DIV</code></em>
</div>
```
Вам не кажется это странным? Почему же сработал обработчик на `<div>`, если клик произошёл на `<em>`?
## Всплытие
Основной принцип всплытия:
**При наступлении события обработчики сначала срабатывают на самом вложенном элементе, затем на его родителе, затем выше и так далее, вверх по цепочке вложенности.**
Например, есть 3 вложенных элемента `FORM > DIV > P`, с обработчиком на каждом:
```html
<!--+ run -->
<form onclick="alert('form')">FORM
<div onclick="alert('div')">DIV
<p onclick="alert('p')">P</p>
</div>
</form>
```
Всплытие гарантирует, что клик по внутреннему `P` вызовет обработчик `onclick` (если есть) сначала на самом `P`, затем на элементе `DIV` далее на элементе `FORM`, и так далее вверх по цепочке родителей до самого `document`.
<img src="event-order-bubbling.png" alt="Порядок всплытия событий">
Этот процесс называется *всплытием*, потому что события "всплывают" от внутреннего элемента вверх через родителей, подобно тому, как всплывает пузырек воздуха в воде.
[warn header="Всплывают *почти* все события."]
Ключевое слово в этой фразе -- "почти".
Например, событие `focus` не всплывает. В дальнейших главах мы будем детально знакомиться с различными событиями и увидим ещё примеры.
[/warn]
## Целевой элемент event.target
На каком бы элементе мы ни поймали событие, всегда можно узнать, где конкретно оно произошло.
**Самый глубокий элемент, который вызывает событие, называется *"целевым"* или *"исходным"* элементом и доступен как `event.target`.**
Отличия от `this` (=`event.currentTarget`):
<ul>
<li>`event.target` -- это **исходный элемент**, на котором произошло событие, в процессе всплытия он неизменен.</li>
<li>`this` -- это **текущий элемент**, до которого дошло всплытие, на нём сейчас выполняется обработчик.</li>
</ul>
Например, если стоит только один обработчик `form.onclick`, то он "поймает" все клики внутри него. Где бы ни был клик внутри -- он всплывёт до элемента `<form>`, на котором сработает обработчик.
<ul>
<li>`event.target` будет содержать элемент, на котором произошёл клик.</li>
<li>`this` (`=event.currentTarget`) всегда будет сама форма, так как обработчик сработал на ней.</li>
</ul>
[example height=220 src="bubble-target"]
## Прекращение всплытия
Всплытие идет прямо наверх. Обычно событие будет всплывать наверх и наверх, до элемента `<html>`, а затем до `document` и даже до `window`, вызывая все обработчики на своем пути.
**Но любой промежуточный обработчик может решить, что событие полностью обработано, и остановить всплытие.**
Для остановки всплытия нужно вызвать метод `event.stopPropagation()`.
Например, здесь при клике на кнопку обработчик `body.onclick` не сработает:
```html
<!--+ run autorun -->
<body onclick="alert('сюда обработка не дойдёт')">
<button onclick="event.stopPropagation()">Кликни меня</button>
</body>
```
[smart header="event.stopImmediatePropagation()"]
Если у элемента есть несколько обработчиков на одно событие, то даже при прекращении всплытия все они будут выполнены.
То есть, `stopPropagation` препятствует продвижению события дальше, но на текущем элементе все обработчики отработают.
Для того, чтобы полностью остановить обработку, современные браузеры поддерживают метод `event.stopImmediatePropagation()`. Он не только предотвращает всплытие, но и останавливает обработку событий на текущем элементе.
[/smart]
[warn header="Не прекращайте всплытие без необходимости!"]
Всплытие -- это удобно. Не прекращайте его без явной нужды, очевидной и архитектурно прозрачной.
Зачастую прекращение всплытия создаёт свои подводные камни, которые потом приходится обходить.
Например:
<ol>
<li>Мы делаем меню. Оно обрабатывает клики на своих элементах и делает для них `stopPropagation`. Вроде бы, всё работает.</li>
<li>Позже мы решили отслеживать все клики в окне, для какой-то своей функциональности, к примеру, для статистики -- где вообще у нас кликают люди. Например, Яндекс.Метрика так делает, если включить соответствующую опцию.</li>
<li>Над областью, где клики убиваются `stopPropagation`, статистика работать не будет! Получилась "мёртвая зона".</li>
</ol>
Проблема в том, что `stopPropagation` убивает всякую возможность отследить событие сверху, а это бывает нужно для реализации чего-нибудь "эдакого", что к меню отношения совсем не имеет.
[/warn]
## Погружение
В современном стандарте, кроме "всплытия" событий, предусмотрено ещё и "погружение".
Оно гораздо менее востребовано, но иногда, очень редко, знание о нём может быть полезным.
[cut]
## Три стадии прохода события
Кроме всплытия, есть ещё стадии прохода события.
В соответствии со стандартом, их три:
<ol>
<li>Событие сначала идет сверху вниз. Эта стадия называется *"стадия перехвата"* (capturing stage).</li>
<li>Событие достигло целевого элемента. Это -- *"стадия цели"* (target stage).</li>
<li>После этого событие начинает всплывать. Это -- *"стадия всплытия"* (bubbling stage).</li>
</ol>
В [стандарте DOM Events 3](http://www.w3.org/TR/DOM-Level-3-Events/) это продемонстрировано так:
<img src="eventflow.png">
То есть, при клике на `TD` событие путешествует по цепочке родителей сначала вниз к элементу ("погружается"), а потом наверх ("всплывает"), по пути задействуя обработчики.
**Ранее мы говорили только о всплытии, потому что другие стадии, как правило, не используются и проходят незаметно для нас.**
Обработчики, добавленные через `on...`, ничего не знают о стадии перехвата, а начинают работать со всплытия.
**Чтобы поймать событие на стадии перехвата, нужно использовать третий аргумент `addEventListener`:**
<ul>
<li>Если аргумент `true`, то событие будет перехвачено по дороге вниз.</li>
<li>Если аргумент `false`, то событие будет поймано при всплытии.</li>
</ul>
Стадия цели, обозначенная на рисунке цифрой `(2)`, особо не обрабатывается, так как обработчики, назначаемые обоими этими способами, срабатывают также на целевом элементе.
## Примеры
В примере ниже на `form, div, p` стоят те же обработчики, что и раньше, но на этот раз -- на стадии погружения.
Чтобы увидеть перехват в действии, кликните на элементе `P`:
[example height=220 src="capture"]
Обработчики сработают в порядке "сверху-вниз": `FORM` -> `DIV` -> `P`.
JS-код здесь такой:
```js
var elems = document.querySelectorAll('form,div,p');
// на каждый элемент повесить обработчик на стадии перехвата
for(var i=0; i<elems.length; i++) {
elems[i].addEventListener("click", highlightThis, true);
}
```
Никто не мешает назначить обработчики для обеих стадий, вот так:
```js
var elems = document.querySelectorAll('form,div,p');
for(var i=0; i<elems.length; i++) {
elems[i].addEventListener("click", highlightThis, true);
elems[i].addEventListener("click", highlightThis, false);
}
```
Кликните по внутреннему элементу `P`, чтобы увидеть порядок прохода события:
[example height=220 src="both"]
Должно быть `FORM` -> `DIV` -> `P` -> `P` -> `DIV` -> `FORM`. Заметим, что элемент `P` участвует в обоих стадиях.
Как видно из примера, один и тот же обработчик можно назначить на разные стадии. При этом номер текущей стадии он, при необходимости, может получить из свойства `event.eventPhase` (=1, если погружение, =3, если всплытие).
[smart header="Есть события, которые не всплывают, но которые можно перехватить"]
Есть события, которые можно поймать только на стадии перехвата, а на стадии всплытия -- нельзя..
Например, таково событие фокусировки на элементе [onfocus](/focus-blur).
[/smart]
## Отличия IE8-
Чтобы было проще ориентироваться, я собрал отличия IE8-, которые имеют отношение ко всплытию, в одну секцию.
Их знание понадобится, если вы решите писать на чистом JS, без фреймворков и вам понадобится поддержка IE8-.
<dl>
<dt>Нет свойства `event.currentTarget`</dt>
<dd>Обратим внимание, что при назначении обработчика через `onсвойство` у нас есть `this`, поэтому `event.currentTarget`, как правило, не нужно, а вот при назначении через `attachEvent` обработчик не получает `this`, так что текущий элемент, если нужен, можно будет взять лишь из замыкания.</dd>
<dt>Вместо `event.target` в IE8- используется `event.srcElement`</dt>
<dd>Если мы пишем обработчик, который будет поддерживать и IE8- и современные браузеры, то можно начать его так:
```js
elem.onclick = function(event) {
event = event || window.event;
var target = event.target || event.srcElement;
// ... теперь у нас есть объект события и target
...
}
```
</dd>
<dt>Для остановки всплытия используется код `event.cancelBubble=true`.</dt>
<dd>Кросс-браузерно остановить всплытие можно так:
```js
event.stopPropagation ? event.stopPropagation() : (event.cancelBubble=true);
```
</dd>
</dl>
Далее в учебнике мы будем использовать стандартные свойства и вызовы, поскольку добавление этих строк, обеспечивающих совместимость -- достаточно простая и очевидная задача.
Ещё раз хотелось бы заметить -- эти отличия понадобятся при написании JS-кода с поддержкой IE8- без фреймворков. Почти все JS-фреймворки обеспечивают кросс-браузерную поддержку `target`, `currentTarget` и `stopPropagation()`.
## Итого
Алгоритм:
<ul>
<li>При наступлении события -- элемент, на котором оно произошло, помечается как "целевой" (`event.target`).</li>
<li>Далее событие сначала двигается вниз от корня документа к `event.target`, по пути вызывая обработчики, поставленные через `addEventListener(...., true)`.</li>
<li>Далее событие двигается от `event.target` вверх к корню документа, по пути вызывая обработчики, поставленные через `on*` и `addEventListener(...., false)`.</li>
</ul>
Каждый обработчик имеет доступ к свойствам события:
<ul>
<li>`event.target` -- самый глубокий элемент, на котором прозошло событие.</li>
<li>`event.currentTarget` (=`this`) -- элемент, на котором в данный момент сработал обработчик (до которого "доплыло" событие).</li>
<li>`event.eventPhase` -- на какой фазе он сработал (погружение =1, всплытие = 3).</li>
</ul>
Любой обработчик может остановить событие вызовом `event.stopPropagation()`, но делать это не рекомендуется, так как в дальнейшем это событие может понадобиться, иногда для самых неожиданных вещей.
В современной разработке стадия погружения используется очень редко.
Этому есть две причины:
<ol>
<li>Историческая, так как IE лишь с версии 9 в полной мере поддерживает современный стандарт.</li>
<li>Разумная -- когда происходит событие, то разумно дать возможность первому сработать обработчику на самом элементе, поскольку он наиболее конкретен. Код, который поставил обработчик именно на этот элемент, знает максимум деталей о том, что это за элемент, чем он занимается, и обработчик через замыкание, скорее всего, имеет к ним доступ.
Далее имеет смысл передать обработку события родителю -- он тоже понимает, что происходит, но уже менее детально, далее -- выше, и так далее, до самого объекта `document`, обработчик на котором реализовывает самую общую функциональность уровня документа.</li>
</ol>

View file

@ -0,0 +1,28 @@
form {
background-color: green;
position: relative;
width: 150px;
height: 150px;
text-align: center;
cursor: pointer;
}
div {
background-color: blue;
position: absolute;
top: 25px;
left: 25px;
width: 100px;
height: 100px;
}
p {
background-color: red;
position: absolute;
top: 25px;
left: 25px;
width: 50px;
height: 50px;
line-height: 50px;
margin: 0;
}

View file

@ -0,0 +1,14 @@
<!DOCTYPE HTML>
<html>
<body>
<link type="text/css" rel="stylesheet" href="example.css">
<form>FORM
<div>DIV
<p>P</p>
</div>
</form>
<script src="script.js"></script>
</body>
</html>

View file

@ -0,0 +1,13 @@
var elems = document.querySelectorAll('form,div,p');
for(var i=0; i<elems.length; i++) {
elems[i].addEventListener("click", highlightThis, true);
elems[i].addEventListener("click", highlightThis, false);
}
function highlightThis() {
this.style.backgroundColor = 'yellow';
alert(this.tagName);
this.style.backgroundColor = '';
}

View file

@ -0,0 +1,28 @@
form {
background-color: green;
position: relative;
width: 150px;
height: 150px;
text-align: center;
cursor: pointer;
}
div {
background-color: blue;
position: absolute;
top: 25px;
left: 25px;
width: 100px;
height: 100px;
}
p {
background-color: red;
position: absolute;
top: 25px;
left: 25px;
width: 50px;
height: 50px;
line-height: 50px;
margin: 0;
}

View file

@ -0,0 +1,14 @@
<!DOCTYPE HTML>
<html>
<body>
<link type="text/css" rel="stylesheet" href="example.css">
<form id="form">FORM
<div>DIV
<p>P</p>
</div>
</form>
<script src="script.js"></script>
</body>
</html>

View file

@ -0,0 +1,10 @@
var form = document.querySelector('form');
form.onclick = function(event) {
event.target.style.backgroundColor = 'yellow';
alert("target = " + event.target.tagName + ", this=" + this.tagName);
event.target.style.backgroundColor = '';
};

View file

@ -0,0 +1,28 @@
form {
background-color: green;
position: relative;
width: 150px;
height: 150px;
text-align: center;
cursor: pointer;
}
div {
background-color: blue;
position: absolute;
top: 25px;
left: 25px;
width: 100px;
height: 100px;
}
p {
background-color: red;
position: absolute;
top: 25px;
left: 25px;
width: 50px;
height: 50px;
line-height: 50px;
margin: 0;
}

View file

@ -0,0 +1,14 @@
<!DOCTYPE HTML>
<html>
<body>
<link type="text/css" rel="stylesheet" href="example.css">
<form>FORM
<div>DIV
<p>P</p>
</div>
</form>
<script src="script.js"></script>
</body>
</html>

View file

@ -0,0 +1,12 @@
var elems = document.querySelectorAll('form,div,p');
for(var i=0; i<elems.length; i++) {
elems[i].addEventListener("click", highlightThis, true);
}
function highlightThis() {
this.style.backgroundColor = 'yellow';
alert(this.tagName);
this.style.backgroundColor = '';
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View file

@ -0,0 +1,3 @@
Поставьте обработчик `click` на контейнере. Он должен проверять, произошел ли клик на кнопке удаления (`target`), и если да, то удалять соответствующий ей `DIV`.
[edit src="solution"]Открыть в песочнице[/edit]

View file

@ -0,0 +1,44 @@
<!DOCTYPE HTML>
<html>
<head>
<link rel="stylesheet" type="text/css" href="messages.css">
<meta charset="utf-8">
</head>
<body>
<div id="messages-container">
<div class="pane">
<h3>Лошадь</h3>
<p>Домашняя лошадь — животное семейства непарнокопытных, одомашненный и единственный сохранившийся подвид дикой лошади, вымершей в дикой природе, за исключением небольшой популяции лошади Пржевальского.</p>
<span class="remove-button"></span>
</div>
<div class="pane">
<h3>Осёл</h3>
<p>Домашний осёл или ишак — одомашненный подвид дикого осла, сыгравший важную историческую роль в развитии хозяйства и культуры человека. Все одомашненные ослы относятся к африканским ослам.</p>
<span class="remove-button"></span>
</div>
<div class="pane">
<h3>Корова, а также пара слов о диком быке, о волах и о тёлках. </h3>
<p>Коро́ва — самка домашнего быка, одомашненного подвида дикого быка, парнокопытного жвачного животного семейства полорогих. Самцы вида называются быками, молодняк — телятами, кастрированные самцы — волами. Молодых (до первой стельности) самок называют тёлками.</p>
<span class="remove-button"></span>
</div>
</div>
<script>
document.getElementById('messages-container').onclick = function(e) {
e = e || window.event;
var target = e.target || e.srcElement;
// без цикла, т.к. мы точно знаем, что внутри нет тегов
if (target.className != 'remove-button') return;
target.parentNode.style.display = 'none';
}
</script>
</body>
</html>

View file

@ -0,0 +1,35 @@
body {
margin: 10px auto;
width: 470px;
}
h3 {
margin: 0;
padding-bottom: .3em;
font-size: 1.1em;
}
p {
margin: 0;
padding: 0 0 .5em;
}
.pane {
position: relative;
padding: 10px 20px 10px;
border-top: solid 2px #c4df9b;
background: #edf5e1;
}
.remove-button {
position: absolute;
top: 10px;
right: 10px;
display: block;
width: 16px;
height: 16px;
background: url(http://js.cx/clipart/delete.gif) no-repeat;
cursor: pointer;
}
h3 {
padding-right: 20px;
}

View file

@ -0,0 +1,34 @@
<!DOCTYPE HTML>
<html>
<head>
<link rel="stylesheet" type="text/css" href="messages.css">
<meta charset="utf-8">
</head>
<body>
<div id="messages-container">
<div class="pane">
<h3>Лошадь</h3>
<p>Домашняя лошадь — животное семейства непарнокопытных, одомашненный и единственный сохранившийся подвид дикой лошади, вымершей в дикой природе, за исключением небольшой популяции лошади Пржевальского.</p>
<span class="remove-button"></span>
</div>
<div class="pane">
<h3>Осёл</h3>
<p>Домашний осёл или ишак — одомашненный подвид дикого осла, сыгравший важную историческую роль в развитии хозяйства и культуры человека. Все одомашненные ослы относятся к африканским ослам.</p>
<span class="remove-button"></span>
</div>
<div class="pane">
<h3>Корова, а также пара слов о диком быке, о волах и о тёлках. </h3>
<p>Коро́ва — самка домашнего быка, одомашненного подвида дикого быка, парнокопытного жвачного животного семейства полорогих. Самцы вида называются быками, молодняк — телятами, кастрированные самцы — волами. Молодых (до первой стельности) самок называют тёлками.</p>
<span class="remove-button"></span>
</div>
</div>
<script>
/* ваш код */
</script>
</body>
</html>

View file

@ -0,0 +1,32 @@
body {
margin: 10px auto;
width: 470px;
}
h3 {
margin: 0;
padding-bottom: .3em;
padding-right: 20px;
font-size: 1.1em;
}
p {
margin: 0;
padding: 0 0 .5em;
}
.pane {
background: #edf5e1;
padding: 10px 20px 10px;
border-top: solid 2px #c4df9b;
position: relative;
}
.remove-button {
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
display: block;
background: url(http://js.cx/clipart/delete.gif) no-repeat;
width: 16px;
height: 16px;
}

View file

@ -0,0 +1,12 @@
# Скрытие сообщения с помощью делегирования
[importance 5]
Дан список сообщений. Добавьте каждому сообщению кнопку для его удаления.
**Используйте делегирование событий. Один обработчик для всего.**
В результате, должно работать вот так(кликните на крестик):
[iframe src="solution" height=500]
[edit src="source" task/]

View file

@ -0,0 +1,109 @@
# Схема решения
Дерево устроено как вложенный список.
Клики на все элементы можно поймать, повесив единый обработчик `onclick` на внешний `UL`.
Как поймать клик на заголовке? Элемент `LI` является блочным, поэтому нельзя понять, был ли клик на *тексте*, или справа от него.
Например, ниже -- участок дерева с выделенными рамкой узлами. Кликните справа от любого заголовка. Видите, клик ловится? А лучше бы такие клики (не на тексте) игнорировать.
```html
<!--+ autorun height=190 untrusted -->
<style>
li { border: 1px solid green; }
</style>
<ul onclick="alert(event.target)">
<li>Млекопетающие
<ul>
<li>Коровы</li>
<li>Ослы</li>
<li>Собаки</li>
<li>Тигры</li>
</ul>
</li>
</ul>
```
В примере выше видно, что проблема в верстке, в том что `LI` занимает всю ширину. Можно кликнуть справа от текста, это все еще `LI`.
Один из способов это поправить -- обернуть заголовки в дополнительный элемент `SPAN`, и обрабатывать только клики внутри `SPAN'ов`, получать по `SPAN'у` его родителя `LI` и ставить ему класс открыт/закрыт.
Напишите для этого JavaScript-код.
# Оборачиваем заголовки в SPAN
Следующий код ищет все `LI` и оборачивает текстовые узлы в `SPAN`.
```js
var treeUl = document.getElementsByTagName('ul')[0];
var treeLis = treeUl.getElementsByTagName('li');
for(var i=0; i<treeLis.length; i++) {
var li = treeLis[i];
var span = document.createElement('span');
li.insertBefore(span, li.firstChild); // добавить пустой SPAN
span.appendChild(span.nextSibling); // переместить в него заголовок
}
```
Теперь можно отслеживать клики *на заголовках*.
Так выглядит дерево с обёрнутыми в `SPAN` заголовками и делегированием:
```html
<!--+ autorun height=190 untrusted -->
<style>
span { border: 1px solid red; }
</style>
<ul onclick="alert(event.target.tagName)">
<li><span>Млекопетающие</span>
<ul>
<li><span>Коровы</span></li>
<li><span>Ослы</span></li>
<li><span>Собаки</span></li>
<li><span>Тигры</span></li>
</ul>
</li>
</ul>
```
Так как `SPAN` -- инлайновый элемент, он всегда такого же размера как текст. Да здравствует `SPAN`!
В реальной жизни дерево, скорее всего, будет сразу со `SPAN`: если HTML-код дерева генерируется на сервере, то это несложно, если дерево генерируется в JavaScript -- тем более просто.
# Итоговое решение
Для делегирования нужно по клику понять, на каком узле он произошел.
В нашем случае у `SPAN` нет детей-элементов, поэтому не нужно подниматься вверх по цепочке родителей. Достаточно просто проверить `event.target.tagName == 'SPAN'`, чтобы понять, где был клик, и спрятать потомков.
```js
var tree = document.getElementsByTagName('ul')[0];
tree.onclick = function(event) {
var target = event.target;
if (target.tagName != 'SPAN') {
return; // клик был не на заголовке
}
var li = target.parentNode; // получить родительский LI
// получить UL с потомками -- это первый UL внутри LI
var node = li.getElementsByTagName('ul')[0];
if (!node) return; // потомков нет -- ничего не надо делать
// спрятать/показать (можно и через CSS-класс)
node.style.display = node.style.display ? '' : 'none';
}
```
Выделение узлов жирным при наведении делается при помощи CSS-селектора `:hover`.
[edit src="solution"]Полное решение[/edit]

View file

@ -0,0 +1,86 @@
<!DOCTYPE HTML>
<html>
<head>
<style>
.tree span:hover {
font-weight: bold;
}
.tree span {
cursor: pointer;
}
</style>
<meta charset="utf-8">
</head>
<body>
<ul class="tree">
<li>Животные
<ul>
<li>Млекопитающие
<ul>
<li>Коровы</li>
<li>Ослы</li>
<li>Собаки</li>
<li>Тигры</li>
</ul>
</li>
<li>Другие
<ul>
<li>Змеи</li>
<li>Птицы</li>
<li>Ящерицы</li>
</ul>
</li>
</ul>
</li>
<li>Рыбы
<ul>
<li>Аквариумные
<ul>
<li>Гуппи</li>
<li>Скалярии</li>
</ul>
</li>
<li>Морские
<ul>
<li>Морская форель</li>
</ul>
</li>
</ul>
</li>
</ul>
<script>
var tree = document.getElementsByTagName('ul')[0];
var treeLis = tree.getElementsByTagName('li');
/* wrap all textNodes into spans */
for(var i=0; i<treeLis.length; i++) {
var li = treeLis[i];
var span = document.createElement('span');
li.insertBefore(span, li.firstChild);
span.appendChild(span.nextSibling);
}
/* catch clicks on whole tree */
tree.onclick = function(event) {
var target = event.target;
if (target.tagName != 'SPAN') {
return;
}
/* now we know SPAN is clicked */
var node = target.parentNode.getElementsByTagName('ul')[0];
if (!node) return; // no children
node.style.display = node.style.display ? '' : 'none';
}
</script>
</body>
</html>

View file

@ -0,0 +1,45 @@
<!DOCTYPE HTML>
<html>
<head><meta charset="utf-8"></head>
<body>
<ul class="tree">
<li>Животные
<ul>
<li>Млекопитающие
<ul>
<li>Коровы</li>
<li>Ослы</li>
<li>Собаки</li>
<li>Тигры</li>
</ul>
</li>
<li>Другие
<ul>
<li>Змеи</li>
<li>Птицы</li>
<li>Ящерицы</li>
</ul>
</li>
</ul>
</li>
<li>Рыбы
<ul>
<li>Аквариумные
<ul>
<li>Гуппи</li>
<li>Скалярии</li>
</ul>
</li>
<li>Морские
<ul>
<li>Морская форель</li>
</ul>
</li>
</ul>
</li>
</ul>
</body>
</html>

View file

@ -0,0 +1,19 @@
# Раскрывающееся дерево
[importance 5]
Создайте дерево, которое по клику на заголовок скрывает-показывает детей:
[iframe border=1 src="solution"]
Требования:
<ul>
<li>Использовать делегирование.</li>
<li>Клик вне текста заголовка (на пустом месте) ничего делать не должен.</li>
<li>При наведении на заголовок -- он становится жирным, реализовать через CSS.</li>
</ul>
P.S. При необходимости HTML/CSS дерева можно изменить.
[edit src="source" task/]

View file

@ -0,0 +1,21 @@
# Подсказка (обработчик)
<ol>
<li>Обработчик `onclick` можно повесить один, на всю таблицу или `THEAD`. Он будет игнорировать клики не на `TH`.</li>
<li>При клике на `TH` обработчик будет получать номер из `TH`, на котором кликнули (`TH.cellIndex`) и вызывать функцию `sortColumn`, передавая ей номер колонки и тип.</li>
<li>Функция `sortColumn(colNum, type)` будет сортировать.</li>
</ol>
# Подсказка (сортировка)
Функция сортировки:
<ol>
<li>Переносит все `TR` из `TBODY` в массив `rowsArr`</li>
<li>Сортирует массив, используя `rowsArr.sort(compare)`, функция `compare` зависит от типа столбца.</li>
<li>Добавляет `TR` из массива обратно в `TBODY`</li>
</ol>
# Решение
[edit src="solution"/]

View file

@ -0,0 +1,117 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<style>
th {
cursor: pointer;
}
th:hover {
background: yellow;
}
</style>
</head>
<body>
<table id="grid">
<thead>
<tr>
<th data-type="number">Возраст</th>
<th data-type="string">Имя</th>
</tr>
</thead>
<tbody>
<tr>
<td>5</td>
<td>Вася</td>
</tr>
<tr>
<td>2</td>
<td>Петя</td>
</tr>
<tr>
<td>12</td>
<td>Женя</td>
</tr>
<tr>
<td>9</td>
<td>Маша</td>
</tr>
<tr>
<td>1</td>
<td>Илья</td>
</tr>
</tbody>
</table>
<script>
// сортировка таблицы
// использовать делегирование!
// должно быть масштабируемо:
// код работает без изменений при добавлении новых столбцов и строк
var grid = document.getElementById('grid');
grid.onclick = function(e) {
var target = e && e.target || window.event.srcElement;
if (target.tagName != 'TH') return;
// Если TH -- сортируем
sortGrid(target.cellIndex, target.getAttribute('data-type'));
};
function sortGrid(colNum, type) {
var tbody = grid.getElementsByTagName('tbody')[0];
// Составить массив из TR
var rowsArray = [].slice.call(tbody.rows);
// определить функцию сравнения, в зависимости от типа
var compare;
switch(type) {
case 'number':
compare = function(rowA, rowB) {
return rowA.cells[colNum].innerHTML - rowB.cells[colNum].innerHTML;
};
break;
case 'string':
compare = function(rowA, rowB) {
return rowA.cells[colNum].innerHTML > rowB.cells[colNum].innerHTML ? 1 : -1;
};
break;
}
// сортировать
rowsArray.sort(compare);
// Убрать tbody из большого DOM документа для лучшей производительности
grid.removeChild(tbody);
// Убрать TR из TBODY.
// Присваивание tbody.innerHTML = '' не работает в IE
//
// на самом деле без этих строк можно обойтись!
// при добавлении appendChild все узлы будут сами перемещены на правильное место!
while(tbody.firstChild) {
tbody.removeChild(tbody.firstChild);
}
// добавить результат в нужном порядке в TBODY
for(var i=0; i<rowsArray.length; i++) {
tbody.appendChild(rowsArray[i]);
}
grid.appendChild(tbody);
}
// P.S. В IE7 cells, cellIndex не работают, если элемент вне документа
</script>
</body>
</html>

View file

@ -0,0 +1,53 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<style>
th {
cursor: pointer;
}
th:hover {
background: yellow;
}
</style>
</head>
<body>
<table id="grid">
<thead>
<tr>
<th data-type="number">Возраст</th>
<th data-type="string">Имя</th>
</tr>
</thead>
<tbody>
<tr>
<td>5</td>
<td>Вася</td>
</tr>
<tr>
<td>2</td>
<td>Петя</td>
</tr>
<tr>
<td>12</td>
<td>Женя</td>
</tr>
<tr>
<td>9</td>
<td>Маша</td>
</tr>
<tr>
<td>1</td>
<td>Илья</td>
</tr>
</tbody>
</table>
<script>
/* ваш код */
</script>
</body>
</html>

View file

@ -0,0 +1,20 @@
# Сортировка таблицы
[importance 4]
Сделать сортировку таблицы при клике на заголовок.
Демо:
[iframe border=1 src="solution"]
Требования:
<ul>
<li>Использовать делегирование.</li>
<li>Код не должен меняться при увеличении количества столбцов или строк.</li>
</ul>
[edit src="source" task/]
P.S. Обратите внимание, тип столбца задан атрибутом у заголовка. Это необходимо, ведь числа сортируются иначе чем строки. Соответственно, код это может использовать.
P.P.S. Вам помогут дополнительные [навигационные ссылки по таблицам](#dom-navigation-tables).

View file

@ -0,0 +1,201 @@
# Делегирование событий
Всплытие событий позволяет реализовать один из самых важных приёмов разработки -- *делегирование*.
Он заключается в том, что если у нас есть много элементов, события на которых нужно обрабатывать похожим образом, то вместо того, чтобы назначать обработчик каждому -- мы ставим один обработчик на их общего предка. Из него можно получить целевой элемент `event.target`, понять на каком именно потомке произошло событие и обработать его.
## Пример "Ба Гуа"
Рассмотрим пример -- <a href="http://en.wikipedia.org/wiki/Ba_gua">диаграмму "Ба Гуа"</a>. Это таблица, отражающая древнюю китайскую философию.
Вот она:
[iframe height=350 src="bagua" edit link]
Её HTML (схематично):
```html
<table>
<tr>
<th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
</tr>
<tr>
<td>...<strong>Northwest</strong>...</td>
<td>...</td>
<td>...</td>
</tr>
<tr>...еще 2 строки такого же вида...</tr>
<tr>...еще 2 строки такого же вида...</tr>
</table>
```
В этой таблице всего 9 ячеек, но могло быть и 99, и даже 9999, не важно.
**Наша задача -- реализовать подсветку ячейки `<td>` при клике.**
Вместо того, чтобы назначать обработчик для каждой ячейки, мы повесим *один обработчик* на элемент `<table>`.
Он использует `event.target`, чтобы получить элемент, на котором произошло событие, и подсветить его.
Код будет таким:
```js
var selectedTd;
*!*
table.onclick = function(event) {
var target = event.target; // где был клик?
if (target.tagName != 'TD') return; // не на TD? тогда не интересует
highlight(target); // подсветить TD
};
*/!*
function highlight(node) {
if (selectedTd) {
selectedTd.classList.remove('highlight');
}
selectedTd = node;
selectedTd.classList.add('highlight');
}
```
Такой код будет работать и ему без разницы, сколько ячеек в таблице. Обработчик всё равно один. Я могу добавлять, удалять `<td>` из таблицы, менять их количество -- моя подсветка будет стабильно работать, так как обработчик стоит на `<table>`.
Однако, у текущей версии кода есть недостаток.
**Клик может быть не на том теге, который нас интересует, а внутри него.**
В нашем случае клик может произойти на вложенном элементе, внутри `<td>`, например на `<strong>`. Такой клик будет пойман по пути наверх, но `target` у него будет не `<td>`, а `<strong>`:
<img src="bagua.png">
**Внутри обработчика `table.onclick` мы должны найти нужный `<td>` по `event.target`.**
Для этого мы вручную, используя ссылку `parentNode`, будем идти вверх по иерархии родителей от `event.target` и выше и проверять:
<ul>
<li>Если нашли `<td>`, значит это то что нужно.</li>
<li>Если дошли до элемента `table` и при этом `<td>` не найден, то наверное клик был вне `<td>`, например на элементе заголовка таблицы.</li>
</ul>
Улучшенный обработчик `table.onclick` с циклом `while`, который этот делает:
```js
table.onclick = function(event) {
var target = event.target;
// цикл двигается вверх от target к родителям до table
while(target != table) {
if (target.tagName == 'TD') {
// нашли элемент, который нас интересует!
highlight(target);
return;
}
target = target.parentNode;
}
// возможна ситуация, когда клик был вне <td>
// если цикл дошёл до table и ничего не нашёл,
// то обработчик просто заканчивает работу
}
```
[smart]
Кстати, в проверке `while` можно бы было использовать `this` вместо `table`:
```js
while(target != this) {
// ...
}
```
Это тоже будет работать, так как в обработчике `table.onclick` значением `this` является текущий элемент, то есть `table`.
[/smart]
## Применение делегирования: действия в разметке
Обычно делегирование -- это средство оптимизации интерфейса. Мы используем один обработчик для *схожих* действий на однотипных элементах.
**Но делегирование позволяет использовать обработчик и для абсолютно разных действий.**
Например, нам нужно сделать меню с разными кнопками: "Сохранить", "Загрузить", "Поиск" и т.д. И есть объект с соответствующими методами: `save`, `load`, `search` и т.п...
Первое, что может прийти в голову -- это найти каждую кнопку и назначить ей свой обработчик среди методов объекта.
Но более изящно решить задачу можно путем добавления одного обработчика на всё меню, а для каждой кнопки в специальном атрибуте, который мы назовем `data-action` (можно придумать любое название, но `data-*` является валидным в HTML5), укажем, что она должна вызывать:
```html
<button *!*data-action="save"*/!*>Нажмите, чтобы Сохранить</button>
```
Обработчик считывает содержимое атрибута и выполняет метод. Взгляните на рабочий пример:
```html
<!--+ autorun height=auto -->
<div id="menu">
<button data-action="save">Сохранить</button>
<button data-action="load">Загрузить</button>
<button data-action="search">Поиск</button>
</div>
<script>
function Menu(elem) {
this.save = function() { alert('сохраняю'); };
this.load = function() { alert('загружаю'); };
this.search = function() { alert('ищу'); };
var self = this;
elem.onclick = function(e) {
var target = e.target;
*!*
var action = target.getAttribute('data-action');
if (action) {
self[action]();
}
*/!*
};
}
new Menu(menu);
</script>
```
Обратите внимание, как используется трюк с `var self = this`, чтобы сохранить ссылку на объект `Menu`. Иначе обработчик просто бы не смог вызвать методы `Menu`, потому что *его собственный `this` ссылается на элемент*.
Что в этом случае нам дает использование делегирования событий?
[compare]
+Не нужно писать код, чтобы присвоить обработчик каждой кнопке. Меньше кода, меньше времени, потраченного на инициализацию.
+Структура HTML становится по-настоящему гибкой. Мы можем добавлять/удалять кнопки в любое время.
+Данный подход является семантичным. Мы можем использовать классы `.action-save`, `.action-load` вместо атрибута `data-action`, если захотим.
[/compare]
## Итого
Делегирование событий -- это здорово! Пожалуй, это один из самых полезных приёмов для работы с DOM. Он отлично подходит, если есть много элементов, обработка которых очень схожа.
Алгоритм:
<ol>
<li>Вешаем обработчик на контейнер.</li>
<li>В обработчике: получаем `event.target`.</li>
<li>В обработчике: если необходимо, проходим вверх цепочку `target.parentNode`, пока не найдем нужный подходящий элемент (и обработаем его), или пока не упремся в контейнер (`this`). </li>
</ol>
Зачем использовать:
[compare]
+Упрощает инициализацию и экономит память: не нужно вешать много обработчиков.
+Меньше кода: при добавлении и удалении элементов не нужно ставить или снимать обработчики.
+Удобство изменений: можно массово добавлять или удалять элементы путём изменения `innerHTML`.
[/compare]
Конечно, у делегирования событий есть свои ограничения.
[compare]
-Во-первых, событие должно всплывать, и нельзя, чтобы какой-то промежуточный обработчик вызвал `event.stopPropagation()`.
-Во-вторых, делегирование создает дополнительную нагрузку на браузер, ведь обработчик запускается, когда событие происходит в любом месте контейнера, не обязательно на элементах, которые нам интересны. Но обычно эта нагрузка невелика и не является проблемой.
[/compare]

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,56 @@
#bagua-table th {
text-align: center;
font-weight: bold;
}
#bagua-table td {
width: 150px;
white-space: nowrap;
text-align: center;
vertical-align: bottom;
padding-top: 5px;
padding-bottom: 12px;
}
#bagua-table .nw {
background: #999;
}
#bagua-table .n {
background: #03f;
color: #fff;
}
#bagua-table .ne {
background: #ff6;
}
#bagua-table .w {
background: #ff0;
}
#bagua-table .c {
background: #60c;
color: #fff;
}
#bagua-table .e {
background: #09f;
color: #fff;
}
#bagua-table .sw {
background: #963;
color: #fff;
}
#bagua-table .s {
background: #f60;
color: #fff;
}
#bagua-table .se {
background: #0c3;
color: #fff;
}
#bagua-table .highlight {
background: red;
}

View file

@ -0,0 +1,66 @@
<!DOCTYPE HTML>
<html>
<body>
<link type="text/css" rel="stylesheet" href="bagua.css">
<table id="bagua-table">
<tr>
<th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
</tr>
<tr>
<td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders
</td>
<td class="n"><strong>North</strong><br>Water<br>Blue<br>Change
</td>
<td class="ne"><strong>Northeast</strong><br>Earth<br>Yellow<br>Direction
</td>
</tr>
<tr>
<td class="w"><strong>West</strong><br>Metal<br>Gold<br>Youth
</td>
<td class="c"><strong>Center</strong><br>All<br>Purple<br>Harmony
</td>
<td class="e"><strong>East</strong><br>Wood<br>Blue<br>Future
</td>
</tr>
<tr>
<td class="sw"><strong>Southwest</strong><br>Earth<br>Brown<br>Tranquility
</td>
<td class="s"><strong>South</strong><br>Fire<br>Orange<br>Fame
</td>
<td class="se"><strong>Southeast</strong><br>Wood<br>Green<br>Romance
</td>
</tr>
</table>
<script>
var table = document.getElementById('bagua-table');
var selectedTd;
table.onclick = function(event) {
var target = event.target;
while(target != this) {
if (target.tagName == 'TD') {
highlight(target);
return;
}
target = target.parentNode;
}
}
function highlight(node) {
if (selectedTd) {
selectedTd.classList.remove('highlight');
}
selectedTd = node;
selectedTd.classList.add('highlight');
}
</script>
</body>
</html>

View file

@ -0,0 +1,76 @@
# Приём проектирования "поведение"
Шаблон проектирования "поведение" (behavior) позволяет задавать хитрые обработчики на элементе *декларативно*, установкой специальных HTML-атрибутов и классов.
[cut]
Например, хочется, чтобы при клике на один элемент показывался другой. Конечно, можно поставить обработчик в атрибуте `onclick` или даже описать его где-нибудь в JS-коде страницы.
Но есть решение другое, и очень изящное.
## Описание
Приём проектирования "поведение" состоит из двух частей:
<ol>
<li>Элементу ставится атрибут, описывающий его поведение.</li>
<li>При помощи делегирования ставится обработчик на документ, который ловит все клики и, если элемент имеет нужный атрибут, производит нужное действие.
</ol>
## Пример
Например, я хочу, чтобы при клике на один элемент скрывался/показывался другой.
Конечно, можно написать соответствующий обработчик в JavaScript.
А что, если подобная задача возникает часто?
Хотелось бы получить более простой способ задания такого *поведения*, например -- *декларативно*, при помощи особого атрибута.
**Сделаем так, что при клике на элемент с атрибутом `data-toggle-id` будет скрываться/показываться элемент с заданным `id`.**
```html
<!--+ autorun run height=100 -->
<button *!*data-toggle-id="subscribe-mail"*/!*>
Показать форму подписки
</button>
<form id="subscribe-mail" style="display:none">
Ваша почта: <input type="email">
</form>
<script>
*!*
document.onclick = function(event) {
var target = event.target;
var id = target.getAttribute('data-toggle-id');
if (!id) return;
var elem = document.getElementById(id);
elem.style.display = (elem.style.display == 'none') ? "" : "none";
};
*/!*
</script>
```
**При помощи JavaScript мы добавили "поведение" -- возможность через атрибут указывать, что делает элемент.**
[smart header="Не только атрибут"]
Для своих целей мы можем использовать в HTML любые атрибуты, но стандарт рекомендует для своих целей называть атрибуты `data-*`.
В обработчике `document.onclick` мы могли бы проверять не атрибут, а класс или что-то ещё.
[/smart]
Обратите внимание: обработчик поставлен на `document`, то есть клик на любом элементе страницы пройдёт через него. Теперь для того, чтобы добавить скрытие-раскрытие любому элементу -- даже не надо знать JavaScript, можно просто написать атрибут.
Также для добавления обработчиков на `document` рекомендуется использовать `addEventListener`, чтобы можно было добавлять несколько различных поведений на документ.
## Итого
Шаблон "поведение" удобен тем, что сколь угодно сложное JavaScript-поведение можно "навесить" на элемент одним лишь атрибутом. А можно -- несколькими атрибутами на связанных элементах.
Здесь мы рассмотрели базовый пример, который можно как угодно модифицировать и масштабировать. Важно не переусердствовать.
**Приём разработки "поведение" рекомендуется использовать для расширения возможностей разметки, как альтернативу мини-фрагментам JavaScript.**
Далее у нас ещё будут задачи, где мы реализуем этот приём разработки.

View file

@ -0,0 +1,42 @@
Дело в том, что обработчик из атрибута `onclick` делается браузером как функция с заданным телом.
То есть, он будет таким:
```js
function(event) {
handler()
}
```
При этом возвращаемое `handler` значение никак не используется и не влияет на результат.
Рабочий вариант:
```html
<!--+ run -->
<script>
function handler() {
alert("...");
return false;
}
</script>
<a href="http://w3.org" onclick="*!*return handler()*/!*">w3.org</a>
```
Альтернатива -- передать и использовать объект события для вызова `event.preventDefault()` (или кросс-браузерного варианта для поддержки старых IE).
```html
<!--+ run -->
<script>
*!*
function handler(event) {
alert("...");
event.preventDefault ? event.preventDefault() : (event.returnValue=false);
}
*/!*
</script>
<a href="http://w3.org" onclick="*!*handler(event)*/!*">w3.org</a>
```

View file

@ -0,0 +1,21 @@
# Почему не работает return false?
[importance 3]
Почему в этом документе `return false` не работает?
```html
<!--+ autorun run -->
<script>
function handler() {
alert("...");
return false;
}
</script>
<a href="http://w3.org" onclick="handler()">w3.org</a>
```
По замыслу, переход на `w3.org` при клике должен отменяться. Однако, на самом деле он происходит.
В чём дело и как поправить, сохранив `onclick` в HTML?

View file

@ -0,0 +1,29 @@
Это -- классическая задача на тему делегирования.
В реальной жизни, мы можем перехватить событие и создать AJAX-запрос к серверу, который сохранит информацию о том, по какой ссылке ушел посетитель.
Мы перехватываем событие на `contents` и поднимаемся до `parentNode` пока не получим `A` или не упремся в контейнер.
```js
contents.onclick = function(evt) {
var target = evt.target;
function handleLink(href) {
var isLeaving = confirm('Уйти на '+href+'?');
if (!isLeaving) return false;
}
while(target != this) {
if (target.nodeName == 'A') {
*!*
return handleLink(target.getAttribute('href')); // (*)
*/!*
}
target = target.parentNode;
}
};
```
В строке `(*)` используется атрибут, а не свойство `href`, чтобы показать в `confirm` именно то, что написано в HTML-атрибуте, так как свойство может отличаться, оно обязано содержать полный валидный адрес.
[edit src="solution"]Полное решение[/edit].

View file

@ -0,0 +1,33 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="contents">
<p>
Как насчет почитать <a href="http://wikipedia.org">Википедию</a>, или посетить <a href="http://w3.org"><i>W3.org</i></a> и узнать про современные стандарты?
</p>
</div>
<script>
document.getElementById('contents').onclick = function(evt) {
var evt = evt || event
var target = evt.target || evt.srcElement
function handleLink(href) {
var isLeaving = confirm('Уйти на '+href+'?')
if (!isLeaving) return false
}
while(target != this) {
if (target.nodeName == 'A') {
return handleLink(target.getAttribute('href'));
}
target = target.parentNode
}
}
</script>
</body>
</html>

View file

@ -0,0 +1,15 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="contents">
<p>
Как насчет почитать <a href="http://wikipedia.org">Википедию</a>, или посетить <a href="http://w3.org"><i>W3.org</i></a> и узнать про современные стандарты?
</p>
</div>
</body>
</html>

View file

@ -0,0 +1,18 @@
# Поймайте переход по ссылке
[importance 5]
Сделайте так, чтобы при клике на ссылки внутри <code>&lt;DIV id="contents"&gt;</code> пользователю выводился вопрос о том, действительно ли он хочет покинуть страницу и если он не хочет, то прерывать переход по ссылке.
Так это должно работать:
[iframe height=100 border=1 src="solution"]
Детали:
<ul>
<li>Содержимое блока `DIV` может быть загружено динамически и присвоено при помощи `innerHTML`. Так что найти все ссылки и поставить на них обработчики нельзя. Используйте делегирование.</li>
<li>Содержимое может содержать вложенные теги, *в том числе внутри ссылок*, например, `<a href=".."><i>...</i></a>`.</li>
</ul>
[edit src="source" task/]

View file

@ -0,0 +1,57 @@
Решение состоит в том, чтобы добавить обработчик на контейнер `#thumbs` и отслеживать клики на ссылках.
Когда происходит событие, обработчик должен изменять `src` `#largeImg` на `href` ссылки и заменять `alt` на ее `title`.
Код решения:
```js
var largeImg = document.getElementById('largeImg');
document.getElementById('thumbs').onclick = function(e) {
e = e || window.event;
var target = e.target || e.srcElement;
while(target != this) {
if (target.nodeName == 'A') {
showThumbnail(target.href, target.title);
return false;
}
target = target.parentNode;
}
}
function showThumbnail(href, title) {
largeImg.src = href;
largeImg.alt = title;
}
```
**Предзагрузка картинок**
Для того, чтобы картинка загрузилась, достаточно создать новый элемент `IMG` и указать ему `src`, вот так:
```js
var imgs = thumbs.getElementsByTagName('img');
for(var i=0; i<imgs.length; i++) {
var url = imgs[i].parentNode.href;
*!*
var img = document.createElement('img');
img.src = url;
*/!*
}
```
Как только элемент создан и ему назначен `src`, браузер сам начинает скачивать файл картинки.
При правильных настройках сервера как-то использовать этот элемент не обязательно -- картинка уже закеширована.
**Семантичная верстка**
Для списка картинок используется `DIV`. С точки зрения семантики более верный вариант -- список `UL/LI`.
[edit src="solution"]Полное решение[/edit]

View file

@ -0,0 +1,44 @@
body {
margin: 0;
padding: 0;
font: 75%/120% Arial, Helvetica, sans-serif;
}
h2 {
font: bold 190%/100% Arial, Helvetica, sans-serif;
margin: 0 0 .2em;
}
h2 em {
font: normal 80%/100% Arial, Helvetica, sans-serif;
color: #999999;
}
#largeImg {
border: solid 1px #ccc;
width: 550px;
height: 400px;
padding: 5px;
}
#thumbs a {
border: solid 1px #ccc;
width: 100px;
height: 100px;
padding: 3px;
margin: 2px;
float: left;
}
#thumbs a:hover {
border-color: #FF9900;
}
#thumbs li {
list-style: none;
}
#thumbs {
margin: 0;
padding: 0;
}

View file

@ -0,0 +1,59 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Галерея</title>
<link rel="stylesheet" type="text/css" href="gallery.css">
<meta charset="utf-8">
</head>
<body>
<p><img id="largeImg" src="http://js.cx/gallery/img1-lg.jpg" alt="Large image"></p>
<ul id="thumbs">
<!-- При наведении на изображение показывается встроенная подсказка браузера (title) -->
<li><a href="http://js.cx/gallery/img2-lg.jpg" title="Image 2"><img src="http://js.cx/gallery/img2-thumb.jpg"></a></li>
<li><a href="http://js.cx/gallery/img3-lg.jpg" title="Image 3"><img src="http://js.cx/gallery/img3-thumb.jpg"></a></li>
<li><a href="http://js.cx/gallery/img4-lg.jpg" title="Image 4"><img src="http://js.cx/gallery/img4-thumb.jpg"></a></li>
<li><a href="http://js.cx/gallery/img5-lg.jpg" title="Image 5"><img src="http://js.cx/gallery/img5-thumb.jpg"></a></li>
<li><a href="http://js.cx/gallery/img6-lg.jpg" title="Image 6"><img src="http://js.cx/gallery/img6-thumb.jpg"></a></li>
</ul>
<script>
var largeImg = document.getElementById('largeImg');
var thumbs = document.getElementById('thumbs');
thumbs.onclick = function(e) {
e = e || window.event;
var target = e.target || e.srcElement;
while(target != this) {
if (target.nodeName == 'A') {
showThumbnail(target.href, target.title);
return false;
}
target = target.parentNode;
}
}
function showThumbnail(href, title) {
largeImg.src = href;
largeImg.alt = title;
}
/* предзагрузка */
var imgs = thumbs.getElementsByTagName('img');
for(var i=0; i<imgs.length; i++) {
var url = imgs[i].parentNode.href;
var img = document.createElement('img');
img.src = url;
}
</script>
</body>
</html>

View file

@ -0,0 +1,35 @@
body {
margin: 0;
padding: 0;
font: 75%/120% Arial, Helvetica, sans-serif;
}
h2 {
font: bold 190%/100% Arial, Helvetica, sans-serif;
margin: 0 0 .2em;
}
h2 em {
font: normal 80%/100% Arial, Helvetica, sans-serif;
color: #999999;
}
#largeImg {
border: solid 1px #ccc;
width: 550px;
height: 400px;
padding: 5px;
}
#thumbs a {
border: solid 1px #ccc;
width: 100px;
height: 100px;
padding: 3px;
margin: 2px;
float: left;
}
#thumbs a:hover {
border-color: #FF9900;
}

View file

@ -0,0 +1,23 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Галерея</title>
<link rel="stylesheet" type="text/css" href="gallery.css">
<meta charset="utf-8">
</head>
<body>
<p><img id="largeImg" src="http://js.cx/gallery/img1-lg.jpg" alt="Large image"></p>
<div id="thumbs">
<!-- При наведении на изображение показывается встроенная подсказка браузера (title) -->
<a href="http://js.cx/gallery/img2-lg.jpg" title="Image 2"><img src="http://js.cx/gallery/img2-thumb.jpg"></a>
<a href="http://js.cx/gallery/img3-lg.jpg" title="Image 3"><img src="http://js.cx/gallery/img3-thumb.jpg"></a>
<a href="http://js.cx/gallery/img4-lg.jpg" title="Image 4"><img src="http://js.cx/gallery/img4-thumb.jpg"></a>
<a href="http://js.cx/gallery/img5-lg.jpg" title="Image 5"><img src="http://js.cx/gallery/img5-thumb.jpg"></a>
<a href="http://js.cx/gallery/img6-lg.jpg" title="Image 6"><img src="http://js.cx/gallery/img6-thumb.jpg"></a>
</div>
</body>
</html>

View file

@ -0,0 +1,22 @@
# Галерея изображений
[importance 5]
Создайте галерею изображений, в которой основное изображение изменяется при клике на уменьшенный вариант.
Результат должен выглядеть так:
[iframe src="solution" height=600]
Для обработки событий используйте делегирование, т.е. не более одного обработчика.
[edit src="source" task/]
P.S. Обратите внимание -- клик может быть как на маленьком изображении `IMG`, так и на `A` вне него. При этом `event.target` будет, соответственно, либо `IMG`, либо `A`.
Дополнительно:
<ul>
<li>Если получится -- сделайте предзагрузку больших изображений, чтобы при клике они появлялись сразу.</li>
<li>Всё ли в порядке с семантической вёрсткой в HTML исходного документа? Если нет -- поправьте, чтобы было, как нужно.</li>
</ul>

View file

@ -0,0 +1,152 @@
# Действия браузера по умолчанию
Многие события влекут за собой действие браузера.
Например:
<ul>
<li>Клик по ссылке инициирует переход на новый URL</li>
<li>Нажатие на кнопку "отправить" в форме -- посылку ее на сервер</li>
<li>Двойной клик на тексте -- инициирует его выделение.</li>
</ul>
**Зачастую, мы полностью обрабатываем событие в JavaScript, и такое действие браузера нам не нужно.**
К счастью, его можно отменить.
[cut]
## Отмена действия браузера
Есть два способа отменить действие браузера:
<ul>
<li>**Основной способ -- это воспользоваться объектом события. Для отмены действия браузера существует стандартный метод `event.preventDefault()`.**</li>
<li>Если же обработчик назначен через `on...` (не через `addEventListener/attachEvent`), то можно просто вернуть `false` из обработчика.</li>
</ul>
В следующем примере при клике по ссылке переход не произойдет:
```html
<!--+ autorun -->
<a href="/" onclick="return false">Нажми здесь</a>
или
<a href="/" onclick="event.preventDefault()">здесь</a>
```
[warn header="Возвращать `true` не нужно"]
Вообще говоря, значение, которое возвращает обработчик, игнорируется.
Единственное исключение -- это `return false` из обработчика, назначенного через `onсобытие`.
Иногда в коде начинающих разработчиков можно увидеть `return` других значений. Но они не нужны и никак не обрабатываются.
[/warn]
### Пример: меню
Рассмотрим задачу, когда нужно создать меню для сайта, например такое:
```html
<ul id="menu" class="menu">
<li><a href="/php">PHP</a></li>
<li><a href="/html">HTML</a></li>
<li><a href="/javascript">JavaScript</a></li>
<li><a href="/flash">Flash</a></li>
</ul>
```
Данный пример при помощи CSS может выводиться так:
[iframe height=70 src="menu" link edit]
**Все элементы меню являются ссылками, то есть тегами `<a>`.**
Это потому, что некоторые посетители очень любят сочетание "правый клик - открыть в новом окне". Да, мы можем использовать и `<button>` и `<span>`, но если правый клик не работает -- это их огорчает. Кроме того, если на сайт зайдёт поисковик, то по ссылке из `<a href="...">` он перейдёт, а выполнить сложный JavaScript и получить результат -- вряд ли захочет.
**Значение `<a href="...">` -- это "запасной вариант", для правого клика и для поисковиков, а обычно клик будет обрабатываться JavaScript.**
Например, вот так:
```js
menu.onclick = function(event) {
if (event.target.nodeName != 'A') return;
var href = event.target.getAttribute('href');
alert(href); // может быть подгрузка с сервера, генерация интерфейса и т.п.
*!*
return false; // отменить переход по url
*/!*
};
```
В конце `return false`, иначе браузер перейдёт по адресу из `href`.
Так как мы применили делегирование, то меню может увеличиваться, можно добавить вложенные списки `ul/li`, стилизовать их при помощи CSS -- меню продолжит работать.
## Другие действия браузера
Действий браузера по умолчанию достаточно много.
Вот некоторые примеры событий, которые вызывают действие браузера:
<ul>
<li>`mousedown` -- нажатие кнопкой мыши в то время как курсор находится на тексте начинает его выделение.</li>
<li>`click` на `<input type="checkbox">` -- ставит или убирает галочку.</li>
<li>`submit` -- при нажатии на `<input type="submit">` в форме данные отправляются на сервер.</li>
<li>`wheel` -- движение колёсика мыши инициирует прокрутку.</li>
<li>`keydown` -- при нажатии клавиши в поле ввода появляется символ.</li>
<li>`contextmenu` -- при правом клике показывается контекстное меню браузера.</li>
<li>...</li>
</ul>
Все эти действия можно отменить, если мы хотим обработать событие исключительно при помощи JavaScript.
[warn header="События могут быть связаны между собой"]
Некоторые события естественным образом вытекают друг из друга.
Например, нажатие мышкой `mousedown` на поле ввода `<input>` приводит к фокусировке внутрь него. Если отменить действие `mousedown`, то и фокуса не будет.
Попробуйте нажать мышкой на первый `<input>` -- произойдёт событие `onfocus`. Это обычная ситуация.
Но если нажать на второй, то фокусировки не произойдёт.
```html
<!--+ run autorun -->
<input value="Фокус работает" onfocus="this.value=''">
<input *!*onmousedown="return false"*/!* onfocus="this.value=''" value="Кликни меня">
```
...С другой стороны, во второй `<input>` можно перейти с первого нажатием клавиши [key Tab], и тогда фокусировка сработает. То есть, дело здесь именно в `onmousedown="return false"`.
[/warn]
## Особенности IE8-
В IE8- для отмены действия по умолчанию нужно назначить свойство `event.returnValue = false`.
Кроссбраузерный код для отмены действия по умолчанию:
```js
element.onclick = function(event) {
event = event || window.event;
if (event.preventDefault) { // если метод существует
event.preventDefault(); // то вызвать его
} else { // иначе вариант IE<9:
event.returnValue = false;
}
}
```
Можно записать в одну строку:
```js
...
event.preventDefault ? event.preventDefault() : (event.returnValue=false);
...
```
## Итого
<ul>
<li>Браузер имеет встроенные действия при ряде событий -- переход по ссылке, отправка формы и т.п. Как правило, их можно отменить.</li>
<li>Есть два способа отменить действие по умолчанию: первый -- использовать `event.preventDefault()` (IE<9: `event.returnValue=false`), второй -- `return false` из обработчика. Второй способ работает только если обработчик назначен через `onсобытие`.</li>
</ul>

View file

@ -0,0 +1,19 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="menu.css"/>
</head>
<body>
<ul id="menu" class="menu">
<li><a href="/php">PHP</a></li>
<li><a href="/html">HTML</a></li>
<li><a href="/javascript">JavaScript</a></li>
<li><a href="/flash">Flash</a></li>
</ul>
<script src="menu.js"></script>
</body>
</html>

View file

@ -0,0 +1,25 @@
.menu li {
display: inline-block;
margin: 0;
}
.menu > li a {
display: inline-block;
margin: 0 2px;
outline: none;
text-align: center;
text-decoration: none;
font: 14px/100% Arial, Helvetica, sans-serif;
padding: .5em 2em .55em;
text-shadow: 0 1px 1px rgba(0,0,0,.3);
border-radius: .5em;
box-shadow: 0 1px 2px rgba(0,0,0,.2);
color: #d9eef7;
border: solid 1px #0076a3;
background: #0095cd;
}
.menu > li:hover a {
text-decoration: none;
background: #007ead;
}

View file

@ -0,0 +1,8 @@
menu.onclick = function(event) {
if (event.target.nodeName != 'A') return;
var href = event.target.getAttribute('href');
alert(href);
return false; // prevent url change
};

View file

@ -0,0 +1,384 @@
# Генерация событий на элементах
Можно не только слушать браузерные события, но и генерировать их самому.
Это нужно довольно редко, преимущественно -- для целей автоматического тестирования.
[cut]
## Конструктор Event
Вначале рассмотрим современный способ генерации событий, по стандарту [DOM 4](http://www.w3.org/TR/dom/#introduction-to-dom-events). Он поддерживается всеми браузерами, кроме IE11-. А далее рассмотрим устаревшие варианты, поддерживаемые IE.
**Объект события создаётся при помощи конструктора [Event](http://www.w3.org/TR/dom/#event).**
Синтаксис:
```js
var event = new Event(тип события[, флаги]);
```
Где:
<ul>
<li>*Тип события* -- может быть как своим, так и встроенным, к примеру `"click"`.</li>
<li>*Флаги* -- объект вида `{ bubbles: true/false, cancelable: true/false }`, где свойство `bubbles` указывает, всплывает ли событие, а `cancelable` -- можно ли отменить действие по умолчанию.
Не обязателен, по умолчанию `{bubbles: false, cancelable: false}`.</li>
</ul>
### Метод dispatchEvent
Затем, чтобы инициировать событие, запускается `elem.dispatchEvent(event)`.
Событие обрабатывается "как обычно", передаётся обработчикам, всплывает... Этот метод возвращает `false`, если событие было отменено при помощи `preventDefault()`. Конечно, отмена возможна лишь если событие изначально было создано с флагом `cancelable`.
При просмотре примера ниже кнопка будет нажата скриптом:
```html
<!--+ run -->
<button id="elem" onclick="alert('Клик');">Автоклик</button>
<script>
var event = new Event("click");
elem.dispatchEvent(event);
</script>
```
[smart header="Как отличить реальное нажатие от скриптового?"]
В целях безопасности иногда хорошо бы знать -- инициировано ли действие посетителем или это кликнул скрипт.
Единственный способ, которым код может отличить реальное нажатие от программного, является проверка свойства `event.isTrusted`.
Оно на момент написания статьи поддерживается IE и Firefox и равно `true`, если посетитель кликнул сам, и всегда `false` -- если событие инициировал скрипт.
[/smart]
Браузер автоматически ставит следующие свойства объекта `event`:
<ul>
<li>`isTrusted: false` -- означает, что событие сгенерировано скриптом, это свойство изменить невозможно.</li>
<li>`target: null` -- это свойство ставится автоматически позже при `dispatchEvent`.</li>
<li>`type: тип события` -- первый аргумент `new Event`.</li>
<li>`bubbles`, `cancelable` -- по второму аргументу `new Event`.</li>
</ul>
Другие свойства события, если они нужны, например координаты для события мыши -- можно присвоить в объект события позже, например:
```js
var event = new Event("click");
event.clientX = 100;
event.clientY = 100;
```
### Пример с hello
Можно генерировать события с любыми названиями.
Для примера сгенерируем совершенно новое событие `"hello"`:
```html
<!--+ run -->
<h1 id="elem">Привет от скрипта!</h1>
<script>
document.addEventListener("hello", function(event) { // (1)
alert("Привет");
event.preventDefault(); // (2)
}, false);
var event = new Event("hello", {bubbles: true, cancelable: true}); // (3)
if (elem.dispatchEvent(event) === false) {
alert('Событие было отменено preventDefault');
}
</script>
```
Обратите внимание:
<ol>
<li>Обработчик события `hello` стоит на `document`. Мы его поймаем на всплытии.</li>
<li>Вызов `event.preventDefault()` приведёт к тому, что `dispatchEvent` вернёт `false`.</li>
<li>Чтобы событие всплывало и его можно было отменить, указан второй аргумент `new Event`.</li>
</ol>
Никакой разницы между встроенными событиями (`click`) и своими (`hello`) здесь нет, они создаются и работают совершенно одинаково.
## Конструкторы MouseEvent, KeyboardEvent и другие
Для конкретных типов событий есть свои конструкторы.
Вот список конструкторов для различных событий интерфейса которые можно найти в спецификации [UI Event](http://www.w3.org/TR/uievents/):
<ul>
<li>`UIEvent`</li>
<li>`FocusEvent`</li>
<li>`MouseEvent`</li>
<li>`WheelEvent`</li>
<li>`KeyboardEvent`</li>
<li>`CompositionEvent`</li>
</ul>
Вместо `new Event("click")` можно вызвать `new MouseEvent("click")`.
**Конкретный конструктор позволяет указать стандартные свойства для данного типа события.**
Например:
```js
//+ run
var e = new MouseEvent("click", {
bubbles: true,
cancelable: true,
clientX: 100,
clientY: 100
});
*!*
alert(e.clientX); // 100
*/!*
```
Сравните это с обычным `Event`:
```js
//+ run
var e = new Event("click", {
bubbles: true,
cancelable: true,
clientX: 100,
clientY: 100
});
*!*
alert(e.clientX); // undefined
*/!*
```
...То есть, "мышиные" свойства можно сразу же в конструкторе указать только если это `MouseEvent`, а `Event` их игнорирует.
**Использование конкретного конструктора не является обязательным, можно обойтись `Event`.**
Свойства можно присвоить и явно, после конструктора. Здесь это скорее вопрос удобства и желания следовать правилам. События, которые генерирует браузер, всегда имеют правильный тип.
Полный список свойств по типам событий вы найдёте в спецификации, например для `MouseEvent`: [MouseEvent Constructor](http://www.w3.org/TR/uievents/#constructor-mouseevent).
## Свои события
Для генерации встроенных событий существуют описанные выше конструкторы, а для генерации своих, нестандартных, событий существует конструктор [CustomEvent](http://www.w3.org/TR/dom/#customevent).
Технически, он абсолютно идентичен `Event`, кроме небольшой детали: у второго аргумента-объекта есть дополнительное свойство `detail`, в котором можно указывать информацию для передачи в событие.
Например:
```html
<!--+ run -->
<h1 id="elem">Привет для Васи!</h1>
<script>
elem.addEventListener("hello", function(event) {
alert( *!*event.detail.name*/!* );
}, false);
var event = new CustomEvent("hello", {
*!*
detail: { name: "Вася" }
*/!*
});
elem.dispatchEvent(event);
</script>
```
Надо сказать, что никто не мешает и в обычное `Event` записать любые свойства. Но `CustomEvent` более явно говорит, что событие не встроенное, а своё, и выделяет отдельно "информационное" поле `detail`, в которое можно записать что угодно без конфликта со стандартными свойствами объекта.
## Старое API для IE9+
В предыдущем стандарте [DOM 3 Events](http://www.w3.org/TR/DOM-Level-3-Events) была предусмотрена [иерархия событий](http://www.w3.org/TR/DOM-Level-3-Events/#event-interfaces), с различными методами инициализации.
Она поддерживается как современными браузерами, так и IE9+. Для генерации событий используется немного другой синтаксис, но по возможностям -- всё то же самое, что и в современном стандарте.
Объект события создаётся вызовом `document.createEvent`:
```js
var event = document.createEvent(eventInterface);
```
Аргументы:
<ul>
<li>`eventInterface` -- это тип события, например `MouseEvent`, `FocusEvent`, `KeyboardEvent`. В [секции 5 DOM 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/#events-module) есть подробный список, какое событие к какому интерфейсу относится.
</li>
</ul>
**На практике можно всегда использовать самый общий интерфейс: `document.createEvent("Event")`.**
Далее событие нужно инициализовать:
```js
event.initEvent(type, boolean bubbles, boolean cancelable);
```
Аргументы:
<ul>
<li>`type` -- тип события, например `"click"`.</li>
<li>`bubbles` -- всплывает ли событие.</li>
<li>`cancelable` -- можно ли отменить событие`.</li>
</ul>
Эти два кода аналогичны:
```js
// современный стандарт
var event = new Event("click", { bubbles: true, cancelable: true });
// старый стандарт
var event = document.createEvent("Event");
event.initEvent("click", true, true);
```
Единственная разница -- старый стандарт поддерживается IE9+.
Этот пример с событием `hello` будет работать во всех браузерах, кроме IE8-:
```html
<!--+ run -->
<h1 id="elem">Привет от скрипта!</h1>
<script>
document.addEventListener("hello", function(event) {
alert("Привет");
event.preventDefault();
}, false);
*!*
var event = document.createEvent("Event");
event.initEvent("hello", true, true);
*/!*
if (elem.dispatchEvent(event) === false) {
alert('Событие было отменено preventDefault');
}
</script>
```
[smart header="`initMouseEvent`, `initKeyboardEvent` и другие..."]
У конкретных типов событий, например `MouseEvent`, `KeyboardEvent`, есть методы, которые позволяют указать стандартные свойства.
Они называются по аналогии: `initMouseEvent`, `initKeyboardEvent`.
Их можно использовать вместо базового `initEvent`, если хочется, чтобы свойства событий соответствовали встроенным браузерным.
Выглядят они немного страшновато, например (взято из [спецификации](http://www.w3.org/TR/DOM-Level-3-Events/#idl-interface-MouseEvent-initializers)):
```js
void initMouseEvent (
DOMString typeArg, // тип
boolean bubblesArg, // всплывает?
boolean cancelableArg, // можно отменить?
AbstractView? viewArg, // объект window, null означает текущее окно
long detailArg, // свойство detail и другие...
long screenXArg,
long screenYArg,
long clientXArg,
long clientYArg,
boolean ctrlKeyArg,
boolean altKeyArg,
boolean shiftKeyArg,
boolean metaKeyArg,
unsigned short buttonArg,
EventTarget? relatedTargetArg);
};
```
Для инициализации мышиного события нужно обязательно указать *все* аргументы, например:
```html
<!--+ run -->
<button id="elem">Автоклик</button>
<script>
elem.onclick = function(e) {
alert('Клик на координатах ' + e.clientX + ':' + e.clientY);
};
var event = document.createEvent("MouseEvent");
*!*
event.initMouseEvent("click", true, true, null, 0, 0, 0, 100, 100, true, true, true, null, 1, null);
*/!*
elem.dispatchEvent(event);
</script>
```
Браузер, по стандарту, может сгенерировать отсутствующие свойства самостоятельно, например `pageX`, но это нужно проверять в конкретных случаях, обычно это не работает или работает некорректно.
[/smart]
## Антистандарт: IE8-
В совсем старом IE были "свои" методы `document.createEventObject()` и `elem.fireEvent()`.
Пример с ними для IE8:
```html
<!--+ run -->
<button id="elem">Автоклик</button>
<script>
document.body.onclick = function() {
alert("Клик, event.type=" + event.type);
return false;
};
*!*
var event = document.createEventObject();
if( !elem.fireEvent("onclick", event) ) {
alert('Событие было отменено');
}
*/!*
</script>
```
**При помощи `fireEvent` можно сгенерировать только встроенные события.**
Если указать `"hello"` вместо `"onclick"` в примере выше -- будет ошибка.
Параметры `bubbles` и `cancelable` настраивать нельзя, браузер использует стандартные для данного типа событий.
## Кросс-браузерный пример
Для поддержки IE9+ достаточно использовать методы `document.createEvent` и `event.initEvent`, как показано выше, и всё будет хорошо.
Если же нужен IE8, то подойдёт такой код:
```js
function trigger(elem, type){
if (document.createEvent) {
var event = document.createEvent('Event') :
event.initEvent(type);
return elem.dispatchEvent(event);
}
var event = document.createEventObject();
return elem.fireEvent("on"+type, event);
}
// использование:
trigger(elem, "click");
```
Конечно, надо иметь в виду, что в IE8 события можно использовать только встроенные, а `bubbles` и `cancelable` поставить нельзя.
## Итого
<ul>
<li>Все браузеры, кроме IE, позволяют генерировать любые события, следуя стандарту DOM4.</li>
<li>IE9+ тоже справляется, если использовать вызовы более старого стандарта, и имеет в итоге тот же функционал.</li>
<li>IE8- может генерировать только встроенные события.</li>
</ul>
**Несмотря на техническую возможность генерировать браузерные события -- пользоваться ей стоит с большой осторожностью.**
В 98% случаев, когда разработчик начинающего или среднего уровня хочет сгенерировать *встроенное* событие -- это вызвано "кривой" архитектурой кода, и взаимодействие нужно на уровне выше.
Как правило события имеет смысл генерировать:
<ul>
<li>Либо как явный и грубый хак, чтобы заставить работать сторонние библиотеки, в которых не предусмотрены другие средства взаимодействия.</li>
<li>Либо для автоматического тестирования, чтобы скриптом "нажать на кнопку" и посмотреть, произошло ли нужное действие.</li>
<li>Либо при создании своих "элементов интерфейса". Например, никто не мешает при помощи JavaScript создать из `<div class="calendar">` красивый календарь и генерировать на нём событие `change` при выборе даты. Эту тему мы разовьём позже.</li>
</ul>

View file

@ -0,0 +1,3 @@
# Основы работы с событиями
Введение в браузерные события, общие свойства всех событий и приёмы работы с ними.