I wanted to click on an element, and then have another element respond to pointer move events from all elements in the html document. However it appears that onpointermove events are only captured when the pointer is kept depressed. For example in the following simplified code the onpointermove handler fires when you mouse over the div, or when you click down on the div, move around the screen and then click up anywhere. But the captured is not maintained if you release the mouse button and click up anywhere.
This behaviour kind of makes sense in case you fail to invoke releasePointerCapture but I can't see this mentioned in the MDN docs yet and before I contemplate contributing I was wondering if anyone knew if this was documented somewhere else / correct my interpretation of it.
let captured = false
function toggle_capture (e) {
if (captured) capture_div.releasePointerCapture(e.pointerId)
else capture_div.setPointerCapture(e.pointerId)
captured = !captured
}
const capture_div = document.getElementById("capture_div")
capture_div.onpointerdown = toggle_capture
capture_div.onpointermove = (e) => {
capture_div.innerText = `captured: ${captured} x: ${Math.round(e.clientX)}`
}
div
{
background-color: lightgray;
width: 200px;
height: 60px;
margin: 10px;
}
<div id="capture_div">Mouse over, click or click and hold,
Then move pointer around the screen</div>
<div>Other divs</div>
<div>Other divs</div>
This is correct. The MDN documentation has been updated and the documentation of the spec contains this information.
Related
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.
When hovering over an element and then refreshing the page (without moving the mouse):
Chrome does not fire the mouseenter event on page load
Firefox does fire the mouseenter event on page load
Below is an example snippet. To reproduce the issue, hover over the div and then refresh the page. In Chrome, the div does not contain "mouseenter". In Firefox, it does.
Note that this does not work in the Stacksnippets environment since you need to click "run snippet" first. JSFiddle: https://jsfiddle.net/9fu6cx5d/7/
let div = document.getElementById('my-div');
div.addEventListener('mouseenter', function () {
div.innerHTML = 'mouseenter';
});
#my-div {
width: 150px;
height: 150px;
background-color: #aaaaaa;
}
<div id="my-div">
</div>
Which browser has the correct behaviour? How can I work around the difference in behaviour or at least make them both behave the same?
Chrome version: 59.0.3071.115 (Official Build) (64-bit)
Firefox version: 54.0 (64-bit)
As pointed out in the comments, Chrome's behavior is the correct one according to the specs. Below is an idea on how to work around the difference.
You can make sure you get the value right by checking whether the mouse is inside the bounds of the div on document load. Unfortunately there is no way in JS to check the mouse position without firing events, so you will have to resort to some hack involving CSS hover rules and checking against them on $(document).ready.
To quote this hilarious answer:
Overlay your page with a div that covers the whole document. Inside
that, create (say) 2,000 x 2,000 elements (so that the :hover
pseudo-class will work in IE 6, see), each 1 pixel in size. Create a
CSS :hover rule for those elements that changes a property (let's
say font-family). In your load handler, cycle through each of the 4
million elements, checking currentStyle / getComputedStyle() until
you find the one with the hover font. Extrapolate back from this
element to get the co-ordinates within the document.
N.B. DON'T DO THIS.
While you definitely shouldn't do this, the general idea of using non-effective hover styles for the sake of checking if an element is hovered without needing JS events is a good one if you just need to work around browser quirks. I'm using font-weight in the example below, but you can change it to whatever works for you.
The css
#my-div:hover {font-weight:700;}
The js
// Pseudocode!
var mouseIsInside = false,
div = $('#my-div');
$(document).ready(function(){
if (div.css('font-weight') === 700) {
mouseIsInside = true;
}
doStuffIfMouseInside();
});
div.on('mouseenter', function(){
mouseIsInside = true;
doStuffIfMouseInside();
})
function doStuffIfMouseInside() {
if (mouseIsInside) {
...
}
}
If you add (function(){})(); around your code it seems to work in both browsers.
It seems that firefox might be firing events before the dom is available causing problems with mousein/out events.
See: https://jsfiddle.net/9fu6cx5d/8/
I am currently switching the menu of my site from pure JavaScript to jQuery. My menu has a rollout / rollin effect.
The menu has an outer wrapper which has an onmouseout event set. If this fires, the relatedTarget is checked whether it's a child of the outer wrapper. If not, the rollin shall happen.
What happens right now is, that if the mouse is moved from the menu's inner wrapper (this is to center the actual menu) to the menu's outer wrapper, the onmouseout fires. There seems to be a tiny part which doesn't belong to the menuOuterWrapper.
The site isn't online right now, so I've prepared a Fiddle here. You will see the problem if you move your mouse from the gray area above the handle to the left or right dark area. The menu will roll in and then immediately out again. The rollin shall only occur when the mouse is moved out of the outer wrapper, i.e. under the dark gray area (or the light gray handle area). To see the dark gray areas, you might have to increase the width of the result block. [EDIT: I reduced the width of inner to 600px, so the dark side areas should be visible by default now.]
SO tells me that I shall include code when linking to JSFiddle. I don't want to break the rules but I'll be honest: I'm clueless where the problem comes from. My best idea is that I made a mistake in my isChildOf implementation, so I'll give you this:
jQuery.fn.isChildOf = function (parentId) {
if ($(this).parents("#" + parentId).length > 0) {
return true;
} else {
return false;
}
};
$('#outer').on('mouseout', function(event) {
if (!$(event.relatedTarget).isChildOf("outer")) {
mouseIsOverMenu = false;
menu_rollin();
}
});
Although this is a minimal example, I did nearly the same with pure JS, where it worked fine. So I guess it's something in the jQuery part. Since these are my first steps with jQuery, it is even more likely.
Every help you can provide is highly appreciated :)
[UPDATE]
I got it working now. The problem was that I didn't check for the relatedTarget to be "outer" itself. So when the mouse leaves the content div and enters the outer div, mouseout fires and of course, outer is no child of itself. So I amended it to
$('#outer').on('mouseout', function(event) {
if (!(event.relatedTarget.id == "outer") &&
!$(event.relatedTarget).isChildOf("outer")) {
mouseIsOverMenu = false;
menu_rollin();
}
});
and that fixed the problem.
if i understood your question right.
This might help
$('#inner').on('mouseover', function() {
mouseIsOverMenu = true;
setTimeout(menu_rollout, 500);
});
$('#inner').on('mouseout', function(event) {
if (!$(event.relatedTarget).isChildOf("outer")) {
mouseIsOverMenu = false;
menu_rollin();
}
});
What i did is i have changed the id of #outer to #inner.
This is a dirty hack, but your problem seems to be with the mouseout function applying too frequently, and what functionality you really want is capturing the mouse leaving the bottom of the menu/content.
Here's some code that will do just that.
$('#outer').on('mouseout', function(event) {
if(event.clientY >= document.getElementById('outer').offsetHeight){
mouseIsOverMenu = false;
menu_rollin();
}
});
here's the associated jsFiddle
I have a page with a section to sketch a drawing in. But the touchmove events, at least the vertical ones, are also scrolling the page (which degrades the sketching experience) when using it on a mobile browser. Is there a way to either a) disable & re-enable the scrolling of the page (so I can turn it off when each line is started, but turn it back on after each is done), or b) disable the default handling of touchmove events (and presumably the scrolling) that go to the canvas the sketch is drawn in (I can't just disable them completely, as the sketching uses them)?
I've used jquery-mobile vmouse handlers for the sketch, if that makes a difference.
Update: On an iPhone, if I select the canvas to be sketched in, or just hold my finger for a bit before drawing, the page doesn't scroll, and not because of anything I coded in the page.
Set the touch-action CSS property to none, which works even with passive event listeners:
touch-action: none;
Applying this property to an element will not trigger the default (scroll) behavior when the event is originating from that element.
Note: As pointed out in the comments by #nevf, this solution may no longer work (at least in Chrome) due to performance changes. The recommendation is to use touch-action which is also suggested by #JohnWeisz's answer.
Similar to the answer given by #Llepwryd, I used a combination of ontouchstart and ontouchmove to prevent scrolling when it is on a certain element.
Taken as-is from a project of mine:
window.blockMenuHeaderScroll = false;
$(window).on('touchstart', function(e)
{
if ($(e.target).closest('#mobileMenuHeader').length == 1)
{
blockMenuHeaderScroll = true;
}
});
$(window).on('touchend', function()
{
blockMenuHeaderScroll = false;
});
$(window).on('touchmove', function(e)
{
if (blockMenuHeaderScroll)
{
e.preventDefault();
}
});
Essentially, what I am doing is listening on the touch start to see whether it begins on an element that is a child of another using jQuery .closest and allowing that to turn on/off the touch movement doing scrolling. The e.target refers to the element that the touch start begins with.
You want to prevent the default on the touch move event however you also need to clear your flag for this at the end of the touch event otherwise no touch scroll events will work.
This can be accomplished without jQuery however for my usage, I already had jQuery and didn't need to code something up to find whether the element has a particular parent.
Tested in Chrome on Android and an iPod Touch as of 2013-06-18
There is a little "hack" on CSS that also allows you to disable scrolling:
.lock-screen {
height: 100%;
overflow: hidden;
width: 100%;
position: fixed;
}
Adding that class to the body will prevent scrolling.
document.addEventListener('touchstart', function(e) {e.preventDefault()}, false);
document.addEventListener('touchmove', function(e) {e.preventDefault()}, false);
This should prevent scrolling, but it will also break other touch events unless you define a custom way to handle them.
The ultimate solution would be setting overflow: hidden; on document.documentElement like so:
/* element is an HTML element You want catch the touch */
element.addEventListener('touchstart', function(e) {
document.documentElement.style.overflow = 'hidden';
});
document.addEventListener('touchend', function(e) {
document.documentElement.style.overflow = 'auto';
});
By setting overflow: hidden on start of touch it makes everything exceeding window hidden thus removing availability to scroll anything (no content to scroll).
After touchend the lock can be freed by setting overflow to auto (the default value).
It is better to append this to <html> because <body> may be used to do some styling, plus it can make children behave unexpectedly.
EDIT:
About touch-action: none; - Safari doesn't support it according to MDN.
try overflow hidden on the thing you don't want to scroll while touch event is happening. e.g set overflow hidden on Start and set it back to auto on end.
Did you try it ? I'd be interested to know if this would work.
document.addEventListener('ontouchstart', function(e) {
document.body.style.overflow = "hidden";
}, false);
document.addEventListener('ontouchmove', function(e) {
document.body.style.overflow = "auto";
}, false);
I found that ev.stopPropagation(); worked for me.
To my surprise, the "preventDefault()" method is working for me on latest Google Chrome (version 85) on iOS 13.7. It also works on Safari on the same device and also working on my Android 8.0 tablet.
I am currently implemented it for 2D view on my site here:
https://papercraft-maker.com
this worked for me on iphone
$(".owl-carousel").on('touchstart', function (e) {
e.preventDefault();
});
the modern way (2022) of doing this is using pointer events as outlined here in the mozilla docs: https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events
Pointer events build on touchstart and other touch events and actually stop scroll events by default along with other improvements.
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/