up
|
@ -0,0 +1,22 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<input type="button" id="hider" value="Click to hide the text" />
|
||||
|
||||
<div id="text">Text</div>
|
||||
|
||||
<script>
|
||||
// Here it doesn't matter how we hide the text,
|
||||
// could also use style.display:
|
||||
document.getElementById('hider').onclick = function() {
|
||||
document.getElementById('text').hidden = true;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<input type="button" id="hider" value="Нажмите, чтобы спрятать текст" />
|
||||
|
||||
<div id="text">Текст</div>
|
||||
|
||||
<script>
|
||||
/* ваш код */
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,11 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Hide on click
|
||||
|
||||
Add JavaScript to the `button` to make `<div id="text">` disappear when we click it.
|
||||
|
||||
The demo:
|
||||
|
||||
[iframe border=1 src="solution" height=80]
|
|
@ -0,0 +1,5 @@
|
|||
Can use `this` in the handler to reference "itself" here:
|
||||
|
||||
```html run height=50
|
||||
<input type="button" onclick="this.hidden=true" value="Click to hide">
|
||||
```
|
|
@ -0,0 +1,12 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Hide self
|
||||
|
||||
Create a button that hides itself on click.
|
||||
|
||||
```online
|
||||
Like this:
|
||||
<input type="button" onclick="this.hidden=true" value="Click to hide">
|
||||
```
|
|
@ -0,0 +1,16 @@
|
|||
The answer: `1` and `2`.
|
||||
|
||||
The first handler triggers, because it's not removed by `removeEventListener`. To remove the handler we need to pass exactly the function that was assigned. And in the code a new function is passed, that looks the same, but is still another function.
|
||||
|
||||
To remove a function object, we need to store a reference to it, like this:
|
||||
|
||||
```js
|
||||
function handler() {
|
||||
alert(1);
|
||||
}
|
||||
|
||||
button.addEventListener("click", handler);
|
||||
button.removeEventListener("click", handler);
|
||||
```
|
||||
|
||||
The handler `button.onclick` works independantly and in addition to `addEventListener`.
|
|
@ -0,0 +1,17 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Which handlers run?
|
||||
|
||||
There's a button in the variable. There are no handlers on it.
|
||||
|
||||
Which handlers run on click after the following code? Which alerts show up?
|
||||
|
||||
```js no-beautify
|
||||
button.addEventListener("click", () => alert("1"));
|
||||
|
||||
button.removeEventListener("click", () => alert("1"));
|
||||
|
||||
button.onclick = () => alert(2);
|
||||
```
|
|
@ -0,0 +1,71 @@
|
|||
|
||||
# HTML/CSS
|
||||
First let's create HTML/CSS.
|
||||
|
||||
A menu is a standalone graphical component on the page, so its better to put it into a single DOM element.
|
||||
|
||||
A list of menu items can be layed out as a list `ul/li`.
|
||||
|
||||
Here's the example structure:
|
||||
|
||||
```html
|
||||
<div class="menu">
|
||||
<span class="title">Sweeties (click me)!</span>
|
||||
<ul>
|
||||
<li>Cake</li>
|
||||
<li>Donut</li>
|
||||
<li>Honey</li>
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
We use `<span>` for the title, because `<div>` has an implicit `display:block` on it, and it will occupy 100% of the horizontal width.
|
||||
|
||||
Like this:
|
||||
|
||||
```html autorun height=50
|
||||
<div style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</div>
|
||||
```
|
||||
|
||||
So if we set `onclick` on it, then it will catch clicks to the right of the text.
|
||||
|
||||
...but `<span>` has an implicit `display: inline`, so it occupies exactly enough place to fit all the text:
|
||||
|
||||
```html autorun height=50
|
||||
<span style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</span>
|
||||
```
|
||||
|
||||
# Toggling the menu
|
||||
|
||||
Toggling the menu should change the arrow and show/hide the menu list.
|
||||
|
||||
All these changes are perfectly handled by CSS. In JavaScript we should label the current state of the menu by adding/removing the class `.open`.
|
||||
|
||||
Without it, the menu will be closed:
|
||||
|
||||
```css
|
||||
.menu ul {
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
padding-left: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu .title::before {
|
||||
content: '▶ ';
|
||||
font-size: 80%;
|
||||
color: green;
|
||||
}
|
||||
```
|
||||
|
||||
...And with `.open` the arrow changes and the list shows up:
|
||||
|
||||
```css
|
||||
.menu.open .title::before {
|
||||
content: '▼ ';
|
||||
}
|
||||
|
||||
.menu.open ul {
|
||||
display: block;
|
||||
}
|
||||
```
|
|
@ -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 {
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu .title::before {
|
||||
content: '▶ ';
|
||||
font-size: 80%;
|
||||
color: green;
|
||||
}
|
||||
|
||||
.menu.open .title::before {
|
||||
content: '▼ ';
|
||||
}
|
||||
|
||||
.menu.open ul {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="sweeties" class="menu">
|
||||
<span class="title">Sweeties (click me)!</span>
|
||||
<ul>
|
||||
<li>Cake</li>
|
||||
<li>Donut</li>
|
||||
<li>Honey</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let menuElem = document.getElementById('sweeties');
|
||||
let titleElem = menuElem.querySelector('.title');
|
||||
|
||||
titleElem.onclick = function() {
|
||||
menuElem.classList.toggle('open');
|
||||
};
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
▶ ▼ Sweeties (click me)!
|
||||
<ul>
|
||||
<li>Cake</li>
|
||||
<li>Donut</li>
|
||||
<li>Honey</li>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,11 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Create a menu sliding menu
|
||||
|
||||
Create a menu that opens/collapses on click:
|
||||
|
||||
[iframe border=1 height=100 src="solution"]
|
||||
|
||||
P.S. HTML/CSS of the source document is to be modified.
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
1. Изменим HTML/CSS, чтобы кнопка была в нужном месте сообщения. Кнопка -- это тег `<button>`, поэтому понадобится несколько стилей.
|
||||
|
||||
Расположить кнопку справа можно при помощи `position:relative` для `pane`, а для кнопки `position:absolute + right/top`. Так как `position:absolute` вынимает элемент из потока, то кнопка может частично оказаться "сверху" текста заголовка, перекрыв его конец. Чтобы этого не произошло, можно добавить `padding-right` к заголовку.
|
||||
|
||||
Если использовать `float:right`, то кнопка никогда не перекроет текст. Это, пожалуй хорошо.
|
||||
|
||||
С другой стороны, потенциальным преимуществом способа с `position` по сравнению с `float` в данном случае является возможность поместить элемент кнопки в HTML *после текста*, а не до него.
|
||||
2. Для того, чтобы получить кнопку из контейнера, используем `querySelectorAll`. На каждую кнопку повесим обработчик, который будет убирать родителя. Найти родителя можно через `parentNode`.
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" href="messages.css">
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div>
|
||||
<div class="pane">
|
||||
<h3>Horse</h3>
|
||||
<p>The horse is one of two extant subspecies of Equus ferus. It is an odd-toed ungulate mammal belonging to the taxonomic family Equidae. The horse has evolved over the past 45 to 55 million years from a small multi-toed creature, Eohippus, into the large, single-toed animal of today.</p>
|
||||
</div>
|
||||
<div class="pane">
|
||||
<h3>Donkey</h3>
|
||||
<p>The donkey or ass (Equus africanus asinus) is a domesticated member of the horse family, Equidae. The wild ancestor of the donkey is the African wild ass, E. africanus. The donkey has been used as a working animal for at least 5000 years.</p>
|
||||
</div>
|
||||
<div class="pane">
|
||||
<h3>Cat</h3>
|
||||
<p>The domestic cat (Latin: Felis catus) is a small, typically furry, carnivorous mammal. They are often called house cats when kept as indoor pets or simply cats when there is no need to distinguish them from other felids and felines. Cats are often valued by humans for companionship and for their ability to hunt vermin.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
let panes = document.querySelectorAll('.pane');
|
||||
|
||||
for(let pane of panes) {
|
||||
pane.insertAdjacentHTML("afterbegin", '<button class="remove-button">[x]</button>');
|
||||
// button becomes the first child of pane
|
||||
pane.firstChild.onclick = () => pane.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,37 @@
|
|||
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;
|
||||
font-size: 110%;
|
||||
top: 0;
|
||||
color: darkred;
|
||||
right: 10px;
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="messages.css">
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
The button code (may need to adjust CSS):
|
||||
<button class="remove-button">[x]</button>
|
||||
|
||||
<div>
|
||||
<div class="pane">
|
||||
<h3>Horse</h3>
|
||||
<p>The horse is one of two extant subspecies of Equus ferus. It is an odd-toed ungulate mammal belonging to the taxonomic family Equidae. The horse has evolved over the past 45 to 55 million years from a small multi-toed creature, Eohippus, into the large, single-toed animal of today.</p>
|
||||
</div>
|
||||
<div class="pane">
|
||||
<h3>Donkey</h3>
|
||||
<p>The donkey or ass (Equus africanus asinus) is a domesticated member of the horse family, Equidae. The wild ancestor of the donkey is the African wild ass, E. africanus. The donkey has been used as a working animal for at least 5000 years.</p>
|
||||
</div>
|
||||
<div class="pane">
|
||||
<h3>Cat</h3>
|
||||
<p>The domestic cat (Latin: Felis catus) is a small, typically furry, carnivorous mammal. They are often called house cats when kept as indoor pets or simply cats when there is no need to distinguish them from other felids and felines. Cats are often valued by humans for companionship and for their ability to hunt vermin.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,32 @@
|
|||
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;
|
||||
}
|
||||
|
||||
.remove-button {
|
||||
font-size: 110%;
|
||||
color: darkred;
|
||||
right: 10px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Add a closing button
|
||||
|
||||
There's a list of messages.
|
||||
|
||||
Use JavaScript to add a closing button to the right-upper corner of each message.
|
||||
|
||||
The result should look like this:
|
||||
|
||||
[iframe src="solution" height=450]
|
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 33 KiB |
|
@ -0,0 +1,17 @@
|
|||
The images ribbon can be represented as `ul/li` list of images `<img>`.
|
||||
|
||||
Normally, such a ribbon is wide, but we put a fixed-size `<div>` around to "cut" it, so that only a part of the ribbon is visibble:
|
||||
|
||||

|
||||
|
||||
To make the list show horizontally we need to apply correct CSS properties for `<li>`, like `display: inline-block`.
|
||||
|
||||
For `<img>` we should also adjust `display`, because by default it's `inline`. There's extra space reserved under `inline` elements for "letter tails", so we can use `display:block` to remove it.
|
||||
|
||||
To do the scrolling, we can shift `<ul>`. There are many ways to do it, for instance by changing `margin-left` or (better performance) use `transform: translateX()`:
|
||||
|
||||

|
||||
|
||||
The outer `<div>` has a fixed width, so "extra" images are cut.
|
||||
|
||||
The whole carousel is a self-contained "graphical component" on the page, so we'd better wrap it into a single `<div class="carousel">` and style things inside it.
|
|
@ -0,0 +1,65 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="carousel" class="carousel">
|
||||
<button class="arrow prev">⇦</button>
|
||||
<div class="gallery">
|
||||
<ul class="images">
|
||||
<li><img src="https://js.cx/carousel/1.png"></li>
|
||||
<li><img src="https://js.cx/carousel/2.png"></li>
|
||||
<li><img src="https://js.cx/carousel/3.png"></li>
|
||||
<li><img src="https://js.cx/carousel/4.png"></li>
|
||||
<li><img src="https://js.cx/carousel/5.png"></li>
|
||||
<li><img src="https://js.cx/carousel/6.png"></li>
|
||||
<li><img src="https://js.cx/carousel/7.png"></li>
|
||||
<li><img src="https://js.cx/carousel/8.png"></li>
|
||||
<li><img src="https://js.cx/carousel/9.png"></li>
|
||||
<li><img src="https://js.cx/carousel/10.png"></li>
|
||||
</ul>
|
||||
</div>
|
||||
<button class="arrow next">⇨</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* label the images, just for convenience, to visually track them */
|
||||
let i = 1;
|
||||
for(let li of carousel.querySelectorAll('li')) {
|
||||
li.style.position = 'relative';
|
||||
li.insertAdjacentHTML('beforeend', `<span style="position:absolute;left:0;top:0">${i}</span>`);
|
||||
i++;
|
||||
}
|
||||
|
||||
/* configuration */
|
||||
let width = 130; // image width
|
||||
let count = 3; // visible images count
|
||||
|
||||
let list = carousel.querySelector('ul');
|
||||
let listElems = carousel.querySelectorAll('li');
|
||||
|
||||
let position = 0; // ribbon scroll position
|
||||
|
||||
carousel.querySelector('.prev').onclick = function() {
|
||||
// shift left
|
||||
position += width * count;
|
||||
// can't move to the left too much, end of images
|
||||
position = Math.min(position, 0)
|
||||
list.style.marginLeft = position + 'px';
|
||||
};
|
||||
|
||||
carousel.querySelector('.next').onclick = function() {
|
||||
// shift right
|
||||
position -= width * count;
|
||||
// can only shift the ribbbon for (total ribbon length - visible count) images
|
||||
position = Math.max(position, -width * (listElems.length - count));
|
||||
list.style.marginLeft = position + 'px';
|
||||
};
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,70 @@
|
|||
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;
|
||||
/* make it block to remove space around images */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
padding: 0;
|
||||
background: #ddd;
|
||||
border-radius: 15px;
|
||||
border: 1px solid gray;
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
color: #444;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.arrow:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.arrow:hover {
|
||||
background: #ccc;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.prev {
|
||||
left: 7px;
|
||||
}
|
||||
|
||||
.next {
|
||||
right: 7px;
|
||||
}
|
||||
|
||||
.gallery {
|
||||
width: 390px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gallery ul {
|
||||
height: 130px;
|
||||
width: 9999px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
transition: margin-left 250ms;
|
||||
/* remove white-space between inline-block'ed li */
|
||||
/* http://davidwalsh.name/remove-whitespace-inline-block */
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.gallery li {
|
||||
display: inline-block;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- ваша верстка виджета, ваши стили -->
|
||||
|
||||
<button class="arrow">⇦</button>
|
||||
<button class="arrow">⇨</button>
|
||||
|
||||
|
||||
<ul>
|
||||
<li><img src="https://js.cx/carousel/1.png"></li>
|
||||
<li><img src="https://js.cx/carousel/2.png"></li>
|
||||
<li><img src="https://js.cx/carousel/3.png"></li>
|
||||
<li><img src="https://js.cx/carousel/4.png"></li>
|
||||
<li><img src="https://js.cx/carousel/5.png"></li>
|
||||
<li><img src="https://js.cx/carousel/6.png"></li>
|
||||
<li><img src="https://js.cx/carousel/7.png"></li>
|
||||
<li><img src="https://js.cx/carousel/8.png"></li>
|
||||
<li><img src="https://js.cx/carousel/9.png"></li>
|
||||
<li><img src="https://js.cx/carousel/10.png"></li>
|
||||
</ul>
|
||||
|
||||
|
||||
<script>
|
||||
// label the images, just for convenience, to visually track them
|
||||
// this code can be removed
|
||||
let i = 1;
|
||||
for(let li of carousel.querySelectorAll('li')) {
|
||||
li.style.position = 'relative';
|
||||
li.insertAdjacentHTML('beforeend', `<span style="position:absolute;left:0;top:0">${i}</span>`);
|
||||
i++;
|
||||
}
|
||||
|
||||
// ...your code to make carousel alive!
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,38 @@
|
|||
.arrow {
|
||||
padding: 0;
|
||||
background: #ddd;
|
||||
border-radius: 15px;
|
||||
border: 1px solid gray;
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
color: #444;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.arrow:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.arrow:hover {
|
||||
background: #ccc;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul {
|
||||
height: 130px;
|
||||
width: 9999px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
ul img {
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
display: block; /* removes extra space near images */
|
||||
}
|
||||
|
||||
ul li {
|
||||
display: inline-block; /* removes extra space between list items
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
importance: 4
|
||||
|
||||
---
|
||||
|
||||
# Carousel
|
||||
|
||||
Create a "carousel" -- a ribbon of images that can be scrolled by clicking on arrows.
|
||||
|
||||
[iframe height=200 src="solution"]
|
||||
|
||||
Later we can add more features to it: infinite scrolling, dynamic loading etc.
|
||||
|
||||
P.S. For this task HTML/CSS structure is actually 90% of the solution.
|
342
2-ui/2-events/1-introduction-browser-events/article.md
Normal file
|
@ -0,0 +1,342 @@
|
|||
# Introduction to browser events
|
||||
|
||||
*An event* is a signal that something has happened. All DOM nodes generate such signals (but events are not limited to DOM).
|
||||
|
||||
[cut]
|
||||
|
||||
Here's a list of the most useful DOM events, just to take a look at:
|
||||
|
||||
**Mouse events:**
|
||||
- `click` -- when the mouse clicks on an element (touchscreen devices generate it on a tap).
|
||||
- `contextmenu` -- when the mouse right-clicks on an element.
|
||||
- `mouseover` -- when the mouse cursor comes over an element.
|
||||
- `mousedown` and `mouseup` -- when the mouse button is pressed and released over an element.
|
||||
- `mousemove` -- when the mouse is moved.
|
||||
|
||||
**Form element events:**
|
||||
- `submit` -- when the visitor submits a `<form>`.
|
||||
- `focus` -- when the visitor focuses on an element, e.g. on an `<input>`.
|
||||
|
||||
**Keyboard events:**
|
||||
- `keydown` and `keyup` -- when the visitor presses and then releases the button.
|
||||
|
||||
**Document events**
|
||||
- `DOMContentLoaded` -- when the HTML is loaded and processed, DOM is fully built.
|
||||
|
||||
**CSS events:**
|
||||
- `transitionend` -- when a CSS-animation finishes.
|
||||
|
||||
There are many other events.
|
||||
|
||||
## Event handlers
|
||||
|
||||
To react on events we can assign a *handler* -- a function that runs in case of an event.
|
||||
|
||||
Handlers is a way to run JavaScript code in case of user actions.
|
||||
|
||||
There are several ways to assign a handler. Let's see them, starting from the simplest one.
|
||||
|
||||
### HTML-attribute
|
||||
|
||||
A handler can be set in HTML with an attribute named `on<event>`.
|
||||
|
||||
For instance, to assign a `click` handler for an `input`, we can use `onclick`, like here:
|
||||
|
||||
```html run
|
||||
<input value="Click me" *!*onclick="alert('Click!')"*/!* type="button">
|
||||
```
|
||||
|
||||
On mouse click, the code inside `onclick` runs.
|
||||
|
||||
Please note that inside `onclick` we use single quotes, because the attribute itself is in double quotes. If we fforget that the code is inside the attribute and use double quotes inside, like this: `onclick="alert("Клик!")"`, then it won't work right.
|
||||
|
||||
An HTML-attribute is not a convenient place to write a lot of code, so we'd better create a JavaScript function and call it there.
|
||||
|
||||
Here a click runs the function `countRabbits()`:
|
||||
|
||||
```html autorun height=50
|
||||
<script>
|
||||
function countRabbits() {
|
||||
for(let i=1; i<=3; i++) {
|
||||
alert("Rabbit number " + i);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<input type="button" *!*onclick="countRabbits()"*/!* value="Count rabbits!">
|
||||
```
|
||||
|
||||
As we know, HTML attribute names are not case-sensitive, so `ONCLICK` works as well as `onClick` and `onCLICK`... But usually attributes are lowercased: `onclick`.
|
||||
|
||||
### DOM property
|
||||
|
||||
We can assign a handler using a DOM property `on<event>`.
|
||||
|
||||
For instance, `elem.onclick`:
|
||||
|
||||
```html autorun
|
||||
<input id="elem" type="button" value="Click me">
|
||||
<script>
|
||||
*!*
|
||||
elem.onclick = function() {
|
||||
alert('Thank you');
|
||||
};
|
||||
*/!*
|
||||
</script>
|
||||
```
|
||||
|
||||
If the handler is assigned using an HTML-attribute then the browser reads it, creates a new function from the attribute content and writes it to the DOM property.
|
||||
|
||||
So this way is actually the same as the previous one.
|
||||
|
||||
**The handler is always in the DOM property: the HTML-attribute is just one of the ways to initialize it.**
|
||||
|
||||
These two code pieces work the same:
|
||||
|
||||
1. Only HTML:
|
||||
|
||||
```html autorun height=50
|
||||
<input type="button" *!*onclick="alert('Click!')"*/!* value="Button">
|
||||
```
|
||||
2. HTML + JS:
|
||||
|
||||
```html autorun height=50
|
||||
<input type="button" id="button" value="Button">
|
||||
<script>
|
||||
*!*
|
||||
button.onclick = function() {
|
||||
alert('Click!');
|
||||
};
|
||||
*/!*
|
||||
</script>
|
||||
```
|
||||
|
||||
**As there's only one `onclick` property, we can't assign more than one event handler.**
|
||||
|
||||
In the example below adding a handler with JavaScript overwrites the existing handler:
|
||||
|
||||
```html run height=50 autorun
|
||||
<input type="button" id="elem" onclick="alert('Before')" value="Click me">
|
||||
<script>
|
||||
*!*
|
||||
elem.onclick = function() { // overwrites the existing handler
|
||||
alert('After'); // only this will be shown
|
||||
};
|
||||
*/!*
|
||||
</script>
|
||||
```
|
||||
|
||||
By the way, we can assign an existing function as a handler directly:
|
||||
|
||||
```js
|
||||
function sayThanks() {
|
||||
alert('Thanks!');
|
||||
}
|
||||
|
||||
elem.onclick = sayThanks;
|
||||
```
|
||||
|
||||
To remove a handler -- assign `elem.onclick = null`.
|
||||
|
||||
## Accessing the element: this
|
||||
|
||||
The value of `this` inside a handler is the element. The one which has the handler on it.
|
||||
|
||||
In the code below `button` shows its contents using `this.innerHTML`:
|
||||
|
||||
```html height=50 autorun
|
||||
<button onclick="alert(this.innerHTML)">Click me</button>
|
||||
```
|
||||
|
||||
## Possible mistakes
|
||||
|
||||
If you're starting to work with event -- please note some subtleties.
|
||||
|
||||
**The function should be assigned as `sayThanks`, not `sayThanks()`.**
|
||||
|
||||
```js
|
||||
// right
|
||||
button.onclick = sayThanks;
|
||||
|
||||
// wrong
|
||||
button.onclick = sayThanks();
|
||||
```
|
||||
|
||||
If we add brackets, then `sayThanks()` -- will be the *result* of the function execution, so `onclick` in the last code becomes `undefined` (the function returns nothing). That won't work.
|
||||
|
||||
...But in the markup we do need the brackets:
|
||||
|
||||
```html
|
||||
<input type="button" id="button" onclick="sayThanks()">
|
||||
```
|
||||
|
||||
The difference is easy to explain. When the browser reads the attribute, it creates a handler function with the body from its content.
|
||||
|
||||
So the last example is the same as:
|
||||
```js
|
||||
button.onclick = function() {
|
||||
*!*
|
||||
sayThanks(); // the attribute content
|
||||
*/!*
|
||||
};
|
||||
```
|
||||
|
||||
**Use functions, not strings.**
|
||||
|
||||
The assignment `elem.onclick = "alert(1)"` would work too. It works for compatibility reasons, but strongly not recommended.
|
||||
|
||||
**Don't use `setAttribute` for handlers.**
|
||||
|
||||
Such a call won't work:
|
||||
|
||||
```js run no-beautify
|
||||
// a click on <body> will generate errors,
|
||||
// because attributes are always strings, function becomes a string
|
||||
document.body.setAttribute('onclick', function() { alert(1) });
|
||||
```
|
||||
|
||||
**DOM-property case matters.**
|
||||
|
||||
Assign a handler to `elem.onclick`, not `elem.ONCLICK`, because DOM properties are case-sensitive.
|
||||
|
||||
## addEventListener
|
||||
|
||||
The fundamental problem of the aforementioned ways to assign handlers -- we can't assign multiple handlers to one event.
|
||||
|
||||
For instance, one part of our code wants to highlight a button on click, and another one wants to show a message.
|
||||
|
||||
We'd like to assign two event handlers for that. But a new DOM property will overwrite the existing one:
|
||||
|
||||
```js no-beautify
|
||||
input.onclick = function() { alert(1); }
|
||||
// ...
|
||||
input.onclick = function() { alert(2); } // replaces the previous handler
|
||||
```
|
||||
|
||||
Web-standard developers understood that long ago and suggested an alternative way of managing handlers using special methods `addEventListener` and `removeEventListener`. They are free of such a problem.
|
||||
|
||||
The syntax to add a handler:
|
||||
|
||||
```js
|
||||
element.addEventListener(event, handler[, phase]);
|
||||
```
|
||||
|
||||
`event`
|
||||
: Event name, e.g. `"click"`.
|
||||
|
||||
`handler`
|
||||
: The handler function.
|
||||
|
||||
`phase`
|
||||
: An optional argument, the "phase" for the handler to work. To be covered later. Usually we don't use it.
|
||||
|
||||
To remove the handler, use `removeEventListener`:
|
||||
|
||||
|
||||
```js
|
||||
// exactly the same arguments as addEventListener
|
||||
element.removeEventListener(event, handler[, phase]);
|
||||
```
|
||||
|
||||
````warn header="Removal requires the same function"
|
||||
To remove a handler we should pass exactly the same function as was assigned.
|
||||
|
||||
That doesn't work:
|
||||
|
||||
```js no-beautify
|
||||
elem.addEventListener( "click" , () => alert('Thanks!'));
|
||||
// ....
|
||||
elem.removeEventListener( "click", () => alert('Thanks!'));
|
||||
```
|
||||
|
||||
The handler won't be removed, because `removeEventListener` gets another function -- with the same code, but that doesn't matter.
|
||||
|
||||
Here's the right way:
|
||||
|
||||
```js
|
||||
function handler() {
|
||||
alert( 'Thanks!' );
|
||||
}
|
||||
|
||||
input.addEventListener("click", handler);
|
||||
// ....
|
||||
input.removeEventListener("click", handler);
|
||||
```
|
||||
|
||||
Please note -- if we don't store the function in a variable, then we can't remove it. There's no way to "read back" handlers assigned by `addEventListener`.
|
||||
````
|
||||
|
||||
Multiple calls to `addEventListener` allow to add multiple handlers, like this:
|
||||
|
||||
```html run no-beautify
|
||||
<input id="elem" type="button" value="Click me"/>
|
||||
|
||||
<script>
|
||||
function handler1() {
|
||||
alert('Thanks!');
|
||||
};
|
||||
|
||||
function handler2() {
|
||||
alert('Thanks again!');
|
||||
}
|
||||
|
||||
*!*
|
||||
elem.onclick = () => alert("Hello");
|
||||
elem.addEventListener("click", handler1); // Thanks!
|
||||
elem.addEventListener("click", handler2); // Thanks again!
|
||||
*/!*
|
||||
</script>
|
||||
```
|
||||
|
||||
As we can see in the example above, we can set handlers *both* using a DOM-property and `addEventListener`. But generally we use only one of these ways.
|
||||
|
||||
````warn header="For some events handlers only work with `addEventListener`"
|
||||
There exist events that can't be assigned via a DOM-property. Must use `addEventListener`.
|
||||
|
||||
For instance, the event `transitionend` (CSS animation finished) is like that.
|
||||
|
||||
Try the code below. In most browsers only the second handler works, not the first one.
|
||||
|
||||
```html run
|
||||
<style>
|
||||
input {
|
||||
transition: width 1s;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.wide {
|
||||
width: 300px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<input type="button" id="elem" onclick="this.classList.toggle('wide')" value="Click me">
|
||||
|
||||
<script>
|
||||
elem.ontransitionend = function() {
|
||||
alert("DOM property"); // doesn't work
|
||||
};
|
||||
|
||||
*!*
|
||||
elem.addEventListener("transitionend", function() {
|
||||
alert("addEventListener"); // shows up when the animation finishes
|
||||
});
|
||||
*/!*
|
||||
</script>
|
||||
```
|
||||
````
|
||||
|
||||
## Summary
|
||||
|
||||
There are 3 ways to assign event handlers:
|
||||
|
||||
1. HTML attribute: `onclick="..."`.
|
||||
2. DOM property: `elem.onclick = function`.
|
||||
3. Methods `elem.addEventListener(event, handler[, phase])`, to remove: `removeEventListener`.
|
||||
|
||||
HTML attributes are used sparingly, because JavaScript in the middle of an HTML tag looks a little bit odd and alien. Also can't write lots of code in there.
|
||||
|
||||
DOM properties are ok to use, but we can't assign more than one handler of the particular event. In many cases that limitation is not pressing.
|
||||
|
||||
The last way is the most flexible, but it is also the longest to write. There are few events that only work with it, for instance `transtionend` and `DOMContentLoaded` (to be covered).
|
||||
|
||||
As of now we're just starting to work with events. More details in the next chapters.
|
53
2-ui/2-events/1-introduction-browser-events/head.html
Normal file
|
@ -0,0 +1,53 @@
|
|||
<style>
|
||||
/*
|
||||
.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>
|
||||
/*
|
||||
function highlightMe(elem) {
|
||||
elem.style.backgroundColor='yellow'
|
||||
alert(elem.className)
|
||||
elem.style.backgroundColor = ''
|
||||
}
|
||||
|
||||
function highlightMe2(e) {
|
||||
highlightMe(e.currentTarget);
|
||||
}
|
||||
*/
|
||||
</script>
|
|
@ -0,0 +1,36 @@
|
|||
# Мяч под курсор мыши
|
||||
|
||||
Основная сложность первого этапа -- сдвинуть мяч под курсор, т.к. координаты клика `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]
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
<!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="https://js.cx/clipart/ball.svg" 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>
|
|
@ -0,0 +1,40 @@
|
|||
<!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="https://js.cx/clipart/ball.svg" id="ball"> . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
|
||||
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,22 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Передвигать мяч по полю
|
||||
|
||||
Сделайте так, что при клике по полю мяч перемещался на место клика.
|
||||
|
||||
[iframe src="solution" height="260" link]
|
||||
|
||||
Требования:
|
||||
|
||||
- Мяч после перелёта должен становиться центром ровно под курсор мыши, если это возможно без вылета за край поля.
|
||||
- CSS-анимация не обязательна, но желательна.
|
||||
- Мяч должен останавливаться у границ поля, ни в коем случае не вылетать за них.
|
||||
- При прокрутке страницы с полем ничего не должно ломаться.
|
||||
|
||||
Замечания:
|
||||
|
||||
- Код не должен зависеть от конкретных размеров мяча и поля.
|
||||
- Вам пригодятся свойства `event.clientX/event.clientY`
|
||||
|
108
2-ui/2-events/3-obtaining-event-object/article.md
Normal file
|
@ -0,0 +1,108 @@
|
|||
# Event object
|
||||
|
||||
To handle an event we'd like to know more about what's happened. For a mouse event we may need to know pointer coordinates, for a keyboard event -- which key was pressed and so on.
|
||||
|
||||
Each kind of event has special properties that browser writes into
|
||||
Детали произошедшего браузер записывает в "объект события", который передаётся первым аргументом в обработчик.
|
||||
|
||||
[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`:
|
||||
|
||||
`event.type`
|
||||
: Тип события, в данном случае `click`
|
||||
|
||||
`event.currentTarget`
|
||||
: Элемент, на котором сработал обработчик. Значение -- в точности такое же, как и у `this`, но бывают ситуации, когда обработчик является методом объекта и его `this` при помощи `bind` привязан к этому объекту, тогда мы можем использовать `event.currentTarget`.
|
||||
|
||||
`event.clientX / event.clientY`
|
||||
: Координаты курсора в момент клика (относительно окна)
|
||||
|
||||
Есть также и ряд других свойств, в зависимости от событий, которые мы разберём в дальнейших главах, когда будем подробно знакомиться с событиями мыши, клавиатуры и так далее.
|
||||
|
||||
````smart header="Объект события доступен и в HTML"
|
||||
При назначении обработчика в HTML, тоже можно использовать переменную `event`, это будет работать кросс-браузерно:
|
||||
|
||||
```html autorun height=60
|
||||
<input type="button" onclick="*!*alert(event.type)*/!*" value="Тип события">
|
||||
```
|
||||
|
||||
Это возможно потому, что когда браузер из атрибута создаёт функцию-обработчик, то она выглядит так: `function(event) { alert(event.type) }`. То есть, её первый аргумент называется `"event"`.
|
||||
````
|
||||
|
||||
## Особенности 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 - объект события во всех браузерах.
|
||||
};
|
||||
```
|
||||
|
||||
## Итого
|
||||
|
||||
- Объект события содержит ценную информацию о деталях события.
|
||||
- Он передается первым аргументом `event` в обработчик для всех браузеров, кроме IE8-, в которых используется глобальная переменная `window.event`.
|
||||
|
||||
Кросс-браузерно для JavaScript-обработчика получаем объект события так:
|
||||
|
||||
```js
|
||||
element.onclick = function(event) {
|
||||
event = event || window.event;
|
||||
|
||||
// Теперь event - объект события во всех браузерах.
|
||||
};
|
||||
```
|
||||
|
||||
Еще вариант:
|
||||
|
||||
```js
|
||||
element.onclick = function(e) {
|
||||
e = e || event; // если нет другой внешней переменной event
|
||||
...
|
||||
};
|
||||
```
|
242
2-ui/2-events/4-event-bubbling/article.md
Normal file
|
@ -0,0 +1,242 @@
|
|||
# Всплытие и перехват
|
||||
|
||||
Давайте сразу начнём с примера.
|
||||
|
||||
Этот обработчик для `<div>` сработает, если вы кликните по вложенному тегу `<em>` или `<code>`:
|
||||
|
||||
```html autorun height=60
|
||||
<div onclick="alert('Обработчик для Div сработал!')">
|
||||
<em>Кликните на <code>EM</code>, сработает обработчик на <code>DIV</code></em>
|
||||
</div>
|
||||
```
|
||||
|
||||
Вам не кажется это странным? Почему же сработал обработчик на `<div>`, если клик произошёл на `<em>`?
|
||||
|
||||
## Всплытие
|
||||
|
||||
Основной принцип всплытия:
|
||||
|
||||
**При наступлении события обработчики сначала срабатывают на самом вложенном элементе, затем на его родителе, затем выше и так далее, вверх по цепочке вложенности.**
|
||||
|
||||
Например, есть 3 вложенных элемента `FORM > DIV > P`, с обработчиком на каждом:
|
||||
|
||||
```html run autorun
|
||||
<style>
|
||||
body * {
|
||||
margin: 10px;
|
||||
border: 1px solid blue;
|
||||
}
|
||||
</style>
|
||||
|
||||
<form onclick="alert('form')">FORM
|
||||
<div onclick="alert('div')">DIV
|
||||
<p onclick="alert('p')">P</p>
|
||||
</div>
|
||||
</form>
|
||||
```
|
||||
|
||||
Всплытие гарантирует, что клик по внутреннему `<p>` вызовет обработчик `onclick` (если есть) сначала на самом `<p>`, затем на элементе `<div>` далее на элементе `<form>`, и так далее вверх по цепочке родителей до самого `document`.
|
||||
|
||||

|
||||
|
||||
Поэтому если в примере выше кликнуть на `P`, то последовательно выведутся `alert`: `p` -> `div` -> `form`.
|
||||
|
||||
Этот процесс называется *всплытием*, потому что события "всплывают" от внутреннего элемента вверх через родителей, подобно тому, как всплывает пузырек воздуха в воде.
|
||||
|
||||
```warn header="Всплывают *почти* все события."
|
||||
Ключевое слово в этой фразе -- "почти".
|
||||
|
||||
Например, событие `focus` не всплывает. В дальнейших главах мы будем детально знакомиться с различными событиями и увидим ещё примеры.
|
||||
```
|
||||
|
||||
## Целевой элемент event.target
|
||||
|
||||
На каком бы элементе мы ни поймали событие, всегда можно узнать, где конкретно оно произошло.
|
||||
|
||||
**Самый глубокий элемент, который вызывает событие, называется *"целевым"* или *"исходным"* элементом и доступен как `event.target`.**
|
||||
|
||||
Отличия от `this` (=`event.currentTarget`):
|
||||
|
||||
- `event.target` -- это **исходный элемент**, на котором произошло событие, в процессе всплытия он неизменен.
|
||||
- `this` -- это **текущий элемент**, до которого дошло всплытие, на нём сейчас выполняется обработчик.
|
||||
|
||||
Например, если стоит только один обработчик `form.onclick`, то он "поймает" все клики внутри формы. Где бы ни был клик внутри -- он всплывёт до элемента `<form>`, на котором сработает обработчик.
|
||||
|
||||
При этом:
|
||||
|
||||
- `this` (`=event.currentTarget`) всегда будет сама форма, так как обработчик сработал на ней.
|
||||
- `event.target` будет содержать ссылку на конкретный элемент внутри формы, самый вложенный, на котором произошёл клик.
|
||||
|
||||
[codetabs height=220 src="bubble-target"]
|
||||
|
||||
Возможна и ситуация, когда `event.target` и `this` -- один и тот же элемент, например если в форме нет других тегов и клик был на самом элементе `<form>`.
|
||||
|
||||
## Прекращение всплытия
|
||||
|
||||
Всплытие идёт прямо наверх. Обычно событие будет всплывать наверх и наверх, до элемента `<html>`, а затем до `document`, а иногда даже до `window`, вызывая все обработчики на своем пути.
|
||||
|
||||
**Но любой промежуточный обработчик может решить, что событие полностью обработано, и остановить всплытие.**
|
||||
|
||||
Для остановки всплытия нужно вызвать метод `event.stopPropagation()`.
|
||||
|
||||
Например, здесь при клике на кнопку обработчик `body.onclick` не сработает:
|
||||
|
||||
```html run autorun height=60
|
||||
<body onclick="alert('сюда обработка не дойдёт')">
|
||||
<button onclick="event.stopPropagation()">Кликни меня</button>
|
||||
</body>
|
||||
```
|
||||
|
||||
```smart header="event.stopImmediatePropagation()"
|
||||
Если у элемента есть несколько обработчиков на одно событие, то даже при прекращении всплытия все они будут выполнены.
|
||||
|
||||
То есть, `stopPropagation` препятствует продвижению события дальше, но на текущем элементе все обработчики отработают.
|
||||
|
||||
Для того, чтобы полностью остановить обработку, современные браузеры поддерживают метод `event.stopImmediatePropagation()`. Он не только предотвращает всплытие, но и останавливает обработку событий на текущем элементе.
|
||||
```
|
||||
|
||||
```warn header="Не прекращайте всплытие без необходимости!"
|
||||
Всплытие -- это удобно. Не прекращайте его без явной нужды, очевидной и архитектурно прозрачной.
|
||||
|
||||
Зачастую прекращение всплытия создаёт свои подводные камни, которые потом приходится обходить.
|
||||
|
||||
Например:
|
||||
|
||||
1. Мы делаем меню. Оно обрабатывает клики на своих элементах и делает для них `stopPropagation`. Вроде бы, всё работает.
|
||||
2. Позже мы решили отслеживать все клики в окне, для какой-то своей функциональности, к примеру, для статистики -- где вообще у нас кликают люди. Например, Яндекс.Метрика так делает, если включить соответствующую опцию.
|
||||
3. Над областью, где клики убиваются `stopPropagation`, статистика работать не будет! Получилась "мёртвая зона".
|
||||
|
||||
Проблема в том, что `stopPropagation` убивает всякую возможность отследить событие сверху, а это бывает нужно для реализации чего-нибудь "эдакого", что к меню отношения совсем не имеет.
|
||||
```
|
||||
|
||||
## Погружение
|
||||
|
||||
В современном стандарте, кроме "всплытия" событий, предусмотрено ещё и "погружение".
|
||||
|
||||
Оно гораздо менее востребовано, но иногда, очень редко, знание о нём может быть полезным.
|
||||
|
||||
Строго говоря, стандарт выделяет целых три стадии прохода события:
|
||||
|
||||
1. Событие сначала идет сверху вниз. Эта стадия называется *"стадия перехвата"* (capturing stage).
|
||||
2. Событие достигло целевого элемента. Это -- *"стадия цели"* (target stage).
|
||||
3. После этого событие начинает всплывать. Это -- *"стадия всплытия"* (bubbling stage).
|
||||
|
||||
В [стандарте DOM Events 3](http://www.w3.org/TR/DOM-Level-3-Events/) это продемонстрировано так:
|
||||
|
||||

|
||||
|
||||
То есть, при клике на `TD` событие путешествует по цепочке родителей сначала вниз к элементу ("погружается"), а потом наверх ("всплывает"), по пути задействуя обработчики.
|
||||
|
||||
**Ранее мы говорили только о всплытии, потому что другие стадии, как правило, не используются и проходят незаметно для нас.**
|
||||
|
||||
Обработчики, добавленные через `on...`-свойство, ничего не знают о стадии перехвата, а начинают работать со всплытия.
|
||||
|
||||
Чтобы поймать событие на стадии перехвата, нужно использовать третий аргумент `addEventListener`:
|
||||
|
||||
- Если аргумент `true`, то событие будет перехвачено по дороге вниз.
|
||||
- Если аргумент `false`, то событие будет поймано при всплытии.
|
||||
|
||||
Стадия цели, обозначенная на рисунке цифрой `(2)`, особо не обрабатывается, так как обработчики, назначаемые обоими этими способами, срабатывают также на целевом элементе.
|
||||
|
||||
```smart header="Есть события, которые не всплывают, но которые можно перехватить"
|
||||
Бывают события, которые можно поймать только на стадии перехвата, а на стадии всплытия -- нельзя..
|
||||
|
||||
Например, таково событие фокусировки на элементе [onfocus](/focus-blur). Конечно, это большая редкость, такое исключение существует по историческим причинам.
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
В примере ниже на `<form>`, `<div>`, `<p>` стоят те же обработчики, что и раньше, но на этот раз -- на стадии погружения. Чтобы увидеть перехват в действии, кликните в нём на элементе `<p>`:
|
||||
|
||||
[codetabs 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>`, чтобы увидеть порядок прохода события:
|
||||
|
||||
[codetabs height=220 src="both"]
|
||||
|
||||
Должно быть `FORM` -> `DIV` -> `P` -> `P` -> `DIV` -> `FORM`. Заметим, что элемент `<p>` участвует в обоих стадиях.
|
||||
|
||||
Как видно из примера, один и тот же обработчик можно назначить на разные стадии. При этом номер текущей стадии он, при необходимости, может получить из свойства `event.eventPhase` (=1, если погружение, =3, если всплытие).
|
||||
|
||||
## Отличия IE8-
|
||||
|
||||
Чтобы было проще ориентироваться, я собрал отличия IE8-, которые имеют отношение ко всплытию, в одну секцию.
|
||||
|
||||
Их знание понадобится, если вы решите писать на чистом JS, без фреймворков и вам понадобится поддержка IE8-.
|
||||
|
||||
Нет свойства `event.currentTarget`
|
||||
: Обратим внимание, что при назначении обработчика через `onсвойство` у нас есть `this`, поэтому `event.currentTarget`, как правило, не нужно, а вот при назначении через `attachEvent` обработчик не получает `this`, так что текущий элемент, если нужен, можно будет взять лишь из замыкания.
|
||||
|
||||
Вместо `event.target` в IE8- используется `event.srcElement`
|
||||
: Если мы пишем обработчик, который будет поддерживать и IE8- и современные браузеры, то можно начать его так:
|
||||
|
||||
```js
|
||||
elem.onclick = function(event) {
|
||||
event = event || window.event;
|
||||
var target = event.target || event.srcElement;
|
||||
|
||||
// ... теперь у нас есть объект события и target
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Для остановки всплытия используется код `event.cancelBubble=true`.
|
||||
: Кросс-браузерно остановить всплытие можно так:
|
||||
|
||||
```js no-beautify
|
||||
event.stopPropagation ? event.stopPropagation() : (event.cancelBubble=true);
|
||||
```
|
||||
|
||||
Далее в учебнике мы будем использовать стандартные свойства и вызовы, поскольку добавление этих строк, обеспечивающих совместимость -- достаточно простая и очевидная задача. Кроме того, никто не мешает подключить полифилл.
|
||||
|
||||
Ещё раз хотелось бы заметить -- эти отличия нужно знать при написании JS-кода с поддержкой IE8- без фреймворков. Почти все JS-фреймворки обеспечивают кросс-браузерную поддержку `target`, `currentTarget` и `stopPropagation()`.
|
||||
|
||||
## Итого
|
||||
|
||||
Алгоритм:
|
||||
|
||||
- При наступлении события -- элемент, на котором оно произошло, помечается как "целевой" (`event.target`).
|
||||
- Далее событие сначала двигается вниз от корня документа к `event.target`, по пути вызывая обработчики, поставленные через `addEventListener(...., true)`.
|
||||
- Далее событие двигается от `event.target` вверх к корню документа, по пути вызывая обработчики, поставленные через `on*` и `addEventListener(...., false)`.
|
||||
|
||||
Каждый обработчик имеет доступ к свойствам события:
|
||||
|
||||
- `event.target` -- самый глубокий элемент, на котором произошло событие.
|
||||
- `event.currentTarget` (=`this`) -- элемент, на котором в данный момент сработал обработчик (до которого "доплыло" событие).
|
||||
- `event.eventPhase` -- на какой фазе он сработал (погружение =1, всплытие = 3).
|
||||
|
||||
Любой обработчик может остановить событие вызовом `event.stopPropagation()`, но делать это не рекомендуется, так как в дальнейшем это событие может понадобиться, иногда для самых неожиданных вещей.
|
||||
|
||||
В современной разработке стадия погружения используется очень редко.
|
||||
|
||||
Этому есть две причины:
|
||||
|
||||
1. Историческая -- так как IE лишь с версии 9 в полной мере поддерживает современный стандарт.
|
||||
2. Разумная -- когда происходит событие, то разумно дать возможность первому сработать обработчику на самом элементе, поскольку он наиболее конкретен. Код, который поставил обработчик именно на этот элемент, знает максимум деталей о том, что это за элемент, чем он занимается.
|
||||
|
||||
Далее имеет смысл передать обработку события родителю -- он тоже понимает, что происходит, но уже менее детально, далее -- выше, и так далее, до самого объекта `document`, обработчик на котором реализовывает самую общую функциональность уровня документа.
|
||||
|
33
2-ui/2-events/4-event-bubbling/both.view/example.css
Executable file
|
@ -0,0 +1,33 @@
|
|||
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;
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: 25px;
|
||||
font-size: 16px;
|
||||
}
|
20
2-ui/2-events/4-event-bubbling/both.view/index.html
Executable file
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="example.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<form>FORM
|
||||
<div>DIV
|
||||
<p>P</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
12
2-ui/2-events/4-event-bubbling/both.view/script.js
Executable 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);
|
||||
elems[i].addEventListener("click", highlightThis, false);
|
||||
}
|
||||
|
||||
function highlightThis() {
|
||||
this.style.backgroundColor = 'yellow';
|
||||
alert(this.tagName);
|
||||
this.style.backgroundColor = '';
|
||||
}
|
33
2-ui/2-events/4-event-bubbling/bubble-target.view/example.css
Executable file
|
@ -0,0 +1,33 @@
|
|||
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;
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: 25px;
|
||||
font-size: 16px;
|
||||
}
|
21
2-ui/2-events/4-event-bubbling/bubble-target.view/index.html
Executable file
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="example.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
Клик выведет <code>event.target</code> и <code>this</code>:
|
||||
|
||||
<form id="form">FORM
|
||||
<div>DIV
|
||||
<p>P</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
9
2-ui/2-events/4-event-bubbling/bubble-target.view/script.js
Executable file
|
@ -0,0 +1,9 @@
|
|||
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 = '';
|
||||
};
|
33
2-ui/2-events/4-event-bubbling/capture.view/example.css
Executable file
|
@ -0,0 +1,33 @@
|
|||
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;
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: 25px;
|
||||
font-size: 16px;
|
||||
}
|
16
2-ui/2-events/4-event-bubbling/capture.view/index.html
Executable file
|
@ -0,0 +1,16 @@
|
|||
<!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>
|
11
2-ui/2-events/4-event-bubbling/capture.view/script.js
Executable file
|
@ -0,0 +1,11 @@
|
|||
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 = '';
|
||||
}
|
BIN
2-ui/2-events/4-event-bubbling/event-order-bubbling.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
2-ui/2-events/4-event-bubbling/event-order-bubbling@2x.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
2-ui/2-events/4-event-bubbling/eventflow.png
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
2-ui/2-events/4-event-bubbling/eventflow@2x.png
Normal file
After Width: | Height: | Size: 179 KiB |
|
@ -0,0 +1,2 @@
|
|||
Поставьте обработчик `click` на контейнере. Он должен проверять, произошел ли клик на кнопке удаления (`target`), и если да, то удалять соответствующий ей `DIV`.
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" href="messages.css">
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="messages-container">
|
||||
<div class="pane">
|
||||
<h3>Лошадь</h3>
|
||||
<p>Домашняя лошадь — животное семейства непарнокопытных, одомашненный и единственный сохранившийся подвид дикой лошади, вымершей в дикой природе, за исключением небольшой популяции лошади Пржевальского.</p>
|
||||
<button class="remove-button">[x]</button>
|
||||
</div>
|
||||
<div class="pane">
|
||||
<h3>Осёл</h3>
|
||||
<p>Домашний осёл или ишак — одомашненный подвид дикого осла, сыгравший важную историческую роль в развитии хозяйства и культуры человека. Все одомашненные ослы относятся к африканским ослам.</p>
|
||||
<button class="remove-button">[x]</button>
|
||||
</div>
|
||||
<div class="pane">
|
||||
<h3>Корова, а также пара слов о диком быке, о волах и о тёлках. </h3>
|
||||
<p>Коро́ва — самка домашнего быка, одомашненного подвида дикого быка, парнокопытного жвачного животного семейства полорогих. Самцы вида называются быками, молодняк — телятами, кастрированные самцы — волами. Молодых (до первой стельности) самок называют
|
||||
тёлками.
|
||||
</p>
|
||||
<button class="remove-button">[x]</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
var container = document.getElementById('messages-container');
|
||||
|
||||
container.onclick = function(event) {
|
||||
if (!event.target.classList.contains('remove-button')) return;
|
||||
|
||||
event.target.parentNode.hidden = !event.target.parentNode.hidden;
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,37 @@
|
|||
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;
|
||||
font-size: 110%;
|
||||
top: 0;
|
||||
color: darkred;
|
||||
right: 10px;
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="messages.css">
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
Кнопка для удаления:
|
||||
<button class="remove-button">[x]</button>
|
||||
|
||||
<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>
|
|
@ -0,0 +1,32 @@
|
|||
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;
|
||||
}
|
||||
|
||||
.remove-button {
|
||||
font-size: 110%;
|
||||
color: darkred;
|
||||
right: 10px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Скрытие сообщения с помощью делегирования
|
||||
|
||||
Дан список сообщений. Добавьте каждому сообщению кнопку для его удаления.
|
||||
|
||||
**Используйте делегирование событий. Один обработчик для всего.**
|
||||
|
||||
В результате, должно работать вот так(кликните на крестик):
|
||||
|
||||
[iframe src="solution" height=420]
|
||||
|
110
2-ui/2-events/5-event-delegation/2-sliding-tree/solution.md
Normal file
|
@ -0,0 +1,110 @@
|
|||
# Схема решения
|
||||
|
||||
Дерево устроено как вложенный список.
|
||||
|
||||
Клики на все элементы можно поймать, повесив единый обработчик `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 childrenContainer = li.getElementsByTagName('ul')[0];
|
||||
|
||||
if (!childrenContainer) return; // потомков нет -- ничего не надо делать
|
||||
|
||||
// спрятать/показать (можно и через CSS-класс)
|
||||
childrenContainer.hidden = !childrenContainer.hidden;
|
||||
}
|
||||
```
|
||||
|
||||
Выделение узлов жирным при наведении делается при помощи CSS-селектора `:hover`.
|
||||
|
89
2-ui/2-events/5-event-delegation/2-sliding-tree/solution.view/index.html
Executable file
|
@ -0,0 +1,89 @@
|
|||
<!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 the SPAN is clicked */
|
||||
var childrenContainer = target.parentNode.getElementsByTagName('ul')[0];
|
||||
if (!childrenContainer) return; // no children
|
||||
|
||||
childrenContainer.hidden = !childrenContainer.hidden;
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
50
2-ui/2-events/5-event-delegation/2-sliding-tree/source.view/index.html
Executable file
|
@ -0,0 +1,50 @@
|
|||
<!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>
|
18
2-ui/2-events/5-event-delegation/2-sliding-tree/task.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Раскрывающееся дерево
|
||||
|
||||
Создайте дерево, которое по клику на заголовок скрывает-показывает детей:
|
||||
|
||||
[iframe border=1 src="solution"]
|
||||
|
||||
Требования:
|
||||
|
||||
- Использовать делегирование.
|
||||
- Клик вне текста заголовка (на пустом месте) ничего делать не должен.
|
||||
- При наведении на заголовок -- он становится жирным, реализовать через CSS.
|
||||
|
||||
P.S. При необходимости HTML/CSS дерева можно изменить.
|
||||
|
14
2-ui/2-events/5-event-delegation/3-sort-table/solution.md
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Подсказка (обработчик)
|
||||
|
||||
1. Обработчик `onclick` можно повесить один, на всю таблицу или `THEAD`. Он будет игнорировать клики не на `TH`.
|
||||
2. При клике на `TH` обработчик будет получать номер из `TH`, на котором кликнули (`TH.cellIndex`) и вызывать функцию `sortColumn`, передавая ей номер колонки и тип.
|
||||
3. Функция `sortColumn(colNum, type)` будет сортировать.
|
||||
|
||||
# Подсказка (сортировка)
|
||||
|
||||
Функция сортировки:
|
||||
|
||||
1. Переносит все `TR` из `TBODY` в массив `rowsArr`
|
||||
2. Сортирует массив, используя `rowsArr.sort(compare)`, функция `compare` зависит от типа столбца.
|
||||
3. Добавляет `TR` из массива обратно в `TBODY`
|
||||
|
106
2-ui/2-events/5-event-delegation/3-sort-table/solution.view/index.html
Executable file
|
@ -0,0 +1,106 @@
|
|||
<!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) {
|
||||
if (e.target.tagName != 'TH') return;
|
||||
|
||||
// Если TH -- сортируем
|
||||
sortGrid(e.target.cellIndex, e.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);
|
||||
|
||||
// добавить результат в нужном порядке в TBODY
|
||||
// они автоматически будут убраны со старых мест и вставлены в правильном порядке
|
||||
for (var i = 0; i < rowsArray.length; i++) {
|
||||
tbody.appendChild(rowsArray[i]);
|
||||
}
|
||||
|
||||
grid.appendChild(tbody);
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
56
2-ui/2-events/5-event-delegation/3-sort-table/source.view/index.html
Executable file
|
@ -0,0 +1,56 @@
|
|||
<!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>
|
20
2-ui/2-events/5-event-delegation/3-sort-table/task.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
importance: 4
|
||||
|
||||
---
|
||||
|
||||
# Сортировка таблицы
|
||||
|
||||
Сделать сортировку таблицы при клике на заголовок.
|
||||
|
||||
Демо:
|
||||
|
||||
[iframe border=1 src="solution" height=180]
|
||||
|
||||
Требования:
|
||||
|
||||
- Использовать делегирование.
|
||||
- Код не должен меняться при увеличении количества столбцов или строк.
|
||||
|
||||
P.S. Обратите внимание, тип столбца задан атрибутом у заголовка. Это необходимо, ведь числа сортируются иначе чем строки. Соответственно, код это может использовать.
|
||||
|
||||
P.P.S. Вам помогут дополнительные [навигационные ссылки по таблицам](info:traversing-dom#dom-navigation-tables).
|
234
2-ui/2-events/5-event-delegation/article.md
Normal file
|
@ -0,0 +1,234 @@
|
|||
# Делегирование событий
|
||||
|
||||
Всплытие событий позволяет реализовать один из самых важных приёмов разработки -- *делегирование*.
|
||||
|
||||
Он заключается в том, что если у нас есть много элементов, события на которых нужно обрабатывать похожим образом, то вместо того, чтобы назначать обработчик каждому -- мы ставим один обработчик на их общего предка. Из него можно получить целевой элемент `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>`.
|
||||
|
||||
Однако, у текущей версии кода есть недостаток.
|
||||
|
||||
**Клик может быть не на том теге, который нас интересует, а внутри него.**
|
||||
|
||||
В нашем случае, если взглянуть на HTML таблицы внимательно, видно, что ячейка содержит вложенные теги, например `<strong>`:
|
||||
|
||||
```html
|
||||
<td>
|
||||
*!*
|
||||
<strong>Northwest</strong>
|
||||
*/!*
|
||||
...Metal..Silver..Elders...
|
||||
</td>
|
||||
```
|
||||
|
||||
Естественно, клик может произойти внутри `<td>`, на элементе `<strong>`. Такой клик будет пойман единым обработчиком, но `target` у него будет не `<td>`, а `<strong>`:
|
||||
|
||||

|
||||
|
||||
Внутри обработчика `table.onclick` мы должны по `event.target` разобраться, в каком именно `<td>` был клик.
|
||||
|
||||
Для этого мы, используя ссылку `parentNode`, будем идти вверх по иерархии родителей от `event.target` и выше и проверять:
|
||||
|
||||
- Если нашли `<td>`, значит это то что нужно.
|
||||
- Если дошли до элемента `table` и при этом `<td>` не найден, то наверное клик был вне `<td>`, например на элементе заголовка таблицы.
|
||||
|
||||
Улучшенный обработчик `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`.
|
||||
````
|
||||
|
||||
Можно для этого использовать и метод `closest`, при поддержке браузером:
|
||||
|
||||
```js
|
||||
table.onclick = function(event) {
|
||||
var target = event.target;
|
||||
|
||||
var td = target.closest('td');
|
||||
if (!td) return; // клик вне <td>, не интересует
|
||||
|
||||
// если клик на td, но вне этой таблицы (возможно при вложенных таблицах)
|
||||
// то не интересует
|
||||
if (!table.contains(td)) return;
|
||||
|
||||
// нашли элемент, который нас интересует!
|
||||
highlight(td);
|
||||
}
|
||||
```
|
||||
|
||||
## Применение делегирования: действия в разметке
|
||||
|
||||
Обычно делегирование -- это средство оптимизации интерфейса. Мы используем один обработчик для *схожих* действий на однотипных элементах.
|
||||
|
||||
Выше мы это делали для обработки кликов на `<td>`.
|
||||
|
||||
**Но делегирование позволяет использовать обработчик и для абсолютно разных действий.**
|
||||
|
||||
Например, нам нужно сделать меню с разными кнопками: "Сохранить", "Загрузить", "Поиск" и т.д. И есть объект с соответствующими методами: `save`, `load`, `search` и т.п...
|
||||
|
||||
Первое, что может прийти в голову -- это найти каждую кнопку и назначить ей свой обработчик среди методов объекта.
|
||||
|
||||
Но более изящно решить задачу можно путем добавления одного обработчика на всё меню, а для каждой кнопки в специальном атрибуте, который мы назовем `data-action` (можно придумать любое название, но `data-*` является валидным в HTML5), укажем, что она должна вызывать:
|
||||
|
||||
```html
|
||||
<button *!*data-action="save"*/!*>Нажмите, чтобы Сохранить</button>
|
||||
```
|
||||
|
||||
Обработчик считывает содержимое атрибута и выполняет метод. Взгляните на рабочий пример:
|
||||
|
||||
```html autorun height=60
|
||||
<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`.
|
||||
```
|
||||
|
||||
## Итого
|
||||
|
||||
Делегирование событий -- это здорово! Пожалуй, это один из самых полезных приёмов для работы с DOM. Он отлично подходит, если есть много элементов, обработка которых очень схожа.
|
||||
|
||||
Алгоритм:
|
||||
|
||||
1. Вешаем обработчик на контейнер.
|
||||
2. В обработчике: получаем `event.target`.
|
||||
3. В обработчике: если `event.target` или один из его родителей в контейнере (`this`) -- интересующий нас элемент -- обработать его.
|
||||
|
||||
Зачем использовать:
|
||||
|
||||
```compare
|
||||
+ Упрощает инициализацию и экономит память: не нужно вешать много обработчиков.
|
||||
+ Меньше кода: при добавлении и удалении элементов не нужно ставить или снимать обработчики.
|
||||
+ Удобство изменений: можно массово добавлять или удалять элементы путём изменения `innerHTML`.
|
||||
```
|
||||
|
||||
Конечно, у делегирования событий есть свои ограничения.
|
||||
|
||||
```compare
|
||||
- Во-первых, событие должно всплывать. Нельзя, чтобы какой-то промежуточный обработчик вызвал `event.stopPropagation()` до того, как событие доплывёт до нужного элемента.
|
||||
- Во-вторых, делегирование создает дополнительную нагрузку на браузер, ведь обработчик запускается, когда событие происходит в любом месте контейнера, не обязательно на элементах, которые нам интересны. Но обычно эта нагрузка настолько пустяковая, её даже не стоит принимать во внимание.
|
||||
```
|
||||
|
BIN
2-ui/2-events/5-event-delegation/bagua-bubble.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
2-ui/2-events/5-event-delegation/bagua-bubble@2x.png
Normal file
After Width: | Height: | Size: 21 KiB |
59
2-ui/2-events/5-event-delegation/bagua.view/bagua.css
Executable file
|
@ -0,0 +1,59 @@
|
|||
#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;
|
||||
}
|
94
2-ui/2-events/5-event-delegation/bagua.view/index.html
Executable file
|
@ -0,0 +1,94 @@
|
|||
<!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>
|
0
2-ui/2-events/6-behavior/1-behavior-tooltip/solution.md
Normal file
|
@ -0,0 +1,81 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {
|
||||
height: 2000px;
|
||||
/* подсказка должна работать независимо от прокрутки */
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: fixed;
|
||||
padding: 10px 20px;
|
||||
/* красивости... */
|
||||
|
||||
border: 1px solid #b3c9ce;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
font: italic 14px/1.3 arial, sans-serif;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
box-shadow: 3px 3px 3px rgba(0, 0, 0, .3);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<p>ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя</p>
|
||||
<p>ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя</p>
|
||||
|
||||
<button data-tooltip="подсказка длиннее, чем элемент">Короткая кнопка</button>
|
||||
<button data-tooltip="HTML<br>подсказка">Ещё кнопка</button>
|
||||
|
||||
<p>Прокрутите страницу, чтобы ссылки были вверху и проверьте, правильно ли показываются подсказки.</p>
|
||||
|
||||
|
||||
<script>
|
||||
var showingTooltip;
|
||||
|
||||
document.onmouseover = function(e) {
|
||||
var target = e.target;
|
||||
|
||||
var tooltip = target.getAttribute('data-tooltip');
|
||||
if (!tooltip) return;
|
||||
|
||||
var tooltipElem = document.createElement('div');
|
||||
tooltipElem.className = 'tooltip';
|
||||
tooltipElem.innerHTML = tooltip;
|
||||
document.body.appendChild(tooltipElem);
|
||||
|
||||
var coords = target.getBoundingClientRect();
|
||||
|
||||
var left = coords.left + (target.offsetWidth - tooltipElem.offsetWidth) / 2;
|
||||
if (left < 0) left = 0; // не вылезать за левую границу окна
|
||||
|
||||
var top = coords.top - tooltipElem.offsetHeight - 5;
|
||||
if (top < 0) { // не вылезать за верхнюю границу окна
|
||||
top = coords.top + target.offsetHeight + 5;
|
||||
}
|
||||
|
||||
tooltipElem.style.left = left + 'px';
|
||||
tooltipElem.style.top = top + 'px';
|
||||
|
||||
showingTooltip = tooltipElem;
|
||||
};
|
||||
|
||||
document.onmouseout = function(e) {
|
||||
|
||||
if (showingTooltip) {
|
||||
document.body.removeChild(showingTooltip);
|
||||
showingTooltip = null;
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {
|
||||
height: 2000px;
|
||||
/* подсказка должна работать независимо от прокрутки */
|
||||
}
|
||||
/* ваши стили */
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<p>ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя</p>
|
||||
<p>ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя</p>
|
||||
|
||||
<button data-tooltip="подсказка длиннее, чем элемент">Короткая кнопка</button>
|
||||
<button data-tooltip="HTML<br>подсказка">Ещё кнопка</button>
|
||||
|
||||
<p>Прокрутите страницу, чтобы ссылки были вверху и проверьте, правильно ли показываются подсказки.</p>
|
||||
|
||||
|
||||
<script>
|
||||
// ваш код
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
33
2-ui/2-events/6-behavior/1-behavior-tooltip/task.md
Normal file
|
@ -0,0 +1,33 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Поведение "подсказка"
|
||||
|
||||
При наведении мыши на элемент, на нём возникает событие `mouseover`, при удалении мыши с элемента -- событие `mouseout`.
|
||||
|
||||
Зная это, напишите JS-код, который будет делать так, что при наведении на элемент, если у него есть атрибут `data-tooltip` -- над ним будет показываться подсказка с содержимым этого атрибута.
|
||||
|
||||
Например, две кнопки:
|
||||
|
||||
```html
|
||||
<button data-tooltip="подсказка длиннее, чем элемент">Короткая кнопка</button>
|
||||
<button data-tooltip="HTML<br>подсказка">Ещё кнопка</button>
|
||||
```
|
||||
|
||||
Результат в ифрейме с документом:
|
||||
|
||||
[iframe src="solution" height=200 border=1]
|
||||
|
||||
В этой задаче можно полагать, что в элементе с атрибутом `data-tooltip` -- только текст, то есть нет подэлементов.
|
||||
|
||||
Детали оформления:
|
||||
|
||||
- Подсказка должна появляться при наведении на элемент, по центру и на небольшом расстоянии сверху. При уходе курсора с элемента -- исчезать.
|
||||
- Текст подсказки брать из значения атрибута `data-tooltip`. Это может быть произвольный HTML.
|
||||
- Оформление подсказки должно задаваться CSS.
|
||||
- Подсказка не должна вылезать за границы экрана, в том числе если страница частично прокручена. Если нельзя показать сверху -- показывать снизу элемента.
|
||||
|
||||
Важно: нужно использовать приём разработки "поведение", то есть поставить обработчик (точнее два) на `document`, а не на каждый элемент.
|
||||
|
||||
Плюс этого подхода -- динамически добавленные в DOM позже элементы автоматически получат этот функционал.
|
93
2-ui/2-events/6-behavior/article.md
Normal file
|
@ -0,0 +1,93 @@
|
|||
# Приём проектирования "поведение"
|
||||
|
||||
Шаблон проектирования "поведение" (behavior) позволяет задавать хитрые обработчики на элементе *декларативно*, установкой специальных HTML-атрибутов и классов.
|
||||
|
||||
[cut]
|
||||
|
||||
## Описание
|
||||
|
||||
Приём проектирования "поведение" состоит из двух частей:
|
||||
|
||||
1. Элементу ставится атрибут, описывающий его поведение.
|
||||
2. При помощи делегирования ставится обработчик на документ, который ловит все клики и, если элемент имеет нужный атрибут, производит нужное действие.
|
||||
|
||||
## Пример
|
||||
|
||||
Например, добавим "поведение", которое всем элементам, у которых стоит атрибут `data-counter`, будет при клике увеличивать значение на `1`:
|
||||
|
||||
```html run autorun height=60
|
||||
Счётчик:
|
||||
<button data-counter>1</button>
|
||||
Ещё счётчик:
|
||||
<button data-counter>2</button>
|
||||
|
||||
<script>
|
||||
document.onclick = function(event) {
|
||||
if (!event.target.hasAttribute('data-counter')) return;
|
||||
|
||||
var counter = event.target;
|
||||
|
||||
counter.innerHTML++;
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
Если запустить HTML-код выше, то при клике на каждую кнопку -- её значение будет увеличиваться.
|
||||
|
||||
Конечно, нам важны не счётчики, а общий подход, который они демонстрируют.
|
||||
|
||||
Элементов `data-counter` может быть сколько угодно. Новые могут добавляться в HTML в любой момент. При помощи делегирования мы, фактически, добавили новый "псевдо-стандартный" атрибут в HTML, который добавляет элементу новую возможность ("поведение").
|
||||
|
||||
## Ещё пример
|
||||
|
||||
Добавим ещё поведение.
|
||||
|
||||
Сделаем так, что при клике на элемент с атрибутом `data-toggle-id` будет скрываться/показываться элемент с заданным `id`:
|
||||
|
||||
```html autorun run height=60
|
||||
<button *!*data-toggle-id="subscribe-mail"*/!*>
|
||||
Показать форму подписки
|
||||
</button>
|
||||
|
||||
<form id="subscribe-mail" hidden>
|
||||
Ваша почта: <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.hidden = !elem.hidden;
|
||||
};
|
||||
*/!*
|
||||
</script>
|
||||
```
|
||||
|
||||
Ещё раз заметим, что мы сделали. Теперь для того, чтобы добавить скрытие-раскрытие любому элементу -- даже не надо знать JavaScript, можно просто написать атрибут `data-toggle-id`.
|
||||
|
||||
Это бывает очень удобно -- не нужно писать JavaScript-код для каждого элемента, который должен служить такой кнопкой. Просто используем поведение.
|
||||
|
||||
Обратите внимание: обработчик поставлен на `document`, клик на любом элементе страницы пройдёт через него, так что поведение определено глобально.
|
||||
|
||||
```smart header="Не только атрибут"
|
||||
Для своих целей мы можем использовать в HTML любые атрибуты, но стандарт рекомендует для своих целей называть атрибуты `data-*`.
|
||||
|
||||
В обработчике `document.onclick` мы могли бы проверять не атрибут, а класс или что-то ещё, но с атрибутом -- проще и понятнее всего.
|
||||
|
||||
Также для добавления обработчиков на `document` рекомендуется использовать `addEventListener`, чтобы можно было добавить более одного обработчика для типа события.
|
||||
```
|
||||
|
||||
## Итого
|
||||
|
||||
Шаблон "поведение" удобен тем, что сколь угодно сложное JavaScript-поведение можно "навесить" на элемент одним лишь атрибутом. А можно -- несколькими атрибутами на связанных элементах.
|
||||
|
||||
Здесь мы рассмотрели базовый пример, который можно как угодно модифицировать и масштабировать. Важно не переусердствовать.
|
||||
|
||||
Приём разработки "поведение" рекомендуется использовать для расширения возможностей разметки, как альтернативу мини-фрагментам JavaScript.
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
Дело в том, что обработчик из атрибута `onclick` делается браузером как функция с заданным телом.
|
||||
|
||||
То есть, в данном случае он будет таким:
|
||||
|
||||
```js
|
||||
function(event) {
|
||||
handler() // тело взято из атрибута onclick
|
||||
}
|
||||
```
|
||||
|
||||
При этом возвращаемое `handler` значение никак не используется и не влияет на результат.
|
||||
|
||||
Рабочий вариант:
|
||||
|
||||
```html run
|
||||
<script>
|
||||
function handler() {
|
||||
alert("...");
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<a href="http://w3.org" onclick="*!*return handler()*/!*">w3.org</a>
|
||||
```
|
||||
|
||||
Также можно использовать объект события для вызова `event.preventDefault()`, например:
|
||||
|
||||
```html run
|
||||
<script>
|
||||
*!*
|
||||
function handler(event) {
|
||||
alert("...");
|
||||
event.preventDefault();
|
||||
}
|
||||
*/!*
|
||||
</script>
|
||||
|
||||
<a href="http://w3.org" onclick="*!*handler(event)*/!*">w3.org</a>
|
||||
```
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
importance: 3
|
||||
|
||||
---
|
||||
|
||||
# Почему не работает return false?
|
||||
|
||||
Почему в этом документе `return false` не работает?
|
||||
|
||||
```html autorun run
|
||||
<script>
|
||||
function handler() {
|
||||
alert( "..." );
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<a href="http://w3.org" onclick="handler()">w3.org</a>
|
||||
```
|
||||
|
||||
По замыслу, переход на `w3.org` при клике должен отменяться. Однако, на самом деле он происходит.
|
||||
|
||||
В чём дело и как поправить?
|
|
@ -0,0 +1,28 @@
|
|||
Это -- классическая задача на тему делегирования.
|
||||
|
||||
В реальной жизни, мы можем перехватить событие и создать 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-атрибуте, так как свойство может отличаться, оно обязано содержать полный валидный адрес.
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
#contents {
|
||||
padding: 5px;
|
||||
border: 1px green solid;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<fieldset id="contents">
|
||||
<legend>#contents</legend>
|
||||
<p>
|
||||
Как насчет почитать <a href="http://wikipedia.org">Википедию</a>, или посетить <a href="http://w3.org"><i>W3.org</i></a> и узнать про современные стандарты?
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
<script>
|
||||
document.getElementById('contents').onclick = function(event) {
|
||||
|
||||
function handleLink(href) {
|
||||
var isLeaving = confirm('Уйти на ' + href + '?');
|
||||
if (!isLeaving) return false;
|
||||
}
|
||||
|
||||
var target = event.target;
|
||||
|
||||
while (target != this) {
|
||||
if (target.nodeName == 'A') {
|
||||
return handleLink(target.getAttribute('href'));
|
||||
}
|
||||
target = target.parentNode;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
#contents {
|
||||
padding: 5px;
|
||||
border: 1px green solid;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<fieldset id="contents">
|
||||
<legend>#contents</legend>
|
||||
<p>
|
||||
Как насчет почитать <a href="http://wikipedia.org">Википедию</a>, или посетить <a href="http://w3.org"><i>W3.org</i></a> и узнать про современные стандарты?
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,17 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Поймайте переход по ссылке
|
||||
|
||||
Сделайте так, чтобы при клике на ссылки внутри элемента `#contents` пользователю выводился вопрос о том, действительно ли он хочет покинуть страницу и если он не хочет, то прерывать переход по ссылке.
|
||||
|
||||
Так это должно работать:
|
||||
|
||||
[iframe height=100 border=1 src="solution"]
|
||||
|
||||
Детали:
|
||||
|
||||
- Содержимое `#contents` может быть загружено динамически и присвоено при помощи `innerHTML`. Так что найти все ссылки и поставить на них обработчики нельзя. Используйте делегирование.
|
||||
- Содержимое может содержать вложенные теги, *в том числе внутри ссылок*, например, `<a href=".."><i>...</i></a>`.
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
Решение состоит в том, чтобы добавить обработчик на контейнер `#thumbs` и отслеживать клики на ссылках.
|
||||
|
||||
Когда происходит событие, обработчик должен изменять `src` `#largeImg` на `href` ссылки и заменять `alt` на ее `title`.
|
||||
|
||||
Код решения:
|
||||
|
||||
```js
|
||||
var largeImg = document.getElementById('largeImg');
|
||||
|
||||
document.getElementById('thumbs').onclick = function(e) {
|
||||
var target = e.target;
|
||||
|
||||
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`.
|
||||
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Галерея</title>
|
||||
<link rel="stylesheet" href="gallery.css">
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<p><img id="largeImg" src="https://js.cx/gallery/img1-lg.jpg" alt="Large image"></p>
|
||||
|
||||
<ul id="thumbs">
|
||||
<!-- При наведении на изображение показывается встроенная подсказка браузера (title) -->
|
||||
<li>
|
||||
<a href="https://js.cx/gallery/img2-lg.jpg" title="Image 2"><img src="https://js.cx/gallery/img2-thumb.jpg"></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://js.cx/gallery/img3-lg.jpg" title="Image 3"><img src="https://js.cx/gallery/img3-thumb.jpg"></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://js.cx/gallery/img4-lg.jpg" title="Image 4"><img src="https://js.cx/gallery/img4-thumb.jpg"></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://js.cx/gallery/img5-lg.jpg" title="Image 5"><img src="https://js.cx/gallery/img5-thumb.jpg"></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://js.cx/gallery/img6-lg.jpg" title="Image 6"><img src="https://js.cx/gallery/img6-thumb.jpg"></a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<script>
|
||||
var largeImg = document.getElementById('largeImg');
|
||||
|
||||
var thumbs = document.getElementById('thumbs');
|
||||
|
||||
thumbs.onclick = function(e) {
|
||||
var target = e.target;
|
||||
|
||||
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>
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Галерея</title>
|
||||
<link rel="stylesheet" href="gallery.css">
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<p><img id="largeImg" src="https://js.cx/gallery/img1-lg.jpg" alt="Large image"></p>
|
||||
|
||||
<div id="thumbs">
|
||||
<!-- При наведении на изображение показывается встроенная подсказка браузера (title) -->
|
||||
<a href="https://js.cx/gallery/img2-lg.jpg" title="Image 2"><img src="https://js.cx/gallery/img2-thumb.jpg"></a>
|
||||
<a href="https://js.cx/gallery/img3-lg.jpg" title="Image 3"><img src="https://js.cx/gallery/img3-thumb.jpg"></a>
|
||||
<a href="https://js.cx/gallery/img4-lg.jpg" title="Image 4"><img src="https://js.cx/gallery/img4-thumb.jpg"></a>
|
||||
<a href="https://js.cx/gallery/img5-lg.jpg" title="Image 5"><img src="https://js.cx/gallery/img5-thumb.jpg"></a>
|
||||
<a href="https://js.cx/gallery/img6-lg.jpg" title="Image 6"><img src="https://js.cx/gallery/img6-thumb.jpg"></a>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,20 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Галерея изображений
|
||||
|
||||
Создайте галерею изображений, в которой основное изображение изменяется при клике на уменьшенный вариант.
|
||||
|
||||
Результат должен выглядеть так:
|
||||
|
||||
[iframe src="solution" height=600]
|
||||
|
||||
Для обработки событий используйте делегирование, т.е. не более одного обработчика.
|
||||
|
||||
P.S. Обратите внимание -- клик может быть как на маленьком изображении `IMG`, так и на `A` вне него. При этом `event.target` будет, соответственно, либо `IMG`, либо `A`.
|
||||
|
||||
Дополнительно:
|
||||
|
||||
- Если получится -- сделайте предзагрузку больших изображений, чтобы при клике они появлялись сразу.
|
||||
- Всё ли в порядке с семантической вёрсткой в HTML исходного документа? Если нет -- поправьте, чтобы было, как нужно.
|
145
2-ui/2-events/7-default-browser-action/article.md
Normal file
|
@ -0,0 +1,145 @@
|
|||
# Действия браузера по умолчанию
|
||||
|
||||
Многие события автоматически влекут за собой действие браузера.
|
||||
|
||||
Например:
|
||||
|
||||
- Клик по ссылке инициирует переход на новый URL.
|
||||
- Нажатие на кнопку "отправить" в форме -- отсылку ее на сервер.
|
||||
- Двойной клик на тексте -- инициирует его выделение.
|
||||
|
||||
Если мы обрабатываем событие в JavaScript, то зачастую такое действие браузера нам не нужно. К счастью, его можно отменить.
|
||||
|
||||
[cut]
|
||||
|
||||
## Отмена действия браузера
|
||||
|
||||
Есть два способа отменить действие браузера:
|
||||
|
||||
- **Основной способ -- это воспользоваться объектом события. Для отмены действия браузера существует стандартный метод `event.preventDefault()`.**
|
||||
- Если же обработчик назначен через `onсобытие` (не через `addEventListener`), то можно просто вернуть `false` из обработчика.
|
||||
|
||||
В следующем примере при клике по ссылке переход не произойдет:
|
||||
|
||||
```html autorun height=60 no-beautify
|
||||
<a href="/" onclick="return false">Нажми здесь</a>
|
||||
или
|
||||
<a href="/" onclick="event.preventDefault()">здесь</a>
|
||||
```
|
||||
|
||||
```warn header="Возвращать `true` не нужно"
|
||||
Обычно значение, которое возвращает обработчик события, игнорируется.
|
||||
|
||||
Единственное исключение -- это `return false` из обработчика, назначенного через `onсобытие`.
|
||||
|
||||
Иногда в коде начинающих разработчиков можно увидеть `return` других значений. Но они не нужны и никак не обрабатываются.
|
||||
```
|
||||
|
||||
### Пример: меню
|
||||
|
||||
Рассмотрим задачу, когда нужно создать меню для сайта, например такое:
|
||||
|
||||
```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]
|
||||
|
||||
HTML-разметка сделана так, что все элементы меню являются не кнопками, а ссылками, то есть тегами `<a>`.
|
||||
|
||||
Это потому, что некоторые посетители очень любят сочетание "правый клик - открыть в новом окне". Да, мы можем использовать и `<button>` и `<span>`, но если правый клик не работает -- это их огорчает. Кроме того, если на сайт зайдёт поисковик, то по ссылке из `<a href="...">` он перейдёт, а выполнить сложный JavaScript и получить результат -- вряд ли захочет.
|
||||
|
||||
Поэтому в разметке мы используем именно `<a>`, но обычно клик будет обрабатываться полностью в 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 -- обработчик не потребует изменений.
|
||||
|
||||
## Другие действия браузера
|
||||
|
||||
Действий браузера по умолчанию достаточно много.
|
||||
|
||||
Вот некоторые примеры событий, которые вызывают действие браузера:
|
||||
|
||||
- `mousedown` -- нажатие кнопкой мыши в то время как курсор находится на тексте начинает его выделение.
|
||||
- `click` на `<input type="checkbox">` -- ставит или убирает галочку.
|
||||
- `submit` -- при нажатии на `<input type="submit">` в форме данные отправляются на сервер.
|
||||
- `wheel` -- движение колёсика мыши инициирует прокрутку.
|
||||
- `keydown` -- при нажатии клавиши в поле ввода появляется символ.
|
||||
- `contextmenu` -- при правом клике показывается контекстное меню браузера.
|
||||
- ...
|
||||
|
||||
Все эти действия можно отменить, если мы хотим обработать событие исключительно при помощи JavaScript.
|
||||
|
||||
````warn header="События могут быть связаны между собой"
|
||||
Некоторые события естественным образом вытекают друг из друга.
|
||||
|
||||
Например, нажатие мышкой `mousedown` на поле ввода `<input>` приводит к фокусировке внутрь него. Если отменить действие `mousedown`, то и фокуса не будет.
|
||||
|
||||
Попробуйте нажать мышкой на первый `<input>` -- произойдёт событие `onfocus`. Это обычная ситуация.
|
||||
|
||||
Но если нажать на второй, то фокусировки не произойдёт.
|
||||
|
||||
```html run autorun
|
||||
<input value="Фокус работает" onfocus="this.value=''">
|
||||
<input *!*onmousedown="return false"*/!* onfocus="this.value=''" value="Кликни меня">
|
||||
```
|
||||
|
||||
Это потому, что отменено стандартное действие при `onmousedown`.
|
||||
|
||||
...С другой стороны, во второй `<input>` можно перейти с первого нажатием клавиши `key:Tab`, и тогда фокусировка сработает. То есть, дело здесь именно в `onmousedown="return false"`.
|
||||
````
|
||||
|
||||
## Особенности IE8-
|
||||
|
||||
В IE8- для отмены действия по умолчанию нужно назначить свойство `event.returnValue = false`.
|
||||
|
||||
Кроссбраузерный код для отмены действия по умолчанию:
|
||||
|
||||
```js
|
||||
element.onclick = function(event) {
|
||||
event = event || window.event;
|
||||
|
||||
if (event.preventDefault) { // если метод существует
|
||||
event.preventDefault(); // то вызвать его
|
||||
} else { // иначе вариант IE8-:
|
||||
event.returnValue = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Можно записать в одну строку:
|
||||
|
||||
```js no-beautify
|
||||
...
|
||||
event.preventDefault ? event.preventDefault() : (event.returnValue=false);
|
||||
...
|
||||
```
|
||||
|
||||
## Итого
|
||||
|
||||
- Браузер имеет встроенные действия при ряде событий -- переход по ссылке, отправка формы и т.п. Как правило, их можно отменить.
|
||||
- Есть два способа отменить действие по умолчанию: первый -- использовать `event.preventDefault()` (IE8-: `event.returnValue=false`), второй -- `return false` из обработчика. Второй способ работает только если обработчик назначен через `onсобытие`.
|
||||
|
22
2-ui/2-events/7-default-browser-action/menu.view/index.html
Executable file
|
@ -0,0 +1,22 @@
|
|||
<!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>
|
25
2-ui/2-events/7-default-browser-action/menu.view/menu.css
Executable 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;
|
||||
}
|
8
2-ui/2-events/7-default-browser-action/menu.view/menu.js
Executable 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
|
||||
};
|
429
2-ui/2-events/8-dispatch-events/article.md
Normal file
|
@ -0,0 +1,429 @@
|
|||
# Генерация событий на элементах
|
||||
|
||||
Можно не только назначать обработчики на события, но и генерировать их самому.
|
||||
|
||||
Мы будем использовать это позже для реализации компонентной архитектуры, при которой элемент, представляющий собой, к примеру, меню, генерирует события, к этому меню относящиеся -- `select` (выбран пункт меню) или `open` (меню раскрыто), и другие.
|
||||
|
||||
Кроме того, события можно генерировать для целей автоматического тестирования.
|
||||
|
||||
[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(тип события[, флаги]);
|
||||
```
|
||||
|
||||
Где:
|
||||
|
||||
- *Тип события* -- может быть как своим, так и встроенным, к примеру `"click"`.
|
||||
- *Флаги* -- объект вида `{ bubbles: true/false, cancelable: true/false }`, где свойство `bubbles` указывает, всплывает ли событие, а `cancelable` -- можно ли отменить действие по умолчанию.
|
||||
|
||||
Флаги по умолчанию: `{bubbles: false, cancelable: false}`.
|
||||
|
||||
## Метод dispatchEvent
|
||||
|
||||
Затем, чтобы инициировать событие, запускается `elem.dispatchEvent(event)`.
|
||||
|
||||
При этом событие срабатывает наравне с браузерными, то есть обычные браузерные обработчики на него отреагируют. Если при создании указан флаг `bubbles`, то оно будет всплывать.
|
||||
|
||||
При просмотре примера ниже кнопка обработчик `onclick` на кнопке сработает сам по себе, событие генерируется скриптом:
|
||||
|
||||
```html run no-beautify
|
||||
<button id="elem" onclick="alert('Клик');">Автоклик</button>
|
||||
|
||||
<script>
|
||||
var event = new Event("click");
|
||||
elem.dispatchEvent(event);
|
||||
</script>
|
||||
```
|
||||
|
||||
## Отмена действия по умолчанию
|
||||
|
||||
На сгенерированном событии, как и на встроенном браузерном, обработчик может вызвать метод `event.preventDefault()`. Тогда `dispatchEvent` возвратит `false`.
|
||||
|
||||
Остановимся здесь подробнее. Обычно `preventDefault()` вызов предотвращает действие браузера. В случае, если событие придумано нами, имеет нестандартное имя -- никакого действия браузера, конечно, нет.
|
||||
|
||||
Но код, который генерирует событие, может предусматривать какие-то ещё действия после `dispatchEvent`.
|
||||
|
||||
Вызов `event.preventDefault()` является возможностью для обработчика события сообщить в сгенерировавший событие код, что эти действия продолжать не надо.
|
||||
|
||||
В примере ниже есть функция `hide()`, которая при вызове генерирует событие `hide` на элементе `#rabbit`, уведомляя всех интересующихся, что кролик собирается спрятаться.
|
||||
|
||||
Любой обработчик может узнать об этом, подписавшись на событие через `rabbit.addEventListener('hide',...)` и, при желании, отменить действие по умолчанию через `event.preventDefault()`. Тогда кролик не исчезнет:
|
||||
|
||||
```html run
|
||||
<pre id="rabbit">
|
||||
|\ /|
|
||||
\|_|/
|
||||
/. .\
|
||||
=\_Y_/=
|
||||
{>o<}
|
||||
</pre>
|
||||
|
||||
<script>
|
||||
|
||||
function hide() {
|
||||
var event = new Event("hide", {
|
||||
cancelable: true
|
||||
});
|
||||
if (!rabbit.dispatchEvent(event)) {
|
||||
alert( 'действие отменено обработчиком' );
|
||||
} else {
|
||||
rabbit.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
rabbit.addEventListener('hide', function(event) {
|
||||
if (confirm("Вызвать preventDefault?")) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// прячемся через 2 секунды
|
||||
setTimeout(hide, 2000);
|
||||
|
||||
</script>
|
||||
```
|
||||
|
||||
```smart header="Как отличить реальное нажатие от скриптового?"
|
||||
В целях безопасности иногда хорошо бы знать -- инициировано ли действие посетителем или это кликнул скрипт.
|
||||
|
||||
Единственный способ, которым код может отличить реальное нажатие от программного, является проверка свойства `event.isTrusted`.
|
||||
|
||||
Оно на момент написания статьи поддерживается IE и Firefox и равно `true`, если посетитель кликнул сам, и всегда `false` -- если событие инициировал скрипт.
|
||||
```
|
||||
|
||||
## Другие свойства событий
|
||||
|
||||
При создании события браузер автоматически ставит следующие свойства:
|
||||
|
||||
- `isTrusted: false` -- означает, что событие сгенерировано скриптом, это свойство изменить невозможно.
|
||||
- `target: null` -- это свойство ставится автоматически позже при `dispatchEvent`.
|
||||
- `type: тип события` -- первый аргумент `new Event`.
|
||||
- `bubbles`, `cancelable` -- по второму аргументу `new Event`.
|
||||
|
||||
Другие свойства события, если они нужны, например координаты для события мыши -- можно присвоить в объект события позже, например:
|
||||
|
||||
```js no-beautify
|
||||
var event = new Event("click", {bubbles: true, cancelable: false});
|
||||
event.clientX = 100;
|
||||
event.clientY = 100;
|
||||
```
|
||||
|
||||
## Пример со всплытием
|
||||
|
||||
Сгенерируем совершенно новое событие `"hello"` и поймаем его на `document`.
|
||||
|
||||
Всё, что для этого нужно -- это флаг `bubbles`:
|
||||
|
||||
```html run no-beautify
|
||||
<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>
|
||||
```
|
||||
|
||||
Обратите внимание:
|
||||
|
||||
1. Обработчик события `hello` стоит на `document`. Мы его поймаем на всплытии.
|
||||
2. Вызов `event.preventDefault()` приведёт к тому, что `dispatchEvent` вернёт `false`.
|
||||
3. Чтобы событие всплывало и его можно было отменить, указан второй аргумент `new Event`.
|
||||
|
||||
Никакой разницы между встроенными событиями (`click`) и своими (`hello`) здесь нет, их можно сгенерировать и запустить совершенно одинаково.
|
||||
|
||||
## Конструкторы MouseEvent, KeyboardEvent и другие
|
||||
|
||||
Для некоторых конкретных типов событий есть свои, специфические, конструкторы.
|
||||
|
||||
Вот список конструкторов для различных событий интерфейса которые можно найти в спецификации [UI Event](http://www.w3.org/TR/uievents/):
|
||||
|
||||
- `UIEvent`
|
||||
- `FocusEvent`
|
||||
- `MouseEvent`
|
||||
- `WheelEvent`
|
||||
- `KeyboardEvent`
|
||||
- `CompositionEvent`
|
||||
|
||||
Вместо `new Event("click")` можно вызвать `new MouseEvent("click")`.
|
||||
|
||||
**Специфический конструктор позволяет указать стандартные свойства для данного типа события.**
|
||||
|
||||
Например, `clientX/clientY` для события мыши:
|
||||
|
||||
```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, свойство не присвоено!
|
||||
*/!*
|
||||
```
|
||||
|
||||
Обычный конструктор `Event` не знает про "мышиные" свойства, поэтому их игнорирует.
|
||||
|
||||
Впрочем, использование конкретного конструктора не является обязательным, можно обойтись `Event`, а свойства записать в объект отдельно, после конструктора. Здесь это скорее вопрос удобства и желания следовать правилам. События, которые генерирует браузер, всегда имеют правильный тип.
|
||||
|
||||
Полный список свойств по типам событий вы найдёте в спецификации, например для `MouseEvent`: [MouseEvent Constructor](http://www.w3.org/TR/uievents/#constructor-mouseevent).
|
||||
|
||||
## Свои события
|
||||
|
||||
Для генерации своих, нестандартных, событий, хоть и можно использовать конструктор `Event`, но существует и специфический конструктор [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+
|
||||
|
||||
Способ генерации событий, описанный выше, не поддерживается в IE11-, там нужен другой, более старый способ, описанный в стандарте [DOM 3 Events](http://www.w3.org/TR/DOM-Level-3-Events).
|
||||
|
||||
В нём была предусмотрена [иерархия событий](http://www.w3.org/TR/DOM-Level-3-Events/#event-interfaces), с различными методами инициализации.
|
||||
|
||||
Она поддерживается как современными браузерами, так и IE9+. Там используется немного другой синтаксис, но по возможностям -- всё то же самое, что и в современном стандарте.
|
||||
|
||||
Можно использовать этот немного устаревший способ, если нужно поддерживать IE9+. Далее мы на его основе создадим полифилл.
|
||||
|
||||
Объект события создаётся вызовом `document.createEvent`:
|
||||
|
||||
```js
|
||||
var event = document.createEvent(eventInterface);
|
||||
```
|
||||
|
||||
Аргументы:
|
||||
|
||||
- `eventInterface` -- это тип события, например `MouseEvent`, `FocusEvent`, `KeyboardEvent`. В [секции 5 DOM 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/#events-module) есть подробный список, какое событие к какому интерфейсу относится.
|
||||
|
||||
На практике можно всегда использовать самый общий интерфейс: `document.createEvent("Event")`.
|
||||
|
||||
Далее событие нужно инициализировать:
|
||||
|
||||
```js
|
||||
event.initEvent(type, boolean bubbles, boolean cancelable);
|
||||
```
|
||||
|
||||
Аргументы:
|
||||
|
||||
- `type` -- тип события, например `"click"`.
|
||||
- `bubbles` -- всплывает ли событие.
|
||||
- `cancelable` -- можно ли отменить событие.
|
||||
|
||||
Эти два кода аналогичны:
|
||||
|
||||
```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`, но это нужно проверять в конкретных случаях, иногда это не работает или работает некорректно, так что лучше указать все.
|
||||
````
|
||||
|
||||
## Полифилл CustomEvent
|
||||
|
||||
Для поддержки `CustomEvent` в IE9+ можно сделать небольшой полифилл:
|
||||
|
||||
```js
|
||||
try {
|
||||
new CustomEvent("IE has CustomEvent, but doesn't support constructor");
|
||||
} catch (e) {
|
||||
|
||||
window.CustomEvent = function(event, params) {
|
||||
var evt;
|
||||
params = params || {
|
||||
bubbles: false,
|
||||
cancelable: false,
|
||||
detail: undefined
|
||||
};
|
||||
evt = document.createEvent("CustomEvent");
|
||||
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
|
||||
return evt;
|
||||
};
|
||||
|
||||
CustomEvent.prototype = Object.create(window.Event.prototype);
|
||||
}
|
||||
```
|
||||
|
||||
Здесь мы сначала проверяем -- в IE9-11 есть `CustomEvent`, но его нельзя создать через `new`, будет ошибка. В этом случае заменяем браузерную реализацию на свою, совместимую.
|
||||
|
||||
## Антистандарт: 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` настраивать нельзя, браузер использует стандартные для данного типа событий.
|
||||
|
||||
Существуют полифиллы для генерации произвольных событий и для IE8-, но они, по сути, полностью подменяют встроенную систему обработки событий браузером. И кода это требует тоже достаточно много.
|
||||
|
||||
Альтернатива -- фреймворк, например jQuery, который также реализует свою мощную систему работы с событиями, доступную через методы jQuery.
|
||||
|
||||
## Итого
|
||||
|
||||
- Все браузеры, кроме IE9-11, позволяют генерировать любые события, следуя стандарту DOM4.
|
||||
- В IE9+ поддерживается более старый стандарт, можно легко сделать полифилл, например для `CustomEvent` он рассмотрен в этой главе.
|
||||
- IE8- может генерировать только встроенные события.
|
||||
|
||||
Несмотря на техническую возможность генерировать встроенные браузерные события типа `click` или `keydown` -- пользоваться ей стоит с большой осторожностью.
|
||||
|
||||
В 98% случаев, когда разработчик начинающего или среднего уровня хочет сгенерировать *встроенное* событие -- это вызвано "кривой" архитектурой кода, и взаимодействие нужно на уровне выше.
|
||||
|
||||
Как правило события имеет смысл генерировать:
|
||||
|
||||
- Либо как явный и грубый хак, чтобы заставить работать сторонние библиотеки, в которых не предусмотрены другие средства взаимодействия.
|
||||
- Либо для автоматического тестирования, чтобы скриптом "нажать на кнопку" и посмотреть, произошло ли нужное действие.
|
||||
- Либо при создании своих "элементов интерфейса". Например, никто не мешает при помощи JavaScript создать из `<div class="calendar">` красивый календарь и генерировать на нём событие `change` при выборе даты. Эту тему мы разовьём позже.
|
||||
|
3
2-ui/2-events/index.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Основы работы с событиями
|
||||
|
||||
Введение в браузерные события, общие свойства всех событий и приёмы работы с ними.
|