Raphael is a cross browser vector drawing library. It uses VML for Microsoft browers, and SVG for standards compliant browsers - with IE6 compatability to boot!
As great as Raphael is, I ran into a small problem. Raphael doesn't allow event handlers to run in any specific scope. So when an even handler is called, it runs within the scope of raphael. This complicates inter-component communication, and usually causes more icky global variables to be created.
In this post I'm going to show you how I added scoped event funcionality to raphael.
Click here to download raphael.js with all these changes
It looks like raphael does some JS ninjery because the only mention of events are in one long string:
raphael.js, line 41:
events = "click dblclick mousedown mousemove mouseout mouseover mouseup... |
The string is broken apart, and a loop adds "event" and "un"-"event" functions to the raphael object. These functions are sort of event handler adders- they allow you to add event handlers.
raphael.js, line 2533:
for (var i = events[length]; i--;) { (function (eventName) { R[eventName] = Element[proto][eventName] = function (fn) { if (R.is(fn, "function")) { this.events = this.events || []; this.events.push({name: eventName, f: fn, unbind: addEvent(this.shape || this.node || doc, eventName, fn, this)}); } return this; }; R["un" + eventName] = Element[proto]["un" + eventName] = function (fn) { var events = this.events, l = events[length]; while (l--) if (events[l].name == eventName && events[l].f == fn) { events[l].unbind(); events.splice(l, 1); !events.length && delete this.events; return this; } return this; }; })(events[i]); } |
This is where the handler-adder is created:
R[eventName] = Element[proto][eventName] = function (fn) {
This is where we would pass the new scope argument.
function (fn) {
becomes:
function (fn, scope) {
Since you may not always want to scope the event handlers, lets make sure you can leave this argument blank by adding this line:
scope = scope || this;
Raphael uses a sort of build-in event handler that uses the addEvent() function. So we need to add the scope argument there, and modify the addEvent() function itself.
this.events.push({name: eventName, f: fn, unbind: addEvent(this.shape || this.node || doc, eventName, fn, this)});
becomes:
this.events.push({name: eventName, f: fn, unbind: addEvent(this.shape || this.node || doc, eventName, fn, this, scope)});
addEvent() takes care of browser abstraction by creating an immediately executing function. This allows events to be added for firefox/webkit & internet explorer. This means we will have to make sure that the changes are made in both places.
raphael.js, line 2491:
addEvent = (function () { if (doc.addEventListener) { return function (obj, type, fn, element) { var realName = supportsTouch && touchMap[type] ? touchMap[type] : type; var f = function (e) { if (supportsTouch && touchMap[has](type)) { for (var i = 0, ii = e.targetTouches && e.targetTouches.length; i < ii; i++) { if (e.targetTouches[i].target == obj) { var olde = e; e = e.targetTouches[i]; e.originalEvent = olde; e.preventDefault = preventTouch; e.stopPropagation = stopTouch; break; } } } return fn.call(element, e); }; obj.addEventListener(realName, f, false); return function () { obj.removeEventListener(realName, f, false); return true; }; }; } else if (doc.attachEvent) { return function (obj, type, fn, element) { var f = function (e) { e = e || win.event; e.preventDefault = e.preventDefault || preventDefault; e.stopPropagation = e.stopPropagation || stopPropagation; return fn.call(element, e); }; obj.attachEvent("on" + type, f); var detacher = function () { obj.detachEvent("on" + type, f); return true; }; return detacher; }; } })(); |
So lets pepper this with our scope argument we are passing from the event handler adders:
addEvent = (function () { if (doc.addEventListener) { return function (obj, type, fn, element, scope) { //AR added scope argument var realName = supportsTouch && touchMap[type] ? touchMap[type] : type; var f = function (e) { if (supportsTouch && touchMap[has](type)) { for (var i = 0, ii = e.targetTouches && e.targetTouches.length; i < ii; i++) { if (e.targetTouches[i].target == obj) { var olde = e; e = e.targetTouches[i]; e.originalEvent = olde; e.preventDefault = preventTouch; e.stopPropagation = stopTouch; break; } } } return fn.call(scope, element, e); //AR added call here passing scope }; obj.addEventListener(realName, f, false); return function () { obj.removeEventListener(realName, f, false); return true; }; }; } else if (doc.attachEvent) { return function (obj, type, fn, element, scope) { //AR added scope argument var f = function (e) { e = e || win.event; e.preventDefault = e.preventDefault || preventDefault; e.stopPropagation = e.stopPropagation || stopPropagation; return fn.call(scope, element, e); //AR added call here passing scope }; obj.attachEvent("on" + type, f); var detacher = function () { obj.detachEvent("on" + type, f); return true; }; return detacher; }; } })(); |
There were also three additional event handler adders after this block of code. (Element[proto].hover, Element[proto].unhover, Element[proto].drag) I made extremely similer changes to there as well.
Here is the full text with changes:
addEvent = (function () { if (doc.addEventListener) { return function (obj, type, fn, element, scope) { //AR added scope argument var realName = supportsTouch && touchMap[type] ? touchMap[type] : type; var f = function (e) { if (supportsTouch && touchMap[has](type)) { for (var i = 0, ii = e.targetTouches && e.targetTouches.length; i < ii; i++) { if (e.targetTouches[i].target == obj) { var olde = e; e = e.targetTouches[i]; e.originalEvent = olde; e.preventDefault = preventTouch; e.stopPropagation = stopTouch; break; } } } return fn.call(scope, element, e); //AR added call here passing scope }; obj.addEventListener(realName, f, false); return function () { obj.removeEventListener(realName, f, false); return true; }; }; } else if (doc.attachEvent) { return function (obj, type, fn, element, scope) { //AR added scope argument var f = function (e) { e = e || win.event; e.preventDefault = e.preventDefault || preventDefault; e.stopPropagation = e.stopPropagation || stopPropagation; return fn.call(scope, element, e); //AR added call here passing scope }; obj.attachEvent("on" + type, f); var detacher = function () { obj.detachEvent("on" + type, f); return true; }; return detacher; }; } })(); for (var i = events[length]; i--;) { (function (eventName) { R[eventName] = Element[proto][eventName] = function (fn, scope) { //AR added scope argument if (R.is(fn, "function")) { scope = scope || this; //AR - added scope default this.events = this.events || []; this.events.push({name: eventName, f: fn, unbind: addEvent(this.shape || this.node || doc, eventName, fn, this, scope)}); //AR added scope here } return this; }; R["un" + eventName] = Element[proto]["un" + eventName] = function (fn) { var events = this.events, l = events[length]; while (l--) if (events[l].name == eventName && events[l].f == fn) { events[l].unbind(); events.splice(l, 1); !events.length && delete this.events; return this; } return this; }; })(events[i]); } Element[proto].hover = function (f_in, f_out, scope) { //AR - added scope argument scope = scope || this; //AR - added scope default return this.mouseover(f_in, scope).mouseout(f_out, scope); }; Element[proto].unhover = function (f_in, f_out, scope) { //AR - added scope argument scope = scope || this; //AR - added scope default return this.unmouseover(f_in).unmouseout(f_out); }; Element[proto].drag = function (onmove, onstart, onend, scope) { //AR - added scope argument scope = scope || this; //AR - added scope default this._drag = {}; var el = this.mousedown(function (e) { (e.originalEvent ? e.originalEvent : e).preventDefault(); this._drag.x = e.clientX; this._drag.y = e.clientY; this._drag.id = e.identifier; onstart && onstart.call(this, e.clientX, e.clientY); Raphael.mousemove(move).mouseup(up); }), move = function (e) { var x = e.clientX, y = e.clientY; if (supportsTouch) { var i = e.touches.length, touch; while (i--) { touch = e.touches[i]; if (touch.identifier == el._drag.id) { x = touch.clientX; y = touch.clientY; (e.originalEvent ? e.originalEvent : e).preventDefault(); break; } } } else { e.preventDefault(); } onmove && onmove.call(el, x - el._drag.x, y - el._drag.y, x, y); }, up = function () { el._drag = {}; Raphael.unmousemove(move).unmouseup(up); onend && onend.call(el); }; return this; }; |
Now all the event handlers can be passed an additional scope parameter allowing more decoupling and further separating concerns.