I was playing with drag n drop in full forms (so no instant upload). I though small part was gonna be highlighting a certain fieldset when hovered over with a file. Enter dragover and dragenter events (and dragleave etc).
Turns out it's not such a small part. The Fiddle: http://jsfiddle.net/rudiedirkx/epp74/
Try it out: drag over a fieldset and move around a bit. The first over triggers the fieldset's dragenter event (fieldset is yellow). The moving around after that (within the same fieldset) triggers dragenters and dragleaves (fieldset no more yellow), which is bad.
Which is why I wanted to make what IE made for mouseover and mouseout a long time ago: mouseenter and mouseleave (they trigger just once). For drag events, the exact same thing applies: they should trigger only once in the exact same way. JS libraries spoof these IE events by using Event.fromElement and Event.toElement (and compare them against the event owner element). (See jQuery or Mootools source for specifics.)
To make the same for drag events, I need the same fromElement and toElement. You can see in the Fiddle, I try, but I can't find them.
Anybody know where they are? Why they're not available?
I'm using Chrome primarily, which doesn't have a fromElement in the dragenter event, but does have a toElement in the dragleave event. In Firefox it's slightly worse (but more logical): both are empty.
Any and all ideas are so very welcome.
edit
After a little more debugging I've found out that Chrome's toElement in dragleave isn't always correct. It's never 'bigger' thanthis, but sometimes it should be: when I leave the fieldset (this) to its parent form (toElement). When I do that, both this and toElement are the fieldset (which is incorrect, right?).
edit Solution:
I ended up with something like this: http://jsfiddle.net/rudiedirkx/Lwd3md71/ which ignores elements in the event, and uses the event coordinates to find the element under the mouse. To make it trigger max once per animation frame, it uses requestAnimationframe, which results into 31-59 fps.
Firefox provides the relatedTarget event property, but Chrome and Safari don't. Sadly, this issue has been open for a couple years as this Chrome bug and this Webkit bug.
Edit: The issue has been fixed in Chrome.
There is a way of faking the relatedTarget for a "dragleave" event, which is to set a variable from the accompanying "dragenter" event -- since dragleave is always preceded by dragenter, a variable set in the latter will be available to the former:
var relatedTarget = null;
document.addEventListener('dragenter', function(e)
{
relatedTarget = e.target;
}, false);
document.addEventListener('dragleave', function(e)
{
console.log('target = ' + e.target + ' relatedTarget = ' + relatedTarget);
}, false);
It won't work the other way round, but you don't really need dragenter for anything else if you use it this way -- i.e. the dragleave alone is enough to tell you when the mouse is moving into, or entirely out of, a particular element.
Related
Still trying to combine drag&drop and moving an element with the mouse I'm struggling with dragenter and dragleave not being called for a parent element when I'm moving the child element. This seems quite natural because the element always hovers over the parent element and prevents dragenter being called for the parent.
I tried to call stopPropagation() and preventDefault() in dragenter, dragleave, dragover, dragstart and drag events for the child element but with no real effect.
Another question seems to address a similar issue but with no real solution if I get this correctly.
Maybe it's just too dark down here in the rabbit hole to see the obvious - how do I prevent the dragged item from avoiding its parents dragenter/dragleave events to be called?
On another level I just want to know if the element has been dropped outside the parent element (to then return it to it's original position). Is there an easier approach?
Here is my current code - In the current state the element is being moved with the mouse and thus preventing dragenter or dragleave being called.
Deactivating the actual movement of draggable_element will make dragenter and dragleave be called when leaving the parent/target area but also when the dragged element is being entered (what I somehow can't avoid).
Found it - instead fiddling with the drag/drop events you just have to deactivate pointer-events for the dragged item to avoid unwanted dragenter/dragleave events for the parent and turn it back on again afterwards (it has to be activated by default to enable dragging in the first place).
draggable_element.addEventListener("dragstart", (e) => {
e.srcElement.style.pointerEvents = "none";
... // rest of code
});
elem.addEventListener("dragend", (e) => {
e.srcElement.style.pointerEvents = "auto";
... // rest of code
});
Here is a working example: https://jsfiddle.net/03a9s4ur/10/
I have a (deeply) nested structure of <div>s. With Javascript, I'd like to keep track of which element the mouse is currently over (i.e., 'has focus'), where more deeply nested elements have priority:
I've tried combinations of mouseover, mouseout, mouseenter, mouseleave and mousemove, but there seems to be no simple solution that gives me the expected behavior. I am able to receive mouse-events on the right divs, but often these events are subsequently received by higher level divs, which would then undeservedly take focus.
For example, in the transition above from a to c, the event might then be received by b last, unintentionally giving focus to b rather than c. Or the transition from c to b might not be registered at all for some reason.
I don't understand the underlying mechanism of mouse-event propagation well enough to come up with a reliable solution. It seems like it should be such a simple thing, but I can't figure it out.
I've been able to get it to work as follows: when a div receives a mouseenter/over event, flag it and search the entire DOM subtree. If any other flags are found deeper down, relinquish focus. But this is rather crude, and I can't help but think there must be an easier way.
Edit: Solution
The use of mouseover and mouseout, combined with a call to stopPropagation() seems to work very well. Here is a JSFiddle to show the working solution.
You can use the event stopPropagation() method:
https://developer.mozilla.org/en-US/docs/Web/API/event.stopPropagation
If using jQuery, try calling stopPropagation() on the event passed as the first parameter
http://api.jquery.com/event.stoppropagation/
$( "p" ).on('mouseover', function( event ) {
event.stopPropagation();
// Do something
});
Also, you can check which element is the target, as in event.target.
For non-IE browsers & IE >= 9 use,
evt.stopPropagation();
For Legacy IE browsers(IE<9) browsers use,
evt.cancelBubble = true;
This will prevent bubbling of events to parent elements.
Consider the following Web page:
<html>
<body onscroll="alert('body scroll event')">
<div style='width:200px;height:200px;overflow:auto' onscroll="alert('div scroll event')">
<div style='height:400px'>
</div>
</div>
</body>
</html>
This html creates a div with a scrollbar. If you move the scrollbar, the onscroll event on the div element is triggered. However, the onscroll event on the body is NOT fired. This is expected, since the W3C states that element onscroll events do not "bubble".
However, I'm developing a client-side web framework that needs to know any time a scroll bar on ANY element of the page is scrolled. This would be easy to do if onscroll bubbled, but unfortunately it does not. Is there any other way to detect onscroll events across an entire page? (Right now I'm focusing mainly on Webkit, so a Webkit-specific solution would be fine...)
Here are some things I've tried:
Capturing DOMAttrModified (doesn't seem to fire for moving scrollbars.)
Using DOM Observers (also don't seem to fire for scrollbars)
Changing the onscroll event type to bubble (seems to not be possible)
It seems the ONLY way to capture onscroll events globally is to attach an onscroll event to EVERY element that may scroll, which is very ugly and is going to hurt the performance of my framework.
Anyone know a better way?
The simplest way to detect all scroll events in modern browser would be using 'capturing' rather than 'bubbling' when attaching the event:
window.addEventListener('scroll', function(){ code goes here }, true)
Unfortunately as I am aware there is no equivalent in older browser such as <= IE8
I had this same issue.
The easiest way of course is to use jQuery. Be aware that this method could potentially slow down your page significantly. Also it will not account for any new elements that are added after the event is bound.
$("*").scroll(function(e) {
// Handle scroll event
});
In vanilla JavaScript, you can set the useCapture boolean to true on your addEventListener call, and it will fire on all elements, including those added dynamically.
document.addEventListener('scroll', function(e) {
// Handle scroll event
}, true);
Note though that this will fire before the scroll event actually happens. As I understand it, there's two phases events go through. The capture phase happens first, and starts from the page root (ownerDocument?) and traverses down to the element where the event happened. After this comes the bubbling phase, which traverses from the element back up to the root.
Some quick testing too showed that this may be how jQuery handles it (for tracking scrolls on all page elements at least), but I'm not 100% sure.
Here's a JSFiddle showing the vanilla JavaScript method http://jsfiddle.net/0qpq8pcf/
*...crickets chirping... *
OK, I guess this question isn't going to get any stackoverflow love, so I might as well answer my own question as to the best solution I've found so far, in case another user stumbles across this question:
The best solution I've come up with is to capture "onmousedown" and "onkeydown" for the BODY element: These events bubble, and so if a user tries to move a scrollbar on the page these global functions will fire as a by-product. Then, in these functions, simply look up event.target and attach a temporary "onscroll" event to those objects until the mouse/key is "up" again. Using that method, you can avoid "handler bloat" and still globally capture all "onscroll" events. (I think this will work for "mouse wheel" scrolling as well, but my research on that final wrinkle is still pending.)
The following works fine when you want to i.e. close a dialog after anything in the background is scrolled:
var scrollListener = function(e) {
// TODO: hide dialog
document.removeEventListener('scroll', scrollListener, true);
};
document.addEventListener('scroll', scrollListener, true);
I needed to handle scrolling in any context in a complex of custom elements with scroll events in shadowRoots. This covers at least part of the problem in my scenario and generally borrows from the answers and comments here in the context of these newer web component APIs. Attaching and detaching the listener in the appropriate lifecycle callbacks works well so-far (once might not fit your use-case).
self.addEventListener('mousewheel', handler, {capture: true, once: true});
self.addEventListener('keydown', handler, {capture: true, once: true});
Note too the event.composedPath() provides the entire event path through all the shadowRoots with all the nodes if specifics about the scrolling context are needed. If this is the case it might make sense to use this approach and attach a new handler for that specific scenario--to the node of interest.
Like drcode said, capture on body tag is the trick. I just add touchmove to work on mobile.
document
.querySelector("body")
.addEventListener("mousewheel", e => {
console.log("scroll");
});
document
.querySelector("body")
.addEventListener("touchmove", e => {
console.log("scroll");
});
Regards,
=I am trying to develop a simple drag/drop UI in my web application. An item can be dragged by a mouse or a finger and then can be dropped into one of several drop zones. When an item is dragged over a drop zone (but not yet released), that zone is highlighted, marking safe landing location. That works perfectly fine with mouse events, but I'm stuck with touchstart/touchmove/touchend family on the iPhone/iPad.
The problem is that when an item's ontouchmove event handler is called, its event.touches[0].target always points to the originating HTML element (the item) and not the element which is currently under the finger. Moreover, when an item is dragged by finger over some drop zone, that drop zone's own touchmove handlers isn't called at all. That essentially means I can't determine when a finger is above any of the drop zones, and therefore can't highlight them as needed. At the same time, when using a mouse, mousedown is correctly fired for all HTML elements under the cursor.
Some people confirm that it's supposed to work like that, for instance http://www.sitepen.com/blog/2008/07/10/touching-and-gesturing-on-the-iphone/:
For those of you coming from the normal web design world, in a normal mousemove event, the node passed in the target attribute is usually what the mouse is currently over. But in all iPhone touch events, the target is a reference to the originating node.
Question: is there any way to determine the actual element under a finger (NOT the initially touched element which can be different in many circumstances)?
That's certainly not how event targets are supposed to work. Yet another DOM inconsistency that we're probably all now stuck with forever, due to a vendor coming up with extensions behind closed doors without any review.
Use document.elementFromPoint to work around it.
document.elementFromPoint(event.clientX, event.clientY);
The accepted answer from 2010 no longer works: touchmove does not have a clientX or clientY attribute. (I'm guessing it used to since the answer has a number of upvotes, but it doesn't currently.)
Current solution is:
var myLocation = event.originalEvent.changedTouches[0];
var realTarget = document.elementFromPoint(myLocation.clientX, myLocation.clientY);
Tested and works on:
Safari on iOS
Chrome on iOS
Chrome on Android
Chrome on touch-enabled Windows desktop
FF on touch-enabled Windows desktop
Does NOT work on:
IE on touch-enabled Windows desktop
Not tested on:
Windows Phone
I've encountered the same problem on Android (WebView + Phonegap). I want to be able to drag elements around and detect when they are being dragged over a certain other element.
For some reason touch-events seem to ignore the pointer-events attribute value.
Mouse:
if pointer-events="visiblePainted" is set then event.target will point to the dragged element.
if pointer-events="none" is set then event.target will point to the element under the dragged element (my drag-over zone)
This is how things are supposed to work and why we have the pointer-events attribute in the first place.
Touch:
event.target always points to the dragged element, regardless of pointer-events value which is IMHO wrong.
My workaround is to create my own drag-event object (a common interface for both mouse and touch events) that holds the event coordinates and the target:
for mouse events I simply reuse the mouse event as is
for touch event I use:
DragAndDrop.prototype.getDragEventFromTouch = function (event) {
var touch = event.touches.item(0);
return {
screenX: touch.screenX,
screenY: touch.screenY,
clientX: touch.clientX,
clientY: touch.clientY,
pageX: touch.pageX,
pageY: touch.pageY,
target: document.elementFromPoint(touch.screenX, touch.screenY)
};
};
And then use that for processing (checking whether the dragged object is in my drag-over zone). For some reason document.elementFromPoint() seems to respect the pointer-events value even on Android.
Try using event.target.releasePointerCapture(event.pointerId) in the pointerdown handler.
We're now in 2022, this is intended and specified behavior - it's called "Implict Pointer Capture"
See the W3 spec on Pointer Events
Direct manipulation devices should behave exactly as if setPointerCapture was called on the target element just before the invocation of any pointerdown listeners. The hasPointerCapture API may be used (eg. within any pointerdown listener) to determine whether this has occurred.
elementFromPoint is a possible solution, but it seems you can also use releasePointerCapture as shown in the following demo. Touching and holding on the green div will get mouse move events for targets outside of it, whereas the red div has the default behavior.
const outputDiv = document.getElementById('output-div');
const releaseDiv = document.getElementById('test-release-div');
const noreleaseDiv = document.getElementById('test-norelease-div');
releaseDiv.addEventListener('pointerdown', function(e) {
outputDiv.innerHTML = "releaseDiv-pointerdown";
if (e.target.hasPointerCapture(e.pointerId)) {
e.target.releasePointerCapture(e.pointerId);
}
});
noreleaseDiv.addEventListener('pointerdown', function(e) {
outputDiv.innerHTML = "noreleaseDiv-pointerdown";
});
document.addEventListener('pointermove', function(e) {
outputDiv.innerHTML = e.target.id;
});
<div id="output-div"></div>
<div id="test-release-div" style="width:300px;height:100px;background-color:green;touch-action:none;user-select:none">Touch down here and move around, this releases implicit pointer capture</div>
<div id="test-norelease-div" style="width:300px;height:100px;background-color:red;touch-action:none;user-select:none">Touch down here and move around, this doesn't release implicit pointer capture<div>
So touch events have different "philosophy" when it comes to how they interact:
Mouse moves = "hover" like behavior
Touch moves = "drags" like behavior
This difference comes from the fact that there can not be a touchmove without touchstart event preceding it as a user has to touch screen to start this interaction. With mouse of course a user can mousemove all over the screen without ever pressing buttoon (mousedown event)
This is why basically we can't hope to use things like hover effects with touch:
element:hover {
background-color: yellow;
}
And this is why when user touches the screen with 1 finger the first event (touchstart) acquires the target element and the subsequent events (touchmove) will hold the reference to the original element where touch started. It feels wrong but there is this logic that you might need original target info as well. So ideally in future there should be both (source target and current target) available.
So common practice of today (2018) where screens can be mouse AND touch at the same time is still to attach both listeners (mouse and touch) and then "normalize" event coordinates and use above mentioned browser api to find element in those coordinates:
// get coordinates depending on pointer type:
var xcoord = event.touches? event.touches[0].pageX : event.pageX;
var ycoord = event.touches? event.touches[0].pageY : event.pageY;
// get element in coordinates:
var targetElement = document.elementFromPoint(xcoord, ycoord);
// validate if this is a valid element for our case:
if (targetElement && targetElement.classList.contains("dropZone")) {
}
JSP64's answer didn't fully work since event.originalEvent always returned undefined. A slight modification as follows works now.
var myLocation = event.touches[0];
var realTarget = document.elementFromPoint(myLocation.clientX, myLocation.clientY);
I met one troublesome web page whose structure is complicated. If one DIV is clicked by mouse, everything is OK. However, if it is focus-ed by javascript(i.e. divElement.focus). The layout turns to messy. This only happens in IE7/8.
So, is there any difference between click-to-focus and focus-by-javascript in IE?
Firing a Javascript focus event does not fire a click event. Without seeing the relevant code, I'm led to guess that some click handler is in place that is not being called in the case where you fire a focus event.
You might try, instead, firing a click:
var clickEvent;
if(document.createEvent) {
clickEvent = document.createEvent('click');
clickEvent.initMouseEvent('click');
divElement.dispatchEvent(clickEvent);
} else {
// Semi-pseudocode for IE, not tested, consult documentation if it fails
clickEvent = document.createEventObject();
divElement.fireEvent('onclick');
}
Or if you're into the jQuery thing:
$(divElement).click();
There's similar solutions for Prototype as well (search for Event.simulate).
The definition of the Focus action is to bring the input (keyboard or mouse) to a certain element, usually an input field. When an element gains focus, an OnFocus event is fired. When it loses focus, an OnBlur event is fired.
What you usually get by clicking is the OnClick event, which is not necessarily related to the above two.
This only happens in IE7/8.
Hmm, then I'm sure it's an IE related bug. Not surprising. If there is legitimate Javascript events involved, then they should fire uniformly across all browsers.