Issues dragging elements in Firefox - javascript

Are there any reasons why an image would not be draggable in Firefox even when the attribute draggable="true"? I'm designing an image swapping script in JS but am having some problems when running it in Firefox. While the code works fine in Chrome, Edge, and IE, when I try and drag and drop an image in Firefox it doesn't appear as if the browser is letting me drag the image (no ghost image appears) and thus my drag event isn't firing or triggering any of the drop events. I am generating the image via document.createElement('img') and setting the attributes with
imgElement.setAttribute('draggable', true);
imgElement.setAttribute('ondragstart', 'drag(event)');
my drag function:
function drag(ev) {
if (!ev.target.classList.contains(clickClass)) {
return;
}
ev.dataTransfer.setData("text", ev.target.id);
document.getElementById(ev.target.id).parentElement.setAttribute('class', 'noclick');
};
I read in a different question that the drag event may not fire in Firefox if data isn't being transferred, however, that doesn't seem to be the issue here.

Instead of imgElement.setAttribute('ondragstart', 'drag(event)'); you should add the dragstart listener instead:
imgElement.addEventListener('dragstart', function(e){
drag(e);
});

It seems in Firefox, at least in v61, setting the draggable attribute to false, instead of true will give you the desired result - allowing the image to drag along with the movement of the mouse.
element.setAttribute('draggable', false);
It may be that Firefox assumes it will handle the dragging of images, unless you explictly set an attribute to false, in which case you use your own js handler to move the image yourself.
It also seems Firefox resets the attribute back to true by default once the initial movement has occurred and the mouse button released. The draggable attribute must be reset back to false to start the process all over again. But, that shouldn't be a problem considering you're likely handling all mouse events for that image, and can easily insert a line of code to ensure that dragging is false when you need it to be.

Related

Prevent force touch event on image but still allow long press event in iOS Safari

We have a need in our image gallery to prevent Apple's Force touch events on images, but still allow the long-press which triggers the 'Save Image' callout. We provide instructions for our iOS users to long-press on the image and then select 'Save Image', but users get very confused if they accidentally press too hard and trigger a Force Touch event - especially if it 'pops' and loads the image in a new page.
Initially I thought of listening to the touchforcechange events, and then calling preventDefault when the force reached a certain level. Something like this:
imgEl.addEventListener( 'touchforcechange', 'onTouchForceChange', false )
function onTouchForceChange( e ){
if( e.changedTouches[0].force > 0.5 ){
e.preventDefault()
}
}
However, that seems to block the long press events as well. There also doesn't seem to be one particular force level at which the event switches to a Force Touch.
Adding the css property -webkit-touch-callout: none; to the image does block the Force Touch event, but again, it also blocks the callout on the long press.
Any ideas greatly appreciated!
(Using jQuery, but can probably be accomplished without it)
this seems to be working for me, tested on iPhone 7 plus iOS 10.3.3:
window.addEventListener('touchforcechange', function(event) {
var force = event.changedTouches[0].force;
var id = event.changedTouches[0].target.id;
if ($('#' + id).hasClass('class') && force > 0.1) {
event.preventDefault();
console.log('prevented 3D touch on element with id = ' + id);
}
});
replace 'class' with the class of the elements on which 3d touch should be prevented. in your case, probably a class shared by all the image elements in the galary.
I think the main problem with yours is that 0.5 is probably too high of a threshold.
You can embed your image in an svg tag to prevent this.

Cross-browser method to clear content selection before dragging

Been trying to work through the various cross browser issues involved in clearing a content selection immediately before performing an HTML5 drag.
Current progress is here:
https://jsfiddle.net/kamelkev/36ek328t/
$(document).ready(function() {
var down = 0;
$(document.body).on('dragstart.draggable', function(event) {
if (!$(event.target).attr('draggable')) {
event.preventDefault();
}
return;
});
$('div[draggable]').on('dragstart', function(event) {
event.originalEvent.dataTransfer.setData("Text", '');
});
$('div[draggable]').on('mousedown.selections', function(event) {
down = 1;
return true;
});
$('div[draggable]').on('mouseup.selections', function(event) {
down = 0;
return true;
});
$('div[draggable]').on('mousemove.selections', function(event) {
var doc = event.target.ownerDocument;
if (down) {
if (doc.selection) {
doc.selection.empty();
}
else if (doc.getSelection) {
if (doc.getSelection().empty) { // Chrome
doc.getSelection().empty();
}
else if (doc.getSelection().removeAllRanges) { // Firefox
doc.getSelection().removeAllRanges();
}
}
down = 0;
}
else {
down = 0;
}
return true;
});
});
In Chrome/Safari/Firefox you'll notice that selecting all (command-a) the content and performing a drag on "thing2" will not only clear the selection, but allow the drag event to proceed. For some reason IE clears the selection, but then seemingly cancels the subsequent dragstart event.
I can find literally zero references to this type of behavior either within Microsoft's own documentation, or elsewhere. Any pointers are greatly appreciated.
I've tried quite a few things to work around this problem, moving my various selection clearing code into different events, etc, but no luck.
Any ideas on how to solve this?
I'm specifically doing this to avoid drag image issues when performing an HTML5 drag. The "ghost" image that you see when performing an HTML5 drag is generated from the sum parts of all the dragged content rather than from the element being dragged. If a user happens to do a select-all then the subsequently generated drag image will use all the content for the ghost image rather than just the content of the element being dragged. A non-issue for Firefox/Safari/Chrome as you have the ability to set a custom drag image, but for IE there are few alternatives.
Found a viable workaround after trying a large number of event permutations.
The clearing of selections within the IE mousemove handler seemingly prevents the dragStart event from ever firing. This is (as per usual) inconsistent with what other browsers do. However I discovered that clearing the selection within the dragStart handler sort of works as expected as long as you don't deal with iframes. If you're dealing with iframes I think mousedown is the only viable event you can work with.
However the other browsers are not behaving as one would expect either, as clearing a selection within a dragstart handler seemingly prevents the expected dragmove event from firing.
The solution is to handle selection clearing within the dragstart handler for all IE browsers, and doing so in the mousemove handler for all other browsers.

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.

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

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

Categories