+ 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.
+
+
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 +
+ +-
+
- The event is subscribed to +
- The event is fired +
- 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>
+
+
+