One can determine the element below the mouse cursor (i.e. the top-most hovered element) with the following techniques:
Listen for the mousemove event. The target is
event.target or
document.elementFromPoint(event.clientX, event.clientY).
This does not work when scrolling while not moving the mouse. Then, the mouse technically doesn’t move; thus, no mouse event will fire.
Unfortunately, both techniques from above are no longer applicable when listening for the scroll event. event.target will be whichever element is scrolled (or document). Also, the mouse cursor position is not exposed on the event object.
As described in this answer to “Determine which element the mouse pointer is on top of in Javascript”, one possible solution is querying the hovered element via the CSS :hover pseudo-class.
document.addEventListener('scroll', () => {
const hoverTarget = document.querySelector('.element:hover');
if (hoverTarget) {
hover(hoverTarget);
}
});
However, this is not usable because it is very inefficient and inaccurate. The scroll event is one of the rapidly firing events and needs to be slowed down when performing anything mildly costly (e.g. querying the DOM).
Also, the hovered element lags behind when scrolling. You can observe this on any kind of website with a lot of links: Hover over one of them and scroll to another link without moving the mouse. It updates only after a few milliseconds.
Is there any way, this can be implemented nicely and efficient? Basically, I want the inverse of mouseenter: Instead of knowing when the mouse enters and element, I want to know when an element intersects with the mouse (e.g. when the mouse is not moved but the element [i.e. when scrolling]).
One approach of tackling this is storing the mouse cursor location with the mousemove event and in the scroll event use document.elementFromPoint(x, y) to figure out the element that should be hovered.
Keep in mind that this is still pretty inefficient due to the scroll event being fired with such a high frequency. The event handler should be debounced to limit execution of the function to once per delay. David Walsh explains how to do this in JavaScript Debounce Function.
let hoveredElement;
let mouseX = 0, mouseY = 0;
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('mousemove', event => {
mouseX = event.clientX;
mouseY = event.clientY;
hover(event.target);
});
document.addEventListener('scroll', () => {
const hoverTarget = document.elementFromPoint(mouseX, mouseY);
if (hoverTarget) {
hover(hoverTarget);
}
});
});
function hover(targetElement) {
// If the target and stored element are the same, return early
// because setting it again is unnecessary.
if (hoveredElement === targetElement) {
return;
}
// On first run, `hoveredElement` is undefined.
if (hoveredElement) {
hoveredElement.classList.remove('hover');
}
hoveredElement = targetElement;
hoveredElement.classList.add('hover');
}
.element {
height: 200px;
border: 2px solid tomato;
}
.element.hover {
background-color: lavender;
}
<div class="container">
<div class="element element-1">1</div>
<div class="element element-2">2</div>
<div class="element element-3">3</div>
<div class="element element-4">4</div>
<div class="element element-5">5</div>
</div>
Currently, the solution will hover the top-most element under the mouse both when moving the mouse and when scrolling. It might be more suitable for your needs to attach the mousemove listener to a set of specific elements and then always hover event.currentTarget (i.e. the element the event listener was attached to). As for the scroll part, you can use hoverTarget.closest to find the suitable element up in the DOM tree.
Related
I have an outer div which catches onWheel mouse events. In the handler I would like to know the mouse pointer coordinates relative to the outer div. For example:
constructor() {
this.handleWheel = this.handleWheel.bind(this);
}
handleWheel(event) {
// how do I get mouse position relative to the outer div here?
}
render() {
return (
<div onWheel={this.handleWheel}>
...
<img src="..." />
...
</div>
)
}
When I move mouse pointer over the image and scroll mousewheel, event.target is set to <img> and event.currentTarget is set to <div>. I can also get the mouse position relative to event.target (event.nativeEvent.offsetX/Y), but that doesn't help me because I don't know the position of <img> relative to <div>.
In other words, I'm stuck... How do I get the mouse position relative to the element that has a handler attached?
I have found a solution which works and am posting it here if it helps someone. I'm not sure though if it is the best one, so I would appreciate some comments (and possibly better solutions).
handleWheel(event) {
let currentTargetRect = event.currentTarget.getBoundingClientRect();
const event_offsetX = event.pageX - currentTargetRect.left,
event_offsetY = event.pageY - currentTargetRect.top;
//...
}
I have big parent div and narrow child div in the center of it.
When I scroll mouse wheel over child, it scrolls as it should,
but I want to be able to scroll that child element even if mouse is not on it, but somewhere on parent, and mouse wheel is scrolled.
I'm passing event from parent to child like this:
parent.addEventListener('mousewheel', parentMouseWheel);
function parentMouseWheel(e) {
if (e.target.id === 'parentId') { //to prevent double events when mouse is over child
if (e.wheelDelta > 0) child.scrollTop = child.scrollTop - 120;
else child.scrollTop = child.scrollTop + 120;
}
}
this works, but the problem is, there is no smooth scrolling like there is when I scroll mouse wheel over child (probably because of forcing/passing like this).
So, is there any other way to pass mouse wheel event to child, that will also trigger smooth scrolling?
I'm styling my extension options page, if that means anything...
I have a div with several img-Objects with position:absolute like so:
<div>
<img>
<img>
<img>
<img>
</div>
Now when one of the image's mousedown handler is called, the event will only bubble down, ignoring the other images, even when they might be behind each other.
$('img').mousedown((event) -> if(something) event.stopPropagation());
$('div').mousedown(-> alert('event came through'));
I tried to nest them to work around this issue, but that didn't work either:
<div>
<img>
<div>
<img>
<div>
<img>
...
</div>
</div>
</div>
is there any way I can get this to work without manually running a hit-test on every image?
is there any way I can get this to work without manually running a hit-test on every image?
I believe it's correct that you have to run a hit-test yourself. mousedown only occurs on the front-most element under the mouse pointer, not all elements at those (x, y) coordinates.
In practice, this isn't so hard. Here's a working example: http://jsfiddle.net/TrevorBurnham/GBuZz/
In that example, mousedown events are captured on the container element and handled like so:
$('#container').on 'mousedown', (e) ->
{pageX, pageY} = e
$(#).children().each (i) ->
{top, left} = $(#).offset()
if top <= pageY <= top + $(#).outerHeight() &&
left <= pageX <= left + $(#).outerWidth()
console.log 'collision with box ' + i
I'm making extensive use of the HTML5 native drag & drop, and it's almost entirely behaving itself, with one small exception.
I'm trying to highlight my dropzones when anything is dragged over the page. I originally tried to accomplish this by putting jQuery listeners on the document body, like this:
$("body").live('dragover',function(event){lightdz(event)});
$("body").live('dragexit dragleave drop',function(event){dimdz(event)});
with lightdz() and dimdz() changing the background-color style property of all dropzones on the page to make them stand out. This didn't work. Whenever a dragged object entered a child element on the page (like a div container), the listener would flag this up as a dragleave event and dim the dropzones.
I got around this by applying the listener to all visible elements on the page, instead of just the body. There was occasionally a slight visible flickering on the dropzones when it crossed the boundary between one element and another, but it looked fine.
Anyway, now I've changed lightdz() and dimdz() so that they apply a quick jQuery fadeTo() animation to all non-dropzones. This looks awesome when it works, and makes it very apparent to the user what they can and can't drop things on. The trouble is that when it passes between element boundaries, it applies the fade animation. This is a lot more apparent than the occasional flicker of background-color, especially since if the object is dragged over multiple boundaries very quickly, it will queue the animations and have the page fade in and out repeatedly.
Even if I don't bother with the fadeTo() animation, and just change the opacity, it's a lot more visible than the background-color flicker, because the entire page changes rather than just the dropzone elements.
Is there any way to reference the entire page as a single element for purposes of dragover and dragleave events? Failing that, is there any way to detect a drop that takes place outside of the browser window? If I skip the dragleave event, it looks fine, but if any object is dragged over the browser window and then dropped outside it, the whole page stays faded.
I'm genuinely embarrassed by how easy this one was.
$("*:visible").live('dragenter dragover',function(event){lightdz(event)});
$("#page").live('dragleave dragexit',function(event)
{
if(event.pageX == "0")
dimdz(event);
});
$("*:visible").live('drop',function(event){dimdz(event)});
#page is a page-wide container. If the dragleave event takes the dragged object outside of the browser window, event.pageX will have a value of 0. If it happens anywhere else, it'll have a non-zero value.
I may be getting overly complex here but I would do something like this:
var draggingFile = false;
var event2;
//elements with the class hotspots are OK
var hotspots = $(".hotspots");
//Handlers on the body for drag start & stop
$("body").live("dragover", function(event){ draggingFile = true; event2 = event; });
$("body").live("dragexit dragleave drop", function(event){ draggingFile = false; event2 = event; });
//Function checks to see if file is being dragged over an OK hotspot regardless of other elements infront
var isTargetOK = function(x, y){
hotspots.each(function(i, el){
el2 = $(el);
var pos = el2.offset();
if(x => pos.left && x <= pos.left+el2.width() && y => pos.top && y <= post.top+el2.height()){
return true;
}
});
return false;
};
//Mousemove handler on body
$("body").mousemove(function(e){
//if user is dragging a file
if(draggingFile){
//Check to see if this is an OK element with mouse X & Y
if(isOKTarget(e.pageX, e.pageY)){
//Light em' up!
lightdz(event2);
} else { /* Fade em' :( */ dimdz(event2); }
} else {
dimdz(); //Having no parematers means just makes sure hotspots are off
}
});
BTW that's probably not going to work straight off the bat, so you'll have to tweak it a bit to work with your code.
I tried the accepted solution here, but ended up using setTimeout to overcome the issue. I was having a ton of trouble with the page-wide container blocking the drop element if it was floated on top, and still causing the problem if it was the drop element.
<body style="border: 1px solid black;">
<div id="d0" style="border: 1px solid black;"> </div>
<div id="d1" style="border: 1px solid black; display: none; background-color: red;">-> drop here <-</div>
<div id="d2" style="border: 1px solid black;"> </div>
<div style="float: left;">other element</div>
<div style="float: left;"> - </div>
<div style="float: left;">another element</div>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
</body>
<script type="text/javascript">
var resetTimer;
var f = function(e)
{
if (e.type == "dragover")
{
e.stopPropagation();
e.preventDefault();
if (resetTimer)
{
clearTimeout(resetTimer);
}
document.getElementById('d1').style.display = '';
}
else
{
var f = function()
{
document.getElementById('d1').style.display = 'none';
};
resetTimer = window.setTimeout(f, 25);
}
};
document.body.addEventListener("dragover", f, true);
document.body.addEventListener("dragleave", f, true);
document.getElementById('d1').addEventListener("drop", function(e){ f(); alert('dropped'); }, false);
</script>
If you were to just call f(); instead of window.setTimeout(f, 250);, you'll see some nasty flickering of the element showing and hiding.
http://jsfiddle.net/guYWx/
When the mouse starts hovering over an element because of scrolling (either by wheel, or by keyboard scrolling), it does not trigger a mouseover event on the elements it is hovering (Chrome 6 on OSX). What would be an elegant way to trigger the mouseover event for the correct elements when scrolling?
Honestly, this is gonna be a pain. You'll have to
determine the size and position of every element that should get a mouseover handler.
add a scroll listener to the window.
In the handler, get the mouse cursor position and pageOffset.
Find out which element(s) the cursor is in.
manually call the actual mouseover handler
(Find out which elements the cursor has left, if you want some mouseout behaviour too)
You may need to re-calculate the elements' positions and sizes if they are dynamic. (move 1. beneath 3.)
While this should work fine with block-level elements, I have absolutely no idea on a solution for inline elements.
This is much more simple in the modern day web using document.elementsFromPoint:
Add a scroll listener to the window.
In the handler, call document.elementsFromPoint.
Manually call the actual pointerover handler for those elements. Keep track of these elements.
(optionally) Manually call the actual pointermove handler for those elements.
Check the list of elements from the previous time around. Manually call the actual pointerleave handler for elements no longer being hovered.
Here's some psuedo-code:
let prevHoveredEls = [];
document.addEventListener("scroll", (e) => {
let hoveredEls = document.elementsFromPoint(e.pageX, e.pageY);
hoveredEls = hoveredEls.filter(
(el) => el.classList.contains("elements-cared-about")
);
const notHoveredEls = prevHoveredEls.filter(
(el) => !prevHoveredEls.includes(el)
);
hoveredEls.forEach((el) => {
const bcr = el.getBoundingClientRect();
el.handlePointerEnter({
layerX: e.pageX - bcr.left,
layerY: e.pageY - bcr.top,
});
});
notHoveredEls.forEach((el) => {
const bcr = el.getBoundingClientRect();
el.handlePointerLeave({
layerX: e.pageX - bcr.left,
layerY: e.pageY - bcr.top,
});
});
prevHoveredEls = hoveredEls;
});
Try some hack like myDiv.style.opacity = 1+Math.random(); on scroll ;)