An iPhone friendly, local storage backed, offline TODO list webapp


A while back I had a go at using the local storage features being added to javascript to create a simple TODO list app. The main focus of the app was getting it all to fit under 5K (5120 bytes), but it was a good test of using a client-side sql database in javascript.

As the app was written for size it didn't have many frills. It did however work on the iPhone, as it's version of Safari had support for the openDatabase call needed. However it didn't look so good and although the TODO list items were stored locally the phone still had to be online to access the host webpage - which kind of undermined some of it's utility.

With my recent acquisition of an iPhone I thought I'd revisit this TODO app and make it play nicely on the iPhone. In addition to using a client-side SQL database this new version features:

These features mean that if you have add a bookmark to your homescreen for this app, you might as well be running a native app. It looks pretty native, stores data locally, doesn't require a net connection and even features standard app UI mechanisms. The main giveaway is of course the chrome associated with the Safari browser. Still not bad for some html, css and javascript. Not a total replacement for native Cocoa apps, but it does put the creation of client-side apps for the iPhone into the hands of even more people.

If you are on an iPhone or are running Safari 4.0 you can try out jTODO yourself or watch the video of it in action below:



I won't rehash the sql-side of things in this post - instead I'd refer you to my original post on the matter.

The use of iui was also fairly straightforward - I'm only using the style-sheets and images. This means that there are no animations involved, but at this stage it seemed excessive to add them in. Perhaps I'll add some in a future version.

This leaves the offline application cache and getting drag and drop to work.

Offline application cache

The offline application cache is really simple to implement. It's currently supported by Safari on the iPhone, Safari 4.0 and Firefox 3+.

In my case I want all files to be cached, so I simple specify them in a "manifest" file:

CACHE MANIFEST

# version 1.0.2.37

iui/backButton.png
iui/blueButton.png
iui/cancel.png
iui/grayButton.png
iui/iui-logo-touch-icon.png
iui/iui.css
iui/iui.js
iui/iuix.css
iui/iuix.js
iui/listArrow.png
iui/listArrowSel.png
iui/listGroup.png
iui/loading.gif
iui/pinstripes.png
iui/selection.png
iui/thumb.png
iui/toggle.png
iui/toggleOn.png
iui/toolButton.png
iui/toolbar.png
iui/whiteButton.png

css/todo.css

js/jquery-1.3.2.min.js
js/jquery-ui-1.7.2.custom.min.js
js/todo.js

img/delete.png
img/deleting.png
img/redButton.png
img/handle.png
img/todo-touch-icon.png

This manifest file is then linked to the main html file thus:

<html manifest="todo.manifest">

That's it. We now have an offline capable web-app. The main issue I had when doing this, was that Safari was very strict about the manifest - any file not mentioned in the manifest would not be loaded (even if it was normally accessible). The other issue of course is that we've now introduced another level of caching, so developing can be a bit annoying - as you think you've made a change, but then nothing shows up. I often ended up disabling the manifest for a while when debugging and then re-enabled it after things were working again. There's also a version number in the manifest file, so that it well register as changed - to help refresh the caches after we've made a change.

With all this in place the app can be used when no net connection is available (e.g. in Airplane mode).

Drag and drop

The last job when developing this app was to enable drag and drop for re-ordering items. My first go at this worked pretty easily using jQuery UI's sortable plugin - when running on my mac in Safari:

page.sortable({
    axis: 'y',
    handle: '.handle',
    update: function(){
        db.transaction(function(tx) {
            $('.todo_item').each(function(i,item) {
                var id = $(item).find(':input[type=checkbox]').attr('id');
                id = Number(id.substring('todo_checkbox_'.length));
                tx.executeSql('UPDATE todo SET todo_order=? WHERE id=?', [i, id]);
            });
        });
    }
});

Essentially this is just setup to enabling drag and drop sorting only along the y axis (up and down), using the element with class handle to start the drag. Once the dragging has finished update gets called and I inspect the DOM to work out the current order of the todo_items and update the database accordingly.

That was pretty easy and working really well on the mac. Then of course I thought I'd test it on the iPhone. At that point I realised I'd forgotten that drag and drop doesn't normally work in Safari in the iPhone. Holding and dragging normally scrolls the entire page - so drag and drop was a bit useless here.

However after a little digging I found someone had figured out how to get drag and drop working on the iPhone, by hijacking the various "touch" events that the phone generates. These work a little differently to the normal mouse events, but with some work can made to do our bidding:

function handleTouchEvent(event) {
    /* from http://jasonkuhn.net/mobile/jqui/js/jquery.iphoneui.js
     but changed a bit*/

    var touches = event.changedTouches;
    var first = touches[0];
    var type = '';


    // only want to do this for the drag handles
    if ( !first.target || !first.target.className || first.target.className.indexOf("handle") == -1 ) {
        return;
    }

    switch(event.type) {
        case 'touchstart':
            type = 'mousedown';
            break;

        case 'touchmove':
            type = 'mousemove';
            break;

        case 'touchend':
            type = 'mouseup';
            break;

        default:
            return;
    }

    var simulatedEvent = document.createEvent('MouseEvent');
    simulatedEvent.initMouseEvent(type, true, true, window, 1, first.screenX, first.screenY, first.clientX, first.clientY, false, false, false, false, 0/*left*/, null);

    first.target.dispatchEvent(simulatedEvent);

    if ( event.type == 'touchmove' ) {
        event.preventDefault();
    }
}
document.addEventListener("touchstart", handleTouchEvent, false);
document.addEventListener("touchmove", handleTouchEvent, false);
document.addEventListener("touchend", handleTouchEvent, false);

This registers listeners for the touch events, but only does any extra work if the target of the event has the class "handle". If that's the case a simulated mouse event is sent for the first touched item. To stop the page from scrolling when we want to drag instead event.preventDefault() is called just for the touchmove event. This is sufficient to let jquery UI do it's job and enable drag and drop sorting of the TODO items.