Mouse coordinates relative to currentTarget in React in event handler - javascript

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;
//...
}

Related

How do you add an annotation to a website without altering the layout?

I'm working on a hobby project similar to markup.io where you can load in any website and annotate it. I can't figure out how to add an annotation that behaves like it does in markup.io:
Doesn't interrupt the styling or layout of the website you are annotating
The annotation keeps the correct position when scrolling and resizing the window
From what I can see they place an absolute positioned div inside the element that you clicked on. From my understanding by reading the docs that div would position itself based on the closest positioned ancestor. How would you calculate the correct top and left values to position the annotation to where the user clicked? Is there a better way to do this?
I'm using React if that matters.
Things that I have tried:
Append the following bit of html to the element that was clicked:
<div style="width:0px; height:0px; position:relative;">
<div style="width:50px;height:50px;position:absolute; ">this is the annotation </div>
</div>
Problem: This would mess with the page layout because of the relative positioned div that is not ignored by the document flow.
Create fixed overlay over the entire page. Get the css selector of the clicked element. Draw annotation on the fixed overlay at the x,y position of the element.
Problem: Whenever the user would scroll or resize the window the annotation would need to be redrawn at the new position of the element. I used getBoundintClientRect to get the new position and this would cause a reflow and caused the whole website to have severe perfomance issues when dealing with 100+ annotations.
Hopefully someone can point me in the right direction!
The general idea is as follows:
Find the parent of the element that you clicked on
Check if they are positioned (anything other than static)
If it is static search for the closest element that is positioned.
Set the new badge/annotation top and left position to that of the mouse minus the top and left of the element that you're going to append it to (in this case called parent).
Also account for the width and height by subtracting half of each to perfectly center your annotation.
// In my case I put the webpage in an Iframe. If this is your own page
// you can just use document.
iframe.contentWindow.document.addEventListener(
'click',
(e: MouseEvent) => {
// step 1: find the parent.
let parent = e.target.parentElement;
let computedStyle = window.getComputedStyle(parent);
// step 2 & 3: Look up the first positioned element and make this the
// the element that you're going to append your badge/annotation to.
while (
computedStyle.position === 'static' &&
parent.parentElement !== null
) {
parent = parent.parentElement;
computedStyle = window.getComputedStyle(parent);
}
// style the annotation the way you want to
const badge = document.createElement('div');
const { top, left } = parent.getBoundingClientRect();
badge.style.position = 'absolute';
// step 4 and 5 get the mouse position through e.clientX and Y and
// subtract the appropriate value like below to place it exactly at the mouse position
badge.style.top = `${e.clientY - top - 5}px`;
badge.style.left = `${e.clientX - left - 5}px`;
badge.style.backgroundColor = 'red';
badge.style.width = '10px';
badge.style.height = '10px';
badge.style.borderRadius = '50%';
badge.style.zIndex = '9999';
parent.appendChild(badge);
}
);

How to get mouse X position of a mouse event of a div (and not of its children) with React 17?

I have a simple snippet where I attach a onMouseMove listener to a given div:
<div onMouseMove={handleMouseMove}>
<div>A</div>
<div>B</div>
<div>C</div>
</div>
I want to know the X position of the mouse pointer within the parent div where I attached the mouse listener to. However, the event seems to only contain references to the children divs (as target), and the root (as currentTarget).
The event's nativeEvent property also is not helping it seems.
I'm on React 17. I think this is making it harder as previously the currentTarget would point to that div where I placed the listener to, but on React 17 these listeners to the root.
How can I get the coordinates within the div I attached the listener to?
Thanks!
This works as expected in react version 17.0.2
function Component() {
const onMouseMove = (e) => {
let rect = e.currentTarget.getBoundingClientRect();
let x = e.clientX - rect.left;
let y = e.clientY - rect.top;
console.log(x, y);
};
return (
<div onMouseMove={onMouseMove}>
<div>A</div>
<div>B</div>
<div>C</div>
</div>
);
}
Based on this answer and a comment on it, about using a currentTarget

Hover the element below the mouse cursor when scrolling

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.

mousedown propagation on siblings

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​

How can I make page scrolling trigger mouseover events?

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 ;)

Categories

Resources