#

# Example: Creating an Arrow Event for DOM Subscription

This example will illustrate how to use the synthetic event creation API. We'll create an `arrow` event that fires in response to the user pressing the arrow keys (up, down, left, right) and adds a `direction` property to the generated event.

Subscribing to this new event will look like this:

    node.on("arrow", onArrowHandler);

Support will also be added for delegation, allowing a single subscriber from a node higher up the DOM tree, to listen for the new event emanating from its descendant elements.

    containerNode.delegate("arrow", onArrowHandler, ".robot");

This example is not applicable to touch devices, since they don't have arrow keys.

Step 1.
to the `arrow` event.
Use a delegated subscription

Step 2. Click on a toaster-bot and move it around with the arrow keys.

## `on`, `fire`, and `detach`

The three interesting moments in the lifecycle of a DOM event subscription are

1. The event is subscribed to
2. The event is fired
3. The event is unsubscribed from

Create a new synthetic DOM event with `Y.Event.define( name, config )`. Define the implementation logic for the `on` and `detach` moments in the configuration. Typically the condition triggering the event firing is set up in the `on` phase.

    Y.Event.define("arrow", {
        on: function (node, sub, notifier) {
            // what happens when a subscription is made

            // if (condition) {
                notifier.fire(); // subscribers executed
            // }
        },

        detach: function (node, sub, notifier) {
            // what happens when a subscription is removed
        }
    });

In the case of arrow handling, the trigger is simply a key event with a `keyCode` between 37 and 40. There are a few browser quirks with arrow handling that warrant listening to `keydown` for some browsers and `keypress` for others, so we'll take care of that transparently for `arrow` subscribers.

    Y.Event.define("arrow", {
        on: function (node, sub, notifier) {
            var directions = {
                37: 'left',
                38: 'up',
                39: 'right',
                40: 'down'
            };

            // Webkit and IE repeat keydown when you hold down arrow keys.
            // Opera links keypress to page scroll; others keydown.
            // Firefox prevents page scroll via preventDefault() on either
            // keydown or keypress.
            // Bummer to sniff, but can't test the repeating behavior, and a
            // feature test for the scrolling would more than double the code size.
            var eventName = (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress';

            // To make detaching the associated DOM event easy, store the detach
            // handle from the DOM subscription on the synthethic subscription
            // object.
            sub._detacher = node.on(eventName, function (e) {
                // Only notify subscribers if one of the arrow keys was pressed
                if (directions[e.keyCode]) {
                    // Add the extra property
                    e.direction = directions[e.keyCode];

                    // Firing the notifier event executes the arrow subscribers
                    // Pass along the key event, which will be renamed "arrow"
                    notifier.fire(e);
                }
            });
        },

        detach: function (node, sub, notifier) {
            // Detach the key event subscription using the stored detach handle
            sub._detacher.detach();
        }
    } );

## Add Delegation Support

Since the `arrow` event is simply a filtered `keydown` or `keypress` event, no special handling needs to be done for delegate subscriptions. We will extract the key event handler and use it for both `on("arrow", ...)` and `delegate("arrow", ...)` subscriptions.

    Y.Event.define("arrow", {
        // Webkit and IE repeat keydown when you hold down arrow keys.
        // Opera links keypress to page scroll; others keydown.
        // Firefox prevents page scroll via preventDefault() on either
        // keydown or keypress.
        _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',

        _keys: {
            '37': 'left',
            '38': 'up',
            '39': 'right',
            '40': 'down'
        },

        _keyHandler: function (e, notifier) {
            if (this._keys[e.keyCode]) {
                e.direction = this._keys[e.keyCode];
                notifier.fire(e);
            }
        },

        on: function (node, sub, notifier) {
            // Use the extended subscription signature to set the 'this' object
            // in the callback and pass the notifier as a second parameter to
            // _keyHandler
            sub._detacher = node.on(this._event, this._keyHandler,
                                    this, notifier);
        },

        detach: function (node, sub, notifier) {
            sub._detacher.detach();
        },

        // Note the delegate handler receives a fourth parameter, the filter
        // passed (e.g.) container.delegate('click', callback, '.HERE');
        // The filter could be either a string or a function.
        delegate: function (node, sub, notifier, filter) {
            sub._delegateDetacher = node.delegate(this._event, this._keyHandler,
                                                  filter, this, notifier);
        },

        // Delegate uses a separate detach function to facilitate undoing more
        // complex wiring created in the delegate logic above.  Not needed here.
        detachDelegate: function (node, sub, notifier) {
            sub._delegateDetacher.detach();
        }
    });

## Use it

Subscribe to the new event or detach the event as you would any other DOM event.

    function move(e) {
        // to prevent page scrolling
        e.preventDefault();

        // See full code listing to show the data set up
        var xy = this.getData();

        switch (e.direction) {
            case 'up':    xy.y -= 10; break;
            case 'down':  xy.y += 10; break;
            case 'left':  xy.x -= 10; break;
            case 'right': xy.x += 10; break;
        }

        this.transition({
            top : (xy.y + 'px'),
            left: (xy.x + 'px'),
            duration: .2
        });
    }

    // Subscribe using node.on("arrow", ...);
    Y.one("#A").on("arrow", move),
    Y.one("#B").on("arrow", move)

    // OR using container.delegate("arrow", ...);
    subs = Y.one('#demo').delegate('arrow', move, '.robot');

## Bonus Step: to the Gallery!

Synthetic events are perfect candidates for Gallery modules. There are a number already hosted there, and there are plenty of UI interaction patterns that would benefit from being encapsulated in synthetic events.

The `arrow` event in this example is also in the gallery, but with additional functionality. Check out its source to see what you can do with synthetic events.

## Full Code Listing

    <div id="demo">
        <p>Step 1. <button type="button" id="attach" tabindex="1">subscribe</button> to the <code>arrow</code> event.<br>
        <input type="checkbox" id="delegate" value="1" tabindex="1">
            <label for="delegate">Use a delegated subscription</label></p>
        <p>Step 2. Click on a toaster-bot and move it around with the arrow keys.</p>

        <div id="homebase">
            <img id="A" class="robot" tabindex="3" src="../assets/event/toast-8b-left.png" />
            <img id="B" class="robot" tabindex="3" src="../assets/event/toast-8b-right.png" />
        </div>

        <button type="button" id="detach" tabindex="4">Detach subscriptions</button>
    </div>

    <script>
    YUI({ filter: 'raw' }).use('node', 'event-synthetic', 'transition', function (Y) {
        Y.Event.define("arrow", {
            // Webkit and IE repeat keydown when you hold down arrow keys.
            // Opera links keypress to page scroll; others keydown.
            // Firefox prevents page scroll via preventDefault() on either
            // keydown or keypress.
            _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',

            _keys: {
                '37': 'left',
                '38': 'up',
                '39': 'right',
                '40': 'down'
            },

            _keyHandler: function (e, notifier) {
                if (this._keys[e.keyCode]) {
                    e.direction = this._keys[e.keyCode];
                    notifier.fire(e);
                }
            },

            on: function (node, sub, notifier) {
                sub._detacher = node.on(this._event, this._keyHandler,
                                        this, notifier);
            },

            detach: function (node, sub, notifier) {
                sub._detacher.detach();
            },

            delegate: function (node, sub, notifier, filter) {
                sub._delegateDetacher = node.delegate(this._event, this._keyHandler,
                                                      filter, this, notifier);
            },

            detachDelegate: function (node, sub, notifier) {
                sub._delegateDetacher.detach();
            }
        });

        var robotA = Y.one('#A'),
            robotB = Y.one('#B'),
            subs,
            moving = false;

        robotA.setData('x', parseInt(robotA.getStyle('left'), 10));
        robotA.setData('y', parseInt(robotA.getStyle('top'), 10));
        robotB.setData('x', parseInt(robotB.getStyle('left'), 10));
        robotB.setData('y', parseInt(robotB.getStyle('top'), 10));

        // create variables for image path/filenames
        // Use 8bit pngs for IE
        var imgBits = (Y.UA.ie && Y.UA.ie < 9) ? '-8b-' : '-24b-',
            imgNamePre = '../assets/event/toast' + imgBits,

            imgUp = imgNamePre + 'up.png',
            imgDown = imgNamePre + 'down.png',
            imgLeft = imgNamePre + 'left.png',
            imgRight = imgNamePre + 'right.png';

        Y.one('#A').setAttribute('src', imgLeft);
        Y.one('#B').setAttribute('src', imgRight);

        function move(e) {
            // to prevent page scrolling
            e.preventDefault();
            if(moving){
                return; // Don't move during a transition (a move)
            }else{
                moving = true; // During moving, block other arrow keys from moving
            }

            var xy = this.getData(),
                imgWidth,
                imgHeight,
                // var scale is used to make the image size and distance moved
                // proportional to the Y position of the image
                scale = (150 + xy.y) / 150,
                moveXDistance = 40,
                moveYDistance = 20;

            switch (e.direction) {
                case 'up':
                    if(xy.y < -100){
                        moving = false;
                        return; // Stop moving when image gets too small
                    }
                    xy.y -= Math.round(moveYDistance * scale);
                    e.target.setAttribute('src', imgUp);
                    break;
                case 'down':
                    if(xy.y > 90){
                        moving = false;
                        return; // Stop moving when image gets too big
                    }
                    xy.y += Math.round(moveYDistance * scale);
                    e.target.setAttribute('src', imgDown);
                    break;
                case 'left':
                    xy.x -= Math.round(moveXDistance * scale);
                    e.target.setAttribute('src', imgLeft);
                    break;
                case 'right':
                    xy.x += Math.round(moveXDistance * scale);
                    e.target.setAttribute('src', imgRight);
                    break;
            }
            scale = 150 / (150 - xy.y); // calculate scale with new Y dimentions
            imgWidth = Math.round(scale * 180) + 'px';
            imgHeight = Math.round(scale * 210) + 'px';
            this.transition({
                top : (xy.y + 'px'),
                left: (xy.x + 'px'),
                width: imgWidth,
                height: imgHeight,
                duration: .8
            }, function(){
                moving = false; // now that move is done, allow arrow keys to move again
            });

            this.setData('x', xy.x);
            this.setData('y', xy.y);
        }

        function detachSubs() {
            if (subs) {
                subs.detach();
                subs = null;
                Y.all('.robot').removeClass('yui3-focused');
            }
        }

        Y.one("#attach").on("click", function (e) {
            detachSubs();

            if (Y.one("#delegate").get('checked')) {
                subs = Y.one('#demo').delegate('arrow', move, '.robot');
            } else {
                subs = new Y.EventHandle([
                    robotA.on("arrow", move),
                    robotB.on("arrow", move)
                ]);
            }
        });

        Y.one("#detach").on("click", detachSubs);
        Y.all('.robot').on('focus', function(e){
            if (subs) {
                Y.all('.robot').removeClass('yui3-focused');
                e.target.addClass('yui3-focused');
            }
        });

        Y.all('.robot').on('click', function(e){
            e.target.focus();
        });

    });
    </script>