This commit is contained in:
Ilya Kantor 2020-08-23 19:03:29 +03:00
parent 718d9df37b
commit b24b05d3ca
1436 changed files with 131 additions and 106126 deletions

View file

@ -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. When we're going out of the element, and `over` was executed, run `out`.
But how to measure the speed?
The first idea can be: run a 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 for mouse events, like `mousemove`, and take coordinates from the event object.
So let's set a handler on `mousemove` to track coordinates and remember them. And then compare them, once per `100ms`.
P.S. Please note: the solution tests use `dispatchEvent` to see if the tooltip works right.

View file

@ -0,0 +1,104 @@
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) {
// if there was a stop over the element
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);
}
}

View file

@ -0,0 +1,52 @@
// 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) {
console.error(console.log);
console.log("OVER", event);
/* ... */
}
onMouseOut(event) {
/* ... */
}
onMouseMove(event) {
/* ... */
}
destroy() {
/* your code to "disable" the functionality, remove all handlers */
/* it's needed for the tests to work */
}
}

View file

@ -0,0 +1,24 @@
<link rel="stylesheet" href="style.css">
<script src="hoverIntent.js"></script>
<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>
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;
}
});
</script>

View file

@ -0,0 +1,38 @@
.hours {
color: red;
}
body {
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 sans-serif;
color: #333;
background: #fff;
z-index: 100000;
box-shadow: 3px 3px 3px rgba(0, 0, 0, .3);
}

View file

@ -0,0 +1,96 @@
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 -> when the pointer just arrived, no tooltip", function() {
mouse('mouseover', 10, 10);
assert.isFalse(isOver);
});
it("mouseover -> after a delay, the tooltip shows up", function() {
mouse('mouseover', 10, 10);
this.clock.tick(100);
assert.isTrue(isOver);
});
it("mouseover -> followed by fast mouseout leads doesn't show 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);
});
});

View file

@ -0,0 +1,49 @@
importance: 5
type: html
---
# "Smart" tooltip
Write a function that shows a tooltip over an element only if the visitor moves the mouse *to it*, but not *through it*.
In other words, if the visitor moves the mouse to the element and stops there -- show the tooltip. And if they just moved the mouse through, 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.
Its `options`:
- `elem` -- element to track.
- `over` -- a function to call if the mouse came to the element: that is, it moves slowly or stopped over it.
- `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 moves between the clock subelements.

View file

@ -0,0 +1,3 @@
# An Introduction to JavaScript
Let's see what's so special about JavaScript, what we can achieve with it, and which other technologies play well with it.

3
1-js/02-b/index.md Normal file
View file

@ -0,0 +1,3 @@
# An introduction
About the JavaScript language and the environment to develop with it.