Example: Supporting A Swipe Left Gesture

This example shows how you can support a "swipeleft" gesture, built on top of the synthetic "gesturemove" events, which work not only on touch devices, but also on mouse based input devices.

Modules Used

For the example, the two core modules we'll use are:

The event-move module
Provides the gesturemovestart, gesturemove and gesturemoveend low-level gesture events. These events are fired whenever the user performs a move gesture (mouse button/finger down, mouse/finger move, mouse button/finger up) with the mouse or their finger.
The transitions module
Provides transitions support, leveraging CSS transitions under the hood where supported.

The YUI use statement for the example is shown below:

YUI().use("node-base", "node-event-delegate", "transition", "event-move", function(Y) {
    ...
});

Delegating Gesture Move Events

The basic idea for the example is to listen for a gesturemovestart on a list item, and when we get one, store its position, and then listen for a gesturemoveend. If the gesturemoveend occurs more than X pixels to the left of the start, then we've identified a "swipeleft" gesture. Future versions of the library will package such logic into a higher level gesture event (ala event-flick).

For this example, since we're dealing with a list of items, rather than attach individual listeners to each <li> in the list, we use delegate on the parent <ul> element, which leads to better performance and avoids having to add/remove listeners each time the contents of the list change. The gesturemovestart, gesturemove and gesturemoveend are synthetic events, and can all be used with delegate, just like any DOM event.

We set up a delegate listener on the <ul> which listens for the gesturemovestart event (gesturemovestart abstracts mousedown/touchstart events under the hood). The delegate listener is set up to be notified when the target of the gesturemovestart is an <li>

Y.Node.one("#swipe").delegate("gesturemovestart", function(e) {

    ...

}, "li", {  // filter for "li"
    preventDefault:true
});

The gesturemovestart event supports a configuration object passed as an additional subscription argument, which can be used to set minimum distance and minimum time thresholds at which to fire the start event. The configuration also supports the ability to prevent the default behavior before the minimum time or distance thresholds kick in, which is what we do above by passing preventDefault:true.

The advantage of the gesture synthetic events is that the developer can use the same API without having to worry about whether or not the browser platform is touch based or mouse based.

Gesture Move End

As mentioned above, the gesturemovestart listener is notified whenever a mousedown or touchstart is received on a list item. The listener has access to:

e.currentTarget
The list item targeted.
e.target
The element clicked on (it may be an element inside the targeted list item, the span for example).
e.container
The element to which the delegate listener is attached (the ul in this case).

The event facade also has the page coordinates for the mousedown or touchstart event. We use the list item's setData method, to store the pageX position for the start event, so we can compare it when we get the gesturemoveend event. This way it's stored on the node instance, and we don't need to pass it along separately to the gesturemoveend event, or store it globally.

getData, setData and clearData are useful methods to store ad-hoc node centric data.

Y.Node.one("#swipe").delegate("gesturemovestart", function(e) {

    var item = e.currentTarget,
        target = e.target,
        container = e.container,

    ...

    item.setData("swipeStart", e.pageX);

    item.once("gesturemoveend", function(e) {

        var swipeStart = item.getData("swipeStart"),
            swipeEnd = e.pageX,
            isSwipeLeft = (swipeStart - swipeEnd) > MIN_SWIPE;

        if (isSwipeLeft) {
            item.one(".myapp-delete").removeClass("myapp-hidden");
        }

    });

    ...

});

When we get the gesturemovestart event, we set up a listener for the gesturemoveend event, so we can determine the end of the gesture, and figure out if the user swiped left. Since we don't want to set up a new listener every time we get a gesturemovestart we use once to set up the gesturemoveend listener. once will detach the listener after it's been invoked. Again, since gesturemoveend is a synthetic event, it works with once just like any other DOM event.

In the gesturemoveend listener we check the page position of the event, and if it's far enough to the left of the start position, we display the "Delete" button by removing the hidden class which it contains.

Transitions

To hide and remove the item when the delete button is pressed, we use the transition method, to animate its opacity and height down to 0. Under the hood transition will use CSS transition support where available (WebKit) and set up timer based animation where not (IE). As with the gesture event support, the developer gets to use the same API without having to worry about the browser environment.

item = target.get("parentNode");

item.transition({
    duration:0.3,
    opacity:0,
    height:0
}, function() {
    this.remove();
});

The second argument to transition above is a callback function, which is invoked when the transition is complete. We use this support to remove the item from the DOM.

Full Code Listing

YUI().use('node-base','node-event-delegate', 'transition', 'event-move', function (Y) {

    var MIN_SWIPE = 10;

    Y.all(".myexample-hidden").removeClass("myexample-hidden");

    Y.one("#swipe").delegate("gesturemovestart", function(e) {

        var item = e.currentTarget,
            target = e.target,
            container = e.container,
            isDeleteButton = target.hasClass("myapp-delete");

        // Prevent Text Selection in IE
        item.once("selectstart", function(e) {
            e.preventDefault();
        });

        if (!isDeleteButton) {

            container.all(".myapp-delete").addClass("myapp-hidden");

            item.setData("swipeStart", e.pageX);

            item.once("gesturemoveend", function(e) {

                var swipeStart = item.getData("swipeStart"),
                    swipeEnd = e.pageX,
                    isSwipeLeft = (swipeStart - swipeEnd) > MIN_SWIPE;

                if (isSwipeLeft) {
                    item.one(".myapp-delete").removeClass("myapp-hidden");    
                }

            });
        } else {
            item = target.get("parentNode");

            if (item.get("id") != "kitkat" || confirm("Seriously? The KitKats?")) {
                item.transition({
                    duration:0.3,
                    opacity:0,
                    height:0
                }, function() {
                    this.remove();
                });
            }
        }

    }, "li", {
        preventDefault:true
    });
});