Treat short swipe as click - javascript

I have a mobile web app, which uses a lot of click event handlers on buttons, etc. All of this works fine if the user really "clicks" (i.e. "touchdown-touchup") the button. However if the user does a short swipe, then the click event does not fire. This causes a lot of complaints from my users that the app doesn't register clicks/taps and that other apps work correctly.
Of course, I can get coordinates of the touch in ontouchstart and ontouchend and compute the distance - but I need to also know whether that distance is under the maximum that the browser would treat as 'click'. I do not want to switch to using touchstart/touchend events instead of click.
I used to use fastclick.js library for handling clicks/taps in the past, but now use native 'click' events with touch-action: manipulation. Is there any way of specify/controlling the maximum movement of the finger on the button that still registers as a 'click'?
Update based on comments. The application is very large and there are hundreds if not thousands of event handler assignments throughout it (the app has been developed over the last 8 years). Changing all of these is not practical, therefore I'm looking for a solution that would allow me to either set the threshold once globally or solve the problem with a global-like touchstart/touchend handlers.

I thought this was an interesting problem so I took a shot at solving it for you. In a way it's somewhat similar to the problem of preventing a click event when a dblclick happens.
Using a distance threshold for a "short swipe" seems, to me at least, problematic in that the threshold distance might be system dependent. Instead of that I decided to trigger on if the "click" event actually happens. I used mousedown as a simulated touchstart and mouseup as a simulated touchend. mouseup always happens before click so it is similar to touchend in that respect.
Normally if you "click" (mousedown) on an element and then move your mouse pointer off the element, the click event does not happen. This is much like the situation you describe as being a "short swipe". After a certain distance the click event just doesn't happen. The code below will send a click event for the button even if you mousedown on it, move the pointer off it and then mouseup. I believe that this would solve the problem if you used it for touchstart and touchend instead
// The pre-exisiting click handler
function handleClick(ev) {
console.log('button clicked. do work.');
}
document.getElementById('theButton').addEventListener('click', handleClick);
// our global "touch" handlers
var touchHandler = {
curPending: null,
curElem: null,
handleTouch: function handleTouch(ev) {
switch (ev.type) {
case 'mousedown':
// capture the target that the click is being initiated on
touchHandler.curElem = ev.target;
// add an extra click handler so we know if the click event happens
ev.target.addEventListener('click', touchHandler.specialClick);
break;
case 'mouseup':
// start a pending click timer in case the click event doesn't happen
touchHandler.curPending = setTimeout(touchHandler.pendingClick, 1);
break;
}
},
specialClick: function(ev) {
// the click event happened
// clear our extra handler
touchHandler.curElem.removeEventListener('click', touchHandler.specialClick);
// make sure we don't send an extra click event
clearTimeout(touchHandler.curPending);
},
pendingClick: function() {
// we never heard the click event
// clear our extra handler
touchHandler.curElem.removeEventListener('click', touchHandler.specialClick);
// trigger a click event on the element that started it all
touchHandler.curElem.click();
}
};
// using "mousedown" as "touchstart" and "mouseup" as "touchend"
document.addEventListener('mouseup', touchHandler.handleTouch);
document.addEventListener('mousedown', touchHandler.handleTouch);
<p>I work when clicked normally but I also work when
mousedown, drag pointer off me, mouseup</p>
<button id="theButton">Click Me</button>

Related

JavaScript onMouseDown and onClick events versus event queue

I have following simple JS code (https://stackblitz.com/edit/web-platform-ueq5aq?file=script.js):
const baton = document.querySelector('button');
baton.addEventListener('mousedown', (e) => {
console.log('baton');
baton.addEventListener('click', (e) => {
console.log('baton click');
});
});
When I click a button, I get 'baton' and 'baton click' logged to console. Now my question is what exactly happens here? As I understand it, the moment script is executed, handler mousedown is added to even queue. When I actually click button, this handler is run, so it's taken from event queue, added to call stack and it is executed. When it is executed, handler "click" is added to event queue.
How actually event onClick is triggered after onMouseDown? How is that related to event queue? Why onMouseDown handler is run before click event happens? I'm asking because I have a lot more complex code where result is different in different scenarios.
When user navigates to page in SPA which contains similiar script, and then clicks button 'baton' order is:
mousedown event -> handler mousedown -> handler click -> click event
And when user reloads page, so SPA is loaded right on that page, and clicks button 'baton' order is:
mousedown event -> click event -> handler mousedown
I am seeking answer and truth. Any help will be greatly appreciated.
Ps. Unfortunately I'm not able to reproduce this error in example repository - it happens in quite complex web app which production code I can't share here for obvious reasons.
Ps2. Just to clarify, because probably it isn't stated clearly enough: I'm not asking "why mousedown event is triggered before click event", but "why mousedown HANDLER is run before click event". This is NOT obvious, because handlers are not run immediately. In order of handler to be run, it first have to wait to call stack to be empty, so event queue can be processed by JS engine.
The browser tracks the element you clicked the moused down on. Then it tracks the element you lifted the mouse button on. If the element you lifted the mouse button on is the same element or a child element of the target element. Then a click event is dispatched to the last element you lifted the mouse on. The event then propagates up the element chain to every parent element unless the event is told to stop propagating.
If you click down on element A and mouse up on element B. Then A gets mouse down event, and B gets mouse up event, but neither get a click event. Same thing if you navigate the browser to another page in between the mouse down and mouse up.
From MDN Web Docs
An element receives a click event when a pointing device button (such as a mouse's primary mouse button) is both pressed and released while the pointer is located inside the element.
So there is a mouseup event and then the click event.
EDIT after question edit:
"why mousedown HANDLER is run before click event?"
Your already executing mousedown handler registers the click handler so how should the click handler run before it?
All click handlers registered in all previous mousedown handlers will run after the mousedown and mouseup events too.
Perhaps we should start by clarifying a few things.
Events in the browser, are modeled more like a "nesting hierarchy", then a queue -- How it works is referred to as Event Bubbling -- [Wikipedia][1]
But, essentially what you are doing, when adding an EventListener, is hooking into one or more points of the DOM, and saying hey, when X Event passes through here, use function Y to handle it, before passing it along up the stack.
Once an EventListener has been "added" it remains active waiting to be given an event. What exactly it does is defined in its handler function.
let myYFunction = function( e ) { ... }
let myXListener = baton.addEventListern('X event', myYFunction );
// at this point, anytime 'X event' happens to baton, myYFunction will
// be called to handle it...
Now let's take a look at your examples, lets break things down a little,
const baton = document.querySelector('button');
This first line, is simply querying the DOM, to find the first element of type 'button' in the page. Right... This is "where" we want to insert our event handler. We could add them to any element, anywhere in the DOM, we could even hook into the 'body' element if we wanted to.
Ok, then you have this bit,
baton.addEventListener('mousedown', (e) => {
console.log('baton');
baton.addEventListener('click', (e) => {
console.log('baton click');
});
});
Which is "nesting" the creation of the 'click' Event Listener, but only after a 'mousedown' event has been "handled". There is no real reason the 'click' event had to be registered within the function body of the mousedown handler.
If we re-write it a bit, it may be clearer what is actually going on.
baton.addEventListener('mousedown', (e) => {
console.log('baton mousedown');
}
baton.addEventListener('click', (e) => {
console.log('baton click');
});
Additionally I would also point out, that how it is being done currently "works" -- but it is actually hiding a tiny bit of sloppy coding... you see every time the 'mousedown' event is triggered a new 'click' eventListener is being registered... so eventually you may end up with many, many, many click handlers responding to a single 'click' event... Check out MDN to learn more about [this][2]
I hope this answers your initial questions as to what is going on.
To your question "When I click a button, I get 'baton' and 'baton click' logged to console. Now my question is what exactly happens here?" -- To me, it would look something like this:
a 'mousedown' eventListener is added, however nothing "executes"
a 'mousedown' event takes place, now your 'mousedown' listener executes its function, which in turn logs out to the console, and registers a new 'click' handler -- but again, does not execute.
Moving forward, steps 1 and 2 are repeated for every 'mousedown' seen by baton. Additionally, for any 'click' event passed through baton --- which happens after every 'mousedown' on baton:
A 'click' event occurs, your 'click' handler is then executed and logs out to the console.
SPA event handling strategies
When working with SPAs, where multiple "pages" are displayed, in the same page load... it can get messy, all these event listeners hanging around piling up on one another. If you are going to employ eventListeners between "Pages" of your SPA, you might want to look into how to "remove" them too. - [MDN][3]
That way, you only have eventListeners active for the current "Page" of your SPA.
Also, consider "generalizing" your handlers, and attaching them higher up in the DOM... This would allow you to have only a few event listeners which "route" events to their "logical" handlers.
Random/Different Behaviors
With the steps outlines above, 1, 2 and 3 and how they don't all happen at the same time. You will see what appears to be random output to the console... try and run something like this, to get a proper sense of things:
let cCount = 0;
let mCount = 0;
let tCount = 0;
const baton = document.querySelector('button');
baton.addEventListener('mousedown', (e) => {
console.log('mousedown # ' + (mCount++) + ' order:' + tCount++);
baton.addEventListener('click', (e) => {
console.log('click # ' + (cCount++) + ' order:' + tCount++);
});
});
[1]: https://en.wikipedia.org/wiki/Event_bubbling#:~:text=Event%20bubbling%20is%20a%20type,Provided%20the%20handler%20is%20initialized).
[2]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
[3]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener

How quickly can an element be doubleclicked multiple times?

If an element is clicked twice, the 'dblclick' event is fired. If the element continues to be clicked, the 'dblclick' event does not continue to be fired. The 'dblclick' event will only be fired once until a "cooloff" period is complete, eg. there is a time to wait before another 'dblclick' event can be fired no matter how many times the element is clicked.
I cannot find any documentation that specifies how long must elapse before another 'dblclick' can occur. Does anyone know what the 'dblclick' "cooloff" period is?
I have tried to test this by scripting the clicking of an element, but for some reason javascript-invoked clicks do not trigger 'dblclick' events. So I have tested manually and I can't get a double-click to occurred any sooner than ~400ms after another double-click has occurred. You can try yourself: https://jsfiddle.net/5v4pcx2k/8/
code
If you're wondering, this is basically what it seems like the browser is doing https://jsfiddle.net/b0y5ej2y/3/
There are quite a few bugs and inefficiencies in your jsfiddle.
h1 element has no end tag (you think your ending it with /hi)
you don't need jquery
the general advice around the webs, seem to be don't mix click and dblclick event listeners on the same element
the event already has a property on it that tells you when it fired, you don't need to ask for the current time again with Date.now()
Having said all that, I still couldn't manually click any faster than about ~700ms. I think what's happening is it's the window manager / OS / mouse driver is artificially holding back double clicks to some speed limit.
Here's my version of a jsfiddle test for dblclick speed, with a working programatic dblclick simulation which can dblclick as fast as every 4ms on my computer.
https://jsfiddle.net/stephencarmody/v0b3dpwc/
var lastOne;
foobar.addEventListener('dblclick', function (event) {
log.innerText = 'elapsed: ' + (event.timeStamp - lastOne);
lastOne = event.timeStamp;
});
function simulateClick () {
var event = new MouseEvent('dblclick', {
'view': window,
'bubbles': true,
'cancelable': true
});
foobar.dispatchEvent(event);
}
setInterval(simulateClick, 0);
Comment out the setInterval line to test manual dblclick events.

Synchronising browser events in JS

This is a bit of an abstract question, but I've been pondering its usefulness, and maybe it's either already been solved or inspires someone to do something based on it.
Well recently I ran across an issue whereby three browser events were fired, all as the result of a single user interaction: click, blur and focus. When the user clicks from one input to another, these events occur; and a similar set occur when the user tabs from one to another.
The trouble I had was that they fired in this order: blur, focus, click. It meant that, if the blur event caused DOM changes, the click event could be affected. I really wanted click, blur, focus - but that's not what the browser gave me.
I figured a general utility could be produced, capturing and cancelling browser events, then synchronising them and firing a single handler for all three. Perhaps extending the Event class so that the event could be reinstated.
Is there a more abstract design pattern I can use here? Something that will allow me to set up an arbitrary number of event listeners, and then fire a single event when all are complete? Does it have an implementation already? All advice welcome.
Dont need to break head around this! you can always trigger these events Programmatically
Note: object referenced here is any element selected using javascript selector.
Initially onBlur & onFocus do event.preventDefault which allows onClick to do its job first
var clicked=false;
object.onblur = function(e) {
if (!clicked) {
e.preventDefault
}
};
object.onfocus = function(e) {
if (!clicked) {
e.preventDefault
}
};
inside click event undo the above preventions and trigger the events in the order you wanted
object.onclick=function(){
clicked=true;
//Do anything
object.unbind('blur'); //this do undo prevent default
object.unbind('focus'); //this do undo prevent default
object.blur(); //in order you want
object.focus();
//make sure to put condition if click clicked
};
Thats it ! Hope it helps

Blur event stops click event from working?

It appears that the Blur event stops the click event handler from working? I have a combo box where the options only appear when the text field has focus. Choosing an option link should cause an event to occur.
I have a fiddle example here: http://jsfiddle.net/uXq5p/6/
To reproduce:
Select the text box
Links appear
Click a link
The blur even occurs and the links disappear
Nothing else happens.
Expected behavior:
On step 5, after blur occurs, the click even should also then fire. How do I make that happen?
UPDATE:
After playing with this for a while, it seems that someone has gone to great lengths to prevent an already-occurred click event from being handled if a blur event makes the clicked element Un-clickable.
For example:
$('#ShippingGroupListWrapper').css('left','-20px');
works just fine, but
$('#ShippingGroupListWrapper').css('left','-2000px');
prevents the click event.
This appears to be a bug in Firefox, since making an element un-clickable should prevent future clicks, but not cancel ones that have already occurred when it could be clicked.
Other things that prevent the click event from processing:
$('#ShippingGroupListWrapper').css('z-index','-20');
$('#ShippingGroupListWrapper').css('display','none');
$('#ShippingGroupListWrapper').css('visibility','hidden');
$('#ShippingGroupListWrapper').css('opacity','.5');
I've found a few other questions on this site that are having similar problems. There seem to be two solutions floating around:
Use a delay. This is bad because it creates a race condition between the hiding and the click event handler. Its also sloppy.
Use the mousedown event. But this isn't a great solution either since click is the correct event for a link. The behavior of mousedown is counter-intuitive from a UX perspective, particularly since you can't cancel the click by moving the mouse off the element before releasing the button.
I can think of a few more.
3.Use mouseover and mouseout on the link to enable/disable the blur event for the field. This doesn't work with keyboard tabing since the mouse is not involved.
4.The best solution would be something like:
$('#ShippingGroup').blur(function()
{
if($(document.activeElement) == $('.ShippingGroupLinkList'))
return; // The element that now has focus is a link, do nothing
$('#ShippingGroupListWrapper').css('display','none'); // hide it.
}
Unfortunately, $(document.activeElement) seems to always return the body element, not the one that was clicked. But maybe if there was a reliable way to know either 1. which element now has focus or two, which element caused the blur (not which element is blurring) from within the blur handler. Also, is there any other event (besides mousedown) that fires before blur?
click event triggers after the blur so the link gets hidden. Instead of click use mousedown it will work.
$('.ShippingGroupLinkList').live("mousedown", function(e) {
alert('You wont see me if your cursor was in the text box');
});
Other alternative is to have some delay before you hide the links on blur event. Its upto you which approach to go for.
Demo
You could try the mousedown event instead of click.
$('.ShippingGroupLinkList').live("mousedown", function(e) {
alert('You wont see me if your cursor was in the text box');
});
This is clearly not the best solution as a mousedown event is not achieved the same way for the user than a click event. Unfortunately, the blur event will cancel out mouseup events as well.
Performing an action that should happen on a click on a mousedown is bad UX. Instead, what's a click effectively made up of? A mousedown and a mouseup.
Therefore, stop the propagation of the mousedown event in the mousedown handler, and perform the action in the mouseup handler.
An example in ReactJS:
<a onMouseDown={e => e.preventDefault()}
onMouseUp={() => alert("CLICK")}>
Click me!
</a>
4.The best solution would be something like:
$('#ShippingGroup').blur(function()
{
if($(document.activeElement) == $('.ShippingGroupLinkList'))
return; // The element that now has focus is a link, do nothing
$('#ShippingGroupListWrapper').css('display','none'); // hide it.
}
Unfortunately, $(document.activeElement) seems to always return the
body element, not the one that was clicked. But maybe if there was a
reliable way to know either 1. which element now has focus or two,
which element caused the blur (not which element is blurring) from
within the blur handler.
What you may be looking for is e.relatedTarget. So when clicking the link, e.relatedTarget should get populated with the link element, so in your blur handler, you can choose not to hide the container if the element clicked is within the container (or compare it directly with the link):
$('#ShippingGroup').blur(function(e)
{
if(!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget)) {
// Alt: (!e.relatedTarget || $(e.relatedTarget) == $('.ShippingGroupLinkList'))
$('#ShippingGroupListWrapper').css('display','none'); // hide it.
}
}
(relatedTarget may not be supported in older browsers for blur events, but it appears to work in latest Chrome, Firefox, and Safari)
If this.menuTarget.classList.add("hidden") is the blur behavior that hides the clickable menu, then I succeeded by waiting 100ms before invoking it.
setTimeout(() => {
this.menuTarget.classList.add()
}, 100)
This allowed the click event to be processed upon the menuTarget DOM before it was hidden.
I know this is a later reply, but I had this same issue, and a lot of these solutions didn't really work in my scenario. mousedown is not functional with forms, it can cause the enter key functionality to change on the submit button. Instead, you can set a variable _mouseclick true in the mousedown, check it in the blur, and preventDefault() if it's true. Then, in the mouseup set the variable false. I did not see issues with this, unless someone can think of any.
I have faced a similar issue while using jQuery blur, click handlers where I had an input name field and a Save button. Used blur event to populate name into a title placeholder. But when we click save immediately after typing the name, only the blur event gets fired and the save btn click event is disregarded.
The hack I used was to tap into the event object we get from blur event and check for event.relatedTarget.
PFB the code that worked for me:
$("#inputName").blur(function (event) {
title = event.target.value;
//since blur stops an immediate click event from firing - Firing click event here
if (event.relatedTarget ? event.relatedTarget.id == "btnSave" : false) {
saveBtn();
}
});
$("#btnSave").click(SaveBtn)
As already discussed in this thread - this is due to blur event blocking click event when fired simultaneously. So I have a click event registered for Save Btn calling a function which is also called when blur event's related Target is the Save button to compensate for the click event not firing.
Note: Didnt notice this issue while using native onclick and onblur handlers - tested in html.

How to tell if a mouseup is going to be followed by a click event?

Is there any way to know, in a jQuery onmouseup handler, if the event is going to be followed by a click event for the same element?
I have an event handler for a menu hyperlink which unbinds itself when the user either clicks on an element or "drops" (as in drag-n-drop) on an element. I want to avoid prematurely unbinding the handler on mouseup if a click is coming next.
I realize I can track mousedown and mouseup events myself or otherwise hack up a solution (e.g. wait 50 msecs to see if a click comes soon), but I was hoping to avoid rolling my own implementation if there's something built-in for this purpose.
There is nothing built-in because it's really specific to your needs. Thus, there would kilometers of code and documentation to maintain if jQuery would handle any combination of clicks, long clicks, moves, etc.
It's also hard to give you a snippet that satisfies your needs, but a setTimeout is usually the first step to take, with something like that :
obj.mouseup = function (){
obj.click = action; // do action
setTimeout ( function() {
obj.click = functionOrigin // after 500 ms, disable the click interception
}, 500);
};
you can use $(selector).data('events') for that
$('div').mouseup(function(){
if($(this).data('events').click){
console.log('Has a click event handler')
}
});

Categories