UDPATE:
I've been working on an implementation to use both click and touchend events for the same function, and the function effectively blocks events if the type changes. My goal was to have a more responsive application interface - I wanted to reduce time for the event start to UI feedback loop.
For this implementation to work, the assumption is that you have all your related events added on 'click' and 'touchend'. This prevents one element from being deprived of event bubbling should both events be required to run, but are of different types.
Here is an lightweight API based implementation that I have simplified for demonstration purposes. It demonstrates how to use the functionality on a collapse element.
var tv = {
/**
* @method eventValidator()
* @desc responsible for validating event of the same type.
* @param {Object} e - event object
* @param {Object} element - element event cache
* @param {Function} callback - callback to invoke for events of the same type origin
* @param {Object} [context] - context to pass to callback function
* @param {Array} [args] - arguments array to pass in with context. Requires context to be passed
* @return {Object} - new event cache
*/
eventValidator: function(e, element, callback, context, args){
if(element && element.type && element.type !== e.type){
e.stopPropagation();
e.preventDefault();
return tv.createEventCacheObj({}, true);
} else {
element = tv.createEventCacheObj(e);
(typeof context === "object" ? callback.apply(context, args) : callback());
return element;
}
},
/**
* @method createEventCacheObj()
* @param {Object} event - event object
* @param {String} [event.type] - event type
* @param {Number} [event.timeStamp] - time of event in MS since load
* @param {Boolean} [reset=false] - flag to reset the object
* @returns {{type: *, time: string}}
*/
createEventCacheObj: function (event, reset){
if(typeof reset !== 'boolean') reset = false;
return {
type: !reset ? event.type : null,
time: !reset ? (event.timeStamp).toFixed(2): null
};
}
};
// Here is where the magic happens
var eventCache = [];
var pos = 0;
var $collapses = document.getElementsByClassName('tv-collapse__heading');
Array.prototype.forEach.call($collapses, function(ele){
ele.addEventListener('click', toggleCollapse);
ele.addEventListener('touchend', toggleCollapse);
// Cache mechanism
ele.setAttribute('data-event-cache', String(pos++));
});
/**
* @func toggleCollapse()
* @param {Object} e - event object
* @desc responsible for toggling the state of a collapse element
*/
function toggleCollapse(e){
eventCache[pos] = tv.eventValidator(e, eventCache[pos], function(){
// Any event which isn't blocked will run the callback and its content
// the context and arguments of the anonymous function match the event function context and arguments (assuming they are passed using the last two parameters of tv.eventValidator)
}, this, arguments);
}
Original Answer:
Here is a response which is a modification of Rafael Fragoso's answer - pure JS.
(function(){
button = document.getElementById('sayHi');
button.addEventListener('touchstart', ohHai);
button.addEventListener('click', ohHai);
function ohHai(event){
event.stopPropagation();
event.preventDefault();
console.log('ohHai is:', event.type);
};
})();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SO - Answer</title>
</head>
<body>
<button id="sayHi">Anyone there?</button>
</body>
</html>
Run the snippet on the following and pay attention to the output:
- Phone
- Tablet
- Tablet (Desktop Mode - if applicable)
- Desktop
- Desktop (Touch Screen - if applicable)
The key is we are stopping successive events from firing. Mobile browsers do their best to emulate a click when a touch happens. I wish I could find the link to an article a saw a while back that explains all the events that occur after touchstart through click. (I was searching for the 300ms delay between double tap and click actually firing).
Touch and Mouse Devices
I ran a couple of tests using a Surface Pro and a windows 10 desktop with a touchscreen. What I found was that they both triggered events as you would suspect, touchstart for touches and click for trackpad, mouse, and stylist. The interesting thing was that a touch event which was near, but not on the button, would triggering a click event without a touch event. It seems that the built in functionality in Windows 10 looks for the closest nodes within a radius and if a node is found it will fire a mouse based event.
Multiple Events of the Same Type
If two events of the same type are on an element, stopping the event from bubbling up could prevent one of the events from firing. There are a couple of different ways to handle this using some sort of cache. My initial thoughts were to modify the event object, but we get a reference so I'm thinking a cache solution will have to suffice.
.bind('touchstart mouseup')
will solve it (based on one of the comments below) – Suture.one
method api.jquery.com/one – Suture