Click, Scroll and pseudo :active on Mobile Webkit

In the mobile application world, you still sometimes need to build plumbing for things to work correctly. One area I put effort into recently was getting the right visual feedback on a Touch device (namely, iPhone).

With a standard mouse device, the feedback is straightforward, the:active CSS pseudo class makes it easy to give instant visual feedback. For example, a web designer might want to darken the background when a user presses a mouse button on a widget. The web browser simply activates the pseudo class when the button is pressed and deactivates it when the mouse is released. This can be achieved with a couple of lines of CSS:

    .mybutton:active {
       background-color: 0;
    }

Touch interfaces such as iPhone require more dedicated treatment in some situations. In an iPhone, a touch-and-move gesture will initiate scrolling, which should also deactivate the pseudo class. But this behavior is not built in. As such it needs to be reinvented.
Based on jQTouch, I re-implemented this behavior. The original code set a timeout handler to activate the class (.active, instead of :active) after a brief delay to ensure a scroll does not trigger .active. However, this means a quick touch and release will not generate the active.

 

style.css:

    .mybutton.active {
       background-color: 0;
    }

script.js:

$(tapSelectors.join(', ')).live(START_EVENT, touchstart);

function touchstart(e) {
    var $el = null;
    var startX, startY, startTime;
    var deltaX, deltaY, deltaT;
    var endX, endY, endTime;
    var swipped = false, tapped = false, moved = false, inprogress = false;

    function bindEvents($el) {
        $el.bind(MOVE_EVENT, handlemove).bind(END_EVENT, handleend);
        if ($.support.touch) {
            $el.bind(CANCEL_EVENT, handlecancel);
        } else {
            $(document).bind('mouseout', handleend);
        }
    }

    function unbindEvents($el) {
        $el.unbind(MOVE_EVENT, handlemove).unbind(END_EVENT, handleend);
        if ($.support.touch) {
            $el.unbind(CANCEL_EVENT, handlecancel);
        } else {
            $(document).unbind('mouseout', handlecancel);
        }
    }

    function updateChanges() {
        var first = $.support.touch? event.changedTouches[0]: event;
        deltaX = first.pageX - startX;
        deltaY = first.pageY - startY;
        deltaT = (new Date).getTime() - startTime;
    }
    function handlestart(e) {
        inprogress = true, swipped = false, tapped = false, moved = false, timed = false;
        startX = $.support.touch? event.changedTouches[0].clientX: event.clientX;
        startY = $.support.touch? event.changedTouches[0].clientY: event.clientY;
        startTime = (new Date).getTime();
        endX = null, endY = null, endTime = null;
        deltaX = 0;
        deltaY = 0;
        deltaT = 0;

        if (!!$el) {
            $el.removeClass('active');
        }
        $el = $(e.currentTarget);

        // Let's bind these after the fact, so we can keep some internal values
        bindEvents($el);

        setTimeout(function() {
            handlehover(e);
        }, 50);
    };

    function handlemove(e) {
        updateChanges();
        var absX = Math.abs(deltaX);
        var absY = Math.abs(deltaY);

        if (absX >= 1 || absY >= 1) {
            moved = true;
        }
        if (absY <= 5) {
            if (absX > absY && (absX > 35) && deltaT < 1000) {
                inprogress = false;
                $el.removeClass('active');
                unbindEvents($el);

                swipped = true;
                $el.trigger('swipe', {direction: (deltaX < 0) ? 'left' : 'right', deltaX: deltaX, deltaY: deltaY });
            }
        } else {
            // moved too much, can't swipe anymore
            inprogress = false;
            $el.removeClass('active');
            unbindEvents($el);
        }
    };

    function handleend(e) {
        updateChanges();
        var absX = Math.abs(deltaX);
        var absY = Math.abs(deltaY);

        inprogress = false;
        unbindEvents($el);
        if (!tapped && (absX <= 1 && absY <= 1)) {
            tapped = true;
            setTimeout(function() {
              $el.trigger('tap');
            }, 10); /* give a chance other touch to end */
            setTimeout(function() {
              $el.removeClass('active');
          }, 1000);
        } else {
            $el.removeClass('active');
            e.preventDefault();
        }
    };

    function handlecancel(e) {
        inprogress = false;
        $el.removeClass('active');
        unbindEvents();
    };

    function handlehover(e) {
        timed = true;
        if (tapped) {
            // flash the selection
            $el.addClass('active');
            setTimeout(function() {
                $el.removeClass('active');
            }, 1000);
        } else if (inprogress && !moved) {
            $el.addClass('active');
        }
    };

    handlestart(e);

}; // End touch handler 

Find the full code listing here: http://github.com/beedesk/jQTouch/blob/master/jqtouch/jqtouch.js