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.
Feb 2009
15
Replies
21

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?

Advetise on Cubiq.org. Reach über-specialized audience, sponsor open source MIT licensed software, improve your karma. Text-only AD for just $30/month. Contact me for details.

Reactions

  1. punkassjim2009/02/17 at 00:05
    Reply

    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.

     
  2. Cristopher2009/02/26 at 23:30
    Reply

    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!

     
  3. Matteo Spinelli2009/02/27 at 10:12
    Reply

    @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 :)

     
  4. Christopher2009/02/27 at 17:51
    Reply

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

     
  5. Lugpolla2009/03/30 at 09:54
    Reply

    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.

     
  6. Brad Parks2009/04/05 at 03:09
    Reply

    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.

     
  7. Joe Russ2009/06/16 at 22:08
    Reply

    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.

     
  8. Sam2009/08/13 at 13:09
    Reply

    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 !!

     
  9. David Kaneda2009/09/19 at 11:03
    Reply

    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?

     
  10. Matteo Spinelli2009/09/20 at 09:24
    Reply

    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 doTheSecondClick
    A piece of cake :)

     
  11. guide2009/10/01 at 11:24
    Reply

    This was a great site. I needed to find something for my Homework and This site helped me out so much! Thanx alot!!!!

     
  12. David Kaneda2009/10/02 at 19:55
    Reply

    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-

     
  13. rit2009/10/07 at 16:40
    Reply

    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!

     
  14. kevin2009/11/19 at 19:54
    Reply

    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’); } );

     
  15. pingback iPhone Click Event Delay | The True Tribe2010/01/23 at 00:34
  16. themis2010/05/26 at 16:43
    Reply

    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?

     
    • Matteo Spinelli2010/05/26 at 17:00

      The scroller already implements the delay removal, you don’t need to remove the delayed click inside iScroll.

       
  17. Neil2010/06/07 at 07:15
    Reply

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

     
    • Matteo Spinelli2010/06/07 at 08:15

      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 :)

       
  18. Alberto2010/06/13 at 10:21
    Reply

    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.

     

Speak your mind