Skip to main content
Ben Nadel at NCDevCon 2011 (Raleigh, NC) with: Dave Ferguson
Ben Nadel at NCDevCon 2011 (Raleigh, NC) with: Dave Ferguson

Mouse vs. Keyboard - Determining Click Initiator Using A jQuery Custom Event

By
Published in Comments (7)

When filling out online forms, I love to use my keyword as a means to both provide information as well as to navigate from form field to form field. This works great; but from time to time, an "itchy Tab finger" causes me to accidentally hit "Enter" on an inappropriate form element (such as a Cancel link). Falling victim to this problem the other day, I wondered if there was a way to determine which device - the mouse or the keyboard - triggered the "click" event. If I could, then I thought it might provide an opportunity to confirm an action if, say, a Cancel link were triggered from the keyboard rather than the mouse.

There are two ways for a user to activate a link: either clicking the link directly with the mouse; or, bringing the link into focus and then hitting the Enter key on the keyboard. Both of these actions trigger a "click" event. But, after I looked at the Event object being produced, I couldn't find a consistent, cross-browser way to determine which device initiated the event.

As such, I thought I might try to create a custom jQuery Event Type - "clickwith" - that would piggyback the native click event and include keyboard conditions during the event dispatch.

Before I get into the code, I should warn you that the jQuery custom Event system is a bit of mystery to me. I can get it to, "work." But, I don't necessarily have a great mental model of how it all fits together or how the underlying events are bound and dispatched. And, forget about including data and namespaces in the mix - that's already way beyond my current understanding.

That said, the idea behind this custom event, clickwith, is that we'll bind some key events as well as a click event. The key events - keyup and keydown - will keep track of key activity surrounding the click event. Then, when the custom "clickwith" event is being dispatched, it can use this key-based metadata as part of the outgoing event data.

The pseudo-code for the approach is as follows:

  • If key pressed and key is Enter, set flag to True.
  • If click event is raised and flag is true, announce keyboard.
  • If click event is raised and flag is false, announce mouse.
  • If key released, set flag to false.

As you can see, the keyboard events simply set a flag that is used within the click event handler. Ok, let's take a look at the code:

<!DOCTYPE html>
<html>
<head>
	<title>Determine Link Trigger Method With jQuery</title>
</head>
<body>

	<h1>
		Determine Link Trigger Method With jQuery
	</h1>

	<p>
		<a href="#" class="link">Click me please</a>!<br />
		<a href="#" class="link">Click me please</a>!<br />
		<a href="#" class="link">Click me please</a>!<br />
		<a href="#" class="link">Click me please</a>!<br />
	</p>


	<!-- Configure scripts. -->
	<script type="text/javascript" src="../jquery-1.7.1.js"></script>
	<script type="text/javascript">


		// Set up a special Event Type which will help us to
		// determine what physical device was used to initiate the
		// click event on a given element: Mouse or Keywboard. Since
		// there doesn't appear to be anything inherent to the click
		// event that denotes device (cross-browser), we'll have to
		// use some surrounding events to setup the click event
		// handler data. The default device is considered the Mouse;
		// the alternate device is the keyboard.
		(function( $ ){


			// When a key is depressed, we want to signal that this
			// might be a keyboard-initiated click event. As such,
			// we'll store a boolean to be used in the click event.
			function handleKeyDown( event ){

				// Check to make sure that this key is one that is
				// capable of triggering a click event (ie. the enter
				// button, 13).
				if (event.which === 13){

					$.data( this, "clickwithevent:keyboard", true );

				}

			}

			// When a key is released, we know that the click event
			// will have already take place (if at all); as such, we
			// set the boolean flag to false since any subsequent
			// click event will be triggered by a mouse (or preceeded
			// by a keydown event).
			function handleKeyUp( event ){

				$.data( this, "clickwithevent:keyboard", false );

			}

			// When the click event is triggered, we need to
			// determine if the event was initiated by the keyboard
			// or the mouse. If the boolean flag is true, it means
			// that the click event was preceeded by the depression
			// of the Enter key, which indicates that the click event
			// was initiated by the keyboard.
			function handleClick( event ){

				// Get the flag for keyboard-based click.
				var isKeyPress = $.data( this, "clickwithevent:keyboard" );

				// Let's create a new event that extends the click
				// event. This way, when we trigger our "clickwith"
				// event, we get all of the contextual information
				// associated with the click; but, we don't
				// accidientally trigger click events.
				var clickEvent = createEvent( "clickwith", event )

				// Tell jQuery to trigger the new event with a second
				// argument that indicates the initiator of the click
				// event.
				$.event.handle.apply(
					this,
					[clickEvent, (isKeyPress ? "keyboard" : "mouse")]
				);

			}

			// I create a new jQuery event object using the given
			// event object as the collection of properties to copy.
			// This way, we can "extend" an existing Event object
			// without worrying about copying data we shouldn't.
			function createEvent( eventType, event ){

				// For each event object, we will try to copy all of
				// the following properties that are available.
				var properties = [
					"altKey", "bubbles", "button", "cancelable",
					"charCode", "clientX", "clientY", "ctrlKey",
					"currentTarget", "data", "detail", "eventPhase",
					"metaKey", "offsetX", "offsetY", "originalTarget",
					"pageX", "pageY", "prevValue", "relatedTarget",
					"screenX", "screenY", "shiftKey", "target",
					"view", "which"
				];

				// Create a new properties object that will be used
				// to create the new event.
				var eventProperties = {}

				// Copy over all properties from the old event.
				$.each(
					properties,
					function( index, property ){

						// Make sure this property is available on
						// the original event.
						if (property in event){

							// Copy it over to the new event property
							// collection.
							eventProperties[ property ] = event[ property ];

						}

					}
				);

				// Create and return the new event object with the
				// duplicated properties.
				return(
					new $.Event( eventType, eventProperties )
				);

			}


			// Configure the special event, "clickwith", so that
			// jQuery knows how to bind and unbind event handlers.
			$.event.special.clickwith = {

				// I configure each element that is bound to the
				// clickwith event. I am only called once per element.
				setup: function( data, namespaces ){

					// Set up the key events that surround the click
					// events that setup the meta data.
					$( this )
						.data( "clickwithevent:keyboard", false )
						.on( "keydown.clickwithevent", handleKeyDown )
						.on( "keyup.clickwithevent", handleKeyUp )
						.on( "click.clickwithevent", handleClick )
					;

				},

				// I remove the configuration from each element that
				// is bound to the clickwith event. I am only called
				// oncer per element.s
				teardown: function( namespaces ){

					// Remove all traces of the special event.
					$( this )
						.removeData( "clickwithevent:keyboard" )
						.off( "keydown.clickwithevent" )
						.off( "keyup.clickwithevent" )
						.off( "click.clickwithevent" )
					;

				}

			};


		})( jQuery );


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// Make sure this event works on a direct event binding.
		$( "a.link" ).on(
			"clickwith",
			function( event, trigger ){

				console.log( "LOCAL[ " + trigger + " ]", event );

			}
		);

		// Make sure this event works on a delegated event binding.
		$( document ).on(
			"clickwith",
			"a.link",
			function( event, trigger ){

				console.log( "GLOBAL[ " + trigger + " ]", event );

			}
		);

		// Try manually triggering a click event (which should, in
		// turn, trigger a clickwith event, using the Mouse as the
		// default device trigger).
		$( "a.link:first" )
			.trigger( "click" )
		;

	</script>
</body>
</html>

Again, the way the special Events work in jQuery is not something that I have a strong grasp of. As such, I won't try to explain this code too much. If you look at the video above, however, you will see that this is working both for locally-bound events as well as delegated event bindings.

For a very in-depth exploration of jQuery special events, take a look at this article by Ben Alman - his understanding is a world better than mine.

Want to use code from this post? Check out the license.

Reader Comments

7 Comments

"...I love to use my keyword as a means to both provide information as well as to navigate from form field to form field."

Maverick!

1 Comments

what if u monkey patch the jQuery click event to return a property like let's say origin: "keyboard" or origin: "mouse"?

1 Comments

I am curious if the new movement toward touch screens might negate this question altogether? Windows 8 is designed to move us to that environment

2 Comments

Thanks Ben, that's very useful. I was going round the bend trying to figure out the source of a click event. @ross: touchscreens might make this more relevant, not less. In fact, I think there's a case for building this into jquery core, so that you could easily query an origin property on any click event : mouse, keyboard, or touch.

2 Comments

On second thought, if you look at the top level of the jquery Event object, it is pretty weird that keyboard events are in fact stored as mouse events. But if you dig a little deeper into the object, you can see that X-Y coordinates are stored for each click. For example, "clientX". For clicks that come from the enter key, these coordinates are always zero. (Nobody ever mouse-clicks at 0,0.) This essentially creates a simple hook that you can use within click event handlers to discriminate keyboard "clicks" from mouse "clicks".

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel