Cross-browser method to clear content selection before dragging - javascript

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.

Related

How to completely remove mobile browser long-press vibration?

I'm trying to completely remove the vibration that occurs on a long press of an element in a mobile browser.
Specifically, I have an image that I'm adding my own long-press functionality to, but the subtle short-vibrate that happens by default is interfering and I need to disable it.
I've successfully stopped the usual context menu from appearing by overriding it as follows:
window.oncontextmenu = function (event) {
event.preventDefault();
event.stopPropagation();
return false;
};
I've also added CSS to stop highlights and text selection etc - but I haven't been able to figure out what's causing the default vibrate after a few hundred ms.
Is there another lifecycle event that's popped on a long-press in a mobile browser, in or around oncontextmenu?
Full plunker Example here, long-press on the image from a mobile browser (I'm using chrome on Android) to see what I mean: https://plnkr.co/edit/y1KVPltTzEhQeMWGMa1F?p=preview
Disable the text selection when its clicked on.
document.querySelector("#your_target").addEventListener("touchstart", (e) =>
{
e.preventDefault();
e.stopPropagation();
this.style.userSelect = "none";
});
document.querySelector("#your_target").addEventListener("touchend", (e) =>
{
e.preventDefault();
e.stopPropagation();
this.style.userSelect = "default";
});
You could use touch-action: none; in CSS. Then you might be able to handle an interaction event to do what you want.
https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action

Issues dragging elements in Firefox

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.

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

JavaScript touchend versus click dilemma

I am working on some javascript UI, and using a lot of touch events like 'touchend' for improved response on touch devices. However, there are some logical issues which are bugging me ...
I have seen that many developers mingle 'touchend' and 'click' in the same event. In many cases it will not hurt, but essentially the function would fire twice on touch devices:
button.on('click touchend', function(event) {
// this fires twice on touch devices
});
It has been suggested that one could detect touch capability, and set the event appropriately for example:
var myEvent = ('ontouchstart' in document.documentElement) ? 'touchend' : 'click';
button.on(myEvent, function(event) {
// this fires only once regardless of device
});
The problem with the above, is that it will break on devices that support both touch and mouse. If the user is currently using mouse on a dual-input device, the 'click' will not fire because only 'touchend' is assigned to the button.
Another solution is to detect the device (e.g. "iOS") and assign an event based on that:
Click event called twice on touchend in iPad.
Of course, the solution in the link above is only for iOS (not Android or other devices), and seems more like a "hack" to solve something quite elementary.
Another solution would be to detect mouse-motion, and combine it with touch-capability to figure out if the user is on mouse or touch. Problem of course being that the user might not be moving the mouse from when you want to detect it ...
The most reliable solution I can think of, is to use a simple debounce function to simply make sure the function only triggers once within a short interval (for example 100ms):
button.on('click touchend', $.debounce(100, function(event) {
// this fires only once on all devices
}));
Am I missing something, or does anyone have any better suggestions?
Edit: I found this link after my post, which suggests a similar solution as the above:
How to bind 'touchstart' and 'click' events but not respond to both?
After a day of research, I figured the best solution is to just stick to click and use https://github.com/ftlabs/fastclick to remove the touch delay. I am not 100% sure this is as efficient as touchend, but not far from at least.
I did figure out a way to disable triggering events twice on touch by using stopPropagation and preventDefault, but this is dodgy as it could interfere with other touch gestures depending on the element where it is applied:
button.on('touchend click', function(event) {
event.stopPropagation();
event.preventDefault();
// this fires once on all devices
});
I was in fact looking for a solution to combine touchstart on some UI elements, but I can't see how that can be combined with click other than the solution above.
This question is answered but maybe needs to be updated.
According to a notice from Google, there will be no 300-350ms delay any more if we include the line below in the <head> element.
<meta name="viewport" content="width=device-width">
That's it! And there will be no difference between click and touch event anymore!
Yes disabling double-tap zoom (and hence the click delay) is usually the best option. And we finally have good advice for doing this that will soon work on all browsers.
If, for some reason, you don't want to do that. You can also use UIEvent.sourceCapabilities.firesTouchEvents to explicitly ignore the redundant click. The polyfill for this does something similar to your debouncing code.
Hello you can implement the following way.
function eventHandler(event, selector) {
event.stopPropagation(); // Stop event bubbling.
event.preventDefault(); // Prevent default behaviour
if (event.type === 'touchend') selector.off('click'); // If event type was touch turn off clicks to prevent phantom clicks.
}
// Implement
$('.class').on('touchend click', function(event) {
eventHandler(event, $(this)); // Handle the event.
// Do somethings...
});
Your debounce function will delay handling of every click for 100 ms:
button.on('click touchend', $.debounce(100, function(event) {
// this is delayed a minimum of 100 ms
}));
Instead, I created a cancelDuplicates function that fires right away, but any subsequent calls within 10 ms will be cancelled:
function cancelDuplicates(fn, threshhold, scope) {
if (typeof threshhold !== 'number') threshhold = 10;
var last = 0;
return function () {
var now = +new Date;
if (now >= last + threshhold) {
last = now;
fn.apply(scope || this, arguments);
}
};
}
Usage:
button.on('click touchend', cancelDuplicates(function(event) {
// This fires right away, and calls within 10 ms after are cancelled.
}));
For me using 'onclick' in the html element itself, worked for both touch and click.
<div onclick="cardClicked(this);">Click or Touch Me</div>

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