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.

14 thoughts on “An iPhone friendly, local storage backed, offline TODO list webapp

  1. Yes, add the meta tag apple-mobile-web-app-capable content=”yes” tag to your app. That meta tag makes the safari chrome disappear (both the location bar AND the status bar).. removing the last indication that your app is not a native iphone app. Seems like at that point, it would very difficult to tell the difference. Great work on your app btw. -Steven

  2. Added that line and it does the job. Had to mess around with re-creating the bookmark and even had to restart iPhone to get it to work (I presume there’s some caching involved). Now though it really does look like a native app – nice!

  3. Thanks! This is a nice working example of an offline webapp with local sqlite. There are a lot of snippets showing how one could set this up, but amazingly few that show an entire application.

    I’d love to find a library that could handle either Gears or HTML5 manifest/sqlite storage. Maddening to have to develop different versions for local storage depending on the browser.

    Anyway, great job, thanks for writing and documenting this!

    Tac

  4. Hi,
    love the example, but: I am totally new to this stuff and have no idea where to start. Unfortuanately, the is no download button for the iPhone app source. I would think that would be a real help to me to try and get into this stuff. Is there a way to get the source Luke? 😉
    This old Kraut would really appreciate that. Thanks in advance,
    Robert

  5. The handleTouchEvent() added for the iPhone, as listed on this page is correct. But when I downloaded from GitHub, lines 83 and 84 end with commas instead of semicolons. That causes the Javascript to not parse, breaking things for iPhone.

    FYI, to retest things, I needed to Clear History and Clear Cookies and Data in Safari.

    I know it’s been years since you’ve touched this but thanks for leaving this great example online.

    -kjc

Comments are closed.