> Remove onClick delay on webkit for iPhone
Developing on the webkit for iPhone I encountered a curious delay on onClick events. It seems that the click is triggered with about 300 milliseconds delay. While this is unnoticeable on a standard web page, it can be annoying on a web application. Fortunately the click event can be overridden thus eliminating the delay.
I assume that 300ms is the time frame Apple guesses an user needed to perform gestures, but there are situations where this delay can be really annoying. Think to a calculator application with 300ms delay each time you press a button. Unacceptable.
The simplest solution is to use onTouchStart instead of onClick events. Something like <div ontouchstart="doSomething()"> is perfectly logical and overrides the onClick delay. But the action is triggered as soon as you touch the screen and may end up to undesirable results, so I tried to recreate the mouseDown/mouseUp events sequence with touchStart/touchMove/touchEnd.
Point your iPhone or simulator to my demo page. Clicking on the first button the standard click event is fired (with infamous 300ms delay), the second button instead overrides the onClick event and the action is actually cast on touchEnd with no delay.
The code I use is the following:
function NoClickDelay(el) {
this.element = el;
if( window.Touch ) this.element.addEventListener('touchstart', this, false);
}
NoClickDelay.prototype = {
handleEvent: function(e) {
switch(e.type) {
case 'touchstart': this.onTouchStart(e); break;
case 'touchmove': this.onTouchMove(e); break;
case 'touchend': this.onTouchEnd(e); break;
}
},
onTouchStart: function(e) {
e.preventDefault();
this.moved = false;
this.element.addEventListener('touchmove', this, false);
this.element.addEventListener('touchend', this, false);
},
onTouchMove: function(e) {
this.moved = true;
},
onTouchEnd: function(e) {
this.element.removeEventListener('touchmove', this, false);
this.element.removeEventListener('touchend', this, false);
if( !this.moved ) {
// Place your code here or use the click simulation below
var theTarget = document.elementFromPoint(e.changedTouches[0].clientX, e.changedTouches[0].clientY);
if(theTarget.nodeType == 3) theTarget = theTarget.parentNode;
var theEvent = document.createEvent('MouseEvents');
theEvent.initEvent('click', true, true);
theTarget.dispatchEvent(theEvent);
}
}
};
The script creates a touchStart event and performs the click action on touchEnd which occurs 300ms before the standard click event. This is just an example to get you started, my function triggers the click event on touchEnd so you still need to add an onClick event (or an Anchor) somewhere if you want something to happen. You could better place directly your code on touchEnd but if you use my method your application will be compatible with both touch (the iphone) and non-touch enabled devices (the standard browser).
To activate the script all you need to do is: new NoClickDelay(document.getElementById('element'));. From now on all your clicks inside the element will be performed with no delay.
Note that you don’t need to apply the NoClickDelay() function to all the objects in the page, but just to a container. If for instance you have an unordered list, you don’t need to add the script to each <li> elements, but just to the <ul>. This has been done to reduce the number of event listeners so less resources are needed.
To closely mimic the standard UI you could add a hover class on touchStart to highlight the pressed object in someway and remove it on touchMove. (Apple places a gray rectangle over pressed elements).
Update 2009/02/27: By popular demand here follows the code that assigns the “pressed” CSS class to the clicked element.
function NoClickDelay(el) {
this.element = typeof el == 'object' ? el : document.getElementById(el);
if( window.Touch ) this.element.addEventListener('touchstart', this, false);
}
NoClickDelay.prototype = {
handleEvent: function(e) {
switch(e.type) {
case 'touchstart': this.onTouchStart(e); break;
case 'touchmove': this.onTouchMove(e); break;
case 'touchend': this.onTouchEnd(e); break;
}
},
onTouchStart: function(e) {
e.preventDefault();
this.moved = false;
this.theTarget = document.elementFromPoint(e.targetTouches[0].clientX, e.targetTouches[0].clientY);
if(this.theTarget.nodeType == 3) this.theTarget = theTarget.parentNode;
this.theTarget.className+= ' pressed';
this.element.addEventListener('touchmove', this, false);
this.element.addEventListener('touchend', this, false);
},
onTouchMove: function(e) {
this.moved = true;
this.theTarget.className = this.theTarget.className.replace(/ ?pressed/gi, '');
},
onTouchEnd: function(e) {
this.element.removeEventListener('touchmove', this, false);
this.element.removeEventListener('touchend', this, false);
if( !this.moved && this.theTarget ) {
this.theTarget.className = this.theTarget.className.replace(/ ?pressed/gi, '');
var theEvent = document.createEvent('MouseEvents');
theEvent.initEvent('click', true, true);
this.theTarget.dispatchEvent(theEvent);
}
this.theTarget = undefined;
}
};
Are you aware of any simpler solution?
Please keep up the good work! Each of your iPhone-related posts has really helped to put things in perspective, and has given me a fresh way to look at iPhone web development. I agree, this platform is really not being taken advantage of, and it really gives us a wide-open playing field to set ourselves apart. Very inspirational. Thanks! I look forward to many more posts like this in the future.
matt, hey
Could you show me how to exacty add that class?? i am developing
http://iwebkit.net and it would be awsome to have hover effects
ontouch instad of when you release. Thanks a lot in advance for your
tips
p.s. i don’t know any javascript, well, a little so please explain a
bit
Thanks!
@Cristopher, I added the code that assigns a style to the clicked element.
@punkassjim, thank you Jim, really appreciated. I’m working on a spinning wheel (slot-machine alike) element for iPhone. I bet you’ll love it
I already know I do!!
Hey are you maybe interested in working together on iwebkit. Currently we are 2 people. Me as an iPhone web designer and johan as a graphics designer. I do some photoshoping too but he does tv webdtsign for the main website.
Our only “weakness” against the other packs is the lack of effects like an Ajax sliding effect, load 25 more elements… Etc… And that’s because we do not know javascript.
I see you are really interesting in webkit development and like me see the fantastic possibilities of it. I touchy you might of liked using your knowledge and passion for the pack since it has been downloaded over 10000 times and I get around 500 to 1000 visitors per day.
If you are interested and want to share your abilities with people from all over the world I think this might be a great opportunity for us both.
Thanks a lot for your work and please contact me
Chris
Ps. Sorry for the spelling :p I’m typing this on my iPod
Thank’s for the great tutorial.
I added it to my latest site and it works very well, the whole interface becomes much more responsive.
However I added it to a website which uses iUi, and something weird happens, when there is a long list to scroll, everytime I flick my finger, instead of scrolling down the list it moves to the topmost part of the slide.
I think this has something to do with having overridden the touchmome event handler.
I tried to fix this myself but unsuccessfully. Have you looked into this and found a solution?
BTW, using your class together with iUi makes for really native looking webApps.
Thanks so much for posting this! This really improves the responsiveness of WebKit based apps on the iPhone… I love the idea of building hybrid apps…. Apps that are native executables, but essentially run a HTML/JS/CSS bundle… and although I was going forward with it, I felt that the UI wasn’t quite as responsive as I’d like… But your posts on decreasing the reaction time for clicks, and how to lock a header and footer for a scrollable area have made a big difference… In case I haven’t been clear enough
Kudos! You Rock!
Brad.
Hi, thanks for this awesome script. This is super helpful as it does provide native speed button clicking on iphone web apps.
I was wondering (because I am not a javascript guru) how I would use this, but still allow scrolling when the mousedown is going on?
If i use this on pages taller than the screen height…you can no longer scroll…I understand the basics of why this happens, but I am not savvy enough to understand how to allow scrolling with the mouse down.
Thanks, and keep up the amazing work.
On my ipod touch v2, there are no such big difference in you demo between onClickEvent (about 200ms) and the touchStartEvent (about 130ms).
But thanks anyway, that’s still 70ms !!
I’d like to do something like this in a jQTouch — but Safari manually fires a second click after I trigger the first one… I’ve tried every combination of preventDefault/stopPropagation/etc. to no avail. It seems that if the user taps quickly, it fires that second click no matter what…
Any thoughts?
I was having the same problem. It’s due to adding a second (or third, fourth, etc) touchstart event handler to existing objects. This fixes it by adding a flag to the objects that have already had the event handler added.
var buttons = document.getElementsByClassName(“button”);
for (i=0;i<buttons.length;i++) {
if (!buttons[i].clickDelayRemoved) {
new NoClickDelay(buttons[i]);
buttons[i].clickDelayRemoved = true;
}
}
I suppose you could add the check to the original code like so, but the above is fine for my needs:
if( window.Touch && !this.element.clickDelayRemoved) {
this.element.addEventListener('touchstart', this, false);
}
hi David,
the first solution that comes to my mind is to place a timeout just after the first click which prevents the second to occur.
Even easier: on the first click set a variable with the target of the click and the timestamp of the event. On the second click
IF firstClickTarget == secondClickTarget AND secondClickTimestamp-firstClickTimestamp<200ms THEN RETURN FALSE ELSE doTheSecondClickA piece of cake
I’m about 2 years late responding to this. I just noticed that I had this problem using jQuery’s element.click() and couldn’t get Matteo’s solution to work without adding in lots of code. For any other readers, I decided to do a quick workaround and specify ‘click’ and ‘touchstart’ depending on the viewer’s device. Using touchstart won’t cause a repeat event, but won’t work with a mouse, naturally.
Loosely:
IF (window.Touch) clickTrigger = ‘click’
ELSE clickTrigger = ‘touchstart’
element.bind(clickTrigger, function () { … })
Matteo, love your work—keep it up, not convinced about dropping jQuery just yet though
This was a great site. I needed to find something for my Homework and This site helped me out so much! Thanx alot!!!!
Thanks for the feedback, but this thing’s still driving me nuts- See: http://davidkaneda.dyndns.org/jqtouch/trunk/demos/main/#home
Notice, if you tap on Ajax > Post, the automatic second click focusses on the input. I can’t wrap my head around how to fix this at all. If you have any interest, feel free to email me directly — I’d love your input. I’ll be happy to give you credit in jQTouch-
Wanted to quote Joe and ask if anyone has found a solution:
——————-
I was wondering (because I am not a javascript guru) how I would use this, but still allow scrolling when the mousedown is going on?
If i use this on pages taller than the screen height…you can no longer scroll…I understand the basics of why this happens, but I am not savvy enough to understand how to allow scrolling with the mouse down.
Thanks, and keep up the amazing work.
——————-
Cheers!
Simplified cross-browser version:
Object.prototype.addOnClick = function(func){ if( window.Touch ){ this.addEventListener('touchstart', function(e){ e.preventDefault(); this.moved = false; this.addEventListener('touchmove', function(){ this.moved = true; }, false); this.addEventListener('touchend', function(){ this.removeEventListener('touchmove', this, false); this.removeEventListener('touchend', this, false); if( !this.moved ) func(); }, false); }, false); } else{ this.onclick = func; } }Usage:
some_element.addOnclick( function(){ alert(‘hello world’); } );
Dear Matteo,
I was very excited when i found your script, but after i used i realized that it breaks scrolling alltogether. Is there something that it can be done for this?
The scroller already implements the delay removal, you don’t need to remove the delayed click inside iScroll.
Using this script has given me fantastic results on a highly interactive site. I have one addition that allows the X and Y coordinates to be passed along (necessary on my site) that you may consider adding.
var theEvent = document.createEvent(‘MouseEvents’);
//theEvent.initEvent(‘click’, true, true);
theEvent.initMouseEvent( ‘click’, true, true, window, 1, e.changedTouches[0].clientX, e.changedTouches[0].clientY, e.changedTouches[0].clientX, e.changedTouches[0].clientY, false, false, true, false, 0, null );
the “perfect” click I think is:
theEvent.initMouseEvent("click", true, true, e.view, 1, e.changedTouches[0].screenX, e.changedTouches[0].screenY, e.changedTouches[0].clientX, e.changedTouches[0].clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey,0, null)I think it’s time to update the script
Ciao, non so se ti ricordi della mail e della richiesta che ti avevo fatto.
Ho aspettato a richiedertelo via mail perchè immaginavo che avesti aggiornato lo script.
Comunque ti ricordo i due “buchi” che avevo riscontrato (io ho usato e uso il primo script e non il secondo script relativo a come applicarlo ad una precisa classe css):
-lo scroll non funziona, ma l’ho risolto spostato il preventdefault nel touchend.
-Ora c’è il problema che lo scroll funziona ma quando scrolli e passi il dito su dei menu in tali menu si evidenzia l’effetto hover del menu anche se non li vuoi cliccare ma ci passi il dito sopra solo per scrollare.
-Ho un altro problema: il select del forms non funziona, o meglio, togliendo il tuo script funziona, lasciandolo funziona ma soltanto se quando ci clicco tengo premuto per un po’ più di tempo il dito.
Aspetto questo aggiornamento, grazie.
Great to have – was beating my head against the wall over this. It appears that safari for iphone/ipad does even worse than suspected: we’ve encountered the kink that if you move the touched element in the window between touchend and click (that 300ms delay), safari fires the event – at the screen coordinate, not on the original target. This means you could clickjack just by swapping elements during that delay window.
Wow… just realized, if the browser is using the more specific initMouseEvent (to fake/substitute a ‘late’ click event), it is specifying the x and y, but (perhaps) assuming the target will be the same… a completely artificial click with properties carried over from the touchend event.
I have one issue that scroll will be killed if preventDefault is called in onTouchStart. So If I also want scroll is supported, what should be the appropriate solution? Thank you!
And I tried fire the click event in onTouchEnd, it seems that two click event (self-created click event and the original click event) happens. PreventDefault and StopPropagation are all called in onTouchEnd, but there are still two click event happens:(
I experienced the two-click problem as well. It only happens occasionally, and it may depend on how long one holds the button.
Hi Iam Prabhu from chennai,joined today in this forum…
Quick question : is there any way to make this work for form fields? I still see noticeable lag on form fields unless I’m doin it wrong.
One issue I encountered is that it breaks native scrolling.
So if I have a ul tag with the id of “main-menu” and apply the NoClickDelay() to that ul#main-menu container I basically can’t scroll to the bottom of my list.
Is there a way to allow the view to scroll if you start your touch on an element inside a NoClickDelay() container and your touch moves? This would be ideal in most scenarios.
I did try removing e.preventDefault(); from inside onTouchStart in your example code but the elements will still act as if they were highlighted.
So if I go that route, removing e.preventDefault() how would you go about removing the “pressed” state immediately when the user begins to scroll?
Hope that makes sense!
Thanks for a wonderful head start!
mh maybe with a timeout. I’d have to make some tests, but shouldn’t be horribly difficult.
Android has same problem with laggy onClicks. Your demo doesn’t work on Android, unless I comment out window.Touch below, so I believe that DOM property is only visible on iOS.
function NoClickDelay(el) {
this.element = el;
// if (window.Touch) not available on android
this.element.addEventListener(‘touchstart’, this, false);
}
With the above change Android gets non-laggy touch event! Here I come phonegap!
Hi,
You should also handle the the event “touchcancel”.
In some cases touchcancel event if fired after a touchstart and not a touchend.
If that happen, with your current implementation, the element will have the class “pressed” for ever.
.-
For me, waiting to call e.preventDefault until the onTouchEnd function allows the native scrolling to still work while still removing the delay when tapping buttons/links.
With this approach you will disable the native scrolling initiated on one button having this event handler attached to it. A better approach is: http://code.google.com/intl/ro-RO/mobile/articles/fast_buttons.html
Didn’t see a visual click indicator as expected. iPad v2.
Great script.
A slight modification would allow for some wiggle room if the movement occurs inside the boundaries of the “button” (the element that has the onClick event attached to it).
function NoClickDelay(el) {
this.element = typeof el == ‘object’ ? el : document.getElementById(el);
if( window.Touch ) this.element.addEventListener(‘touchstart’, this, false);
}
NoClickDelay.prototype = {
handleEvent: function(e) {
switch(e.type) {
case ‘touchstart’: this.onTouchStart(e); break;
case ‘touchmove’: this.onTouchMove(e); break;
case ‘touchend’: this.onTouchEnd(e); break;
}
},
onTouchStart: function(e) {
e.preventDefault();
//e.stopPropagation();
//this.moved = false;
this.theTarget = document.elementFromPoint(e.targetTouches[0].clientX, e.targetTouches[0].clientY);
if(this.theTarget.nodeType == 3) this.theTarget = theTarget.parentNode;
this.checkTarget = this.theTarget;
this.theTarget.className+= ‘ pressed’;
this.element.addEventListener(‘touchmove’, this, false);
this.element.addEventListener(‘touchend’, this, false);
},
onTouchMove: function(e) {
//this.moved = true;
this.checkTarget = document.elementFromPoint(e.targetTouches[0].clientX, e.targetTouches[0].clientY);
if(this.checkTarget.nodeType == 3) this.checkTarget = this.checkTarget.parentNode;
},
onTouchEnd: function(e) {
this.element.removeEventListener(‘touchmove’, this, false);
this.element.removeEventListener(‘touchend’, this, false);
if(this.checkTarget == this.theTarget ) {
//if( !this.moved && this.theTarget ) {
this.theTarget.className = this.theTarget.className.replace(/ ?pressed/gi, ”);
var theEvent = document.createEvent(‘MouseEvents’);
theEvent.initEvent(‘click’, true, true);
this.theTarget.dispatchEvent(theEvent);
}
this.theTarget = undefined;
}
};
Is it possible to replace all “onclick” keywords inside the HTML and other loaded scripts’ with the “onTouchEnd” before the DOM content is not loaded; (if the browser supports onTouchEnd functionality) ?
I think it would be more simple and generic solution.
Has anyone had problems with this working on tags?
I am using it on a table full of various elements and it seems to render my tags unusable.
oh… it broke my HTML.. .I meant <select> tags…
Matteo, this is great work as usual. With the update from 2009/02/27 that includes the “pressed” state class, does the class get added automatically or do I need to do anything else in addition to “new NoClickDelay(document.getElementById(‘element’));”?
I solved it a little bit differently with Zepto https://github.com/suprMax/Zepto-onTouch/blob/master/zepto.onpress.js