What is going on with jQuery UI Sortable on a touch screen device?

Super simple solution for this one. I wish I could say it was super simple to find, but no... it took a while.

To clarify the symptom, it's that the initially-dragged element is always the one that is dragged on subsequent drags. If you start out dragging b, on subsequent drags b is the one that always moved. Likewise for a and c.

This made me question if perhaps the event was being "recycled." I confirmed that the pageX and pageY values were correct on the touchstart (and touchmove) event, but the values getting to _mouseDown in Sortable were wrong. So, I went to jquery.ui.mouse.js and looked at _mouseDown there. I confirmed that the proper values were coming through, but that the handler was exiting at this line at the top of the method:

// don't let more than one widget handle mouseStart
if( mouseHandled ) { return };

So, I started looking at mouseHandled. There was only one line where this was reset back to false - the following listener at the top:

$( document ).mouseup( function( e ) {
    mouseHandled = false;
});

I realized I must be close. I looked back at the _touchEnd method in the compatibility add-in you're using:

_touchEnd: function(event) {
   this.element.unbind("touchmove." + this.widgetName).unbind("touchend." + this.widgetName);
   this._mouseUp(event);
}, 

Note that _mouseUp is only called on the widget itself -- not the document! Thus, clearly mouseHandled was never being reset. So, I added a document dispatch line to _touchEnd:

_touchEnd: function(event) {
   this.element.unbind("touchmove." + this.widgetName).unbind("touchend." + this.widgetName);
   this._mouseUp(event);
   $(document).trigger('mouseup', event);
}, 

And presto, everything worked correctly.

So, in summary, this one line is the magic one:

$(document).trigger('mouseup', event);

Working forked fiddle here [direct link for iOS viewing].


Note: I also changed this line:

/ iPad | iPhone /.test(navigator.userAgent) && (function($) {

To:

/iPad|iPhone|iPod/.test(navigator.userAgent) && (function($) {

Because it didn't work properly with spaces, and you should include support for the iPod touch.