> Remove onClick delay on webkit for iPhone

Stop Watch

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?

/Share the joy

/Reactions

    • Author: Constantine Mureev
    • Posted on: 2012/08/20
    • At: 08:25
    • Author: LUIS
    • Posted on: 2012/08/26
    • At: 15:06

    hi,

    I’ve tried your code to improve the behavior of the JQM events and the delay which they have after clicking a button element, but I have a problem: after clicking the class ui-btn-active is still on the element and it isn’t removed. I’m not a totally newbie on JQ but, but I’m learning about the events handler, so, I’ll appreciate a little help.

    Thanks in advance!
    Luis Miguel.

    Reply
  • Great bit of code.

    I modified it a bit to allow the finger to move up to 50 pixels before canceling the click.

    I am confused with .prototype. It seems that somehow handleEvent gets called. How?


    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.x = e.targetTouches[0].clientX;
    this.y = e.targetTouches[0].clientY;
    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) {
    var x = e.targetTouches[0].clientX;
    var y = e.targetTouches[0].clientY;
    if( Math.sqrt(Math.pow(x-this.x,2)+Math.pow(y-this.y,2))>50){
    this.moved = true;
    this.theTarget.className = this.theTarget.className.replace(/ ?pressed/gi, '');
    this.theTarget.className = this.theTarget.className.replace(/ ?active/gi, '');
    } else {
    if(this.moved==true){
    this.moved=false;
    this.theTarget.className+= ' pressed';
    }
    }
    },
    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, '');
    this.theTarget.className+= ' active';
    var theEvent = document.createEvent('MouseEvents');
    theEvent.initEvent('click', true, true);
    this.theTarget.dispatchEvent(theEvent);
    }
    this.theTarget = undefined;
    }
    };

    Reply
  • Nice script! I made it into a jQuery plug-in and added a function to make the script set focus on the element selected after firing the click event. I found this fixes an issue where input fields are not properly selected on an iPhone if they are within the NoClickDelay element.

    Code:

    (function( $ ) {
    $.fn.noClickDelay = function() {

    var $wrapper = this;
    var $target = this;
    var moved = false;

    $wrapper.bind('touchstart mousedown',function(e) {
    e.preventDefault();
    moved = false;
    $target = $(e.target);
    if($target.nodeType == 3) {
    $target = $($target.parent());
    }
    $target.addClass('pressed');

    $wrapper.bind('touchmove mousemove',function(e) {
    moved = true;
    $target.removeClass('pressed');
    });

    $wrapper.bind('touchend mouseup',function(e) {
    $wrapper.unbind('mousemove touchmove');
    $wrapper.unbind('mouseup touchend');
    if(!moved && $target.length) {
    $target.removeClass('pressed');
    $target.trigger('click');
    $target.focus();
    }
    });
    });

    };
    })( jQuery );

    Use as such:
    $('#wrapperElement').noClickDelay();

    Reply
    • Author: mundi
    • Posted on: 2013/01/26
    • At: 22:55

    I my iphone4 I get a click delay of 560ms!!

    What are apple smoking?

    Even with your improvement its still 153ms seconds… better than 560.

    Reply

/Leave a reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>