<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example: Creating an Arrow Event for DOM Subscription</title>
<link rel="stylesheet" href="http://fonts.googleapis.com/css?family=PT+Sans:400,700,400italic,700italic">
<link rel="stylesheet" href="../../build/cssgrids/cssgrids-min.css">
<link rel="stylesheet" href="../assets/css/main.css">
<link rel="stylesheet" href="../assets/vendor/prettify/prettify-min.css">
<link rel="shortcut icon" type="image/png" href="../assets/favicon.png">
<script src="../../build/yui/yui-min.js"></script>
</head>
<body>
<!--
<a href="https://github.com/yui/yui3"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub"></a>
-->
<div id="doc">
<div id="hd">
<h1><img src="http://yuilibrary.com/img/yui-logo.png"></h1>
</div>
<h1>Example: Creating an Arrow Event for DOM Subscription</h1>
<div class="yui3-g">
<div class="yui3-u-3-4">
<div id="main">
<div class="content"><style scoped>
#demo {
position: relative;
}
#homebase {
margin-left: 100px;
position: absolute;
left: 0;
top: 40px;
height: 150px;
width: 200px;
}
.robot {
height: 210px;
width: 180px;
position: absolute;
top: 0px;
left: 0;
outline-style: none;
opacity: 0.5;
filter: alpha(opacity=50);
}
.yui3-focused {
opacity: 1;
z-index: 1;
filter: alpha(opacity=100);
}
#B {
left: 125px;
}
#demo input {
margin-left: 4em;
}
#demo label {
font-size: 87%;
color: #555;
}
#detach {
margin-top: 150px;
}
</style>
<div class="intro">
<p>
This example will illustrate how to use the synthetic event creation
API. We'll create an <code>arrow</code> event that fires in response
to the user pressing the arrow keys (up, down, left, right) and adds a
<code>direction</code> property to the generated event.
</p>
<p>Subscribing to this new event will look like this:</p>
<pre class="code prettyprint">node.on("arrow", onArrowHandler);</pre>
<p>
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.
</p>
<pre class="code prettyprint">containerNode.delegate("arrow", onArrowHandler, ".robot");</pre>
<p>
This example is not applicable to touch devices, since they don't have arrow keys.
</p>
</div>
<div class="example yui3-skin-sam">
<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>
</div>
<h2><code>on</code>, <code>fire</code>, and <code>detach</code></h2>
<p>
The three interesting moments in the lifecycle of a DOM event subscription
are
</p>
<ol>
<li>The event is subscribed to</li>
<li>The event is fired</li>
<li>The event is unsubscribed from</li>
</ol>
<p>
Create a new synthetic DOM event with `Y.Event.define( <em>name</em>,
<em>config</em> )`. Define the implementation logic for the
<code>on</code> and <code>detach</code> moments in the configuration.
Typically the condition triggering the event firing is set up in the
<code>on</code> phase.
</p>
<pre class="code prettyprint">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
}
});</pre>
<p>
In the case of arrow handling, the trigger is simply a key event with a
<code>keyCode</code> between 37 and 40. There are a few browser quirks with arrow
handling that warrant listening to <code>keydown</code> for some browsers and
<code>keypress</code> for others, so we'll take care of that transparently for <code>arrow</code>
subscribers.
</p>
<pre class="code prettyprint">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();
}
} );</pre>
<h2>Add Delegation Support</h2>
<p>
Since the <code>arrow</code> event is simply a filtered <code>keydown</code> or <code>keypress</code> event,
no special handling needs to be done for delegate subscriptions. We will
extract the key event handler and use it for both <code>on("arrow", ...)</code> and
<code>delegate("arrow", ...)</code> subscriptions.
</p>
<pre class="code prettyprint">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();
}
});</pre>
<h2>Use it</h2>
<p>
Subscribe to the new event or detach the event as you would any other DOM
event.
</p>
<pre class="code prettyprint">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');</pre>
<h2>Bonus Step: to the Gallery!</h2>
<p>
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.
</p>
<p>
The <code>arrow</code> event in this example is also
<a href="http://yuilibrary.com/gallery/show/event-arrow">in the gallery</a>,
but with additional functionality. Check out
<a href="https://github.com/lsmith/yui3-gallery/blob/master/build/gallery-event-arrow/gallery-event-arrow-debug.js">its source</a>
to see what you can do with synthetic events.
</p>
<h2>Full Code Listing</h2>
<pre class="code prettyprint"><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></pre>
</div>
</div>
</div>
<div class="yui3-u-1-4">
<div class="sidebar">
<div class="sidebox">
<div class="hd">
<h2 class="no-toc">Examples</h2>
</div>
<div class="bd">
<ul class="examples">
<li data-description="Use the Event Utility to attach simple DOM event handlers.">
<a href="basic-example.html">Simple DOM Events</a>
</li>
<li data-description="Using the synthetic event API to create a DOM event that fires in response to arrow keys being pressed.">
<a href="synth-example.html">Creating an Arrow Event for DOM Subscription</a>
</li>
<li data-description="Supporting cross-device swipe gestures, using the event-move gesture events">
<a href="swipe-example.html">Supporting A Swipe Left Gesture</a>
</li>
</ul>
</div>
</div>
<div class="sidebox">
<div class="hd">
<h2 class="no-toc">Examples That Use This Component</h2>
</div>
<div class="bd">
<ul class="examples">
<li data-description="Creating an accessible menu button using the Focus Manager Node Plugin, Event's delegation support and mouseenter event, along with the Overlay widget and Node's support for the WAI-ARIA Roles and States.">
<a href="../node-focusmanager/node-focusmanager-button.html">Accessible Menu Button</a>
</li>
<li data-description="Shows how to extend the base widget class, to create your own Widgets.">
<a href="../widget/widget-extend.html">Extending the Base Widget Class</a>
</li>
<li data-description="Example Photo Browser application.">
<a href="../dd/photo-browser.html">Photo Browser</a>
</li>
<li data-description="Portal style example using Drag & Drop Event Bubbling and Animation.">
<a href="../dd/portal-drag.html">Portal Style Example</a>
</li>
<li data-description="Use IO to request data over HTTP.">
<a href="../io/get.html">HTTP GET to request data</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="../assets/vendor/prettify/prettify-min.js"></script>
<script>prettyPrint();</script>
<script>
YUI.Env.Tests = {
examples: [],
project: '../assets',
assets: '../assets/event',
name: 'synth-example',
title: 'Creating an Arrow Event for DOM Subscription',
newWindow: '',
auto: false
};
YUI.Env.Tests.examples.push('basic-example');
YUI.Env.Tests.examples.push('synth-example');
YUI.Env.Tests.examples.push('swipe-example');
YUI.Env.Tests.examples.push('node-focusmanager-button');
YUI.Env.Tests.examples.push('widget-extend');
YUI.Env.Tests.examples.push('photo-browser');
YUI.Env.Tests.examples.push('portal-drag');
YUI.Env.Tests.examples.push('get');
</script>
<script src="../assets/yui/test-runner.js"></script>
</body>
</html>