The Story of jQuery UI Touch Punch

Hello, @furfa. k. a. Dave Furfero

UI Engineer(Unemployed Interface Engineer)

tl;dr

jQuery UI Touch Punch is a small hack that enables the use of touch events on sites using jQuery UI.

This is the story of a small piece of code that was almost never written yet now runs on over 60 million websites.

It's a story about helping others, sharing your work and following your creative impulses without regard for the consequences.

And the mother*bleep*ing consequences!

October 2010

Meet Caleb

Ack.
Acktual Photo.

"Any rebroadcast, retransmission, or account of this anecdote, without the express written consent of Major League Baseball, is prohibited."

Now Back to Caleb

The Problem

jQuery UI was written to support mouse events.

iOS generates touch events.*

* and some mouse events

(jQuery UI 1.8 was released two weeks before the iPad.)

Mouse & Touch Events

MouseTouch
Movement
  • mouseover
  • mouseenter
  • mousemove
  • mouseout
  • mouseleave
  • touchstart
  • touchmove
  • touchend
Action
  • mousedown
  • mouseup
  • click
  • touchstart
  • touchmove
  • touchend
  • mouseover
  • mouseenter
  • mousemove
  • mousedown
  • mouseup
  • click

The Solution

Make fingers sound like mice!

Touch2MouseABC, BBD…

TouchMouse
  • touchstart
  • mouseover
  • mousemove
  • mousedown
  • touchmove
  • mousemove
  • touchend
  • mouseup
  • mouseout
  • click
(function ($) {

  $.support.touch = typeof Touch === 'object';
  
  if (!$.support.touch) {
    return;
  }
  
  var $doc        = $(document),
      mouseProto  = $.ui.mouse.prototype,
      _mouseInit  = mouseProto._mouseInit,
      touchHandled;
      
  // Translate touch events to mouse events
  function translateEvent (event, translatedType) {
  
    var touch = event.originalEvent.changedTouches[0];
    
    return $.extend(event, {
      type:    'mouse' + translatedType,
      which:   1,
      pageX:   touch.pageX,
      pageY:   touch.pageY,
      screenX: touch.screenX,
      screenY: touch.screenY,
      clientX: touch.clientX,
      clientY: touch.clientY
    });
  }
  
  function _touchStart (event) {
  
    if (touchHandled || event.originalEvent.touches.length > 1) {
      return;
    }
    
    touchHandled = true;
    
    var self = this;
    
    // Map touchstart to mouseover
    $(event.target).trigger(translateEvent(event, 'over'));
    
    $doc.bind('touchmove.' + self.widgetName, self._touchMove)
        .bind('touchend.' + self.widgetName, self._touchEnd)
        .trigger('mousedown');
        
    // Map touchstart to mousedown
    self._mouseDown(translateEvent(event, 'down'));
  }
  
  function _touchMove (event) {
  
    var self = this;
    
    if (event.originalEvent.touches.length > 1) {
      self._touchEnd(event);
    } else {
      // Map touchstart to mousemove
      self._mouseMove(translateEvent(event, 'move'));
    }
  }
  
  function _touchEnd (event) {
  
    var self = this;
    
    // Map touchstart to mouseup
    self._mouseUp(translateEvent(event, 'up'));
    
    $doc.unbind('touchmove.' + self.widgetName, self._touchMove)
        .unbind('touchend.' + self.widgetName, self._touchEnd);
        
    touchHandled = false;
    
    // Map touchstart to mouseout
    $(event.target).trigger(translateEvent(event, 'out'));
  }
  
  mouseProto._mouseInit = function () {
  
    var self = this;
    
    self._touchStart = $.proxy(_touchStart, self);
    self._touchMove = $.proxy(_touchMove, self);
    self._touchEnd = $.proxy(_touchEnd, self);
    
    self.element.bind('touchstart.' + self.widgetName, self._touchStart);
    
    _mouseInit.call(self);
  };
  
})(jQuery);      

The Approach

Do The Right Thing

  1. Fork jQuery UI on GitHub.
  2. Open jQuery UI in my favorite text editor.
  3. Find all mouse listeners and add analogous touch event listeners.
  4. Write corresponding tests.
  5. Submit a pull request.
  6. Wait.

or..

DUCK PUNCH!

Duck Punching*a. k. a. Monkey Patching

* Not to be confused with Donkey Punching

[dek penCH]

noun a way to extend or modify the run-time code of a dynamic language without altering the original code.

Not "Donkey Punch"

How to Fulfill Your Own Feature Request -or- Duck Punching With jQuery!


@paul_irish did that first.

"...if it walks like a duck and talks like a duck, it’s a duck, right? So if this duck is not giving you the noise that you want, you’ve got to just punch that duck until it returns what you expect."

Patrick Ewing

Who's Down With AOP?Aspect-Oriented Programming

A programming approach that allows you to add hooks called "advice" before and/or after object methods.

JavaScript Facts(Assuming Assumptions Are Also Facts)

  1. Very little in JavaScript is truly private
  2. A usable API is inherently public
  3. Therefore, we can overwrite API functions to add advice or completely replace functionality.

var API = {
  method: function () {
    // ...
  }
};

// Cache the method locally
var method = API.method;

// Override the existing method on the object or prototype
API.method = function () {

  // Do something before... modify arguments, logging, etc.
  
  // (Optionally) Execute the original method in the current scope
  var result = method.apply(this, arguments);
  
  // Do something after... modify results, logging, etc.
  
  return result;
};

The Rules of Duck Punch

  1. There are no rules
  2. You do not talk about Duck Punch.
  3. Respect the original API.

Rule # 1Respect the API

Maintain the functional interface

  • Receive the same arguments
  • Return the same value type

Rule # 2You do not talk
about Duck Punch.

Back to the Solution

(function ($) {

  $.support.touch = typeof Touch === 'object';
  
  if (!$.support.touch) {
    return;
  }
  
  var $doc        = $(document),
  
      // Cache the method locally
      mouseProto  = $.ui.mouse.prototype,
      _mouseInit  = mouseProto._mouseInit,
      
      touchHandled;
      
  function translateEvent (event, translatedType) {
  
    var touch = event.originalEvent.changedTouches[0];
    
    return $.extend(event, {
      type:    'mouse' + translatedType,
      which:   1,
      pageX:   touch.pageX,
      pageY:   touch.pageY,
      screenX: touch.screenX,
      screenY: touch.screenY,
      clientX: touch.clientX,
      clientY: touch.clientY
    });
  }
  
  function _touchStart (event) {
  
    if (touchHandled || event.originalEvent.touches.length > 1) {
      return;
    }
    
    touchHandled = true;
    
    var self = this;
    
    $(event.target).trigger(translateEvent(event, 'over'));
    
    $doc.bind('touchmove.' + self.widgetName, self._touchMove)
        .bind('touchend.' + self.widgetName, self._touchEnd)
        .trigger('mousedown');
        
    self._mouseDown(translateEvent(event, 'down'));
  }
  
  function _touchMove (event) {
  
    var self = this;
    
    if (event.originalEvent.touches.length > 1) {
      self._touchEnd(event);
    } else {
      self._mouseMove(translateEvent(event, 'move'));
    }
  }
  
  function _touchEnd (event) {
  
    var self = this;
    
    self._mouseUp(translateEvent(event, 'up'));
    
    $doc.unbind('touchmove.' + self.widgetName, self._touchMove)
        .unbind('touchend.' + self.widgetName, self._touchEnd);
        
    touchHandled = false;
    
    $(event.target).trigger(translateEvent(event, 'out'));
  }
  
  // Override the existing method on the object or prototype
  mouseProto._mouseInit = function () {
  
    // Do something before... modify arguments, logging, etc.
    var self = this;
    
    self._touchStart = $.proxy(_touchStart, self);
    self._touchMove = $.proxy(_touchMove, self);
    self._touchEnd = $.proxy(_touchEnd, self);
    
    self.element.bind('touchstart.' + self.widgetName, self._touchStart);
    
    // Execute the original method in the current scope
    _mouseInit.call(self);
  };
  
})(jQuery);      

Fin?

git init
git add README.md
git commit -m "first"
git remote add origin https://github.com/furf/jquery-ui-touch-punch.git
git push -u origin master
November 2009

Getting Involved

My First Dev Conference

jQuery Conference 2009 Boston
Jupiter Room, 11:45am

“Get Involved”@brandonaaron

  • Just do it!
  • File issues, open tickets, report bugs.
  • Fix bugs and submit pull requests.
  • Write tests.
  • Publish original work.
  • There will always be critics; ignore the haters.
  • Because, you never know who you will inspire…

15 minutes later...

Two days later...

$.deep

var obj = {
  deeply: {
    nested: {
      value: 42
    }
  }
};

$.deep(obj, 'deeply.nested.value'); // 42
$.deep(obj, 'deeply.nested.bird'); // undefined

$.deep(obj, 'deeply.held.belief', 'skateboarding is not a crime.');
// {
//   deeply: {
//     nested: {
//       value: 42
//     },
//     held: {
//       belief: 'skateboarding is not a crime.'
//     }
//   }
// }
furf/jquery-etc

Speaking of Bugs…

November 2011

You've Got Bugs

By November 2011, there were 11 issues logged against Touch Punch.

I didn't even think there were 11 people using Touch Punch!

If that then…

  • 11 people filing issues on GitHub
  • 11,000 people wondering where to file issues
  • 11,000,000 people who don't know they have issues
  • 11,000,000,000 people using jQuery UI Touch Punch!

11 Billion People!

Googling "Touch Punch"

  1. Some things shouldn't be Googled.
  2. Touch Punch was being discussed on Stack Overflow as a de facto fix for the missing touch events in jQuery UI.

Even My Issues Got Issues

(Grandissues?)

Cohabitation

Simulated Events

Fake It 'til You Make ItAn Introduction to Simulated Events

Easy as…

  1. Create an event
    document.createEvent('MouseEvents');
  2. Initialize the event
    event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0,
      false, false, false, false, 0, null);
  3. Dispatch the event
    element.dispatchEvent(event);
    

1. document.createEvent

document.createEvent - MDN

var event = document.createEvent(type);
type

a string that represents the type of event to be created.

Possible event types include:

  • "UIEvents"
  • "MouseEvents"
  • "MutationEvents"
  • "HTMLEvents"

2. event.initMouseEvent

event.initMouseEvent - MDN

eventsent.initMouseEvent(type, canBubble, cancelable, view, 
  detail, screenX, screenY, clientX, clientY, 
  ctrlKey, altKey, shiftKey, metaKey, 
  button, relatedTarget);

Argument Clinic

type the string to set the event's type to. Possible types for mouse events include: click, mousedown, mouseup, mouseover, mousemove, mouseout.
canBubble whether or not the event can bubble.
cancelable whether or not the event's default action can be prevented.
view the Event's AbstractView. You should pass the window object here.
detail the Event's mouse click count.
screenX the Event's screen x coordinate.
screenY the Event's screen y coordinate.
clientX the Event's client x coordinate.
clientY the Event's client y coordinate.
ctrlKey whether or not control key was depressed during the Event.
altKey whether or not alt key was depressed during the Event.
shiftKey whether or not shift key was depressed during the Event.
metaKey whether or not meta key was depressed during the Event.
button the Event's mouse event.button.
relatedTarget the Event's related EventTarget. Only used with some event types (e.g. mouseover and mouseout). In other cases, pass null.

3. element.dispatchEvent

element.dispatchEvent - MDN

var notPrevented = element.dispatchEvent(event);

Putting It All Together

function simulateClick (element) {

  // 1. Create an event
  var event = document.createEvent('MouseEvents');
  
  // 2. Initialize the event
  event.initMouseEvent('click', true, true, window,
    0, 0, 0, 0, 0, false, false, false, false, 0, null);
    
  // 3. Dispatch the event
  if (element.dispatchEvent(event)) {              
    alert('Allowed!');
  } else {
    alert('Prevented.');
  }
}

Return to
Back to the Solution

(function ($) {

  // Detect touch support
  $.support.touch = 'ontouchend' in document;
  
  // Ignore browsers without touch support
  if (!$.support.touch) {
    return;
  }
  
  var mouseProto = $.ui.mouse.prototype,
      _mouseInit = mouseProto._mouseInit,
      touchHandled;
      
  /**
   * Simulate a mouse event based on a corresponding touch event
   * @param {Object} event A touch event
   * @param {String} simulatedType The corresponding mouse event
   */
  function simulateMouseEvent (event, simulatedType) {
  
    // Ignore multi-touch events
    if (event.originalEvent.touches.length > 1) {
      return;
    }
    
    event.preventDefault();
    
    var touch = event.originalEvent.changedTouches[0],
        simulatedEvent = document.createEvent('MouseEvents');
        
    // Initialize the simulated mouse event using the touch event's coordinates
    simulatedEvent.initMouseEvent(
      simulatedType,    // type
      true,             // bubbles                    
      true,             // cancelable                 
      window,           // view                       
      1,                // detail                     
      touch.screenX,    // screenX                    
      touch.screenY,    // screenY                    
      touch.clientX,    // clientX                    
      touch.clientY,    // clientY                    
      false,            // ctrlKey                    
      false,            // altKey                     
      false,            // shiftKey                   
      false,            // metaKey                    
      0,                // button                     
      null              // relatedTarget              
    );
    
    // Dispatch the simulated event to the target element
    event.target.dispatchEvent(simulatedEvent);
  }
  
  /**
   * Handle the jQuery UI widget's touchstart events
   * @param {Object} event The widget element's touchstart event
   */
  mouseProto._touchStart = function (event) {
  
    var self = this;
    
    // Ignore the event if another widget is already being handled
    if (touchHandled || !self._mouseCapture(event.originalEvent.changedTouches[0])) {
      return;
    }
    
    // Set the flag to prevent other widgets from inheriting the touch event
    touchHandled = true;
    
    // Track movement to determine if interaction was a click
    self._touchMoved = false;
    
    // Simulate the mouseover event
    simulateMouseEvent(event, 'mouseover');
    
    // Simulate the mousemove event
    simulateMouseEvent(event, 'mousemove');
    
    // Simulate the mousedown event
    simulateMouseEvent(event, 'mousedown');
  };
  
  /**
   * Handle the jQuery UI widget's touchmove events
   * @param {Object} event The document's touchmove event
   */
  mouseProto._touchMove = function (event) {
  
    // Ignore event if not handled
    if (!touchHandled) {
      return;
    }
    
    // Interaction was not a click
    this._touchMoved = true;
    
    // Simulate the mousemove event
    simulateMouseEvent(event, 'mousemove');
  };
  
  /**
   * Handle the jQuery UI widget's touchend events
   * @param {Object} event The document's touchend event
   */
  mouseProto._touchEnd = function (event) {
  
    // Ignore event if not handled
    if (!touchHandled) {
      return;
    }
    
    // Simulate the mouseup event
    simulateMouseEvent(event, 'mouseup');
    
    // Simulate the mouseout event
    simulateMouseEvent(event, 'mouseout');
    
    // If the touch interaction did not move, it should trigger a click
    if (!this._touchMoved) {
    
      // Simulate the click event
      simulateMouseEvent(event, 'click');
    }
    
    // Unset the flag to allow other widgets to inherit the touch event
    touchHandled = false;
  };
  
  /**
   * A duck punch of the $.ui.mouse _mouseInit method to support touch events.
   * This method extends the widget with bound touch event handlers that
   * translate touch events to mouse events and pass them to the widget's
   * original mouse event handling methods.
   */
  mouseProto._mouseInit = function () {
  
    var self = this;
    
    // Delegate the touch handlers to the widget's element
    self.element
      .bind('touchstart', $.proxy(self, '_touchStart'))
      .bind('touchmove', $.proxy(self, '_touchMove'))
      .bind('touchend', $.proxy(self, '_touchEnd'));
      
    // Call the original $.ui.mouse init method
    _mouseInit.call(self);
  };
  
})(jQuery);

Abracadabra!

Simulated Events fixed all 11 issues.

I'm Blowin' Up!

I'm International!

March 2012

Eurosport

Unicorns?

Kittens?

Make Lemonade?

Not. Good. Enough!

The Sexiest Page on the Internet

That Did It!

What Me Troll?

Crock did that first.

Be Mean

for(;;){}
http://json.org/json.js

Be Gentle

(function ($) {

  if (!(/mysite\.com$/).test(location.hostname)) {
  
    // 1. Warn 
    alert('It is extremely unwise to load code from servers you do not control.');
    
    // 2. Log
    $.getScript('http://mysite.com/hotlink?l=' + location.href + '&t=' + document.title); 
    
    // 3. Fail
    return false;
  }
  
  // ...
  
})(jQuery);
March 2012

WordPress

March 24, 2012

May 3, 2012

"For touch-supportive devices, we’ve added a fantastic jQuery extension to the core, jQuery UI Touch Punch. This has enabled ‘drag and drop’ support on mobile devices. Whether editing a post, customizing the dashboard, or modifying a Nav Menu, you’re now able to easily reposition items on a touch interface to your heart’s content, just as you would on your desktop browser."

If I Had a Nickel…

$(3,348,456.25);

And I'm all…

"I can finally buy skis!"
July 2012

Finally!

On Tuesday, July 3, 2012 at 9:39 AM, Michael wrote:

Just wanted to say a big thanks for Touch Punch. I was running into a
wall with the home run derby form for iPad and Amo found it using a 
google search. Works perfectly.
 
So, thanks furf!
March 2013

Paying It Forward

Meet Dave

  • Talented writer
  • Talented comedian
  • Loves music
  • Not a talented musician
  • Quit his high-paying job to inspire others
theacousticguitarproject.com

One guitar. One week. One Song.

  1. A musician is provided an acoustic guitar and a handheld recorder.
  2. That musician has one week to record an original song using only the equipment provided. No editing.
  3. When the musician is finished, they choose the next musician to participate, and the guitar becomes a viral memento with its own path.

One city. Two city. Three city…

  1. New York, NY
  2. Helsinki, Finland
  3. Bogotá, Columbia
  4. Port-au-Prince, Haiti

The TV Pilot

Donors vote on the next city.

One Dave Inspires Another

Regarding Henry

One of my followers (a good friend), whom I have helped over the years, saw that tweet and clicked the link.

Funded!

Henry's donation pushed The Acoustic Guitar Project across the funding threshold.

And the winner is…

  1. New York, NY
  2. Helsinki, Finland
  3. Bogotá, Columbia
  4. Port-au-Prince, Haiti
  5. Detroit, MI!
Today

jQuery Conference 2013Portland, OR

jQuery UI 1.13


"rewrite of mouse but with full mouse/touch/pointer support"

jQuery UI Roadmap

In the Meanwhile…

A lot of users

A lot of issues

With scary terms like:

  • Android
  • Galaxy
  • Windows
  • Jelly Beans

One Man

Two Devices

Maybe I can fix this one?

Tomorrow

Hug their FacesThey Do The Right Thing

The jQuery Project is run by a distributed group of volunteers that all want to see jQuery become the best JavaScript tool possible.

jquery.org/team

Join

jquery.org/join

"Get Involved"

  • Just do it!
  • File issues, open tickets, report bugs.
  • Fix bugs and submit pull requests.
  • Write tests.
  • Publish original work.
  • There will always be critics; ignore the haters.
  • Because, you never know who you will inspire…

Thanks!

touchpunch.furf.com