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

alex picture alex · Oct 13, 2011 · Viewed 8k times · Source

I am using jQuery UI Sortable.

I am developing a mobile only website, so I looked around how to map touch events to mouse events for this to work, and ended up using some code by Oleg Slobodskoi.

This appeared to work nicely. However, on a mobile device (I am using an iPhone to test, but it also happens on the iPhone simulator), when you attempt to shift a second item (i.e. the next item to sort after you have sorted one already), its initial position is offset either side by about the width of the element.

Example

When I attempt the second time to drag one of these pink boxes, instead of appearing underneath my finger and shifting with it, it starts about the width of a box away, to the left or right, and then jumps under my finger.

I've set up an example on jsFiddle. Hit the attach button to add jQuery Sortable to the list items.

I have tried many things, including playing with the change event and attempting to realign it back, but I couldn't get it to reliably work.

How can I stop this problem?

Answer

N Rohler picture N Rohler · Oct 22, 2011

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.