up
|
@ -1,3 +1,3 @@
|
||||||
# Events
|
# Introduction into Events
|
||||||
|
|
||||||
An introduction to browser events, event properties and handling patterns.
|
An introduction to browser events, event properties and handling patterns.
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
.selected {
|
||||||
|
background: #0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
Click on a list item to select it.
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<ul id="ul">
|
||||||
|
<li>Christopher Robin</li>
|
||||||
|
<li>Winnie-the-Pooh</li>
|
||||||
|
<li>Tigger</li>
|
||||||
|
<li>Kanga</li>
|
||||||
|
<li>Rabbit. Just rabbit.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
ul.onclick = function(event) {
|
||||||
|
if (event.target.tagName != "LI") return;
|
||||||
|
|
||||||
|
if (event.ctrlKey || event.metaKey) {
|
||||||
|
toggleSelect(event.target);
|
||||||
|
} else {
|
||||||
|
singleSelect(event.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// prevent unneeded selection of list elements on clicks
|
||||||
|
ul.onmousedown = function() {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
function toggleSelect(li) {
|
||||||
|
li.classList.toggle('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
function singleSelect(li) {
|
||||||
|
let selected = ul.querySelector('.selected');
|
||||||
|
if (selected) {
|
||||||
|
selected.classList.remove('selected');
|
||||||
|
}
|
||||||
|
li.classList.add('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,35 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
.selected {
|
||||||
|
background: #0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
Click on a list item to select it.
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<ul id="ul">
|
||||||
|
<li>Christopher Robin</li>
|
||||||
|
<li>Winnie-the-Pooh</li>
|
||||||
|
<li>Tigger</li>
|
||||||
|
<li>Kanga</li>
|
||||||
|
<li>Rabbit. Just rabbit.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ...your code...
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,17 @@
|
||||||
|
importance: 5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Selectable list
|
||||||
|
|
||||||
|
Create a list where elements are selectable, like in file-managers.
|
||||||
|
|
||||||
|
- A click on a list element selects only that element (adds the class `.selected`), deselects all others.
|
||||||
|
- If a click is made with `key:Ctrl` (`key:Cmd` for Mac), then the selection is toggled on the element, but other elements are not modified.
|
||||||
|
|
||||||
|
The demo:
|
||||||
|
|
||||||
|
[iframe border="1" src="solution" height=180]
|
||||||
|
|
||||||
|
P.S. For this task we can assume that list items are text-only. No nested tags.
|
||||||
|
P.P.S. Prevent the native browser selection of the text on clicks.
|
260
2-ui/3-event-details/1-mouse-clicks/article.md
Normal file
|
@ -0,0 +1,260 @@
|
||||||
|
# Mouse: clicks, coordinates
|
||||||
|
|
||||||
|
In this chapter we'll get into more details of mouse events and their properties.
|
||||||
|
|
||||||
|
[cut]
|
||||||
|
|
||||||
|
## Mouse event types
|
||||||
|
|
||||||
|
We can split mouse events into two categories: "simple" and "complex"
|
||||||
|
|
||||||
|
### Simple events
|
||||||
|
|
||||||
|
The most used simple events are:
|
||||||
|
|
||||||
|
`mousedown/mouseup`
|
||||||
|
: Mouse button is clicked/released over an element.
|
||||||
|
|
||||||
|
`mouseover/mouseout`
|
||||||
|
: Mouse pointer comes over/out an element.
|
||||||
|
|
||||||
|
`mousemove`
|
||||||
|
: Every mouse move over an element triggers that event.
|
||||||
|
|
||||||
|
There are several other event types too, we'll cover them later.
|
||||||
|
|
||||||
|
### Complex events
|
||||||
|
|
||||||
|
`click`
|
||||||
|
: Triggers after `mousedown` and then `mouseup` over the same element.
|
||||||
|
|
||||||
|
`contextmenu`
|
||||||
|
: Triggers after `mousedown` if the right mouse button was used.
|
||||||
|
|
||||||
|
`dblclick`
|
||||||
|
: Triggers after a double click over an element.
|
||||||
|
|
||||||
|
Complex events are made of simple ones, so in theory we could live without them. But they exist, and that's good, because they are convenient.
|
||||||
|
|
||||||
|
For touchscreen devices mouse events also happen, because they are emulated.
|
||||||
|
|
||||||
|
### Events order
|
||||||
|
|
||||||
|
An action may trigger multiple events.
|
||||||
|
|
||||||
|
For instance, a click first triggers `mousedown`, when the button is pressed, then `mouseup` and `click` when it's released.
|
||||||
|
|
||||||
|
In cases when a single action initiated multiple events, their order is fixed. That is, the handlers are be called in the order `mousedown` -> `mouseup` -> `click`. Events are handled in sequence: `onmouseup` finishes before `onclick` runs.
|
||||||
|
|
||||||
|
```online
|
||||||
|
Click the button below and you'll see which events happen. Try double-click too.
|
||||||
|
|
||||||
|
On the teststand below all mouse events are logged, and if there are more than 1 second delay between them, then they are separated by a horizontal ruler.
|
||||||
|
|
||||||
|
Also we can see the `which` property that allows to detect the mouse button. We'll cover it a bit later.
|
||||||
|
|
||||||
|
<input onmousedown="return logMouse(event)" onmouseup="return logMouse(event)" onclick="return logMouse(event)" oncontextmenu="return logMouse(event)" ondblclick="return logMouse(event)" value="Click me with the right or the left mouse button" type="button"> <input onclick="logClear('test')" value="Clear" type="button"> <form id="testform" name="testform"> <textarea style="font-size:12px;height:150px;width:360px;"></textarea></form>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting the button: which
|
||||||
|
|
||||||
|
Click-related events always have the `which` property that allows to see the button.
|
||||||
|
|
||||||
|
It is not used for `click` and `contextmenu` events, because the former happens only on left-click, and the latter -- only on right-click.
|
||||||
|
|
||||||
|
But if we track `mousedown` and `mouseup`, then we need it, because they trigger on any button.
|
||||||
|
|
||||||
|
There are the three possible values:
|
||||||
|
|
||||||
|
- `event.which == 1` -- the left button
|
||||||
|
- `event.which == 2` - the middle button
|
||||||
|
- `event.which == 3` - the right button
|
||||||
|
|
||||||
|
The middle button is somewhat exotic right now.
|
||||||
|
|
||||||
|
## Modifiers: shift, alt, ctrl and meta
|
||||||
|
|
||||||
|
All mouse events include the information about pressed modifier keys.
|
||||||
|
|
||||||
|
The properties are:
|
||||||
|
|
||||||
|
- `shiftKey`
|
||||||
|
- `altKey`
|
||||||
|
- `ctrlKey`
|
||||||
|
- `metaKey` (for Mac)
|
||||||
|
|
||||||
|
For instance, the button below only works on `key:Alt+Shift`+click:
|
||||||
|
|
||||||
|
```html autorun height=60
|
||||||
|
<button id="button">Alt+Shift+Click on me!</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
button.onclick = function(event) {
|
||||||
|
*!*
|
||||||
|
if (event.altKey && event.shiftKey) {
|
||||||
|
*/!*
|
||||||
|
alert('Hooray!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
```warn header="Attention: on Mac it's usually `Cmd` instead of `Ctrl`"
|
||||||
|
On Windows and Linux there are modifier keys `key:Alt`, `key:Shift` and `key:Ctrl`. On Mac there's one more: `key:Cmd`, that corresponds to the property `metaKey`.
|
||||||
|
|
||||||
|
In most cases when Windows/Linux uses `key:Ctrl`, on Mac people use `key:Cmd`. So where a Windows user presses `key:Ctrl+Enter` or `key:Ctrl+A`, a Mac user would press `key:Cmd+Enter` or `key:Cmd+A`, and so on, most apps use `key:Cmd` instead of `key:Ctrl`.
|
||||||
|
|
||||||
|
So if we want to support combinations like `key:Ctrl`+click, then for Mac it makes sense to use `key:Cmd`+click. That's more comfortable for Mac users.
|
||||||
|
|
||||||
|
Even if we'd like to force Mac users to `key:Ctrl`+click -- that's kind of difficult. The problem is: a regular click with `key:Ctrl` is interpreted as a *right click* on Mac, and it generates the `contextmenu` event, not `click` like Windows/Linux.
|
||||||
|
|
||||||
|
So if we want users of all operational systems to feel comfortable, then together with `ctrlKey` we should use `metaKey`.
|
||||||
|
|
||||||
|
For JS-code it means that we should check `if (event.ctrlKey || event.metaKey)`.
|
||||||
|
```
|
||||||
|
|
||||||
|
```warn header="There are also mobile devices"
|
||||||
|
Keyboard combinations are good as an addition to the workflow. So that if you have keyboard -- it works. And if your device doesn't have it -- then there's another way to do the same.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coordinates: clientX/Y, pageX/Y
|
||||||
|
|
||||||
|
All mouse events have coordinates in two flavours:
|
||||||
|
|
||||||
|
1. Window-relative: `clientX` and `clientY`.
|
||||||
|
2. Document-relative: `pageX` and `pageY`.
|
||||||
|
|
||||||
|
See more about coordinates the chapter <info:coordinates>.
|
||||||
|
|
||||||
|
For instance, if we have a window of the size 500x500, and the mouse is in the center, then `clientX` and `clientY` are `250`.
|
||||||
|
|
||||||
|
If we scroll the page, but the mouse is still in the center, then `clientX/Y` don't change, because they are window-relative.
|
||||||
|
|
||||||
|
````online
|
||||||
|
Move the mouse over the input field to see `clientX/clientY`:
|
||||||
|
|
||||||
|
```html autorun height=50
|
||||||
|
<input onmousemove="this.value = event.clientX+':'+event.clientY">
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
That's like `elem.getBoundingClientRect()` and `position:fixed`.
|
||||||
|
|
||||||
|
Document-relative coordinates are counted from the left-upper corner of the document, not the window. In case of a scrolled page, they also include the scrolled out left-upper part.
|
||||||
|
|
||||||
|
These coordinates are connected by the formulas:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// for an arbitrary mouse event
|
||||||
|
event.pageX = pageXOffset + event.clientX
|
||||||
|
event.pageY = pageYOffset + event.clientY
|
||||||
|
```
|
||||||
|
|
||||||
|
So technically we don't need `pageX/Y`, because we can always calculate them using the formulas. But it's good that we have them, as a matter of convenience.
|
||||||
|
|
||||||
|
## No selection on mousedown
|
||||||
|
|
||||||
|
Mouse clicks have a side-effect that may be disturbing. A double click or an occasional cursor move with a pressed button select the text.
|
||||||
|
|
||||||
|
If we want to handle click events ourselves, then the "extra" selection doesn't look good.
|
||||||
|
|
||||||
|
For instance, a double-click on the text below selects it in addition to our handler:
|
||||||
|
|
||||||
|
```html autorun height=50
|
||||||
|
<b ondblclick="alert('dblclick')">Double-click me</b>
|
||||||
|
```
|
||||||
|
|
||||||
|
There's a CSS way to stop the selection: the `user-select` property from [CSS UI Draft](https://www.w3.org/TR/css-ui-4/).
|
||||||
|
|
||||||
|
It's yet in the draft, so browser support it with prefixes:
|
||||||
|
|
||||||
|
```html autorun height=50
|
||||||
|
<style>
|
||||||
|
b {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
Before...
|
||||||
|
<b ondblclick="alert('Test')">
|
||||||
|
Unselectable
|
||||||
|
</b>
|
||||||
|
...After
|
||||||
|
```
|
||||||
|
|
||||||
|
Now if you double-click on `"Unselectable"`, it doesn't get selected. Seems to work.
|
||||||
|
|
||||||
|
...But there is a side-effect! The text became truly unselectable. Even if a user starts the selection from `"Before"` and ends with `"After"`, the selection skips `"Unselectable"` part. Do we really want to make our text unselectable?
|
||||||
|
|
||||||
|
Most of time, not really. A user may want to select it, for copying or other needs. That may be disturbing if we don't allow him to do it. So the solution is not that good.
|
||||||
|
|
||||||
|
What we want is to "fix" our interface. We don't want the selection to occur on double-click, that's it.
|
||||||
|
|
||||||
|
An alternative solution would be to handle `mousedown`, like this:
|
||||||
|
|
||||||
|
```html autorun height=50
|
||||||
|
Before...
|
||||||
|
<b ondblclick="alert('Click!')" *!*onmousedown="return false"*/!*>
|
||||||
|
Double-click me
|
||||||
|
</b>
|
||||||
|
...After
|
||||||
|
```
|
||||||
|
|
||||||
|
The selection is started on `mousedown` as a default browser action. So if we prevent it, then the bold element is not selected any more on clicks. That's as intended.
|
||||||
|
|
||||||
|
From the other hand, the text inside it is still selectable. The only limitation: the selection should start not on the text itself, but from "before" or "after" it. Usually that's not a problem.
|
||||||
|
|
||||||
|
````smart header="Canceling the selection"
|
||||||
|
Instead of *preventing* the selection, we can cancel it "post-factum" in the event handler.
|
||||||
|
|
||||||
|
Here's how:
|
||||||
|
|
||||||
|
```html autorun height=50
|
||||||
|
Before...
|
||||||
|
<b ondblclick="*!*getSelection().removeAllRanges()*/!*">
|
||||||
|
Double-click me
|
||||||
|
</b>
|
||||||
|
...After
|
||||||
|
```
|
||||||
|
|
||||||
|
If you double-click on the bold element, then the selection appears and then is immediately removed. That doesn't look nice, and is not fully reliable though.
|
||||||
|
````
|
||||||
|
|
||||||
|
````smart header="Preventing copying"
|
||||||
|
If we want to disable selection to protect our content from copy-pasting, then we can use another event: `oncopy`.
|
||||||
|
|
||||||
|
```html autorun height=80 no-beautify
|
||||||
|
<div *!*oncopy="alert('Copying forbidden!');return false"*/!*>
|
||||||
|
Dear user,
|
||||||
|
The copying is forbidden for you.
|
||||||
|
If you know JS or HTML, then that's not a problem of course,
|
||||||
|
otherwise we're sorry.
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
If you try to copy a piece of text in the `<div>`, that won't work, because the default action `oncopy` is prevented.
|
||||||
|
|
||||||
|
Surely that doesn't stop from opening HTML-source and doing things manually, but not everyone knows how to do it.
|
||||||
|
````
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Mouse events have following properties:
|
||||||
|
|
||||||
|
- Button: `which`
|
||||||
|
- Modifier keys (`true` if pressed): `altKey`, `ctrlKey`, `shiftKey` and `metaKey` (Mac).
|
||||||
|
- If you want to handle `key:Ctrl`, then don't forget Mac users, they use `key:Cmd`, so it's better to check `if (e.metaKey || e.ctrlKey)`.
|
||||||
|
|
||||||
|
- Window-relative coordinates: `clientX/clientY`
|
||||||
|
- Document-relative coordinates: `pageX/clientX`
|
||||||
|
|
||||||
|
In the tasks below it's also important to deal with the selection as an unwanted side-effect of clicks.
|
||||||
|
|
||||||
|
There are several ways, for instance:
|
||||||
|
1. CSS-property `user-select:none` (with browser prefixes) completely disables it.
|
||||||
|
2. Cancel the selection post-factum using `getSelection().removeAllRanges()`.
|
||||||
|
3. Handle `mousedown` and prevent the default action.
|
||||||
|
|
||||||
|
The third way is preferred most of the time.
|
47
2-ui/3-event-details/1-mouse-clicks/head.html
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<script>
|
||||||
|
{
|
||||||
|
let timer = 0;
|
||||||
|
|
||||||
|
function showmesg(t, form) {
|
||||||
|
|
||||||
|
if (timer==0) timer = new Date()
|
||||||
|
|
||||||
|
let tm = new Date()
|
||||||
|
if (tm-timer > 300) {
|
||||||
|
t = '------------------------------\n'+t
|
||||||
|
}
|
||||||
|
|
||||||
|
let area = document.forms[form+'form'].getElementsByTagName('textarea')[0]
|
||||||
|
|
||||||
|
area.value += t + '\n';
|
||||||
|
area.scrollTop = area.scrollHeight
|
||||||
|
|
||||||
|
timer = tm
|
||||||
|
}
|
||||||
|
|
||||||
|
function logMouse(e) {
|
||||||
|
let evt = e.type;
|
||||||
|
while (evt.length < 11) evt += ' ';
|
||||||
|
showmesg(evt+" which="+e.which, 'test')
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyval(n) {
|
||||||
|
if (n == null) return 'undefined';
|
||||||
|
let s = '' + n;
|
||||||
|
if (n >= 32 && n < 127) s += ' ' + String.fromCharCode(n);
|
||||||
|
while (s.length < 6) s += ' ';
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function logClear(form) {
|
||||||
|
timer = 0;
|
||||||
|
document.forms[form+'form'].getElementsByTagName('textarea')[0].value ='';
|
||||||
|
lines = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.logClear = logClear;
|
||||||
|
window.logMouse = logMouse;
|
||||||
|
}
|
||||||
|
</script>
|
204
2-ui/3-event-details/10-onload-ondomcontentloaded/article.md
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
# Загрузка документа: DOMContentLoaded, load, beforeunload, unload
|
||||||
|
|
||||||
|
Процесс загрузки HTML-документа, условно, состоит из трёх стадий:
|
||||||
|
|
||||||
|
- `DOMContentLoaded` -- браузер полностью загрузил HTML и построил DOM-дерево.
|
||||||
|
- `load` -- браузер загрузил все ресурсы.
|
||||||
|
- `beforeunload/unload` -- уход со страницы.
|
||||||
|
|
||||||
|
Все эти стадии очень важны. На каждую можно повесить обработчик, чтобы совершить полезные действия:
|
||||||
|
|
||||||
|
- `DOMContentLoaded` -- означает, что все DOM-элементы разметки уже созданы, можно их искать, вешать обработчики, создавать интерфейс, но при этом, возможно, ещё не догрузились какие-то картинки или стили.
|
||||||
|
- `load` -- страница и все ресурсы загружены, используется редко, обычно нет нужды ждать этого момента.
|
||||||
|
- `beforeunload/unload` -- можно проверить, сохранил ли посетитель изменения, уточнить, действительно ли он хочет покинуть страницу.
|
||||||
|
|
||||||
|
Далее мы рассмотрим важные детали этих событий.
|
||||||
|
|
||||||
|
[cut]
|
||||||
|
|
||||||
|
## DOMContentLoaded
|
||||||
|
|
||||||
|
Событие `DOMContentLoaded` происходит на `document` и поддерживается во всех браузерах, кроме IE8-. Про поддержку аналогичного функционала в старых IE мы поговорим в конце главы.
|
||||||
|
|
||||||
|
Обработчик на него вешается только через `addEventListener`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
document.addEventListener("DOMContentLoaded", ready);
|
||||||
|
```
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```html run height=150
|
||||||
|
<script>
|
||||||
|
function ready() {
|
||||||
|
alert( 'DOM готов' );
|
||||||
|
alert( "Размеры картинки: " + img.offsetWidth + "x" + img.offsetHeight );
|
||||||
|
}
|
||||||
|
|
||||||
|
*!*
|
||||||
|
document.addEventListener("DOMContentLoaded", ready);
|
||||||
|
*/!*
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<img id="img" src="https://js.cx/clipart/yozhik.jpg?speed=1">
|
||||||
|
```
|
||||||
|
|
||||||
|
В примере выше обработчик `DOMContentLoaded` сработает сразу после загрузки документа, не дожидаясь получения картинки.
|
||||||
|
|
||||||
|
Поэтому на момент вывода `alert` и сама картинка будет невидна и её размеры -- неизвестны (кроме случая, когда картинка взята из кеша браузера).
|
||||||
|
|
||||||
|
В своей сути, событие `onDOMContentLoaded` -- простое, как пробка. Полностью создано DOM-дерево -- и вот событие. Но с ним связан ряд существенных тонкостей.
|
||||||
|
|
||||||
|
### DOMContentLoaded и скрипты
|
||||||
|
|
||||||
|
Если в документе есть теги `<script>`, то браузер обязан их выполнить до того, как построит DOM. Поэтому событие `DOMContentLoaded` ждёт загрузки и выполнения таких скриптов.
|
||||||
|
|
||||||
|
Исключением являются скрипты с атрибутами `async` и `defer`, которые подгружаются асинхронно.
|
||||||
|
|
||||||
|
**Побочный эффект: если на странице подключается скрипт с внешнего ресурса (к примеру, реклама), и он тормозит, то событие `DOMContentLoaded` и связанные с ним действия могут сильно задержаться.**
|
||||||
|
|
||||||
|
Современные системы рекламы используют атрибут `async`, либо вставляют скрипты через DOM: `document.createElement('script')...`, что работает так же как `async`: такой скрипт выполняется полностью независимо от страницы и от других скриптов -- сам ничего не ждёт и ничего не блокирует.
|
||||||
|
|
||||||
|
### DOMContentLoaded и стили
|
||||||
|
|
||||||
|
Внешние стили никак не влияют на событие `DOMContentLoaded`. Но есть один нюанс.
|
||||||
|
|
||||||
|
**Если после стиля идёт скрипт, то этот скрипт обязан дождаться, пока стиль загрузится:**
|
||||||
|
|
||||||
|
```html
|
||||||
|
<link type="text/css" rel="stylesheet" href="style.css">
|
||||||
|
<script>
|
||||||
|
// сработает после загрузки style.css
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Такое поведение прописано в стандарте. Его причина -- скрипт может захотеть получить информацию со страницы, зависящую от стилей, например, ширину элемента, и поэтому обязан дождаться загрузки `style.css`.
|
||||||
|
|
||||||
|
**Побочный эффект -- так как событие `DOMContentLoaded` будет ждать выполнения скрипта, то оно подождёт и загрузки стилей, которые идут перед `<script>`.**
|
||||||
|
|
||||||
|
### Автозаполнение
|
||||||
|
|
||||||
|
Firefox/Chrome/Opera автозаполняют формы по `DOMContentLoaded`.
|
||||||
|
|
||||||
|
Это означает, что если на странице есть форма для ввода логина-пароля, то браузер введёт в неё запомненные значения только по `DOMContentLoaded`.
|
||||||
|
|
||||||
|
**Побочный эффект: если `DOMContentLoaded` ожидает множества скриптов и стилей, то автозаполнение не сработает до полной их загрузки.**
|
||||||
|
|
||||||
|
Конечно, это довод в пользу того, чтобы не задерживать `DOMContentLoaded`, в частности -- использовать у скриптов атрибуты `async` и `defer`.
|
||||||
|
|
||||||
|
## window.onload [#window-onload]
|
||||||
|
|
||||||
|
Событие `onload` на `window` срабатывает, когда загружается *вся* страница, включая ресурсы на ней -- стили, картинки, ифреймы и т.п.
|
||||||
|
|
||||||
|
Пример ниже выведет `alert` лишь после полной загрузки окна, включая `IFRAME` и картинку:
|
||||||
|
|
||||||
|
```html run
|
||||||
|
<script>
|
||||||
|
*!*
|
||||||
|
window.onload = function() {
|
||||||
|
alert( 'Документ и все ресурсы загружены' );
|
||||||
|
};
|
||||||
|
*/!*
|
||||||
|
</script>
|
||||||
|
<iframe src="https://example.com/" style="height:60px"></iframe>
|
||||||
|
<img src="https://js.cx/clipart/yozhik.jpg?speed=1">
|
||||||
|
```
|
||||||
|
|
||||||
|
## window.onunload
|
||||||
|
|
||||||
|
Когда человек уходит со страницы или закрывает окно, на `window` срабатывает событие `unload`. В нём можно сделать что-то, не требующее ожидания, например, закрыть вспомогательные popup-окна, но отменить сам переход нельзя.
|
||||||
|
|
||||||
|
Это позволяет другое событие -- `onbeforeunload`, которое поэтому используется гораздо чаще.
|
||||||
|
|
||||||
|
## window.onbeforeunload [#window.onbeforeunload]
|
||||||
|
|
||||||
|
Если посетитель инициировал переход на другую страницу или нажал "закрыть окно", то обработчик `onbeforeunload` может приостановить процесс и спросить подтверждение.
|
||||||
|
|
||||||
|
Для этого ему нужно вернуть строку, которую браузеры покажут посетителю, спрашивая -- нужно ли переходить.
|
||||||
|
|
||||||
|
Например:
|
||||||
|
|
||||||
|
```js
|
||||||
|
window.onbeforeunload = function() {
|
||||||
|
return "Данные не сохранены. Точно перейти?";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
```warn header="Firefox игнорирует текст, он показывает своё сообщение"
|
||||||
|
Firefox игнорирует текст, а всегда показывает своё сообщение. Это сделано в целях большей безопасности посетителя, чтобы его нельзя было ввести в заблуждение сообщением.
|
||||||
|
```
|
||||||
|
|
||||||
|
```online
|
||||||
|
Кликните на кнопку в `IFRAME'е` ниже, чтобы поставить обработчик, а затем по ссылке, чтобы увидеть его в действии:
|
||||||
|
|
||||||
|
[iframe src="window-onbeforeunload" border="1" height="80" link]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Эмуляция DOMContentLoaded для IE8-
|
||||||
|
|
||||||
|
Прежде чем что-то эмулировать, заметим, что альтернативой событию `onDOMContentLoaded` является вызов функции `init` из скрипта в самом конце `BODY`, когда основная часть DOM уже готова:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<body>
|
||||||
|
...
|
||||||
|
<script>
|
||||||
|
init();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
```
|
||||||
|
|
||||||
|
Причина, по которой обычно предпочитают именно событие -- одна: удобство. Вешается обработчик и не надо ничего писать в конец `BODY`.
|
||||||
|
|
||||||
|
### Мини-скрипт documentReady
|
||||||
|
Если вы всё же хотите использовать `onDOMContentLoaded` кросс-браузерно, то нужно либо подключить какой-нибудь фреймворк -- почти все предоставляют такой функционал, либо использовать функцию из мини-библиотеки [jquery.documentReady.js](https://github.com/Couto/jquery.parts/blob/master/jquery.documentReady.js).
|
||||||
|
|
||||||
|
Несмотря на то, что в названии содержится слово "jquery", эта библиотечка не требует [jQuery](http://jquery.com). Наоборот, она представляет собой единственную функцию с названием `$`, вызов которой `$(callback)` добавляет обработчик `callback` на `DOMContentLoaded` (можно вызывать много раз), либо, если документ уже загружен -- выполняет его тут же.
|
||||||
|
|
||||||
|
Пример использования:
|
||||||
|
|
||||||
|
```html run
|
||||||
|
<script src="https://js.cx/script/jquery.documentReady.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
*!*
|
||||||
|
$(function() {
|
||||||
|
alert( "DOMContentLoaded" );
|
||||||
|
});
|
||||||
|
*/!*
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<img src="https://js.cx/clipart/yozhik.jpg?speed=1">
|
||||||
|
<div>Текст страницы</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Здесь `alert` сработает до загрузки картинки, но после создания DOM, в частности, после появления текста. И так будет для всех браузеров, включая даже очень старые IE.
|
||||||
|
|
||||||
|
````smart header="Как именно эмулируется `DOMContentLoaded`?"
|
||||||
|
Технически, эмуляция `DOMContentLoaded` для старых IE осуществляется очень забавно.
|
||||||
|
|
||||||
|
Основной приём -- это попытка прокрутить документ вызовом:
|
||||||
|
|
||||||
|
```js
|
||||||
|
document.documentElement.doScroll("left");
|
||||||
|
```
|
||||||
|
|
||||||
|
Метод `doScroll` работает только в IE и "методом тыка" было обнаружено, что он бросает исключение, если DOM не полностью создан.
|
||||||
|
|
||||||
|
Поэтому библиотека пытается вызвать прокрутку, если не получается -- через `setTimeout(.., 1)` пытается прокрутить его ещё раз, и так до тех пор, пока действие не перестанет вызывать ошибку. На этом этапе документ считается загрузившимся.
|
||||||
|
|
||||||
|
Внутри фреймов и в очень старых браузерах такой подход может ошибаться, поэтому дополнительно ставится резервный обработчик на `onload`, чтобы уж точно сработал.
|
||||||
|
````
|
||||||
|
|
||||||
|
## Итого
|
||||||
|
|
||||||
|
- Самое востребованное событие из описанных -- без сомнения, `DOMContentLoaded`. Многие страницы сделаны так, что инициализуют интерфейсы именно по этому событию.
|
||||||
|
|
||||||
|
Это удобно, ведь можно в `<head>` написать скрипт, который будет запущен в момент, когда все DOM-элементы доступны.
|
||||||
|
|
||||||
|
С другой стороны, следует иметь в виду, что событие `DOMContentLoaded` будет ждать не только, собственно, HTML-страницу, но и внешние скрипты, подключенные тегом `<script>` без атрибутов `defer/async`, а также стили перед такими скриптами.
|
||||||
|
|
||||||
|
Событие `DOMContentLoaded` не поддерживается в IE8-, но почти все фреймворки умеют его эмулировать. Если нужна отдельная функция только для кросс-браузерного аналога `DOMContentLoaded` -- можно использовать [jquery.documentReady.js](https://github.com/Couto/jquery.parts/blob/master/jquery.documentReady.js).
|
||||||
|
- Событие `window.onload` используют редко, поскольку обычно нет нужды ждать подгрузки *всех* ресурсов. Если же нужен конкретный ресурс (картинка или ифрейм), то можно поставить событие `onload` непосредственно на нём, мы посмотрим, как это сделать, далее.
|
||||||
|
- Событие `window.onunload` почти не используется, как правило, оно бесполезно -- мало что можно сделать, зная, что окно браузера прямо сейчас закроется.
|
||||||
|
- Гораздо чаще применяется `window.onbeforeunload` -- это де-факто стандарт для того, чтобы проверить, сохранил ли посетитель данные, действительно ли он хочет покинуть страницу. В системах редактирования документов оно используется повсеместно.
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
function setHandler() {
|
||||||
|
window.onbeforeunload = function() {
|
||||||
|
return "Данные не сохранены. Точно перейти?";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick="setHandler()">Поставить window.onbeforeunload</button>
|
||||||
|
|
||||||
|
<a href="http://example.com">Уйти на EXAMPLE.COM</a>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Подсказка
|
||||||
|
|
||||||
|
Текст на странице пусть будет изначально `DIV`, с классом `img-replace` и атрибутом `data-src` для картинки.
|
||||||
|
|
||||||
|
Функция `replaceImg()` должна искать такие `DIV` и загружать изображение с указанным `src`. По `onload` осуществляется замена `DIV` на картинку.
|
||||||
|
|
||||||
|
# Решение
|
||||||
|
|
49
2-ui/3-event-details/11-onload-onerror/1-nice-alt/solution.view/index.html
Executable file
|
@ -0,0 +1,49 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
.img-replace {
|
||||||
|
float: left;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<button onclick="window.location.reload(true)">Перезагрузить ифрейм</button>
|
||||||
|
<hr>
|
||||||
|
<div style="width:114px;height:40px;font-size:32px;letter-spacing:3px" data-src="https://js.cx/search/google.png" class="img-replace">
|
||||||
|
<span style="color:#1A53F7">G</span><span style="color:#E42131">o</span><span style="color:#FEB819">o</span><span style="color:#164AF2">g</span><span style="color:#00a315">l</span><span style="color:#E42131">e</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="width:101px;height:40px;font-size:32px" data-src="https://js.cx/search/yandex.png" class="img-replace">
|
||||||
|
<span style="color:#F00">Я</span>ндекс
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="width:100;height:40px;font-size:32px;color:#006dd4;font-weight:bold;letter-spacing:3px;font-family:Arial" data-src="bing.png" class="img-replace">bing</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function replaceImg() {
|
||||||
|
var divs = document.querySelectorAll('div.img-replace');
|
||||||
|
for (var i = 0; i < divs.length; i++)(function(i) {
|
||||||
|
var img = document.createElement('img');
|
||||||
|
img.src = divs[i].getAttribute('data-src');
|
||||||
|
img.className = 'img-replace';
|
||||||
|
|
||||||
|
img.onload = function() {
|
||||||
|
divs[i].parentNode.replaceChild(img, divs[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(replaceImg, 1000); // задержка на 1 сек для демонстрации
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
40
2-ui/3-event-details/11-onload-onerror/1-nice-alt/source.view/index.html
Executable file
|
@ -0,0 +1,40 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
.img-replace {
|
||||||
|
float: left;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Google -->
|
||||||
|
|
||||||
|
<div style="width:114px;height:40px;font-size:32px;letter-spacing:3px" class="img-replace">
|
||||||
|
<span style="color:#1A53F7">G</span><span style="color:#E42131">o</span><span style="color:#FEB819">o</span><span style="color:#164AF2">g</span><span style="color:#00a315">l</span><span style="color:#E42131">e</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Яндекс -->
|
||||||
|
<div style="width:101px;height:40px;font-size:32px" class="img-replace">
|
||||||
|
<span style="color:#F00">Я</span>ндекс
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bing -->
|
||||||
|
<div style="width:100;height:40px;font-size:32px;color:#006dd4;font-weight:bold;letter-spacing: 3px; font-family:Arial">bing</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<!-- картинки (для bing картинки специально нет, чтобы протестировать случай "загрузка не удалась") -->
|
||||||
|
|
||||||
|
<img src="https://js.cx/search/yandex.png" width="114" height="40" alt="Яндекс">
|
||||||
|
<img src="https://js.cx/search/google.png" width="101" height="40" alt="Google">
|
||||||
|
<img src="https://js.cx/search/bing.png" width="101" height="40" alt="Файла нет (bing)">
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
20
2-ui/3-event-details/11-onload-onerror/1-nice-alt/task.md
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
importance: 5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Красивый "ALT"
|
||||||
|
|
||||||
|
Обычно, до того как изображение загрузится (или при отключенных картинках), посетитель видит пустое место с текстом из "ALT". Но этот атрибут не допускает HTML-форматирования.
|
||||||
|
|
||||||
|
При мобильном доступе скорость небольшая, и хочется, чтобы посетитель сразу видел красивый текст.
|
||||||
|
|
||||||
|
**Реализуйте "красивый" (HTML) аналог `alt` при помощи CSS/JavaScript, который затем будет заменён картинкой сразу же как только она загрузится.** А если загрузка не состоится -- то не заменён.
|
||||||
|
|
||||||
|
Демо: (нажмите "перезагрузить", чтобы увидеть процесс загрузки и замены)
|
||||||
|
|
||||||
|
[iframe src="solution" height="100"]
|
||||||
|
|
||||||
|
Картинки для `bing` специально нет, так что текст остается "как есть".
|
||||||
|
|
||||||
|
Исходный документ содержит разметку текста и ссылки на изображения.
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
Создайте переменную-счетчик для подсчёта количества загруженных картинок, и увеличивайте при каждом `onload/onerror`.
|
||||||
|
|
||||||
|
Когда счетчик станет равен количеству картинок -- вызывайте `callback`.
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function preloadImages(sources, callback) {
|
||||||
|
var counter = 0;
|
||||||
|
|
||||||
|
function onLoad() {
|
||||||
|
counter++;
|
||||||
|
if (counter == sources.length) callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < sources.length; i++) {
|
||||||
|
var img = document.createElement('img');
|
||||||
|
// сначала onload/onerror, затем src - важно для IE8-
|
||||||
|
img.onload = img.onerror = onLoad;
|
||||||
|
img.src = sources[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Проверка ----------
|
||||||
|
|
||||||
|
var sources = [
|
||||||
|
"https://js.cx/images-load/1.jpg",
|
||||||
|
"https://js.cx/images-load/2.jpg",
|
||||||
|
"https://js.cx/images-load/3.jpg"
|
||||||
|
];
|
||||||
|
for (var i = 0; i < sources.length; i++) {
|
||||||
|
sources[i] += '?' + Math.random();
|
||||||
|
}
|
||||||
|
|
||||||
|
function testLoaded() {
|
||||||
|
var widthSum = 0;
|
||||||
|
for (var i = 0; i < sources.length; i++) {
|
||||||
|
var img = document.createElement('img');
|
||||||
|
img.src = sources[i];
|
||||||
|
widthSum += img.width;
|
||||||
|
}
|
||||||
|
alert(widthSum); // 300!
|
||||||
|
}
|
||||||
|
|
||||||
|
// до загрузки - 0
|
||||||
|
testLoaded();
|
||||||
|
// после загрузки - 300
|
||||||
|
preloadImages(sources, testLoaded);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,50 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function preloadImages(sources, callback) {
|
||||||
|
/* ваш код */
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Проверка ----------
|
||||||
|
|
||||||
|
/* файлы для загрузки */
|
||||||
|
var sources = [
|
||||||
|
"https://js.cx/images-load/1.jpg",
|
||||||
|
"https://js.cx/images-load/2.jpg",
|
||||||
|
"https://js.cx/images-load/3.jpg"
|
||||||
|
];
|
||||||
|
for (var i = 0; i < sources.length; i++) {
|
||||||
|
sources[i] += '?' + Math.random(); // добавляем параметр, чтобы без кеша (для теста)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** если картинка загружена, то можно будет сразу получить её ширину
|
||||||
|
* создадим все картинки и проверим, есть ли у них ширина
|
||||||
|
*/
|
||||||
|
function testLoaded() {
|
||||||
|
var widthSum = 0;
|
||||||
|
for (var i = 0; i < sources.length; i++) {
|
||||||
|
var img = document.createElement('img');
|
||||||
|
img.src = sources[i];
|
||||||
|
widthSum += img.width;
|
||||||
|
}
|
||||||
|
// каждое изображение 100x100, общая ширина должна быть 300px
|
||||||
|
alert(widthSum);
|
||||||
|
}
|
||||||
|
|
||||||
|
// до загрузки - выведет 0
|
||||||
|
testLoaded();
|
||||||
|
|
||||||
|
// после загрузки - выведет 300
|
||||||
|
preloadImages(sources, testLoaded);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,20 @@
|
||||||
|
importance: 4
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Загрузить изображения с коллбэком
|
||||||
|
|
||||||
|
Создайте функцию `preloadImages(sources, callback)`, которая предзагружает изображения из массива `sources`, и после загрузки вызывает функцию `callback`.
|
||||||
|
|
||||||
|
Пример использования:
|
||||||
|
|
||||||
|
```js
|
||||||
|
preloadImages(["1.jpg", "2.jpg", "3.jpg"], callback);
|
||||||
|
```
|
||||||
|
|
||||||
|
Если вдруг возникает ошибка при загрузке -- считаем такое изображение загруженным, чтобы не ломать поток выполнения.
|
||||||
|
|
||||||
|
Такая функция может полезна, например, для фоновой загрузки картинок в онлайн-галерею.
|
||||||
|
|
||||||
|
В исходном документе содержатся ссылки на картинки, а также код для проверки, действительно ли изображения загрузились. Он должен выводить "0", затем "300".
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Подсказка
|
||||||
|
|
||||||
|
Добавляйте `SCRIPT` при помощи методов `DOM`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = src;
|
||||||
|
|
||||||
|
// в документе может не быть HEAD или BODY,
|
||||||
|
// но хотя бы один (текущий) SCRIPT в документе есть
|
||||||
|
var s = document.getElementsByTagName('script')[0];
|
||||||
|
s.parentNode.insertBefore(script, s); // перед ним и вставим
|
||||||
|
```
|
||||||
|
|
||||||
|
На скрипт повесьте обработчики `onload/onreadystatechange`.
|
||||||
|
|
||||||
|
# Решение
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
function go() {
|
||||||
|
alert("ok");
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function addScript(src, callback) {
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = src;
|
||||||
|
var s = document.getElementsByTagName('script')[0]
|
||||||
|
s.parentNode.insertBefore(script, s);
|
||||||
|
|
||||||
|
var loaded = false;
|
||||||
|
|
||||||
|
function onload() {
|
||||||
|
if (loaded) return; // повторный вызов
|
||||||
|
loaded = true;
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
script.onload = onload; // все браузеры, IE с версии 9
|
||||||
|
|
||||||
|
script.onreadystatechange = function() { // IE8-
|
||||||
|
if (this.readyState == 'loaded' || this.readyState == 'complete') {
|
||||||
|
setTimeout(onload, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
addScript("go.js", function() {
|
||||||
|
go();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,3 @@
|
||||||
|
function go() {
|
||||||
|
alert("ok");
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function addScript(src, callback) {
|
||||||
|
/* ваш код */
|
||||||
|
}
|
||||||
|
|
||||||
|
addScript("go.js", function() {
|
||||||
|
go();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,21 @@
|
||||||
|
importance: 4
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Скрипт с коллбэком
|
||||||
|
|
||||||
|
Создайте функцию `addScript(src, callback)`, которая загружает скрипт с данным `src`, и после его загрузки и выполнения вызывает функцию `callback`.
|
||||||
|
|
||||||
|
Скрипт может быть любым, работа функции не должна зависеть от его содержимого.
|
||||||
|
|
||||||
|
Пример использования:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// go.js содержит функцию go()
|
||||||
|
addScript("go.js", function() {
|
||||||
|
go();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Ошибки загрузки обрабатывать не нужно.
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Подсказки
|
||||||
|
|
||||||
|
Создайте переменную-счетчик для подсчёта количества загруженных скриптов.
|
||||||
|
|
||||||
|
Чтобы один скрипт не учитывался два раза (например, `onreadystatechange` запустился при `loaded` и `complete`), учитывайте его состояние в объекте `loaded`. Свойство `loaded[i] = true` означает что `i`-й скрипт уже учтён.
|
||||||
|
|
||||||
|
# Решение
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
function a() {
|
||||||
|
b();
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
function b() {
|
||||||
|
c();
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
function c() {
|
||||||
|
alert('ok');
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function addScript(src) {
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = src;
|
||||||
|
var s = document.getElementsByTagName('script')[0]
|
||||||
|
s.parentNode.insertBefore(script, s);
|
||||||
|
return script;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addScripts(scripts, callback) {
|
||||||
|
var loaded = {}; // Для загруженных файлов loaded[i] = true
|
||||||
|
var counter = 0;
|
||||||
|
|
||||||
|
function onload(i) {
|
||||||
|
if (loaded[i]) return; // лишний вызов onload/onreadystatechange
|
||||||
|
loaded[i] = true;
|
||||||
|
counter++;
|
||||||
|
if (counter == scripts.length) callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < scripts.length; i++)(function(i) {
|
||||||
|
var script = addScript(scripts[i]);
|
||||||
|
|
||||||
|
script.onload = function() {
|
||||||
|
onload(i);
|
||||||
|
};
|
||||||
|
|
||||||
|
script.onreadystatechange = function() { // IE8-
|
||||||
|
if (this.readyState == 'loaded' || this.readyState == 'complete') {
|
||||||
|
setTimeout(this.onload, 0); // возможны повторные вызовы onload
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}(i));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
addScripts(["a.js", "b.js", "c.js"], function() {
|
||||||
|
a()
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,3 @@
|
||||||
|
function a() {
|
||||||
|
b();
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
function b() {
|
||||||
|
c();
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
function c() {
|
||||||
|
alert('ok');
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* ваш код */
|
||||||
|
|
||||||
|
// функция a() сработает только если загружены a.js, b.js, c.js
|
||||||
|
addScripts(["a.js", "b.js", "c.js"], function() {
|
||||||
|
a()
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,22 @@
|
||||||
|
importance: 5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Скрипты с коллбэком
|
||||||
|
|
||||||
|
Создайте функцию `addScripts(scripts, callback)`, которая загружает скрипты из массива `scripts`, и *после загрузки и выполнения их всех* вызывает функцию `callback`.
|
||||||
|
|
||||||
|
Скрипт может быть любым, работа функции не должна зависеть от его содержимого.
|
||||||
|
|
||||||
|
Пример использования:
|
||||||
|
|
||||||
|
```js no-beautify
|
||||||
|
addScripts(["a.js", "b.js", "c.js"], function() { a() });
|
||||||
|
/* функция a() описана в a.js и использует b.js,c.js */
|
||||||
|
```
|
||||||
|
|
||||||
|
- Ошибки загрузки обрабатывать не нужно.</li>
|
||||||
|
- Один скрипт не ждёт другого. Они все загружаются, а по окончании вызывается обработчик `callback`.
|
||||||
|
|
||||||
|
Исходный код содержит скрипты `a.js`, `b.js`, `c.js`:
|
||||||
|
|
228
2-ui/3-event-details/11-onload-onerror/article.md
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
# Загрузка скриптов, картинок, фреймов: onload и onerror
|
||||||
|
|
||||||
|
Браузер позволяет отслеживать загрузку внешних ресурсов -- скриптов, ифреймов, картинок и других.
|
||||||
|
|
||||||
|
Для этого есть два события:
|
||||||
|
|
||||||
|
- `onload` -- если загрузка успешна.
|
||||||
|
- `onerror` -- если при загрузке произошла ошибка.
|
||||||
|
|
||||||
|
## Загрузка SCRIPT
|
||||||
|
|
||||||
|
Рассмотрим следующую задачу.
|
||||||
|
|
||||||
|
В браузере работает сложный интерфейс и, чтобы создать очередной компонент, нужно загрузить скрипт с сервера.
|
||||||
|
|
||||||
|
Подгрузить внешний скрипт -- достаточно просто:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = "my.js";
|
||||||
|
|
||||||
|
document.body.appendChild(script);
|
||||||
|
```
|
||||||
|
|
||||||
|
...Но как после подгрузки выполнить функцию, которая объявлена в этом скрипте? Для этого нужно отловить момент окончания загрузки и выполнения тега `<script>`.
|
||||||
|
|
||||||
|
### script.onload
|
||||||
|
|
||||||
|
Главным помощником станет событие `onload`. Оно сработает, когда скрипт загрузился и выполнился.
|
||||||
|
|
||||||
|
Например:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"
|
||||||
|
document.body.appendChild(script);
|
||||||
|
|
||||||
|
*!*
|
||||||
|
script.onload = function() {
|
||||||
|
// после выполнения скрипта становится доступна функция _
|
||||||
|
alert( _ ); // её код
|
||||||
|
}
|
||||||
|
*/!*
|
||||||
|
```
|
||||||
|
|
||||||
|
Это даёт возможность, как в примере выше, получить переменные из скрипта и выполнять объявленные в нём функции.
|
||||||
|
|
||||||
|
...А что, если загрузка скрипта не удалась? Например, такого скрипта на сервере нет (ошибка 404) или сервер "упал" (ошибка 500).
|
||||||
|
|
||||||
|
Такую ситуацию тоже нужно как-то обрабатывать, хотя бы сообщить посетителю о возникшей проблеме.
|
||||||
|
|
||||||
|
### script.onerror
|
||||||
|
|
||||||
|
Любые ошибки загрузки (но не выполнения) скрипта отслеживаются обработчиком `onerror`.
|
||||||
|
|
||||||
|
Например, сделаем запрос заведомо отсутствующего скрипта:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = "https://example.com/404.js"
|
||||||
|
document.body.appendChild(script);
|
||||||
|
|
||||||
|
*!*
|
||||||
|
script.onerror = function() {
|
||||||
|
alert( "Ошибка: " + this.src );
|
||||||
|
};
|
||||||
|
*/!*
|
||||||
|
```
|
||||||
|
|
||||||
|
### IE8-: script.onreadystatechange [#onreadystatechange]
|
||||||
|
|
||||||
|
Примеры выше работают во всех браузерах, кроме IE8-.
|
||||||
|
|
||||||
|
В IE для отслеживания загрузки есть другое событие: `onreadystatechange`. Оно срабатывает многократно, при каждом обновлении состояния загрузки.
|
||||||
|
|
||||||
|
Текущая стадия процесса находится в `script.readyState`:
|
||||||
|
|
||||||
|
`loading`
|
||||||
|
: В процессе загрузки.
|
||||||
|
|
||||||
|
`loaded`
|
||||||
|
: Получен ответ с сервера -- скрипт или ошибка. Скрипт на фазе `loaded` может быть ещё не выполнен.
|
||||||
|
|
||||||
|
`complete`
|
||||||
|
: Скрипт выполнен.
|
||||||
|
|
||||||
|
Например, рабочий скрипт:
|
||||||
|
|
||||||
|
```js run no-beautify
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = "https://code.jquery.com/jquery.js";
|
||||||
|
document.documentElement.appendChild(script);
|
||||||
|
|
||||||
|
*!*
|
||||||
|
script.onreadystatechange = function() {
|
||||||
|
alert(this.readyState); // loading -> loaded -> complete
|
||||||
|
}
|
||||||
|
*/!*
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт с ошибкой:
|
||||||
|
|
||||||
|
```js run no-beautify
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = "http://ajax.googleapis.com/404.js";
|
||||||
|
document.documentElement.appendChild(script);
|
||||||
|
|
||||||
|
*!*
|
||||||
|
script.onreadystatechange = function() {
|
||||||
|
alert(this.readyState); // loading -> loaded
|
||||||
|
}
|
||||||
|
*/!*
|
||||||
|
```
|
||||||
|
|
||||||
|
Обратим внимание на две особенности:
|
||||||
|
|
||||||
|
- **Стадии могут пропускаться.**
|
||||||
|
|
||||||
|
Если скрипт в кэше браузера -- он сразу даст `complete`. Вы можете увидеть это, если несколько раз запустите первый пример.
|
||||||
|
- **Нет особой стадии для ошибки.**
|
||||||
|
|
||||||
|
В примере выше это видно, обработка останавливается на `loaded`.
|
||||||
|
|
||||||
|
Итак, самое надёжное средство для IE8- поймать загрузку (или ошибку загрузки) -- это повесить обработчик на событие `onreadystatechange`, который будет срабатывать и на стадии `complete` и на стадии `loaded`. Так как скрипт может быть ещё не выполнен к этому моменту, то вызов функции лучше сделать через `setTimeout(.., 0)`.
|
||||||
|
|
||||||
|
Пример ниже вызывает `afterLoad` после загрузки скрипта и работает только в IE:
|
||||||
|
|
||||||
|
```js run no-beautify
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = "https://code.jquery.com/jquery.js";
|
||||||
|
document.documentElement.appendChild(script);
|
||||||
|
|
||||||
|
function afterLoad() {
|
||||||
|
alert("Загрузка завершена: " + typeof(jQuery));
|
||||||
|
}
|
||||||
|
|
||||||
|
*!*
|
||||||
|
script.onreadystatechange = function() {
|
||||||
|
if (this.readyState == "complete") { // на случай пропуска loaded
|
||||||
|
afterLoad(); // (2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.readyState == "loaded") {
|
||||||
|
setTimeout(afterLoad, 0); // (1)
|
||||||
|
|
||||||
|
// убираем обработчик, чтобы не сработал на complete
|
||||||
|
this.onreadystatechange = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/!*
|
||||||
|
```
|
||||||
|
|
||||||
|
Вызов `(1)` выполнится при первой загрузке скрипта, а `(2)` -- при второй, когда он уже будет в кеше, и стадия станет сразу `complete`.
|
||||||
|
|
||||||
|
Функция `afterLoad` может и не обнаружить `jQuery`, если при загрузке была ошибка, причём не важно какая -- файл не найден или синтаксис скрипта ошибочен.
|
||||||
|
|
||||||
|
### Кросс-браузерное решение
|
||||||
|
|
||||||
|
Для кросс-браузерной обработки загрузки скрипта или её ошибки поставим обработчик на все три события: `onload`, `onerror`, `onreadystatechange`.
|
||||||
|
|
||||||
|
Пример ниже выполняет функцию `afterLoad` после загрузки скрипта *или* при ошибке.
|
||||||
|
|
||||||
|
Работает во всех браузерах:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = "https://code.jquery.com/jquery.js";
|
||||||
|
document.documentElement.appendChild(script);
|
||||||
|
|
||||||
|
function afterLoad() {
|
||||||
|
alert( "Загрузка завершена: " + typeof(jQuery) );
|
||||||
|
}
|
||||||
|
|
||||||
|
script.onload = script.onerror = function() {
|
||||||
|
if (!this.executed) { // выполнится только один раз
|
||||||
|
this.executed = true;
|
||||||
|
afterLoad();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
script.onreadystatechange = function() {
|
||||||
|
var self = this;
|
||||||
|
if (this.readyState == "complete" || this.readyState == "loaded") {
|
||||||
|
setTimeout(function() {
|
||||||
|
self.onload()
|
||||||
|
}, 0); // сохранить "this" для onload
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Загрузка других ресурсов
|
||||||
|
|
||||||
|
Поддержка этих событий для других типов ресурсов различна:
|
||||||
|
|
||||||
|
`<img>`, `<link>` (стили)
|
||||||
|
: Поддерживает `onload/onerror` во всех браузерах.
|
||||||
|
|
||||||
|
`<iframe>`
|
||||||
|
: Поддерживает `onload` во всех браузерах. Это событие срабатывает как при успешной загрузке, так и при ошибке.
|
||||||
|
|
||||||
|
Обратим внимание, что если `<iframe>` загружается с того же домена, то можно, используя `iframe.contentWindow.document` получить ссылку на документ и поставить обработчик `DOMContentLoaded`. А вот если `<iframe>` -- с другого домена, то так не получится, однако сработает `onload`.
|
||||||
|
|
||||||
|
## Итого
|
||||||
|
|
||||||
|
В этой статье мы рассмотрели события `onload/onerror` для ресурсов.
|
||||||
|
|
||||||
|
Их можно обобщить, разделив на рецепты:
|
||||||
|
|
||||||
|
Отловить загрузку скрипта (включая ошибку)
|
||||||
|
: Ставим обработчики на `onload` + `onerror` + (для IE8-) `onreadystatechange`, как указано в рецепте выше
|
||||||
|
|
||||||
|
Отловить загрузку картинки `<img>` или стиля `<link>`
|
||||||
|
: Ставим обработчики на `onload` + `onerror`
|
||||||
|
|
||||||
|
```js no-beautify
|
||||||
|
var img = document.createElement('img');
|
||||||
|
img.onload = function() { alert("Успех "+this.src };
|
||||||
|
img.onerror = function() { alert("Ошибка "+this.src };
|
||||||
|
img.src = ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Изображения начинают загружаться сразу при создании, не нужно их для этого вставлять в HTML.
|
||||||
|
|
||||||
|
**Чтобы работало в IE8-, `src` нужно ставить *после* `onload/onerror`.**
|
||||||
|
|
||||||
|
Отловить загрузку `<iframe>`
|
||||||
|
: Поддерживается только обработчик `onload`. Он сработает, когда `IFRAME` загрузится, со всеми подресурсами, а также в случае ошибки.
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
height: 2000px;
|
||||||
|
/* the tooltip should work after page scroll too */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#house {
|
||||||
|
margin-top: 50px;
|
||||||
|
width: 400px;
|
||||||
|
border: 1px solid brown;
|
||||||
|
}
|
||||||
|
|
||||||
|
#roof {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 200px solid transparent;
|
||||||
|
border-right: 200px solid transparent;
|
||||||
|
border-bottom: 20px solid brown;
|
||||||
|
margin-top: -20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
text-align: justify;
|
||||||
|
margin: 10px 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
<div data-tooltip="Here is the house interior" id="house">
|
||||||
|
<div data-tooltip="Here is the roof" id="roof"></div>
|
||||||
|
|
||||||
|
<p>Once upon a time there was a mother pig who had three little pigs.</p>
|
||||||
|
|
||||||
|
<p>The three little pigs grew so big that their mother said to them, "You are too big to live here any longer. You must go and build houses for yourselves. But take care that the wolf does not catch you."
|
||||||
|
|
||||||
|
<p>The three little pigs set off. "We will take care that the wolf does not catch us," they said.</p>
|
||||||
|
|
||||||
|
<p>Soon they met a man. <a href="https://en.wikipedia.org/wiki/The_Three_Little_Pigs" data-tooltip="Read on…">Hover over me</a></p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let tooltip;
|
||||||
|
|
||||||
|
document.onmouseover = function(event) {
|
||||||
|
// important: a fast-moving mouse may "jump" right to a child on an annotated node, skipping the parent
|
||||||
|
// so mouseover may happen on a child.
|
||||||
|
|
||||||
|
let anchorElem = event.target.closest('[data-tooltip]');
|
||||||
|
|
||||||
|
if (!anchorElem) return;
|
||||||
|
|
||||||
|
// show tooltip and remember it
|
||||||
|
tooltip = showTooltip(anchorElem, anchorElem.dataset.tooltip);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.onmouseout = function() {
|
||||||
|
// возможно такое, что mouseout сработал, а мы все еще внутри элемента (всплытие)
|
||||||
|
// но в этом случае сразу же будет и mouseover,
|
||||||
|
// то есть подсказка будет уничтожена и тут же показана заново
|
||||||
|
//
|
||||||
|
// это лишние расходы, их можно избежать дополнительными проверками
|
||||||
|
if (tooltip) {
|
||||||
|
tooltip.remove();
|
||||||
|
tooltip = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function showTooltip(anchorElem, html) {
|
||||||
|
let tooltipElem = document.createElement('div');
|
||||||
|
tooltipElem.className = 'tooltip';
|
||||||
|
tooltipElem.innerHTML = html;
|
||||||
|
document.body.append(tooltipElem);
|
||||||
|
|
||||||
|
let coords = anchorElem.getBoundingClientRect();
|
||||||
|
|
||||||
|
// position the tooltip over the center of the element
|
||||||
|
let left = coords.left + (anchorElem.offsetWidth - tooltipElem.offsetWidth) / 2;
|
||||||
|
if (left < 0) left = 0;
|
||||||
|
|
||||||
|
var top = coords.top - tooltipElem.offsetHeight - 5;
|
||||||
|
if (top < 0) {
|
||||||
|
top = coords.top + anchorElem.offsetHeight + 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltipElem.style.left = left + 'px';
|
||||||
|
tooltipElem.style.top = top + 'px';
|
||||||
|
|
||||||
|
return tooltipElem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,70 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
height: 2000px;
|
||||||
|
/* the tooltip should work after page scroll too */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#house {
|
||||||
|
margin-top: 50px;
|
||||||
|
width: 400px;
|
||||||
|
border: 1px solid brown;
|
||||||
|
}
|
||||||
|
|
||||||
|
#roof {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 200px solid transparent;
|
||||||
|
border-right: 200px solid transparent;
|
||||||
|
border-bottom: 20px solid brown;
|
||||||
|
margin-top: -20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
text-align: justify;
|
||||||
|
margin: 10px 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
<div data-tooltip="Here is the house interior" id="house">
|
||||||
|
<div data-tooltip="Here is the roof" id="roof"></div>
|
||||||
|
|
||||||
|
<p>Once upon a time there was a mother pig who had three little pigs.</p>
|
||||||
|
|
||||||
|
<p>The three little pigs grew so big that their mother said to them, "You are too big to live here any longer. You must go and build houses for yourselves. But take care that the wolf does not catch you."
|
||||||
|
|
||||||
|
<p>The three little pigs set off. "We will take care that the wolf does not catch us," they said.</p>
|
||||||
|
|
||||||
|
<p>Soon they met a man. <a href="https://en.wikipedia.org/wiki/The_Three_Little_Pigs" data-tooltip="Read on…">Hover over me</a></p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ...your code...
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,25 @@
|
||||||
|
importance: 5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Improved tooltip behavior
|
||||||
|
|
||||||
|
Write JavaScript that shows a tooltip over an element with the attribute `data-tooltip`.
|
||||||
|
|
||||||
|
That's like the task <info:task/behavior-tooltip>, but here the annotated elements can be nested. The most deeply nested tooltip is shown.
|
||||||
|
|
||||||
|
For instance:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div data-tooltip="Here – is the house interior" id="house">
|
||||||
|
<div data-tooltip="Here – is the roof" id="roof"></div>
|
||||||
|
...
|
||||||
|
<a href="https://en.wikipedia.org/wiki/The_Three_Little_Pigs" data-tooltip="Read on…">Hover over me</a>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
The result in iframe:
|
||||||
|
|
||||||
|
[iframe src="solution" height=300 border=1]
|
||||||
|
|
||||||
|
P.S. Hint: only one tooltip may show up at the same time.
|
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
The algorithm looks simple:
|
||||||
|
1. Put `onmouseover/out` handlers on the element. Also can use `onmouseenter/leave` here, but they are less universal, won't work if we introduce delegation.
|
||||||
|
2. When a mouse cursor entered the element, start measuring the speed on `mousemove`.
|
||||||
|
3. If the speed is slow, then run `over`.
|
||||||
|
4. Later if we're out of the element, and `over` was executed, run `out`.
|
||||||
|
|
||||||
|
The question is: "How to measure the speed?"
|
||||||
|
|
||||||
|
The first idea would be: to run our function every `100ms` and measure the distance between previous and new coordinates. If it's small, then the speed is small.
|
||||||
|
|
||||||
|
Unfortunately, there's no way to get "current mouse coordinates" in JavaScript. There's no function like `getCurrentMouseCoordinates()`.
|
||||||
|
|
||||||
|
The only way to get coordinates is to listen to mouse events, like `mousemove`.
|
||||||
|
|
||||||
|
So we can set a handler on `mousemove` to track coordinates and remember them. Then we can compare them, once per `100ms`.
|
||||||
|
|
||||||
|
P.S. Please note: the solution tests use `dispatchEvent` to see if the tooltip works right.
|
|
@ -0,0 +1,106 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
class HoverIntent {
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
sensitivity = 0.1, // speed less than 0.1px/ms means "hovering over an element"
|
||||||
|
interval = 100, // measure mouse speed once per 100ms
|
||||||
|
elem,
|
||||||
|
over,
|
||||||
|
out
|
||||||
|
}) {
|
||||||
|
this.sensitivity = sensitivity;
|
||||||
|
this.interval = interval;
|
||||||
|
this.elem = elem;
|
||||||
|
this.over = over;
|
||||||
|
this.out = out;
|
||||||
|
|
||||||
|
// make sure "this" is the object in event handlers.
|
||||||
|
this.onMouseMove = this.onMouseMove.bind(this);
|
||||||
|
this.onMouseOver = this.onMouseOver.bind(this);
|
||||||
|
this.onMouseOut = this.onMouseOut.bind(this);
|
||||||
|
|
||||||
|
// and in time-measuring function (called from setInterval)
|
||||||
|
this.trackSpeed = this.trackSpeed.bind(this);
|
||||||
|
|
||||||
|
elem.addEventListener("mouseover", this.onMouseOver);
|
||||||
|
|
||||||
|
elem.addEventListener("mouseout", this.onMouseOut);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseOver(event) {
|
||||||
|
|
||||||
|
if (this.isOverElement) {
|
||||||
|
// if we're over the element, then ignore the event
|
||||||
|
// we are already measuring the speed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isOverElement = true;
|
||||||
|
|
||||||
|
// after every mousemove we'll be check the distance
|
||||||
|
// between the previous and the current mouse coordinates
|
||||||
|
// if it's less than sensivity, then the speed is slow
|
||||||
|
|
||||||
|
this.prevX = event.pageX;
|
||||||
|
this.prevY = event.pageY;
|
||||||
|
this.prevTime = Date.now();
|
||||||
|
|
||||||
|
elem.addEventListener('mousemove', this.onMouseMove);
|
||||||
|
this.checkSpeedInterval = setInterval(this.trackSpeed, this.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseOut(event) {
|
||||||
|
// if left the element
|
||||||
|
if (!event.relatedTarget || !elem.contains(event.relatedTarget)) {
|
||||||
|
this.isOverElement = false;
|
||||||
|
this.elem.removeEventListener('mousemove', this.onMouseMove);
|
||||||
|
clearInterval(this.checkSpeedInterval);
|
||||||
|
if (this.isHover) {
|
||||||
|
// если была остановка над элементом
|
||||||
|
this.out.call(this.elem, event);
|
||||||
|
this.isHover = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove(event) {
|
||||||
|
this.lastX = event.pageX;
|
||||||
|
this.lastY = event.pageY;
|
||||||
|
this.lastTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
trackSpeed() {
|
||||||
|
|
||||||
|
let speed;
|
||||||
|
|
||||||
|
if (!this.lastTime || this.lastTime == this.prevTime) {
|
||||||
|
// cursor didn't move
|
||||||
|
speed = 0;
|
||||||
|
} else {
|
||||||
|
speed = Math.sqrt(
|
||||||
|
Math.pow(this.prevX - this.lastX, 2) +
|
||||||
|
Math.pow(this.prevY - this.lastY, 2)
|
||||||
|
) / (this.lastTime - this.prevTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (speed < this.sensitivity) {
|
||||||
|
clearInterval(this.checkSpeedInterval);
|
||||||
|
this.isHover = true;
|
||||||
|
this.over.call(this.elem, event);
|
||||||
|
} else {
|
||||||
|
// speed fast, remember new coordinates as the previous ones
|
||||||
|
this.prevX = this.lastX;
|
||||||
|
this.prevY = this.lastY;
|
||||||
|
this.prevTime = this.lastTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
elem.removeEventListener('mousemove', this.onMouseMove);
|
||||||
|
elem.removeEventListener('mouseover', this.onMouseOver);
|
||||||
|
elem.removeEventListener('mouseout', this.onMouseOut);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<script src="hoverIntent.js"></script>
|
||||||
|
<script src="https://en.js.cx/test/libs.js"></script>
|
||||||
|
<script src="test.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="elem" class="clock">
|
||||||
|
<span class="hours">12</span> :
|
||||||
|
<span class="minutes">30</span> :
|
||||||
|
<span class="seconds">00</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tooltip" hidden>Tooltip</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// for the demo
|
||||||
|
setTimeout(function() {
|
||||||
|
new HoverIntent({
|
||||||
|
elem,
|
||||||
|
over() {
|
||||||
|
tooltip.style.left = elem.getBoundingClientRect().left + 5 + 'px';
|
||||||
|
tooltip.style.top = elem.getBoundingClientRect().bottom + 5 + 'px';
|
||||||
|
tooltip.hidden = false;
|
||||||
|
},
|
||||||
|
out() {
|
||||||
|
tooltip.hidden = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,38 @@
|
||||||
|
.hours {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
body#mocha {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minutes {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seconds {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock {
|
||||||
|
border: 1px dashed black;
|
||||||
|
padding: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
background: yellow;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tooltip {
|
||||||
|
position: absolute;
|
||||||
|
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;
|
||||||
|
z-index: 100000;
|
||||||
|
box-shadow: 3px 3px 3px rgba(0, 0, 0, .3);
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
describe("hoverIntent", function() {
|
||||||
|
|
||||||
|
function mouse(eventType, x, y, options) {
|
||||||
|
let eventOptions = Object.assign({
|
||||||
|
bubbles: true,
|
||||||
|
clientX: x,
|
||||||
|
clientY: y,
|
||||||
|
pageX: x,
|
||||||
|
pageY: y,
|
||||||
|
target: elem
|
||||||
|
}, options || {});
|
||||||
|
|
||||||
|
elem.dispatchEvent(new MouseEvent(eventType, eventOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let isOver;
|
||||||
|
let hoverIntent;
|
||||||
|
|
||||||
|
|
||||||
|
before(function() {
|
||||||
|
this.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function() {
|
||||||
|
this.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
isOver = false;
|
||||||
|
|
||||||
|
hoverIntent = new HoverIntent({
|
||||||
|
elem: elem,
|
||||||
|
over: function() {
|
||||||
|
isOver = true;
|
||||||
|
},
|
||||||
|
out: function() {
|
||||||
|
isOver = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
if (hoverIntent) {
|
||||||
|
hoverIntent.destroy();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("mouseover -> immediately no tooltip", function() {
|
||||||
|
mouse('mouseover', 10, 10);
|
||||||
|
assert.isFalse(isOver);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("mouseover -> pause shows tooltip", function() {
|
||||||
|
mouse('mouseover', 10, 10);
|
||||||
|
this.clock.tick(100);
|
||||||
|
assert.isTrue(isOver);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("mouseover -> fast mouseout no tooltip", function() {
|
||||||
|
mouse('mouseover', 10, 10);
|
||||||
|
setTimeout(
|
||||||
|
() => mouse('mouseout', 300, 300, { relatedTarget: document.body}),
|
||||||
|
30
|
||||||
|
);
|
||||||
|
this.clock.tick(100);
|
||||||
|
assert.isFalse(isOver);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("mouseover -> slow move -> tooltips", function() {
|
||||||
|
mouse('mouseover', 10, 10);
|
||||||
|
for(let i=10; i<200; i+= 10) {
|
||||||
|
setTimeout(
|
||||||
|
() => mouse('mousemove', i/5, 10),
|
||||||
|
i
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.clock.tick(200);
|
||||||
|
assert.isTrue(isOver);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("mouseover -> fast move -> no tooltip", function() {
|
||||||
|
mouse('mouseover', 10, 10);
|
||||||
|
for(let i=10; i<200; i+= 10) {
|
||||||
|
setTimeout(
|
||||||
|
() => mouse('mousemove', i, 10),
|
||||||
|
i
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.clock.tick(200);
|
||||||
|
assert.isFalse(isOver);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,50 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Here's a brief sketch of the class
|
||||||
|
// with things that you'll need anyway
|
||||||
|
class HoverIntent {
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
sensitivity = 0.1, // speed less than 0.1px/ms means "hovering over an element"
|
||||||
|
interval = 100, // measure mouse speed once per 100ms: calculate the distance between previous and next points
|
||||||
|
elem,
|
||||||
|
over,
|
||||||
|
out
|
||||||
|
}) {
|
||||||
|
this.sensitivity = sensitivity;
|
||||||
|
this.interval = interval;
|
||||||
|
this.elem = elem;
|
||||||
|
this.over = over;
|
||||||
|
this.out = out;
|
||||||
|
|
||||||
|
// make sure "this" is the object in event handlers.
|
||||||
|
this.onMouseMove = this.onMouseMove.bind(this);
|
||||||
|
this.onMouseOver = this.onMouseOver.bind(this);
|
||||||
|
this.onMouseOut = this.onMouseOut.bind(this);
|
||||||
|
|
||||||
|
// assign the handlers
|
||||||
|
elem.addEventListener("mouseover", this.onMouseOver);
|
||||||
|
elem.addEventListener("mouseout", this.onMouseOut);
|
||||||
|
|
||||||
|
// continue from this point
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseOver(event) {
|
||||||
|
/* ... */
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseOut(event) {
|
||||||
|
/* ... */
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove(event) {
|
||||||
|
/* ... */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
/* your code to "disable" the functionality, remove all handlers */
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Document</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<script src="hoverIntent.js"></script>
|
||||||
|
<script src="https://js.cx/test/libs.js"></script>
|
||||||
|
<script src="test.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="elem" class="clock">
|
||||||
|
<span class="hours">12</span> :
|
||||||
|
<span class="minutes">30</span> :
|
||||||
|
<span class="seconds">00</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tooltip" hidden>Tooltip</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// for the demo
|
||||||
|
setTimeout(function() {
|
||||||
|
new HoverIntent({
|
||||||
|
elem,
|
||||||
|
over() {
|
||||||
|
tooltip.hidden = false;
|
||||||
|
},
|
||||||
|
out() {
|
||||||
|
tooltip.hidden = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,28 @@
|
||||||
|
.hours {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minutes {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seconds {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock {
|
||||||
|
border: 1px dashed black;
|
||||||
|
padding: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
background: yellow;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
background: #eee;
|
||||||
|
border: 1px brown solid;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
describe("hoverIntent", function() {
|
||||||
|
|
||||||
|
function mouse(eventType, x, y, options) {
|
||||||
|
let eventOptions = Object.assign({
|
||||||
|
bubbles: true,
|
||||||
|
clientX: x,
|
||||||
|
clientY: y,
|
||||||
|
pageX: x,
|
||||||
|
pageY: y,
|
||||||
|
target: elem
|
||||||
|
}, options || {});
|
||||||
|
|
||||||
|
elem.dispatchEvent(new MouseEvent(eventType, eventOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let isOver;
|
||||||
|
let hoverIntent;
|
||||||
|
|
||||||
|
|
||||||
|
before(function() {
|
||||||
|
this.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function() {
|
||||||
|
this.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
isOver = false;
|
||||||
|
|
||||||
|
hoverIntent = new HoverIntent({
|
||||||
|
elem: elem,
|
||||||
|
over: function() {
|
||||||
|
isOver = true;
|
||||||
|
},
|
||||||
|
out: function() {
|
||||||
|
isOver = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
if (hoverIntent) {
|
||||||
|
hoverIntent.destroy();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("mouseover -> immediately no tooltip", function() {
|
||||||
|
mouse('mouseover', 10, 10);
|
||||||
|
assert.isFalse(isOver);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("mouseover -> pause shows tooltip", function() {
|
||||||
|
mouse('mouseover', 10, 10);
|
||||||
|
this.clock.tick(100);
|
||||||
|
assert.isTrue(isOver);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("mouseover -> fast mouseout no tooltip", function() {
|
||||||
|
mouse('mouseover', 10, 10);
|
||||||
|
setTimeout(
|
||||||
|
() => mouse('mouseout', 300, 300, { relatedTarget: document.body}),
|
||||||
|
30
|
||||||
|
);
|
||||||
|
this.clock.tick(100);
|
||||||
|
assert.isFalse(isOver);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("mouseover -> slow move -> tooltips", function() {
|
||||||
|
mouse('mouseover', 10, 10);
|
||||||
|
for(let i=10; i<200; i+= 10) {
|
||||||
|
setTimeout(
|
||||||
|
() => mouse('mousemove', i/5, 10),
|
||||||
|
i
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.clock.tick(200);
|
||||||
|
assert.isTrue(isOver);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("mouseover -> fast move -> no tooltip", function() {
|
||||||
|
mouse('mouseover', 10, 10);
|
||||||
|
for(let i=10; i<200; i+= 10) {
|
||||||
|
setTimeout(
|
||||||
|
() => mouse('mousemove', i, 10),
|
||||||
|
i
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.clock.tick(200);
|
||||||
|
assert.isFalse(isOver);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,47 @@
|
||||||
|
importance: 5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# "Smart" tooltip
|
||||||
|
|
||||||
|
Write a function that shows a tooltip over an element only if the visitor moves the mouse *over it*, but not *through it*.
|
||||||
|
|
||||||
|
In other words, if the visitor moves the mouse on the element and stopped -- show the tooltip. And if he just moved the mouse through fast, then no need, who wants extra blinking?
|
||||||
|
|
||||||
|
Technically, we can measure the mouse speed over the element, and if it's slow then we assume that it comes "over the element" and show the tooltip, if it's fast -- then we ignore it.
|
||||||
|
|
||||||
|
Make a universal object `new HoverIntent(options)` for it. With `options`:
|
||||||
|
|
||||||
|
- `elem` -- element to track.
|
||||||
|
- `over` -- a function to call if the mouse is slowly moving the element.
|
||||||
|
- `out` -- a function to call when the mouse leaves the element (if `over` was called).
|
||||||
|
|
||||||
|
An example of using such object for the tooltip:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// a sample tooltip
|
||||||
|
let tooltip = document.createElement('div');
|
||||||
|
tooltip.className = "tooltip";
|
||||||
|
tooltip.innerHTML = "Tooltip";
|
||||||
|
|
||||||
|
// the object will track mouse and call over/out
|
||||||
|
new HoverIntent({
|
||||||
|
elem,
|
||||||
|
over() {
|
||||||
|
tooltip.style.left = elem.getBoundingClientRect().left + 'px';
|
||||||
|
tooltip.style.top = elem.getBoundingClientRect().bottom + 5 + 'px';
|
||||||
|
document.body.append(tooltip);
|
||||||
|
},
|
||||||
|
out() {
|
||||||
|
tooltip.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The demo:
|
||||||
|
|
||||||
|
[iframe src="solution" height=140]
|
||||||
|
|
||||||
|
If you move the mouse over the "clock" fast then nothing happens, and if you do it slow or stop on them, then there will be a tooltip.
|
||||||
|
|
||||||
|
Please note: the tooltip doesn't "blink" when the cursor between the clock subelements.
|
|
@ -0,0 +1,186 @@
|
||||||
|
# Moving: mouseover/out, mouseenter/leave
|
||||||
|
|
||||||
|
Let's dive into more details about events that happen when mouse moves between elements.
|
||||||
|
|
||||||
|
[cut]
|
||||||
|
|
||||||
|
## Mouseover/mouseout, relatedTarget
|
||||||
|
|
||||||
|
The `mouseover` event occurs when a mouse pointer comes over an element, and `mouseout` -- when it leaves.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
These events are special, because they have a `relatedTarget`.
|
||||||
|
|
||||||
|
For `mouseover`:
|
||||||
|
|
||||||
|
- `event.target` -- is the element where the mouse came over.
|
||||||
|
- `event.relatedTarget` -- is the element from which the mouse came.
|
||||||
|
|
||||||
|
For `mouseout` the reverse:
|
||||||
|
|
||||||
|
- `event.target` -- is the element that mouse left.
|
||||||
|
- `event.relatedTarget` -- is the new under-the-pointer element (that mouse left for).
|
||||||
|
|
||||||
|
```online
|
||||||
|
In the example below each face feature is an element. When you move the mouse, you can see mouse events in the text area.
|
||||||
|
|
||||||
|
Each event has the information about where the element came and where it came from.
|
||||||
|
|
||||||
|
[codetabs src="mouseoverout" height=280]
|
||||||
|
```
|
||||||
|
|
||||||
|
```warn header="`relatedTarget` can be `null`"
|
||||||
|
The `relatedTarget` property can be `null`.
|
||||||
|
|
||||||
|
That's normal and just means that the mouse came not from another element, but from out of the window. Or that it left the window.
|
||||||
|
|
||||||
|
We should keep that possibility in mind when using `event.relatedTarget` in our code. If we access `event.relatedTarget.tagName`, then there will be an error.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Events frequency
|
||||||
|
|
||||||
|
The `mousemove` event triggers when the mouse moves. But that doesn't mean that every pixel leads to an event.
|
||||||
|
|
||||||
|
The browser checks the mouse position from time to time. And if he notices changes then triggers the events.
|
||||||
|
|
||||||
|
That means that if the visitor is moving the mouse very fast then DOM-elements may be skipped:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
If the mouse moves very fast from `#FROM` to `#TO` elements as painted above, then intermediate `<div>` (or some of them) may be skipped. The `mouseout` event may trigger on `#FROM` and then immediately `mouseover` on `#TO`.
|
||||||
|
|
||||||
|
In practice that's helpful, because if there may be many intermediate elements. We don't really want to process in and out of each one.
|
||||||
|
|
||||||
|
From the other side, we should keep in mind that we can't assume that the mouse slowly moves from one event to another. No, it can "jump".
|
||||||
|
|
||||||
|
In particular it's possible that the cursor jumps right inside the middle of the page from out of the window. And `relatedTarget=null`, because it came from "nowhere":
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<div style="display:none">
|
||||||
|
In case of a fast move, intermediate elements may trigger no events. But if the mouse enters the element (`mouseover`), when we're guaranteed to have `mouseout` when it leaves it.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
```online
|
||||||
|
Check it out "live" on a teststand below.
|
||||||
|
|
||||||
|
The HTML is two nested `<div>` elements. If you move the mouse fast over them, then there may be no events at all, or maybe only the red div triggers events, or maybe the green one.
|
||||||
|
|
||||||
|
Also try to move the pointer over the red `div`, and then move it out quickly down through the green one. If the movement is fast enough then the parent element is ignored.
|
||||||
|
|
||||||
|
[codetabs height=360 src="mouseoverout-fast"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## "Extra" mouseout when leaving for a child
|
||||||
|
|
||||||
|
Imagine -- a mouse pointer entered an element. The `mouseover` triggered. Then the cursor goes into a child element. The interesting fact is that `mouseout` triggers in that case. The cursor is still in the element, but we have a `mouseout` from it!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
That seems strange, but can be easily explained.
|
||||||
|
|
||||||
|
**According to the browser logic, the mouse cursor may be only over a *single* element at any time -- the most nested one (and top by z-index).**
|
||||||
|
|
||||||
|
So if it goes to another element (even a descendant), then it leaves the previous one. That simple.
|
||||||
|
|
||||||
|
There's a funny consequence that we can see on the example below.
|
||||||
|
|
||||||
|
The red `<div>` is nested inside the blue one. The blue `<div>` has `mouseover/out` handlers that log all events in the textarea below.
|
||||||
|
|
||||||
|
Try entering the blue element and then moving the mouse on the red one -- and watch the events:
|
||||||
|
|
||||||
|
[codetabs height=360 src="mouseoverout-child"]
|
||||||
|
|
||||||
|
1. On entering the blue one -- we get `mouseover [target: blue]`.
|
||||||
|
2. Then after moving from the blue to the red one -- we get `mouseout [target: blue]` (left the parent).
|
||||||
|
3. ...And immediately `mouseover [target: red]`.
|
||||||
|
|
||||||
|
So, for a handler that does not take `target` into account, it looks like we left the parent in `mouseout` in `(2)` and returned back to it by `mouseover` in `(3)`.
|
||||||
|
|
||||||
|
If we perform some actions on entering/leaving the element, then we'll get a lot of extra "false" runs. For simple stuff may be unnoticeable. For complex things that may bring unwanted side-effects.
|
||||||
|
|
||||||
|
We can fix it by using `mouseenter/mouseleave` events instead.
|
||||||
|
|
||||||
|
## Events mouseenter and mouseleave
|
||||||
|
|
||||||
|
Events `mouseenter/mouseleave` are like `mouseover/mouseout`. They also trigger when the mouse pointer enters/leaves the element.
|
||||||
|
|
||||||
|
But there are two differences:
|
||||||
|
|
||||||
|
1. Transitions inside the element are not counted.
|
||||||
|
2. Events `mouseenter/mouseleave` do not bubble.
|
||||||
|
|
||||||
|
These events are intuitively very clear.
|
||||||
|
|
||||||
|
When the pointer enters an element -- the `mouseenter` triggers, and then doesn't matter where it goes while inside the element. The `mouseleave` event only triggers when the cursor leaves it.
|
||||||
|
|
||||||
|
If we make the same example, but put `mouseenter/mouseleave` on the blue `<div>`, and do the same -- we can see that events trigger only on entering and leaving the blue `<div>`. No extra events when going to the red one and back. Children are ignored.
|
||||||
|
|
||||||
|
[codetabs height=340 src="mouseleave"]
|
||||||
|
|
||||||
|
## Event delegation
|
||||||
|
|
||||||
|
Events `mouseenter/leave` are very simple and easy to use. But they do not bubble. So we can't use event delegation with them.
|
||||||
|
|
||||||
|
Imagine we want to handle mouse enter/leave for table cells. And there are hundreds of cells.
|
||||||
|
|
||||||
|
The natural solution would be -- to set the handler on `<table>` and process events there. But `mouseenter/leave` don't bubble. So if such event happens on `<td>`, then only a handler on that `<td>` can catch it.
|
||||||
|
|
||||||
|
Handlers for `mouseenter/leave` on `<table>` only trigger on entering/leaving the whole table. It's impossible to get any information about transitions inside it.
|
||||||
|
|
||||||
|
Not a problem -- let's use `mouseover/mouseout`.
|
||||||
|
|
||||||
|
A simple handler may look like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// let's highlight cells under mouse
|
||||||
|
table.onmouseover = function(event) {
|
||||||
|
let target = event.target;
|
||||||
|
target.style.background = 'pink';
|
||||||
|
};
|
||||||
|
|
||||||
|
table.onmouseout = function(event) {
|
||||||
|
let target = event.target;
|
||||||
|
target.style.background = '';
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
```online
|
||||||
|
[codetabs height=480 src="mouseenter-mouseleave-delegation"]
|
||||||
|
```
|
||||||
|
|
||||||
|
These handlers work when going from any element to any inside the table.
|
||||||
|
|
||||||
|
But we'd like to handle only transitions in and out of `<td>` as a whole. And highlight the cells as a whole. We don't want to handle transitions that happen between the children of `<td>`.
|
||||||
|
|
||||||
|
One of solutions:
|
||||||
|
|
||||||
|
- Remember the currently highlighted `<td>` in a variable.
|
||||||
|
- On `mouseover` -- ignore the event if we're still inside the current `<td>`.
|
||||||
|
- On `mouseout` -- ignore if we didn't leave the current `<td>`.
|
||||||
|
|
||||||
|
That filters out "extra" events when we are moving between the children of `<td>`.
|
||||||
|
|
||||||
|
```offline
|
||||||
|
The details are in the [full example](sandbox:mouseenter-mouseleave-delegation-2).
|
||||||
|
```
|
||||||
|
|
||||||
|
```online
|
||||||
|
Here's the full example with all details:
|
||||||
|
|
||||||
|
[codetabs height=380 src="mouseenter-mouseleave-delegation-2"]
|
||||||
|
|
||||||
|
Try to move the cursor in and out of table cells and inside them. Fast or slow -- doesn't better. Only `<td>` as a whole is highlighted unlike the example before.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Итого
|
||||||
|
|
||||||
|
У `mouseover, mousemove, mouseout` есть следующие особенности:
|
||||||
|
|
||||||
|
- При быстром движении мыши события `mouseover, mousemove, mouseout` могут пропускать промежуточные элементы.
|
||||||
|
- События `mouseover` и `mouseout` -- единственные, у которых есть вторая цель: `relatedTarget` (`toElement/fromElement` в IE).
|
||||||
|
- События `mouseover/mouseout` подразумевают, что курсор находится над одним, самым глубоким элементом. Они срабатывают при переходе с родительского элемента на дочерний.
|
||||||
|
|
||||||
|
События `mouseenter/mouseleave` не всплывают и не учитывают переходы внутри элемента.
|
|
@ -0,0 +1,74 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
<table id="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 src="script.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,46 @@
|
||||||
|
// элемент TD, внутри которого сейчас курсор
|
||||||
|
var currentElem = null;
|
||||||
|
|
||||||
|
table.onmouseover = function(event) {
|
||||||
|
if (currentElem) {
|
||||||
|
// перед тем, как зайти в новый элемент, курсор всегда выходит из предыдущего
|
||||||
|
//
|
||||||
|
// если мы еще не вышли, значит это переход внутри элемента, отфильтруем его
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// посмотрим, куда пришёл курсор
|
||||||
|
var target = event.target;
|
||||||
|
|
||||||
|
// уж не на TD ли?
|
||||||
|
while (target != this) {
|
||||||
|
if (target.tagName == 'TD') break;
|
||||||
|
target = target.parentNode;
|
||||||
|
}
|
||||||
|
if (target == this) return;
|
||||||
|
|
||||||
|
// да, элемент перешёл внутрь TD!
|
||||||
|
currentElem = target;
|
||||||
|
target.style.background = 'pink';
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
table.onmouseout = function(event) {
|
||||||
|
// если курсор и так снаружи - игнорируем это событие
|
||||||
|
if (!currentElem) return;
|
||||||
|
|
||||||
|
// произошёл уход с элемента - проверим, куда, может быть на потомка?
|
||||||
|
var relatedTarget = event.relatedTarget;
|
||||||
|
if (relatedTarget) { // может быть relatedTarget = null
|
||||||
|
while (relatedTarget) {
|
||||||
|
// идём по цепочке родителей и проверяем,
|
||||||
|
// если переход внутрь currentElem - игнорируем это событие
|
||||||
|
if (relatedTarget == currentElem) return;
|
||||||
|
relatedTarget = relatedTarget.parentNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// произошло событие mouseout, курсор ушёл
|
||||||
|
currentElem.style.background = '';
|
||||||
|
currentElem = null;
|
||||||
|
};
|
|
@ -0,0 +1,65 @@
|
||||||
|
#text {
|
||||||
|
display: block;
|
||||||
|
height: 100px;
|
||||||
|
width: 456px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table th {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table td {
|
||||||
|
width: 150px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: bottom;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .nw {
|
||||||
|
background: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .n {
|
||||||
|
background: #03f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .ne {
|
||||||
|
background: #ff6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .w {
|
||||||
|
background: #ff0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .c {
|
||||||
|
background: #60c;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .e {
|
||||||
|
background: #09f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .sw {
|
||||||
|
background: #963;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .s {
|
||||||
|
background: #f60;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .se {
|
||||||
|
background: #0c3;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .highlight {
|
||||||
|
background: red;
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
<table id="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>
|
||||||
|
|
||||||
|
<textarea id="text"></textarea>
|
||||||
|
|
||||||
|
<input type="button" onclick="text.value=''" value="Clear">
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,13 @@
|
||||||
|
table.onmouseover = function(event) {
|
||||||
|
var target = event.target;
|
||||||
|
target.style.background = 'pink';
|
||||||
|
text.value += "mouseover " + target.tagName + "\n";
|
||||||
|
text.scrollTop = text.scrollHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
table.onmouseout = function(event) {
|
||||||
|
var target = event.target;
|
||||||
|
target.style.background = '';
|
||||||
|
text.value += "mouseout " + target.tagName + "\n";
|
||||||
|
text.scrollTop = text.scrollHeight;
|
||||||
|
};
|
|
@ -0,0 +1,65 @@
|
||||||
|
#text {
|
||||||
|
display: block;
|
||||||
|
height: 100px;
|
||||||
|
width: 456px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table th {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table td {
|
||||||
|
width: 150px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: bottom;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .nw {
|
||||||
|
background: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .n {
|
||||||
|
background: #03f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .ne {
|
||||||
|
background: #ff6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .w {
|
||||||
|
background: #ff0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .c {
|
||||||
|
background: #60c;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .e {
|
||||||
|
background: #09f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .sw {
|
||||||
|
background: #963;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .s {
|
||||||
|
background: #f60;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .se {
|
||||||
|
background: #0c3;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .highlight {
|
||||||
|
background: red;
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
<table id="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>
|
||||||
|
|
||||||
|
<textarea id="text"></textarea>
|
||||||
|
|
||||||
|
<input type="button" onclick="text.value=''" value="Очистить">
|
||||||
|
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,6 @@
|
||||||
|
table.onmouseenter = table.onmouseleave = log;
|
||||||
|
|
||||||
|
function log(event) {
|
||||||
|
text.value += event.type + ' [target: ' + event.target.tagName + ']\n';
|
||||||
|
text.scrollTop = text.scrollHeight;
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
#text {
|
||||||
|
display: block;
|
||||||
|
height: 100px;
|
||||||
|
width: 456px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table th {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table td {
|
||||||
|
width: 150px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: bottom;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .nw {
|
||||||
|
background: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .n {
|
||||||
|
background: #03f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .ne {
|
||||||
|
background: #ff6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .w {
|
||||||
|
background: #ff0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .c {
|
||||||
|
background: #60c;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .e {
|
||||||
|
background: #09f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .sw {
|
||||||
|
background: #963;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .s {
|
||||||
|
background: #f60;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .se {
|
||||||
|
background: #0c3;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table .highlight {
|
||||||
|
background: red;
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="blue" onmouseenter="log(event)" onmouseleave="log(event)">
|
||||||
|
<div id="red"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea id="text"></textarea>
|
||||||
|
<input type="button" onclick="text.value=''" value="Clear">
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,4 @@
|
||||||
|
function log(event) {
|
||||||
|
text.value += event.type + ' [target: ' + event.target.id + ']\n';
|
||||||
|
text.scrollTop = text.scrollHeight;
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
#blue {
|
||||||
|
background: blue;
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#red {
|
||||||
|
background: red;
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
position: absolute;
|
||||||
|
left: 45px;
|
||||||
|
top: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#text {
|
||||||
|
display: block;
|
||||||
|
height: 100px;
|
||||||
|
width: 400px;
|
||||||
|
}
|
After Width: | Height: | Size: 9.1 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 8.2 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 6 KiB |
After Width: | Height: | Size: 16 KiB |
|
@ -0,0 +1,22 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="blue" onmouseover="mouselog(event)" onmouseout="mouselog(event)">
|
||||||
|
<div class="red"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea id="text"></textarea>
|
||||||
|
<input type="button" onclick="text.value=''" value="Clear">
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,4 @@
|
||||||
|
function mouselog(event) {
|
||||||
|
text.value += event.type + ' [target: ' + event.target.className + ']\n'
|
||||||
|
text.scrollTop = text.scrollHeight
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
.blue {
|
||||||
|
background: blue;
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red {
|
||||||
|
background: red;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
position: absolute;
|
||||||
|
left: 30px;
|
||||||
|
top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
height: 100px;
|
||||||
|
width: 400px;
|
||||||
|
display: block;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="green">
|
||||||
|
<div id="red">Test</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input onclick="clearText()" value="Clear" type="button">
|
||||||
|
|
||||||
|
<textarea id="text"></textarea>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,47 @@
|
||||||
|
green.onmouseover = green.onmouseout = green.onmousemove = handler;
|
||||||
|
|
||||||
|
function handler(event) {
|
||||||
|
var type = event.type;
|
||||||
|
while (type < 11) type += ' ';
|
||||||
|
|
||||||
|
log(type + " target=" + event.target.id)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function clearText() {
|
||||||
|
text.value = "";
|
||||||
|
lastMessage = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastMessageTime = 0;
|
||||||
|
var lastMessage = "";
|
||||||
|
var repeatCounter = 1;
|
||||||
|
|
||||||
|
function log(message) {
|
||||||
|
if (lastMessageTime == 0) lastMessageTime = new Date();
|
||||||
|
|
||||||
|
var time = new Date();
|
||||||
|
|
||||||
|
if (time - lastMessageTime > 500) {
|
||||||
|
message = '------------------------------\n' + message;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message === lastMessage) {
|
||||||
|
repeatCounter++;
|
||||||
|
if (repeatCounter == 2) {
|
||||||
|
text.value = text.value.trim() + ' x 2\n';
|
||||||
|
} else {
|
||||||
|
text.value = text.value.slice(0, text.value.lastIndexOf('x') + 1) + repeatCounter + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
repeatCounter = 1;
|
||||||
|
text.value += message + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
text.scrollTop = text.scrollHeight;
|
||||||
|
|
||||||
|
lastMessageTime = time;
|
||||||
|
lastMessage = message;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
#green {
|
||||||
|
height: 50px;
|
||||||
|
width: 160px;
|
||||||
|
background: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
#red {
|
||||||
|
height: 20px;
|
||||||
|
width: 110px;
|
||||||
|
background: red;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 5px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#text {
|
||||||
|
font-size: 12px;
|
||||||
|
height: 200px;
|
||||||
|
width: 360px;
|
||||||
|
display: block;
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="container">
|
||||||
|
<div class="smiley-green">
|
||||||
|
<div class="left-eye"></div>
|
||||||
|
<div class="right-eye"></div>
|
||||||
|
<div class="smile"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="smiley-yellow">
|
||||||
|
<div class="left-eye"></div>
|
||||||
|
<div class="right-eye"></div>
|
||||||
|
<div class="smile"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="smiley-red">
|
||||||
|
<div class="left-eye"></div>
|
||||||
|
<div class="right-eye"></div>
|
||||||
|
<div class="smile"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea id="log">Events will show up here!
|
||||||
|
</textarea>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,21 @@
|
||||||
|
container.onmouseover = container.onmouseout = handler;
|
||||||
|
|
||||||
|
function handler(event) {
|
||||||
|
|
||||||
|
function str(el) {
|
||||||
|
if (!el) return "null"
|
||||||
|
return el.className || el.tagName;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.value += event.type + ': ' +
|
||||||
|
'target=' + str(event.target) +
|
||||||
|
', relatedTarget=' + str(event.relatedTarget) + "\n";
|
||||||
|
log.scrollTop = log.scrollHeight;
|
||||||
|
|
||||||
|
if (event.type == 'mouseover') {
|
||||||
|
event.target.style.background = 'pink'
|
||||||
|
}
|
||||||
|
if (event.type == 'mouseout') {
|
||||||
|
event.target.style.background = ''
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,179 @@
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
border: 1px solid brown;
|
||||||
|
padding: 10px;
|
||||||
|
width: 330px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
#log {
|
||||||
|
height: 120px;
|
||||||
|
width: 350px;
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
[class^="smiley-"] {
|
||||||
|
display: inline-block;
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smiley-green {
|
||||||
|
background: #a9db7a;
|
||||||
|
border: 5px solid #92c563;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smiley-green .left-eye {
|
||||||
|
width: 18%;
|
||||||
|
height: 18%;
|
||||||
|
background: #84b458;
|
||||||
|
position: relative;
|
||||||
|
top: 29%;
|
||||||
|
left: 22%;
|
||||||
|
border-radius: 50%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smiley-green .right-eye {
|
||||||
|
width: 18%;
|
||||||
|
height: 18%;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
background: #84b458;
|
||||||
|
top: 29%;
|
||||||
|
right: 22%;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smiley-green .smile {
|
||||||
|
position: absolute;
|
||||||
|
top: 67%;
|
||||||
|
left: 16.5%;
|
||||||
|
width: 70%;
|
||||||
|
height: 20%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smiley-green .smile:after,
|
||||||
|
.smiley-green .smile:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: 0%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #84b458;
|
||||||
|
height: 100%;
|
||||||
|
width: 97%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smiley-green .smile:after {
|
||||||
|
background: #84b458;
|
||||||
|
height: 80%;
|
||||||
|
top: -40%;
|
||||||
|
left: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smiley-yellow {
|
||||||
|
background: #eed16a;
|
||||||
|
border: 5px solid #dbae51;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smiley-yellow .left-eye {
|
||||||
|
width: 18%;
|
||||||
|
height: 18%;
|
||||||
|
background: #dba652;
|
||||||
|
position: relative;
|
||||||
|
top: 29%;
|
||||||
|
left: 22%;
|
||||||
|
border-radius: 50%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smiley-yellow .right-eye {
|
||||||
|
width: 18%;
|
||||||
|
height: 18%;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
background: #dba652;
|
||||||
|
top: 29%;
|
||||||
|
right: 22%;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smiley-yellow .smile {
|
||||||
|
position: absolute;
|
||||||
|
top: 67%;
|
||||||
|
left: 19%;
|
||||||
|
width: 65%;
|
||||||
|
height: 14%;
|
||||||
|
background: #dba652;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smiley-red {
|
||||||
|
background: #ee9295;
|
||||||
|
border: 5px solid #e27378;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smiley-red .left-eye {
|
||||||
|
width: 18%;
|
||||||
|
height: 18%;
|
||||||
|
background: #d96065;
|
||||||
|
position: relative;
|
||||||
|
top: 29%;
|
||||||
|
left: 22%;
|
||||||
|
border-radius: 50%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smiley-red .right-eye {
|
||||||
|
width: 18%;
|
||||||
|
height: 18%;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
background: #d96065;
|
||||||
|
top: 29%;
|
||||||
|
right: 22%;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smiley-red .smile {
|
||||||
|
position: absolute;
|
||||||
|
top: 57%;
|
||||||
|
left: 16.5%;
|
||||||
|
width: 70%;
|
||||||
|
height: 20%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smiley-red .smile:after,
|
||||||
|
.smiley-red .smile:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #d96065;
|
||||||
|
height: 100%;
|
||||||
|
width: 97%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smiley-red .smile:after {
|
||||||
|
background: #d96065;
|
||||||
|
height: 80%;
|
||||||
|
top: 60%;
|
||||||
|
left: 0%;
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
We have a horizontal Drag'n'Drop here.
|
||||||
|
|
||||||
|
To position the element we use `position:relative` and slider-relative coordinates for the thumb. Here it's more convenient here than `position:absolute`.
|
|
@ -0,0 +1,56 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="slider" class="slider">
|
||||||
|
<div class="thumb"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let thumb = slider.querySelector('.thumb');
|
||||||
|
|
||||||
|
thumb.onmousedown = function(event) {
|
||||||
|
event.preventDefault(); // prevent selection start (browser action)
|
||||||
|
|
||||||
|
let shiftX = event.clientX - thumb.getBoundingClientRect().left;
|
||||||
|
// shiftY not needed, the thumb moves only horizontally
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
document.addEventListener('mouseup', onMouseUp);
|
||||||
|
|
||||||
|
function onMouseMove(event) {
|
||||||
|
let newLeft = event.clientX - shiftX - slider.getBoundingClientRect().left;
|
||||||
|
|
||||||
|
// the pointer is out of slider => lock the thumb within the bounaries
|
||||||
|
if (newLeft < 0) {
|
||||||
|
newLeft = 0;
|
||||||
|
}
|
||||||
|
var rightEdge = slider.offsetWidth - thumb.offsetWidth;
|
||||||
|
if (newLeft > rightEdge) {
|
||||||
|
newLeft = rightEdge;
|
||||||
|
}
|
||||||
|
|
||||||
|
thumb.style.left = newLeft + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseUp() {
|
||||||
|
document.removeEventListener('mouseup', onMouseUp);
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
thumb.ondragstart = function() {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,19 @@
|
||||||
|
.slider {
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #E0E0E0;
|
||||||
|
background: linear-gradient(left top, #E0E0E0, #EEEEEE);
|
||||||
|
width: 310px;
|
||||||
|
height: 15px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb {
|
||||||
|
width: 10px;
|
||||||
|
height: 25px;
|
||||||
|
border-radius: 3px;
|
||||||
|
position: relative;
|
||||||
|
left: 10px;
|
||||||
|
top: -5px;
|
||||||
|
background: blue;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="slider" class="slider">
|
||||||
|
<div class="thumb"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ...your code...
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,19 @@
|
||||||
|
.slider {
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #E0E0E0;
|
||||||
|
background: linear-gradient(left top, #E0E0E0, #EEEEEE);
|
||||||
|
width: 310px;
|
||||||
|
height: 15px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb {
|
||||||
|
width: 10px;
|
||||||
|
height: 25px;
|
||||||
|
border-radius: 3px;
|
||||||
|
position: relative;
|
||||||
|
left: 10px;
|
||||||
|
top: -5px;
|
||||||
|
background: blue;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
16
2-ui/3-event-details/4-drag-and-drop/1-slider/task.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
importance: 5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Slider
|
||||||
|
|
||||||
|
Create a slider:
|
||||||
|
|
||||||
|
[iframe src="solution" height=60 border=1]
|
||||||
|
|
||||||
|
Drag the blue thumb with the mouse and move it.
|
||||||
|
|
||||||
|
Important details:
|
||||||
|
|
||||||
|
- When the mouse button is pressed, during the dragging the mouse may go over or below the slider. The slider will still work (convenient for the user).
|
||||||
|
- If the mouse moves very fast to the left or to the right, the thumb should stop exactly at the edge.
|
|
@ -0,0 +1,5 @@
|
||||||
|
В решении этой задачи для переноса мы используем координаты относительно окна и `position:fixed`. Так проще.
|
||||||
|
|
||||||
|
А по окончании -- прибавляем прокрутку и делаем `position:absolute`, чтобы элемент был привязан к определённому месту в документе, а не в окне. Можно было и сразу `position:absolute` и оперировать в абсолютных координатах, но код был бы немного длиннее.
|
||||||
|
|
||||||
|
Детали решения расписаны в комментариях в исходном коде.
|
|
@ -0,0 +1,37 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="stylesheet" href="soccer.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h2>Расставьте супергероев по полю.</h2>
|
||||||
|
|
||||||
|
<p>Супергерои и мяч -- это элементы с классом "draggable". Сделайте так, чтобы их можно было переносить.</p>
|
||||||
|
|
||||||
|
<p>Важно: если супергероя подносят к низу или верху страницы, она должна автоматически прокручиваться. Если страница помещается на вашем экране целиком и не имеет вертикальной прокрутки -- сделайте окно браузера меньше, чтобы протестировать эту возможность.</p>
|
||||||
|
|
||||||
|
<p>Да, и ещё: супергерои ни при каких условиях не должны попасть за край экрана.</p>
|
||||||
|
|
||||||
|
<div id="field">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hero draggable" id="hero1"></div>
|
||||||
|
<div class="hero draggable" id="hero2"></div>
|
||||||
|
<div class="hero draggable" id="hero3"></div>
|
||||||
|
<div class="hero draggable" id="hero4"></div>
|
||||||
|
<div class="hero draggable" id="hero5"></div>
|
||||||
|
<div class="hero draggable" id="hero6"></div>
|
||||||
|
|
||||||
|
<img src="https://en.js.cx/drag-heroes/ball.png" class="draggable">
|
||||||
|
|
||||||
|
<div style="clear:both"></div>
|
||||||
|
|
||||||
|
<script src="soccer.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,48 @@
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#field {
|
||||||
|
background: url(https://js.cx/drag-heroes/field.png);
|
||||||
|
width: 800px;
|
||||||
|
height: 600px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
/* герои и мяч (переносимые элементы) */
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
background: url(https://js.cx/drag-heroes/heroes.png);
|
||||||
|
width: 130px;
|
||||||
|
height: 128px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hero1 {
|
||||||
|
background-position: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hero2 {
|
||||||
|
background-position: 0 -128px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hero3 {
|
||||||
|
background-position: -120px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hero4 {
|
||||||
|
background-position: -125px -128px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hero5 {
|
||||||
|
background-position: -248px -128px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hero6 {
|
||||||
|
background-position: -244px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
document.addEventListener('mousedown', function(event) {
|
||||||
|
|
||||||
|
let dragElement = event.target.closest('.draggable');
|
||||||
|
|
||||||
|
if (!dragElement) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
let coords, shiftX, shiftY;
|
||||||
|
|
||||||
|
startDrag(event.clientX, event.clientY);
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
|
||||||
|
dragElement.onmouseup = function() {
|
||||||
|
finishDrag();
|
||||||
|
};
|
||||||
|
|
||||||
|
function onMouseMove(event) {
|
||||||
|
moveAt(event.clientX, event.clientY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// on drag start:
|
||||||
|
// remember the initial shift
|
||||||
|
// move the element position:fixed and a direct child of body
|
||||||
|
function startDrag(clientX, clientY) {
|
||||||
|
|
||||||
|
shiftX = clientX - dragElement.getBoundingClientRect().left;
|
||||||
|
shiftY = clientY - dragElement.getBoundingClientRect().top;
|
||||||
|
|
||||||
|
dragElement.style.position = 'fixed';
|
||||||
|
|
||||||
|
document.body.append(dragElement);
|
||||||
|
|
||||||
|
moveAt(clientX, clientY);
|
||||||
|
};
|
||||||
|
|
||||||
|
// switch to absolute coordinates at the end, to fix the element in the document
|
||||||
|
function finishDrag() {
|
||||||
|
dragElement.style.top = parseInt(dragElement.style.top) + pageYOffset + 'px';
|
||||||
|
dragElement.style.position = 'absolute';
|
||||||
|
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
dragElement.onmouseup = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveAt(clientX, clientY) {
|
||||||
|
// new window-relative coordinates
|
||||||
|
let newX = clientX - shiftX;
|
||||||
|
let newY = clientY - shiftY;
|
||||||
|
|
||||||
|
// check if the new coordinates are below the bottom window edge
|
||||||
|
let newBottom = newY + dragElement.offsetHeight; // new bottom
|
||||||
|
|
||||||
|
// below the window? let's scroll the page
|
||||||
|
if (newBottom > document.documentElement.clientHeight) {
|
||||||
|
// window-relative coordinate of document end
|
||||||
|
let docBottom = document.documentElement.getBoundingClientRect().bottom;
|
||||||
|
|
||||||
|
// scroll the document down by 10px has a problem
|
||||||
|
// it can scroll beyond the end of the document
|
||||||
|
// Math.min(how much left to the end, 10)
|
||||||
|
let scrollY = Math.min(docBottom - newBottom, 10);
|
||||||
|
|
||||||
|
// calculations are imprecise, there may be rounding errors that lead to scrolling up
|
||||||
|
// that should be impossible, fix that here
|
||||||
|
if (scrollY < 0) scrollY = 0;
|
||||||
|
|
||||||
|
window.scrollBy(0, scrollY);
|
||||||
|
|
||||||
|
// a swift mouse move make put the cursor beyond the document end
|
||||||
|
// if that happens -
|
||||||
|
// limit the new Y by the maximally possible (right at the bottom of the document)
|
||||||
|
newY = Math.min(newY, document.documentElement.clientHeight - dragElement.offsetHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the new coordinates are above the top window edge (similar logic)
|
||||||
|
if (newY < 0) {
|
||||||
|
// scroll up
|
||||||
|
let scrollY = Math.min(-newY, 10);
|
||||||
|
if (scrollY < 0) scrollY = 0; // check precision errors
|
||||||
|
|
||||||
|
window.scrollBy(0, -scrollY);
|
||||||
|
// a swift mouse move can put the cursor beyond the document start
|
||||||
|
newY = Math.max(newY, 0); // newY may not be below 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// limit the new X within the window boundaries
|
||||||
|
// there's no scroll here so it's simple
|
||||||
|
if (newX < 0) newX = 0;
|
||||||
|
if (newX > document.documentElement.clientWidth - dragElement.offsetWidth) {
|
||||||
|
newX = document.documentElement.clientWidth - dragElement.offsetWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragElement.style.left = newX + 'px';
|
||||||
|
dragElement.style.top = newY + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,37 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="stylesheet" href="soccer.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h2>Расставьте супергероев по полю.</h2>
|
||||||
|
|
||||||
|
<p>Супергерои и мяч -- это элементы с классом "draggable". Сделайте так, чтобы их можно было переносить.</p>
|
||||||
|
|
||||||
|
<p>Важно: если супергероя подносят к низу или верху страницы, она должна автоматически прокручиваться. Если страница помещается на вашем экране целиком и не имеет вертикальной прокрутки -- сделайте окно браузера меньше, чтобы протестировать эту возможность.</p>
|
||||||
|
|
||||||
|
<p>Да, и ещё: супергерои ни при каких условиях не должны попасть за край экрана.</p>
|
||||||
|
|
||||||
|
<div id="field">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hero draggable" id="hero1"></div>
|
||||||
|
<div class="hero draggable" id="hero2"></div>
|
||||||
|
<div class="hero draggable" id="hero3"></div>
|
||||||
|
<div class="hero draggable" id="hero4"></div>
|
||||||
|
<div class="hero draggable" id="hero5"></div>
|
||||||
|
<div class="hero draggable" id="hero6"></div>
|
||||||
|
|
||||||
|
<img src="https://js.cx/drag-heroes/ball.png" class="draggable">
|
||||||
|
|
||||||
|
<div style="clear:both"></div>
|
||||||
|
|
||||||
|
<script src="soccer.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,48 @@
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#field {
|
||||||
|
background: url(https://js.cx/drag-heroes/field.png);
|
||||||
|
width: 800px;
|
||||||
|
height: 600px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
/* герои и мяч (переносимые элементы) */
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
background: url(https://js.cx/drag-heroes/heroes.png);
|
||||||
|
width: 130px;
|
||||||
|
height: 128px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hero1 {
|
||||||
|
background-position: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hero2 {
|
||||||
|
background-position: 0 -128px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hero3 {
|
||||||
|
background-position: -120px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hero4 {
|
||||||
|
background-position: -125px -128px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hero5 {
|
||||||
|
background-position: -248px -128px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hero6 {
|
||||||
|
background-position: -244px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
// Ваш код
|
21
2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/task.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
importance: 5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Расставить супергероев по полю
|
||||||
|
|
||||||
|
В этой задаче вы можете проверить своё понимание сразу нескольких аспектов Drag'n'Drop.
|
||||||
|
|
||||||
|
Сделайте так, чтобы элементы с классом `draggable` можно было переносить мышкой. По окончании переноса элемент остаётся на том месте в документе, где его положили.
|
||||||
|
|
||||||
|
Требования к реализации:
|
||||||
|
|
||||||
|
- Должен быть 1 обработчик на `document`, использующий делегирование.
|
||||||
|
- Если элементы подносят к вертикальным краям окна -- оно должно прокручиваться вниз/вверх.
|
||||||
|
- Горизонтальной прокрутки в этой задаче не существует.
|
||||||
|
- Элемент при переносе, даже при резких движениях мышкой, не должен попасть вне окна.
|
||||||
|
|
||||||
|
Футбольное поле в этой задаче слишком большое, чтобы показывать его здесь, поэтому откройте его, кликнув по ссылке ниже. Там же и подробное описание задачи (осторожно, винни-пух и супергерои!).
|
||||||
|
|
||||||
|
[demo src="solution"]
|
||||||
|
|
296
2-ui/3-event-details/4-drag-and-drop/article.md
Normal file
|
@ -0,0 +1,296 @@
|
||||||
|
# Mouse: Drag'n'Drop [TODO]
|
||||||
|
|
||||||
|
Drag'n'Drop is a great interface solution. Taking something, dragging and dropping is a clear and simple way to do many things, from copying and moving (see file managers) to ordering (drop into cart).
|
||||||
|
|
||||||
|
In the modern HTML standard there's a [section about Drag Events](https://html.spec.whatwg.org/multipage/interaction.html#dnd).
|
||||||
|
|
||||||
|
They are interesting, because they allow to solve simple tasks easily, and also allow to handle drag'n'drop of "external" files into the browser. So we can take a file in the OS file-manager and drop it into the browser window. Then JavaScript gains access to its contents.
|
||||||
|
|
||||||
|
But native Drag Events also have limitations. For instance, we can limit dragging by a certain area. Also we can't make it "horizontal" or "vertical" only. There are other drag'n'drop tasks that can't be implemented using that API.
|
||||||
|
|
||||||
|
So here we'll see how to implement Drag'n'Drop using mouse events. Not that hard either.
|
||||||
|
|
||||||
|
## Drag'n'Drop algorithm
|
||||||
|
|
||||||
|
The basic Drag'n'Drop algorithm looks like this:
|
||||||
|
|
||||||
|
1. Catch `mousedown` on a draggable element.
|
||||||
|
2. Prepare the element to moving (maybe create a copy of it or whatever).
|
||||||
|
3. Then on `mousemove` move it by changing `left/top` and `position:absolute`.
|
||||||
|
4. On `mouseup` (button release) -- perform all actions related to a finished Drag'n'Drop.
|
||||||
|
|
||||||
|
These are the basics. We can extend it, for instance, by highlighting droppable (available for the drop) elements when hovering over them.
|
||||||
|
|
||||||
|
Here's the algorithm for drag'n'drop of a ball:
|
||||||
|
|
||||||
|
```js
|
||||||
|
ball.onmousedown = function(event) { // (1) start the process
|
||||||
|
|
||||||
|
// (2) prepare to moving: make absolute and on top by z-index
|
||||||
|
ball.style.position = 'absolute';
|
||||||
|
ball.style.zIndex = 1000;
|
||||||
|
document.body.append(ball);
|
||||||
|
// ...and put that absolutely positioned ball under the cursor
|
||||||
|
|
||||||
|
moveAt(event.pageX, event.pageY);
|
||||||
|
|
||||||
|
// centers the ball at (pageX, pageY) coordinates
|
||||||
|
function moveAt(pageX, pageY) {
|
||||||
|
ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
|
||||||
|
ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(event) {
|
||||||
|
moveAt(event.pageX, event.pageY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (3) move the ball on mousemove
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
|
||||||
|
// (4) drop the ball, remove unneeded handlers
|
||||||
|
ball.onmouseup = function() {
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
ball.onmouseup = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
If we run the code, we can notice something strange. On the beginning of the drag'n'drop, the ball "forks": we start to dragging it's "clone".
|
||||||
|
|
||||||
|
```online
|
||||||
|
Here's an example in action:
|
||||||
|
|
||||||
|
[iframe src="ball" height=230]
|
||||||
|
|
||||||
|
Try to drag'n'drop the mouse and you'll see the strange behavior.
|
||||||
|
```
|
||||||
|
|
||||||
|
That's because the browser has its own Drag'n'Drop for images and some other elements that runs automatically and conflicts with ours.
|
||||||
|
|
||||||
|
To disable it:
|
||||||
|
|
||||||
|
```js
|
||||||
|
ball.ondragstart = function() {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Now everything will be all right.
|
||||||
|
|
||||||
|
```online
|
||||||
|
In action:
|
||||||
|
|
||||||
|
[iframe src="ball2" height=230]
|
||||||
|
```
|
||||||
|
|
||||||
|
Another important aspect -- we track `mousemove` on `document`, not on `ball`. From the first sight it may seem that the mouse is always over the ball, and we can put `mousemove` on it.
|
||||||
|
|
||||||
|
But as we remember, `mousemove` triggers often, but not for every pixel. So after swift move the cursor can jump from the ball somewhere in the middle of document (or even outside of the window).
|
||||||
|
|
||||||
|
So we should listen on `document` to catch it.
|
||||||
|
|
||||||
|
## Correct positioning
|
||||||
|
|
||||||
|
In the examples above the ball is always centered under the pointer:
|
||||||
|
|
||||||
|
```js
|
||||||
|
ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
|
||||||
|
ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
|
||||||
|
```
|
||||||
|
|
||||||
|
Not bad, but there's a side-effect. To initiate the drag'n'drop can we `mousedown` anywhere on the ball. If do it at the edge, then the ball suddenly "jumps" to become centered.
|
||||||
|
|
||||||
|
It would be better if we keep the initial shift of the element relative to the pointer.
|
||||||
|
|
||||||
|
For instance, if we start dragging by the edge of the ball, then the cursor should remain over the edge while dragging.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1. When a visitor presses the button (`mousedown`) -- we can remember the distance from the cursor to the left-upper corner of the ball in variables `shiftX/shiftY`. We should keep that distance while dragging.
|
||||||
|
|
||||||
|
To get these shifts we can substract the coordinates:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// onmousedown
|
||||||
|
let shiftX = event.clientX - ball.getBoundingClientRect().left;
|
||||||
|
let shiftY = event.clientY - ball.getBoundingClientRect().top;
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note that there's no method to get document-relative coordinates in JavaScript, so we use window-relative coordinates here.
|
||||||
|
|
||||||
|
2. Then while dragging we position the ball on the same shift relative to the pointer, like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// onmousemove
|
||||||
|
// ball has position:absoute
|
||||||
|
ball.style.left = event.pageX - *!*shiftX*/!* + 'px';
|
||||||
|
ball.style.top = event.pageY - *!*shiftY*/!* + 'px';
|
||||||
|
```
|
||||||
|
|
||||||
|
The final code with better positioning:
|
||||||
|
|
||||||
|
```js
|
||||||
|
ball.onmousedown = function(event) {
|
||||||
|
|
||||||
|
*!*
|
||||||
|
let shiftX = event.clientX - ball.getBoundingClientRect().left;
|
||||||
|
let shiftY = event.clientY - ball.getBoundingClientRect().top;
|
||||||
|
*/!*
|
||||||
|
|
||||||
|
ball.style.position = 'absolute';
|
||||||
|
ball.style.zIndex = 1000;
|
||||||
|
document.body.append(ball);
|
||||||
|
|
||||||
|
moveAt(event.pageX, event.pageY);
|
||||||
|
|
||||||
|
// centers the ball at (pageX, pageY) coordinates
|
||||||
|
function moveAt(pageX, pageY) {
|
||||||
|
ball.style.left = pageX - *!*shiftX*/!* + 'px';
|
||||||
|
ball.style.top = pageY - *!*shiftY*/!* + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(event) {
|
||||||
|
moveAt(event.pageX, event.pageY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (3) move the ball on mousemove
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
|
||||||
|
// (4) drop the ball, remove unneeded handlers
|
||||||
|
ball.onmouseup = function() {
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
ball.onmouseup = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
ball.ondragstart = function() {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
```online
|
||||||
|
In action (inside `<iframe>`):
|
||||||
|
|
||||||
|
[iframe src="ball3" height=230]
|
||||||
|
```
|
||||||
|
|
||||||
|
The difference is especially noticeable if we drag the ball by it's right-bottom corner. In the previous example the ball "jumps" under the pointer. Now it fluently follows the cursor from the current position.
|
||||||
|
|
||||||
|
## Detecting droppables
|
||||||
|
|
||||||
|
In previous examples the ball could be dropped just "anywhere" to stay. In real-life we usually take one element and drop it onto another. For instance, a file into a folder, or a user into a trash can or whatever.
|
||||||
|
|
||||||
|
Abstractly, we take a "draggable" element and drop it onto "droppable" element.
|
||||||
|
|
||||||
|
We need to know the target droppable at the end of Drag'n'Drop -- to do the corresponding action, and, preferably, during the dragging process, to highlight it.
|
||||||
|
|
||||||
|
The solution is kind-of interesting and just a little bit tricky, so let's cover it here.
|
||||||
|
|
||||||
|
What's the first idea? Probably to put `onmouseover/mouseup` handlers on potential droppables and detect when the mouse pointer appears over them. And then we know that we are dragging/dropping on that element.
|
||||||
|
|
||||||
|
But that doesn't work.
|
||||||
|
|
||||||
|
The problem is that, while we're dragging, the draggable element is always above other elements. And mouse events only happen on the top element, not on those below it.
|
||||||
|
|
||||||
|
For instance, below are two `<div>` elements, red on top of blue. There'no way to catch an event on the blue one, because the red is on top:
|
||||||
|
|
||||||
|
```html run autorun height=60
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div style="background:blue" onmouseover="alert('never works')"></div>
|
||||||
|
<div style="background:red" onmouseover="alert('over red!')"></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
The same with a draggable element. The ball in always on top over other elements, so events happen on it. Whatever handlers we set on lower elements, they won't work.
|
||||||
|
|
||||||
|
That's why the initial idea to put handlers on potential droppables doesn't work in practice. They won't run.
|
||||||
|
|
||||||
|
So, what to do?
|
||||||
|
|
||||||
|
There's a method called `document.elementFromPoint(clientX, clientY)`. It returns the most nested element on given window-relative coordinates (or `null` if coordinates are out of the window).
|
||||||
|
|
||||||
|
So in any of our mouse event handlers we can detect the potential droppable under the pointer like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// in a mouse event handler
|
||||||
|
ball.hidden = true; // (*)
|
||||||
|
let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
|
||||||
|
ball.hidden = false;
|
||||||
|
// elemBelow is the element below the ball. If it's droppable, we can handle it.
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note: we need to hide the ball before the call `(*)`. Otherwise we'll usually have a ball on these coordinates, as it's the top element under the pointer: `elemBelow=ball`.
|
||||||
|
|
||||||
|
We can use that code to check what we're "flying over" at any time. And handle the drop when it happens.
|
||||||
|
|
||||||
|
An extended code of `onMouseMove` to find "droppable" elements:
|
||||||
|
|
||||||
|
```js
|
||||||
|
let currentDroppable = null; // potential droppable that we're flying over right now
|
||||||
|
|
||||||
|
function onMouseMove(event) {
|
||||||
|
moveAt(event.pageX, event.pageY);
|
||||||
|
|
||||||
|
ball.hidden = true;
|
||||||
|
let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
|
||||||
|
ball.hidden = false;
|
||||||
|
|
||||||
|
// mousemove events may trigger out of the window (when the ball is dragged off-screen)
|
||||||
|
// if clientX/clientY are out of the window, then elementfromPoint returns null
|
||||||
|
if (!elemBelow) return;
|
||||||
|
|
||||||
|
// potential droppables are labeled with the class "droppable" (can be other logic)
|
||||||
|
let droppableBelow = elemBelow.closest('.droppable');
|
||||||
|
|
||||||
|
if (currentDroppable != droppableBelow) { // if there are any changes
|
||||||
|
// we're flying in or out...
|
||||||
|
// note: both values can be null
|
||||||
|
// currentDroppable=null if we were not over a droppable (e.g over an empty space)
|
||||||
|
// droppableBelow=null if we're not over a droppable now, during this event
|
||||||
|
|
||||||
|
if (currentDroppable) {
|
||||||
|
// the logic to process "flying out" of the droppable (remove highlight)
|
||||||
|
leaveDroppable(currentDroppable);
|
||||||
|
}
|
||||||
|
currentDroppable = droppableBelow;
|
||||||
|
if (currentDroppable) {
|
||||||
|
// the logic to process "flying in" of the droppable
|
||||||
|
enterDroppable(currentDroppable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the example below when the ball is dragged over the soccer gate, the gate is highlighted.
|
||||||
|
|
||||||
|
[codetabs height=250 src="ball4"]
|
||||||
|
|
||||||
|
Now we have the current "drop target" in the variable `currentDroppable` during the whole process and can use it to highlight or any other stuff.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
We considered a basic `Drag'n'Drop` algorithm.
|
||||||
|
|
||||||
|
The key components:
|
||||||
|
|
||||||
|
1. Events flow: `ball.mousedown` -> `document.mousemove` -> `ball.mouseup` (cancel native `ondragstart`).
|
||||||
|
2. At the drag start -- remember the initial shift of the pointer relative to the element: `shiftX/shiftY` and keep it during the dragging.
|
||||||
|
3. Detect droppable elements under the pointer using `document.elementFromPoint`.
|
||||||
|
|
||||||
|
We can lay a lot on this foundation.
|
||||||
|
|
||||||
|
- On `mouseup` we can finalize the drop: change data, move elements around.
|
||||||
|
- We can highlight the elements we're flying over.
|
||||||
|
- We can limit dragging by a certain area or direction.
|
||||||
|
- We can use event delegation for `mousedown/up`. A large-area event handler that checks `event.target` can manage Drag'n'Drop for hundreds of elements.
|
||||||
|
- And so on.
|
||||||
|
|
||||||
|
There are frameworks that build architecture over it: `DragZone`, `Droppable`, `Draggable` and other classes. Most of them do the similar stuff to described above, so it should be easy to understand them now. Or roll our own, because you already know how to handle the process, and it may be more flexible than to adapt something else.
|
48
2-ui/3-event-details/4-drag-and-drop/ball.view/index.html
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="height: 200px">
|
||||||
|
|
||||||
|
<p>Drag the ball.</p>
|
||||||
|
|
||||||
|
<img src="https://js.cx/clipart/ball.svg" style="cursor:pointer" width="40" height="40" id="ball">
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
ball.onmousedown = function(event) { // (1) start the process
|
||||||
|
|
||||||
|
// (2) prepare to moving: make absolute and top by z-index
|
||||||
|
ball.style.position = 'absolute';
|
||||||
|
ball.style.zIndex = 1000;
|
||||||
|
document.body.appendChild(ball);
|
||||||
|
// ...and put that absolutely positioned ball under the cursor
|
||||||
|
moveAt(event.pageX, event.pageY);
|
||||||
|
|
||||||
|
// centers the ball at (pageX, pageY) coordinates
|
||||||
|
function moveAt(pageX, pageY) {
|
||||||
|
ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
|
||||||
|
ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(event) {
|
||||||
|
moveAt(event.pageX, event.pageY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (3) move the ball on mousemove
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
|
||||||
|
// (4) drop the ball, remove unneeded handlers
|
||||||
|
ball.onmouseup = function() {
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
ball.onmouseup = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
54
2-ui/3-event-details/4-drag-and-drop/ball2.view/index.html
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="height: 200px">
|
||||||
|
|
||||||
|
<p>Drag the ball.</p>
|
||||||
|
|
||||||
|
<img src="https://js.cx/clipart/ball.svg" style="cursor:pointer" width="40" height="40" id="ball">
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
ball.onmousedown = function(event) { // (1) start the process
|
||||||
|
|
||||||
|
// (2) prepare to moving: make absolute and top by z-index
|
||||||
|
ball.style.position = 'absolute';
|
||||||
|
ball.style.zIndex = 1000;
|
||||||
|
document.body.appendChild(ball);
|
||||||
|
// ...and put that absolutely positioned ball under the cursor
|
||||||
|
moveAt(event.pageX, event.pageY);
|
||||||
|
|
||||||
|
// centers the ball at (pageX, pageY) coordinates
|
||||||
|
function moveAt(pageX, pageY) {
|
||||||
|
ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
|
||||||
|
ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(event) {
|
||||||
|
moveAt(event.pageX, event.pageY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (3) move the ball on mousemove
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
|
||||||
|
// (4) drop the ball, remove unneeded handlers
|
||||||
|
ball.onmouseup = function() {
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
ball.onmouseup = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
ball.ondragstart = function() {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
52
2-ui/3-event-details/4-drag-and-drop/ball3.view/index.html
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="height: 200px">
|
||||||
|
|
||||||
|
<p>Drag the ball.</p>
|
||||||
|
|
||||||
|
<img src="https://js.cx/clipart/ball.svg" style="cursor:pointer" width="40" height="40" id="ball">
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
ball.onmousedown = function(event) {
|
||||||
|
|
||||||
|
let shiftX = event.clientX - ball.getBoundingClientRect().left;
|
||||||
|
let shiftY = event.clientY - ball.getBoundingClientRect().top;
|
||||||
|
|
||||||
|
ball.style.position = 'absolute';
|
||||||
|
ball.style.zIndex = 1000;
|
||||||
|
document.body.append(ball);
|
||||||
|
|
||||||
|
moveAt(event.pageX, event.pageY);
|
||||||
|
|
||||||
|
function moveAt(pageX, pageY) {
|
||||||
|
ball.style.left = pageX - shiftX + 'px';
|
||||||
|
ball.style.top = pageY - shiftY + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(event) {
|
||||||
|
moveAt(event.pageX, event.pageY);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
|
||||||
|
ball.onmouseup = function() {
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
ball.onmouseup = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
ball.ondragstart = function() {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
82
2-ui/3-event-details/4-drag-and-drop/ball4.view/index.html
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<p>Drag the ball.</p>
|
||||||
|
|
||||||
|
<img src="https://en.js.cx/clipart/soccer-gate.svg" id="gate" class="droppable">
|
||||||
|
|
||||||
|
<img src="https://en.js.cx/clipart/ball.svg" id="ball">
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentDroppable = null;
|
||||||
|
|
||||||
|
ball.onmousedown = function(event) {
|
||||||
|
|
||||||
|
let shiftX = event.clientX - ball.getBoundingClientRect().left;
|
||||||
|
let shiftY = event.clientY - ball.getBoundingClientRect().top;
|
||||||
|
|
||||||
|
ball.style.position = 'absolute';
|
||||||
|
ball.style.zIndex = 1000;
|
||||||
|
document.body.append(ball);
|
||||||
|
|
||||||
|
moveAt(event.pageX, event.pageY);
|
||||||
|
|
||||||
|
function moveAt(pageX, pageY) {
|
||||||
|
ball.style.left = pageX - shiftX + 'px';
|
||||||
|
ball.style.top = pageY - shiftY + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(event) {
|
||||||
|
moveAt(event.pageX, event.pageY);
|
||||||
|
|
||||||
|
ball.hidden = true;
|
||||||
|
let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
|
||||||
|
ball.hidden = false;
|
||||||
|
|
||||||
|
if (!elemBelow) return;
|
||||||
|
|
||||||
|
let droppableBelow = elemBelow.closest('.droppable');
|
||||||
|
if (currentDroppable != droppableBelow) {
|
||||||
|
if (currentDroppable) { // null when we were not over a droppable before this event
|
||||||
|
leaveDroppable(currentDroppable);
|
||||||
|
}
|
||||||
|
currentDroppable = droppableBelow;
|
||||||
|
if (currentDroppable) { // null if we're not coming over a droppable now
|
||||||
|
// (maybe just left the droppable)
|
||||||
|
enterDroppable(currentDroppable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
|
||||||
|
ball.onmouseup = function() {
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
ball.onmouseup = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
function enterDroppable(elem) {
|
||||||
|
elem.style.background = 'pink';
|
||||||
|
}
|
||||||
|
|
||||||
|
function leaveDroppable(elem) {
|
||||||
|
elem.style.background = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
ball.ondragstart = function() {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
13
2-ui/3-event-details/4-drag-and-drop/ball4.view/style.css
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
#gate {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 100px;
|
||||||
|
width: 83px;
|
||||||
|
height: 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ball {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
BIN
2-ui/3-event-details/4-drag-and-drop/ball_shift.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
2-ui/3-event-details/4-drag-and-drop/ball_shift@2x.png
Normal file
After Width: | Height: | Size: 22 KiB |