HTML5, CSS3, jQuery, JSON, Responsive Design...

Smooth scrolling, sortable web list for iPhone!

Michael Brown  June 20 2012 11:21:23 PM
Update 2 April 2016

How times change.

Four years ago, this stuff felt so cutting edge.  Now it just looks laughable!  With the advent of frameworks like React Native, the days of trying to impersonate native look and feel using just HTML, JavaScript and CSS are well and truly over.

Following Facebook's own React Native Tutorial, I was able to set up an iPhone list in under an hour.  And it's silky smooth, with inertial scrolling, top and bottom bounce: the works!  It feels just like native, because under the covers, that's exactly what it is.  More to come on this in a later post...

I've been working on a mobile web app that will use a jQueryUI's .sortable() functionality to enable drag and drop sorting of that list.   Sound easy enough, but I ran into several issues that I was, thankfully, able to work around.

Below is a picture of what I'm aiming for.  (Click to see it bigger.)  Note how row 3 is being dragged over row 5 by my very own finger!

Image:Smooth scrolling, sortable web list for iPhone!

Version 1 - Simple code, crap scrolling!

So here's Version 1.  Grab one of the labels with your finger (or mouse if not on a touchscreen) and you can drag and drop to resort the list.

The code's simple enough: an unordered ul list (id = listItems) inside two divs: an inner div (id = scroller) and an outer div (id = wrapper).    You can see that I've fixed position header and footer elements (also divs) surrounding them.  This is where you'd put your buttons and titles on a mobile app.  The UL list is made sortable like so:

handle : "label",
axis : 'y',
disabled : false

There's just one problem though: if you're on an iPhone, you may notice that the ordinary scrolling speed (i.e. when you're not sorting the list) is well, ... rubbish!  It's excruciatingly slow.   Where's the springiness and momentum that you would normally associate with iPhone scrolling?  When you flip the list up or down and let go, the scrolling just stops!  That's not what's supposed to happen on smartphones.

Version 2 - Enter iScroll

It seems that's just the way things are on the iPhone web browsers.  But to the rescue comes a little JavaScript library called iScroll.  It uses CSS transform techniques to give you a scrolling experience that pretty damned close to native, IMHO.  Just how good is iScroll?  Well, it seems that Apple themselves have been known to use it.  Good enough?

So here's Version 2 of my sortable list, reworked using iScroll.  Check out the iPhone scrolling performance now; a bit more like it, yes? Flip that list up or down and then let go; it should continue scrolling when you let go, before coming to a gradual stop, just as you'd expect.  If you're on a desktop browser, you can see this by grabbing an an area of the list (although not a list label) then flicking the mouse up or down and releasing.  

The code to enable the iScroll functionality must be bound to the inner div, wrapper, like so:

myApp.myScroll = new iScroll('wrapper')

But there's a fly in the ointment - there had to be, didn't there?  Try dragging a list label to sort the list with the iScroll version.  Oh dear.  It kind of works, but in a weird way.  When you drag an item to resort, the whole list moves with it.  Not good!

Version 3 - Sort Mode

After quite some research - not to mention trial and error -  I ascertained that the thing to do is to disable the iScroll scrolling when I'm drag/resorting the list and then reenable it when I'm finished.

So, here's Version 3 of the app, which does that.  It brings the concept of an Edit Order mode, enabled by the button at the top.  The list is not sortable until you've clicked this button, at which point iScroll is disabled and you're back to the normal, slowwwww scrolling.  I don't think that's an issue, however, because you're not going to need all that extra scroll speed to sort this list.  Click on the Done button and you're back to iScroll speed but can no longer sort the list.  An acceptable compromise, I think.

When making the list sortable, I have to disable iScroll as well as enabling the list sort.  I then do the reverse operation when I'm done sorting.  The code to disable iScroll and enable the list sorting is:

//disable iScroll

// enable list sort
handle : "label",
axis : 'y',
disabled : false

The code to reenable iScroll and disable list sorting is:

//enable iScroll

// disable list sort
disabled : true

But there's still problems…

If you scroll a good way down the list before enabling Sort Mode, dragging a list item to the top does not cause it to scroll up correctly.  Originally, I couldn't drag an item down past the bottom or top at all!  I fixed that problem via the following line when changing to Sort Mode:

$("#wrapper").css("overflow", "");

But I'm still stuck with the "can't drag up all the way" problem.  I can drag it a up part of the way, but not all the way to the top of the list.  Also, when switching out of Edit Mode via the Done button, the iScroll scrolling won't always go all the way to the top again.  In other words, when switching between modes the lists seem to "lose their place".

Version 4 - Synching Scroll Modes

I think that this is happening because there are, in fact, two separate scrolling mechanisms at work here: the iScroll one (based on CSS transforms) and the "normal" one (which I'll call "the JS one" from here on).  When I'm using iScroll then obviously the iScroll scrolling mechanism is at work.  When I am in Sort Mode, however, I've disabled the iScroll mechanism so that the JS scrolling mechanism comes into play.  At this point, the JS mechanism has no idea what the iScroll mechanism has been doing in terms of scrolling.  Hence, it's "lost its place".

So, I need to reset it those positions when switching modes.  Let me put you out of the suspense that I know you're all feeling!  Here's the final working Version 4 of the app, which has this position resetting code in place.

What I do in the final version is that before switching modes, I log the current y-axis position from current scrolling mode and then pass that value to the new mode.  Then I do a quick scroll to the top then down again switcheroo, to ensure that the scrolling modes are really in sync.

For iScroll mode, I can get the current y-axis position from the iScroll object's "y" property, like so:

var currentY = $("#wrapper").scrollTop();

If that value is less than zero then the iScroll object is not at its top, so I send it programmatically back up to its top:  

// Send iScroll to the top
myUtils.myScroll.scrollTo(0, 0, 0, false);

Now I can disable the iScroll object and call the code to enable the jQuery.sortable().  Having done that, I now send the JS scrolling mechanism back down to where the iScroll position last was.  I do this via a call to jQuery.scrollTop(x), like so:

// Send normal/JS scroll down to iScroll's last pos
$("#wrapper").scrollTop(0 - currentY);

Phew!!  I'm now in Sort Mode and can drag and drop list items in either direction, and drag them right to the top or bottom of the list.

When coming out of Sort Mode, I go through the same process in reverse.  This time, I get the currentY position from JS scrolling mechanism, by another call to jQuery.scrollTop(), only this time without a parameter.  I then feed that position to the iScroll mechanism to send it back the JS scroll mechanism's last recorded position.

All this happens fast enough that the user shouldn't even see it.  It's clunky, but it does work.