How to find out the actual event.target of touchmove javascript event? - javascript

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

Related

With HTML5 can an element be made 'invisible' for drag/drop interaction?

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/

Html Canvas lag when Left Mouse is down and moving on Chrome

I have created a canvas and I have added mouse events to it:
canvas = document.getElementById('canvas');
context = canvas.getContext('2d');
canvas.width = screenWidth;
canvas.height = screenHeight;
...
// CALLED AT START:
function setup() {
// Mouse movement:
document.onmousemove = function(e) {
e.preventDefault();
target.x = e.pageX;
target.y = e.pageY;
angle = Math.atan2((target.y - localPlayer.getY()),
(target.x - localPlayer.getX()));
// Distance to mouse Check:
var dist = Math.sqrt((localPlayer.getX() - target.x)
* (localPlayer.getX() - target.x) + (localPlayer.getY() - target.y)
* (localPlayer.getY() - target.y));
var speedMult = dist / (canvas.height / 4);
socket.emit("update", {
...
});
}
document.onmousedown = function(e) {
e.preventDefault();
}
}
Now the issue is when I hold down the only left mouse button and move the mouse at the same time, my game lags a lot. Simply moving the mouse causes no lag. I have tested this on chrome and on firefox. It seems that I can only recreate the issue on chrome. Using the middle mouse button or right button has the same behaviour in the game and cause no lag. Only when using the left mouse button causes lag.
I have looked around for answers and found that I should prevent default behaviour like so:
e.preventDefault();
But that did not resolve the issue. I have also tried to update a number on the screen that represents the mouse position. And it updated normally. Only the game itself was lagging. Could it be that the onMouseMoved is never called whilst the left button is held down? But then why is it called with the middle and right button?
The issue should be with the code I a calling inside of the move method, because it works fine when I am not holding down the left key, and it works well on firefox. There must be something else going on.
EDIT: I decided to a recording on chrome to see what is going on. Here is the result:
What's really odd, when I press the middle mouse button or the right button, the game does the same thing, but it does not lag at all. What are you doing chrome?
EDIT: Test it out here: www.vertix.io note that not everyone seems to be able to reproduce this issue.
Thank you for your time.
When you hold down the left mouse button and move it in the same time, you are dragging.
Edit: In some versions of Chrome, there is a bug (when I posted this answer I had it, now I don't), which causing the drag events to be fired even without the element having the draggable attribute. Normally, drag events should only be fierd from elements which have the draggable attribute set to true (except images and anchors who are drragable by default).
According to the MDN, when drag events are being fired, mouse events, such as mousemove, are not, which means that your function isn't being called.
A possible solution are to use the same function for both drag and mousemove events:
function mouseMove(e) {
//do your things here
...
}
document.addEventListener('mousemove', mouseMove);
document.addEventListener('drag', mouseMove);
Note: If you'll use the same function for both events, you should be aware of which properties of the event you're using in the function, because of the difference between the drag and mousemove events. The two events doesn't contains the exact same properties and the behavior of some properties may not be the same in both of them.
Have you considered throttling?
Check out https://blog.toggl.com/2013/02/increasing-perceived-performance-with-_throttle/
You have the mouse event on the document. As we can not see what you have on the document it is hard to know if that is a cause of your problems.
Try moving the mouse event to the canvas only, as that is the only place you need it I presume. No point handling events for the document if its not part of the game, plus document is last on the list if child elements have events attached. They go first and then it bubbles up to yours.
As it looks like you are using a framework of some type there is in all possibility another mouse event listener that is part of the frame work that may be slowing you down by not preventing default. You will have to search the framework to see if it has a listener for any of the mouse events.
And use addEventListener rather than directly attaching the event via .onmousedown = eventHandler
eg canvas.addEventListener("mousedown",eventHandler);
And add the event listener to the element you need it for, not the document.
function mouseMove(e) {
//do your things here
...
}
document.onmousemove = mouseMove;
document.ondrag = function(e) {
mouseMove(e);
//do another things
...
}

Get element currently under mouse without using mouse events

WRT building a Firefox Add-on.
Is it possible to get the element under the mouse via some XPCOM or javascript method? (non-js-ctypes please as that requires OS specificity)
I want to detect what is under the mouse when user presses Ctrl + Shift + M.
Right now I'm adding a mouseover listener to the document when the user presses this hotkey, so I can get the element under the mouse when he moves it, but not the element that was under the mouse exactly when he pressed the hotkey combination.
I just looked through the source for code that gets (or stores and makes available) the cursor position. I didn't find anything one could use (from Javascript, XPCOM or not). I might have missed something... MXR is your friend.
However, if you want to avoid mousemove (and this is a good idea in general), you can just look for the innermost hovered element, e.g. like so.
function getInnermostHovered() {
var n = document.querySelector(":hover");
var nn;
while (n) {
nn = n;
n = nn.querySelector(":hover");
}
return nn;
}
(fiddle demoing the principle)
While this is what I'd consider a hack, it seems to work good enough most of the time, but will fail if the element has mouse events disabled via pointer-events. There could be other issues I didn't think of...
Of course, this can return nothing when the document has no hovered element (e.g. the mouse is not actually within the document).

HTML5 Drag and Drop effectAllowed and dropEffect

The relationship between these two properties seems to have been the source of some confusion. Based on reading both the MDN site and MSDN I thought i had figured it out, but now I am not sure...
I figured that when an element is dragged, you can specify what is allowed to happen to it (i.e. it can be moved, copied, linked to - one of the effectAllowed constants). This is the effectAllowed property.
Different drop targets do different things, so when you dragover another element it can control which "effect" takes place on the drop, this is the "dropEffect" property. So I set up a simple example to test this theory out.
JSFiddle
$("[draggable='true']").on("dragstart", function(e) {
var dt = e.originalEvent.dataTransfer;
dt.effectAllowed = "copyMove";
dt.setData("text/plain", "Foo");
});
$("#dropZoneCopy").on("dragover", function(e) {
var dt = e.originalEvent.dataTransfer;
dt.dropEffect = "copy";
e.preventDefault();
});
$("#dropZoneMove").on("dragover", function(e) {
var dt = e.originalEvent.dataTransfer;
dt.dropEffect = "move";
e.preventDefault();
});
I have a box the user can drag - the effects allowed are "copyMove". I have one box that sets dropEffect to copy, and once that sets dropEffect to move. What I expect is that when the user drags over the "copy box" the cursor will change to indicate a copy will happen, as I drag over the "move box" the cursor changes to indicate a move...
Only Chrome behaves as I would expect. Is this because the other browser are wrong or because I don't understand the spec. properly ?
UPDATE
Some more information from fiddling with this.
In both Firefox and Chrome, if you have a dragsource which indicates the effectAllowed is "copy" and a dropzone that says dropEffect is "move" then you cannot drop on the drop zone even if you cancel the event. I thought that dropEffect would be useful to read ondrop to see what to do, but it isn't available on Chrome, the dropEffect does not appear on the drop handler, e.g. trying to read the dataTransfer.dropEffect will say that the dropEffect is "none" even though you set it on dragover. Setting the dropEffect as noted above does influence the way the cursor is displayed.
On Firefox, the dropEffect does come through on the dropzone after being set on dragover, but it does not influence the display of the mouse cursor. On Firefox windows pressing the ctrl key does affect the display of the mouse, but does not affect the dropEffect property.
The spec shows that the source can listen for the dragend event to see what happened. It should look at the dropEffect within this event. Chrome, Mozilla and Safari work as you would hope here, the drop effect appears in the dragend event. In IE if the effect allowed is a simple value e.g. "copy" then any succesfull drop results in this value appearing as the dropEffect on dragend. If the effectAllowed was a compound value like copyMove and you tried to select "move" on dragover by setting the dropEffect, you're out of luck, that will come through as dropEffect = "none" at the source on dragend. You are stuck with one cursor & one dropEffect and that is the effectAllowed set on dragstart if that effect is a simple value. Interestingly the dropEffect it seems does come through when you drag into a native application from IE11 at least (and i assume earlier).
Other notes
On Safari on a mac - effectAllowed cannot be set programatically, therefore any dropEffect that gets set is valid. When you press the cmd key the effectAllowed becomes "move" and when you press the alt key the effectAllowed becomes "copy". Thereafter it works as you would hope, if the dropEffect is not one of these effectAlloweds the drop is not allowed by the browser.
More Info
I've been spending some spare time working on an HTML5 drag and drop library i wrote a bunch more about this and other issues in the docs for it, if you're interested please take a look at
the project
take a look at https://web.dev/drag-and-drop/
function handleDrop(e) {
e.stopPropagation(); // Stops some browsers from redirecting.
e.preventDefault();
var files = e.dataTransfer.files;
for (var i = 0, f; f = files[i]; i++) {
// Read the File objects in this FileList.
}
}

Is there no Event.fromElement in a dragenter/dragover event?

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.

Categories