This commit is contained in:
Ilya Kantor 2019-08-17 12:49:03 +03:00
parent 7fd3eb1797
commit d94b2922dc
16 changed files with 218 additions and 180 deletions

View file

@ -68,7 +68,13 @@ There are other good ways to do the task. For instance, there's a great algorith
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1)); // random index from 0 to i
[array[i], array[j]] = [array[j], array[i]]; // swap elements
// swap elements array[i] and array[j]
// we use "destructuring assignment" syntax to achieve that
// you'll find more details about that syntax in later chapters
// same can be written as:
// let t = array[i]; array[i] = array[j]; array[j] = t
[array[i], array[j]] = [array[j], array[i]];
}
}
```

View file

@ -4,25 +4,28 @@
function showmesg(t, form) {
if (timer==0) timer = new Date()
let tm = new Date()
if (tm-timer > 300) {
t = '------------------------------\n'+t
if (timer == 0) {
timer = new Date();
}
let area = document.forms[form+'form'].getElementsByTagName('textarea')[0]
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
area.scrollTop = area.scrollHeight;
timer = tm
timer = tm;
}
function logMouse(e) {
let evt = e.type;
while (evt.length < 11) evt += ' ';
showmesg(evt+" which="+e.which, 'test')
showmesg(evt + " which=" + e.which, 'test')
return false;
}

View file

@ -1,16 +1,14 @@
# Moving: mouseover/out, mouseenter/leave
# Moving the mouse: mouseover/out, mouseenter/leave
Let's dive into more details about events that happen when mouse moves between elements.
Let's dive into more details about events that happen when the mouse moves between elements.
## Mouseover/mouseout, relatedTarget
## Events mouseover/mouseout, relatedTarget
The `mouseover` event occurs when a mouse pointer comes over an element, and `mouseout` -- when it leaves.
![](mouseover-mouseout.svg)
These events are special, because they have a `relatedTarget`.
This property complements `target`. When a mouse leaves one element for another, one of them becomes `target`, and the other one `relatedTarget`.
These events are special, because they have property `relatedTarget`. This property complements `target`. When a mouse leaves one element for another, one of them becomes `target`, and the other one - `relatedTarget`.
For `mouseover`:
@ -19,13 +17,13 @@ For `mouseover`:
For `mouseout` the reverse:
- `event.target` -- is the element that mouse left.
- `event.target` -- is the element that the mouse left.
- `event.relatedTarget` -- is the new under-the-pointer element, that mouse left for (`target` -> `relatedTarget`).
```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.
In the example below each face and its features are separate elements. 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.
Each event has the information about both `target` and `relatedTarget`:
[codetabs src="mouseoverout" height=280]
```
@ -38,86 +36,111 @@ That's normal and just means that the mouse came not from another element, but f
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
## Skipping elements
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 it notices changes then triggers the events.
That means that if the visitor is moving the mouse very fast then DOM-elements may be skipped:
That means that if the visitor is moving the mouse very fast then some DOM-elements may be skipped:
![](mouseover-mouseout-over-elems.svg)
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.
That's good for performance, because if there may be many intermediate elements. We don't really want to process in and out of each one.
On the other hand, we should keep in mind that we can't assume that the mouse slowly moves from one event to another. No, it can "jump".
On the other hand, we should keep in mind that the mouse pointer doesn't "visit" all elements along the way. 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":
In particular, it's possible that the pointer jumps right inside the middle of the page from out of the window. In that case `relatedTarget` is `null`, because it came from "nowhere":
![](mouseover-mouseout-from-outside.svg)
<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.
You can 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.
Its HTML has two nested elements: the `<div id="child">` is inside the `<div id="parent">`. If you move the mouse fast over them, then maybe only the child div triggers events, or maybe the parent one, or maybe there will be no events at all.
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.
Also move the pointer into the child `div`, and then move it out quickly down through the parent one. If the movement is fast enough, then the parent element is ignored. The mouse will cross the parent element without noticing it.
[codetabs height=360 src="mouseoverout-fast"]
```
## "Extra" mouseout when leaving for a child
```smart header="If `mouseover` triggered, there must be `mouseout`"
In case of fast mouse movements, intermediate elements may be ignores, but one thing we know for sure: elements can be only skipped as a whole.
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!
If the pointer "officially" entered an element with `mouseover`, then upon leaving it we always get `mouseout`.
```
## Mouseout when leaving for a child
An important feature of `mouseout` -- it triggers, when the pointer moves from an element to its descendant.
Visually, the pointer is still on the element, but we get `mouseout`!
![](mouseover-to-child.svg)
That seems strange, but can be easily explained.
That looks 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).**
**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.
So if it goes to another element (even a descendant), then it leaves the previous one.
There's a funny consequence that we can see on the example below.
Please note an important detail.
The red `<div>` is nested inside the blue one. The blue `<div>` has `mouseover/out` handlers that log all events in the textarea below.
The `mouseover` event on a descendant bubbles up. So, if the parent element has such handler, it triggers.
Try entering the blue element and then moving the mouse on the red one -- and watch the events:
![](mouseover-bubble-nested.svg)
So, when we move from a parent element to a child, then two handlers trigger on the parent element: `mouseout` and `mouseover`:
```js
parent.onmouseout = function(event) {
/* event.target: parent element */
};
parent.onmouseover = function(event) {
/* event.target: child element */
};
```
If the code inside the handlers doesn't look at `target`, then it might think that the mouse left the `parent` element, and then came back over it. But it's not the case! The mouse never left, it just moved to the child element.
```online
In the example below the `<div id="child">` is inside the `<div id="parent">`. There are handlers on the parent that listen for `mouseover/out` events and output their details.
If you move the mouse from the parent to the child, you see two events: `mouseout [target: parent]` (left the parent) and `mouseover [target: child]` (came to the child, bubbled).
[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]`.
If there's some action upon leaving the element, e.g. animation runs, then such interpretation may bring unwanted side effects.
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)`.
To avoid it, we can check `relatedTarget` and, if the mouse is still inside the element, then ignore such event.
If we perform some actions on entering/leaving the element, then we'll get a lot of extra "false" runs. For simple stuff that may be unnoticeable. For complex things that may bring unwanted side-effects.
We can fix it by using `mouseenter/mouseleave` events instead.
Alternatively we can use other events: `mouseenter` и `mouseleave`, that we'll be covering now, as they don't have such problems.
## Events mouseenter and mouseleave
Events `mouseenter/mouseleave` are like `mouseover/mouseout`. They also trigger when the mouse pointer enters/leaves the element.
Events `mouseenter/mouseleave` are like `mouseover/mouseout`. They trigger when the mouse pointer enters/leaves the element.
But there are two differences:
But there are two important differences:
1. Transitions inside the element are not counted.
2. Events `mouseenter/mouseleave` do not bubble.
These events are intuitively very clear.
These events are extremely simple.
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.
When the pointer enters an element -- `mouseenter` triggers. The exact location of the pointer inside the element or its descendants doesn't matter.
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.
When the pointer leaves an element -- `mouseleave` triggers.
```online
This example is similar to the one above, but now the top element has `mouseenter/mouseleave` instead of `mouseover/mouseout`.
As you can see, the only generated events are the ones related to moving the pointer in and out of the top element. Nothing happens when the pointer goes to the child and back. Transitions between descendants are ignores
[codetabs height=340 src="mouseleave"]
```
## Event delegation
@ -125,16 +148,16 @@ Events `mouseenter/leave` are very simple and easy to use. But they do not bubbl
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.
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>` is able to 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.
Handlers for `mouseenter/leave` on `<table>` only trigger when the pointer enters/leaves the table as a whole. It's impossible to get any information about transitions inside it.
Not a problem -- let's use `mouseover/mouseout`.
So, let's use `mouseover/mouseout`.
A simple handler may look like this:
Let's start with handlers that highlight the element under mouse:
```js
// let's highlight cells under mouse
// let's highlight an element under the pointer
table.onmouseover = function(event) {
let target = event.target;
target.style.background = 'pink';
@ -147,41 +170,38 @@ table.onmouseout = function(event) {
```
```online
Here they are in action. As the mouse travels across the elements of this table, the current one is highlighted:
[codetabs height=480 src="mouseenter-mouseleave-delegation"]
```
These handlers work when going from any element to any inside the table.
In our case we'd like to handle transitions between table cells `<td>`: entering a cell and leaving it. Other transitions, such as inside the cell or outside of any cells, don't interest us. Let's filter them out.
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>`.
Here's what we can do:
One of solutions:
- Remember the currently highlighted `<td>` in a variable.
- Remember the currently highlighted `<td>` in a variable, let's call it `currentElem`.
- 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>`.
Here's an example of code that accounts for all possible situations:
```offline
The details are in the [full example](sandbox:mouseenter-mouseleave-delegation-2).
```
[js src="mouseenter-mouseleave-delegation-2/script.js"]
```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 matter. Only `<td>` as a whole is highlighted unlike the example before.
Try to move the cursor in and out of table cells and inside them. Fast or slow -- doesn't matter. Only `<td>` as a whole is highlighted, unlike the example before.
```
## Summary
We covered events `mouseover`, `mouseout`, `mousemove`, `mouseenter` and `mouseleave`.
Things that are good to note:
These things are good to note:
- A fast mouse move can make `mouseover, mousemove, mouseout` to skip intermediate elements.
- Events `mouseover/out` and `mouseenter/leave` have an additional target: `relatedTarget`. That's the element that we are coming from/to, complementary to `target`.
- Events `mouseover/out` trigger even when we go from the parent element to a child element. They assume that the mouse can be only over one element at one time -- the deepest one.
- Events `mouseenter/leave` do not bubble and do not trigger when the mouse goes to a child element. They only track whether the mouse comes inside and outside the element as a whole.
- A fast mouse move may skip intermediate elements.
- Events `mouseover/out` and `mouseenter/leave` have an additional property: `relatedTarget`. That's the element that we are coming from/to, complementary to `target`.
- Events `mouseover/out` trigger even when we go from the parent element to a child element. The browser assumes that the mouse can be only over one element at one time -- the deepest one.
- Events `mouseenter/leave` do not bubble and do not trigger when the mouse goes to a child element. They only track whether the mouse comes inside and outside the element as a whole. So, they are simpler than `mouseover/out`, but we can't implement delegation using them.

View file

@ -2,16 +2,21 @@
let currentElem = null;
table.onmouseover = function(event) {
if (currentElem) {
// before entering a new element, the mouse always leaves the previous one
// if we didn't leave <td> yet, then we're still inside it, so can ignore the event
return;
}
// if currentElem is set, we didn't leave the previous <td>,
// that's a mouseover inside it, ignore the event
if (currentElem) return;
let target = event.target.closest('td');
if (!target || !table.contains(target)) return;
// yeah we're inside <td> now
// we moved not into a <td> - ignore
if (!target) return;
// moved into <td>, but outside of our table (possible in case of nested tables)
// ignore
if (!table.contains(target)) return;
// hooray! we entered a new <td>
currentElem = target;
target.style.background = 'pink';
};
@ -19,20 +24,24 @@ table.onmouseover = function(event) {
table.onmouseout = function(event) {
// if we're outside of any <td> now, then ignore the event
// that's probably a move inside the table, but out of <td>,
// e.g. from <tr> to another <tr>
if (!currentElem) return;
// we're leaving the element -- where to? Maybe to a child element?
// we're leaving the element -- where to? Maybe to a descendant?
let relatedTarget = event.relatedTarget;
if (relatedTarget) { // possible: relatedTarget = null
while (relatedTarget) {
// go up the parent chain and check -- if we're still inside currentElem
// then that's an internal transition -- ignore it
if (relatedTarget == currentElem) return;
relatedTarget = relatedTarget.parentNode;
}
}
// we left the element. really.
// we left the <td>. really.
currentElem.style.background = '';
currentElem = null;
};

View file

@ -1,15 +1,15 @@
<!DOCTYPE HTML>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<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 id="parent" onmouseenter="mouselog(event)" onmouseleave="mouselog(event)">parent
<div id="child">child</div>
</div>
<textarea id="text"></textarea>

View file

@ -1,4 +1,5 @@
function log(event) {
text.value += event.type + ' [target: ' + event.target.id + ']\n';
function mouselog(event) {
let d = new Date();
text.value += `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} | ${event.type} [target: ${event.target.id}]\n`.replace(/(:|^)(\d\D)/, '$10$2');
text.scrollTop = text.scrollHeight;
}

View file

@ -1,21 +1,22 @@
#blue {
background: blue;
#parent {
background: #D8D8D8;
width: 160px;
height: 160px;
height: 120px;
position: relative;
}
#red {
background: red;
width: 70px;
height: 70px;
#child {
background: #CFCE95;
width: 50%;
height: 50%;
position: absolute;
left: 45px;
top: 45px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
#text {
textarea {
height: 140px;
width: 300px;
display: block;
height: 100px;
width: 400px;
}

View file

@ -8,8 +8,8 @@
<body>
<div class="blue" onmouseover="mouselog(event)" onmouseout="mouselog(event)">
<div class="red"></div>
<div id="parent" onmouseover="mouselog(event)" onmouseout="mouselog(event)">parent
<div id="child">child</div>
</div>
<textarea id="text"></textarea>

View file

@ -1,4 +1,5 @@
function mouselog(event) {
text.value += event.type + ' [target: ' + event.target.className + ']\n'
text.scrollTop = text.scrollHeight
let d = new Date();
text.value += `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} | ${event.type} [target: ${event.target.id}]\n`.replace(/(:|^)(\d\D)/, '$10$2');
text.scrollTop = text.scrollHeight;
}

View file

@ -1,21 +1,22 @@
.blue {
background: blue;
#parent {
background: #D8D8D8;
width: 160px;
height: 160px;
height: 120px;
position: relative;
}
.red {
background: red;
width: 100px;
height: 100px;
#child {
background: #CFCE95;
width: 50%;
height: 50%;
position: absolute;
left: 30px;
top: 30px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
textarea {
height: 100px;
width: 400px;
height: 140px;
width: 300px;
display: block;
}

View file

@ -8,13 +8,11 @@
<body>
<div id="green">
<div id="red">Test</div>
<div id="parent">parent
<div id="child">child</div>
</div>
<input onclick="clearText()" value="Clear" type="button">
<textarea id="text"></textarea>
<input onclick="clearText()" value="Clear" type="button">
<script src="script.js"></script>

View file

@ -1,24 +1,24 @@
green.onmouseover = green.onmouseout = green.onmousemove = handler;
parent.onmouseover = parent.onmouseout = parent.onmousemove = handler;
function handler(event) {
function handler(event) {
let type = event.type;
while (type < 11) type += ' ';
log(type + " target=" + event.target.id)
return false;
}
}
function clearText() {
function clearText() {
text.value = "";
lastMessage = "";
}
}
let lastMessageTime = 0;
let lastMessage = "";
let repeatCounter = 1;
let lastMessageTime = 0;
let lastMessage = "";
let repeatCounter = 1;
function log(message) {
function log(message) {
if (lastMessageTime == 0) lastMessageTime = new Date();
let time = new Date();
@ -44,4 +44,4 @@
lastMessageTime = time;
lastMessage = message;
}
}

View file

@ -1,23 +1,22 @@
#green {
height: 50px;
#parent {
background: #D8D8D8;
width: 160px;
background: green;
height: 120px;
position: relative;
}
#red {
height: 20px;
width: 110px;
background: red;
color: white;
font-weight: bold;
padding: 5px;
text-align: center;
margin: 20px;
#child {
background: #CFCE95;
width: 50%;
height: 50%;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
#text {
font-size: 12px;
height: 200px;
width: 360px;
textarea {
height: 140px;
width: 300px;
display: block;
}

View file

@ -136,7 +136,7 @@ And here we modify the contents after loading:
let newWindow = open('/', 'example', 'width=300,height=300')
newWindow.focus();
alert(newWin.location.href); // (*) about:blank, loading hasn't started yet
alert(newWindow.location.href); // (*) about:blank, loading hasn't started yet
newWindow.onload = function() {
let html = `<div style="font-size:30px">Welcome!</div>`;
@ -274,4 +274,3 @@ To close the popup: use `close()` call. Also the user may close them (just like
- Methods `focus()` and `blur()` allow to focus/unfocus a window. But they don't work all the time.
- Events `focus` and `blur` allow to track switching in and out of the window. But please note that a window may still be visible even in the background state, after `blur`.

Binary file not shown.