Skip to content

Instantly share code, notes, and snippets.

@Skateside
Created July 12, 2012 21:41
Show Gist options
  • Save Skateside/3101237 to your computer and use it in GitHub Desktop.
Save Skateside/3101237 to your computer and use it in GitHub Desktop.
I'm working on a polyfill for addEventListener. When it's complete, it will work with custom events, support removeEventListener and support the DOMContentLoaded event
// Shim to make sure that we've got the modern functions and methods that we
// need, namely [].forEach, [].map and Object.keys
(function () {
var undef, // = undefined;
isStringArray = 'a'[0] === 'a', // Used in toObject.
toString = Object.prototype.toString, // Used for class checking.
hasDontEnumBug = !{toSring: null}.propertyIsEnumerable('toString'),
dontEnums = [
'constructor',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'toLocaleString',
'toString',
'valueOf'
];
// The "this" given to an Array method should be converted into an object. This
// means that a String should be converted into an Array. Sadly, the native
// Object() does not do this in IE8-, so this helper function will make sure
// that it always happens.
function toObject(obj) {
if (toString.call(obj) === '[object String]' && !isStringArray) {
obj = obj.split('');
}
return Object(obj);
}
// The length of an array must be a number between -2^31 and 2^31 - 1
// inclusive. This little helper function makes sure that the number satisfies
// those conditions, returning 0 if it's not the case.
// http://es5.github.com/#x9.5
function toUnit32(str) {
var num = Number(str),
ret = 0;
if (!isNaN(num) && isFinite(num)) {
ret = Math.abs(num % Math.pow(2, 32));
}
return ret;
}
// Converts any number to an integer, following all the rules laid down in the
// ES5 standard.
function toInteger(str) {
var number = +str,
returnValue = number;
if (isNaN(number)) {
returnValue = 0;
} else if (number !== 0 && isFinite(number)) {
returnValue = ((number < 0 ? -1 : 1) * Math.floor(Math.abs(number)));
}
return returnValue;
}
// Add Array.prototype.forEach if it doesn't already exist. Be sure to convert
// the "this" into a proper object, check that the length is small enough and
// do not execute the function for undefined entries of the array. Check that
// the object can be iterated and that the function can be called. Return
// undefined, though this happens automatically in JavaScript if nothing is
// explicitly returned.
if (!Array.prototype.forEach) {
Array.prototype.forEach = function (func, thisArg) {
var i = 0,
t = toObject(this),
il = toUnit32(t.length);
if (t === undef || t === null) {
throw new ReferenceError('Unable to iterate through object.');
}
if (func === undef || toString.call(func) !== '[object Function]') {
throw new TypeError('Unable to execute function.');
}
while (i < il) {
if (t[i] !== undef) {
func.call(thisArg, t[i], i, t);
}
i += 1;
}
};
}
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function (search, offset) {
var oThis = toObject(this),
len = toUnit32(oThis.length),
n = offset === undef ? 0 : toInteger(offset),
index = -1;
if (len > 0 && len > n && search !== undef) {
if (n < 0) {
n = Math.max(0, len = Math.abs(n));
}
while (n < len) {
if (oThis[n] === search) {
index = n;
break;
}
n += 1;
}
}
return index;
};
}
if (!Object.keys) {
Object.keys = function (obj) {
var name,
keys = [];
for (name in obj) {
if (hasOwn.call(obj, name)) {
keys.push(name);
}
}
if (hasDontEnumBug) {
dontEnums.forEach(function (dont) {
if (obj.hasOwnProperty(dont)) {
keys.push(dont);
}
});
}
return keys;
};
}
}());
var win = window, doc = document, undef;
//(function (win, doc, undef) {
// Here are all the standard functions for retrieving DOM nodes. First the
// functions that find single nodes, then the ones that find multiple. The
// browser may not support all of them.
var singleNode = ['createElement', 'getElementById', 'querySelector'],
manyNodes = ['getElementsByClassName', 'getElementsByName',
'getElementsByTagName', 'querySelectorAll'],
// In this object we store all the extensions that we've needed to add to the
// DOM traversal functions.
extensions = {},
// This object will contain all the native versions of the DOM traversal
// functions so we can easily refer to them again.
methodStore = {},
// A lot of event-related functions will be added here. It makes for easier
// reading if these are added later on, but all variables should be declared at
// the top of the function, so it's here.
event,
// A dummy element used for testing to see whether or not certain methods
// exist.
dummyElement = document.createElement('_'),
// Simple type checking for the sake of validation. Each of these functions
// take a single object and return a boolean.
toString = Object.prototype.toString,
is = {
string: function (o) {
return String(o) === o;
},
callable: function (o) {
return toString.call(o) === '[object Function]';
},
bool: function (o) {
return !!o === o;
/*},
window: function (o) {
return o === win || (o !== null && o !== undef &&
o === o.window);
},
node: function (o) {
return is.window(o) || o === doc ||
toString.call(o.nodeType) === '[object Number]';*/
}
},
tagNames = {
abort: 'img',
change: 'input',
error: 'img',
load: 'img',
reset: 'form',
select: 'input',
submit: 'form'
},
// We need to know this in a few places, so let's work it out now before we do
// anything crazy like try to patch it.
hasNativeAEL = !!doc.addEventListener;
// Based on isEventSupported written by kangax.
function isEventSupported(evt, elem) {
elem = elem || doc.createElem(tagNames[evt] || 'div');
evt = 'on' + evt;
var isSupported = (evt in elem);
if (!isSupported) {
if (!elem.setAttribute) {
elem = doc.createElement('div');
}
if (elem.setAttribute && elem.removeAttribute) {
elem.setAttribute(evt, '');
isSupported = typeof elem[evt] === 'function';
if (elem[evt] !== undef) {
elem[evt] = undef;
}
elem.removeAttribute(evt);
}
}
elem = null;
return isSupported;
}
// Since we have to apply extensions to single and multiple node finding
// functions, it makes more sense to create a function that will handle the
// both of them.
//
// Takes: method (String) the method that we're manipulating.
// isSingle (Boolean) true if this is one of the single
// node functions, false otherwise.
function addExtension(method, isSingle) {
if (doc[method] !== undef) {
methodStore[method] = doc[method];
doc[method] = function (str) {
var elems = methodStore[method](str),
iterable = isSingle ? [elems] : elems;
Array.prototype.forEach.call(iterable, function (elem) {
var i;
for (i in extensions) {
if (extensions.hasOwnProperty(i)) {
elem[i] = extensions[i];
}
}
});
return elems;
};
}
}
// We replace the methods for traversing the DOM with our own ones that have
// the addEventListener polyfill. We need to build in a check so we don't do
// any of the work until we have to and so that we don't end up adding all the
// modern methods multiple times.
function replaceMethods() {
if (!replaceMethods.done) {
singleNode.forEach(function (method) {
addExtension(method, true);
});
manyNodes.forEach(function (method) {
addExtension(method, false);
});
replaceMethods.done = true;
}
}
// Takes a method and adds it to the DOM traversal functions as best it can. If
// HTMLElement.prototype is available then we use it, if not we try for
// Element.prototype and if that doesn't work we have to replace the functions.
// We also add the method to window and document.
//
// Takes: name (String) the name of the method that we're adding.
// fn (Function) the workaround for the method.
function extendDOM(name, fn) {
if (win.HTMLElement) {
HTMLElement.prototype[name] = fn;
} else if (win.Element) {
Element.prototype[name] = fn;
} else {
replaceMethods();
extensions[name] = fn;
}
// NOTE TO SELF: is it worth wrapping these in an if statement with a third
// argument so that we can expose this function to shim things like classList?
win[name] = fn;
doc[name] = fn;
}
// Two functions that simply return true or false, mainly to aid minification.
function returnTrue() {
return true;
}
function returnFalse() {
return false;
}
// Creates a wrapper for the event argument used by addEventListener. Based on
// jQuery.Event.
function Event(src) {
if (src && src.type) {
this.originalEvenbt = src;
this.type = src.type;
this.isDefaultPrevented = (src.defaultPrevened ||
src.returnValue === false ||
src.getPreventDefault && src.getPreventDefault()) ? returnTrue : returnFalse;
} else {
this.type = src;
}
this.timeStamp = src && src.timeStamp || +(new Date());
// Just a little something that native events will never impliment so we know
// this is one of ours.
this.SK80 = true;
}
Event.prototype = {
preventDefault: function () {
var e = this.originalEvent;
this.isDefaultPrevented = returnTrue;
if (e) {
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false;
}
}
},
stopPropagation: function () {
var e = this.originalEvent;
this.isPropagationStopped = returnTrue;
if (e) {
if (e.stopPropagation) {
e.stopPropagation();
} else {
e.cancelBubble = true;
}
}
},
stopImmediatePropagation: function () {
this.isImmediatePropagationStopped = returnTrue;
this.stopPropagation();
},
isDefaultPrevented: returnFalse,
isPropagationStopped: returnFalse,
isImmediatePropagationStopped: returnFalse
};
// The event properties and methods are based on jQuery.event from version
// 1.7.2.
event = {
props: ['altKey', 'bubbles', 'cancelable', 'ctrlKey', 'currentTarget',
'eventPhase', 'metaKey', 'relatedTarget', 'shiftKey', 'target',
'timeStamp', 'view', 'which'],
capture: {
blur: 'focusout',
focus: 'focusin'
},
fixHooks: {},
keyHooks: {
props: ['char', 'charCode', 'key', 'keyCode'],
filter: function (evt, orig) {
if (evt.which === undef) {
evt.which = orig.charCode !== undef ? orig.charCode :
orig.keyCode;
}
return evt;
}
},
mouseHooks: {
props: ['button', 'buttons', 'clientX', 'clientY', 'fromElement',
'offsetX', 'offsetY', 'pageX', 'pageY', 'screenX', 'screenY',
'toElement'],
filter: function (evt, orig) {
var eventDoc,
docElem,
body,
button = orig.button,
from = orig.fromElement;
// Calculate pageX and pageY if they're missing and clientX and clientY are
// available.
if (evt.pageX !== undef && evt.clientX !== undef) {
eventDoc = evt.target.ownerDocument || doc;
docElem = eventDoc.documentElement;
body = eventDoc.body;
evt.pageX = orig.clientX +
(doc && doc.scrollLeft || body && body.scrollLeft || 0) -
(doc && doc.clientLeft || body && body.clientLeft || 0);
evt.pageY = orig.clientY +
(doc && doc.scrollTop || body && body.scrollTop || 0) -
(doc && doc.clientTop || body && body.clientTop || 0);
}
// Add relatedTarget, if necessary.
if (!evt.relatedTarget && from) {
evt.relatedTarget = from === evt.target ? orig.toElement :
from;
}
// Add which for click: 1 = left, 2 = middle, 3 = right.
// Note: copied directly, no idea what it's doing.
if (!evt.which && button !== undef) {
evt.which = (button & 1 ? 1 : (button & 2 ? 3 : (button & 4 ? 2 : 0)));
}
return evt;
}
},
// Fixes the event. When returning, it may pass the event through a hook
// filter, but that filter will return the fixed event as well.
//
// Takes: event (DOMEvent) the event that needs fixing.
// Returns: (Event) the fixed event.
fix: function (evt) {
var fixed = new Event(event),
hook = this.fixHooks[evt.type] || {},
props = hook.props ? this.props.concat(hook.props) : this.props;
props.forEach(function (prop) {
fixed[prop] = event[prop];
});
fixed.target = event.target || event.srcElement || document;
if (fixed.target.nodeType === 3) {
fixed.target = fixed.target.parentNode;
}
if (fixed.metaKey === undef) {
fixed.metaKey = fixed.ctrlKey;
}
return hook.filter ? hook.filter(fixed, evt) : fixed;
}
};
// Populate event.fixHooks.
['click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseover',
'mouseout', 'mouseenter', 'mouseleave',
'contextmenu'].forEach(function (type) {
event.fixHooks[type] = event.mouseHooks;
});
['keydown', 'keypress', 'keyup'].forEach(function (type) {
event.fixHooks[type] = event.keyHooks;
});
// To keep track of the events assigned to an element, we need to store them in
// an array. We keep track of the elements by storing them in element.elems -
// the same index corresponds to element.events which contains the events.
var element = {
// This is a simple array of elements.
elems: [],
// The data structure for the events is a little more complicated. A typical
// collection of events could look something like this:
// [0] = {
// click: [
// [
// function () {alert('non capture');},
// function () {alert('another non capture');}
// ],
// [
// function () {alert('capture');}
// ],
// true,
// true
// ]
// }
// The two booleans at the end are set in element.bind() and are there as flags
// so we know whether or not the event processing has been bound.
events: [],
// As we store the event we need to build up the structure shown above so we
// can store the Function itself in the correct place. As we create the Array
// of Functions, we add an Array of 2 Arrays. By forcing cap to be a Number,
// we can easily store the non-capture Functions in the first Array and the
// capture Functions in the second.
//
// Takes: elem (HTMLElement) the element to which the event is
// bound.
// type (String) the event type.
// func (Function) the event handler.
// cap (Boolean) true to use capture, false otherwise.
store: function (elem, type, func, cap) {
var index = this.elems.indexOf(elem);
if (index < 0) {
index = this.elems.push(elem) - 1;
}
if (!this.events[index]) {
this.events[index] = {};
}
if (!this.events[index].hasOwnProperty(type)) {
this.events[index][type] = [[], []];
}
this.events[index][type][+cap].push(func);
},
// The get method simply returns the list of Functions, if it can find them. If
// not, it returns an empty Array.
//
// Takes: elem (HTMLElement) the element to which the event is
// bound.
// type (String) the event type.
// cap (Boolean) true if a capture Function, false
// otherwise.
// Returns: (Array) any Functions that were found that match the given
// criteria above. If none are found, an empty Array is
// returned.
get: function (elem, type, cap) {
var index = this.elems.indexOf(elem),
stored,
events = [];
if (index > -1) {
stored = this.events[index];
if (stored && stored.hasOwnProperty(type)) {
events = stored[type][+cap];
}
}
return events;
},
// Binds the event to the element but only once. This sets the booleans
// described in the comments for element.events.
//
// Takes: elem (HTMLElement) the element to which the event is
// bound.
// type (String) the event type.
// cap (Boolean) true if a capture Function, false
// otherwise.
bind: function (elem, type, cap) {
var index = this.elems.indexOf(elem);
// Owing to type coersion, we can add a number to a boolean and get a number
// back. true (1) + 2 === 3, false (0) + 2 === 2. This gives us access to the
// event booleans.
if (index > -1 && !this.events[index][type][cap + 2]) {
this.events[index][type][cap + 2] = true;
elem.attachEvent('on' + type, function (e) {
processEvent.call(elem, event.fix(e || win.event), type, cap);
});
}
}
};
// Based on Dean Edwards' Callbacks vs Events
// http://dean.edwards.name/weblog/2009/03/callbacks-vs-events/
var currentHandler,
dispatchFakeEvent = function () {};
if (!hasNativeAEL) {
doc.documentElement.AELpolyfill = 0;
doc.documentElement.attachEvent('onpropertychange', function (e) {
e = e || win.event;
if (e.propertyName === 'AELpolyfill') {
currentHandler();
}
});
dispatchFakeEvent = function () {
doc.documentElement.AELpolyfill += 1;
}
}
function processEvent(e, type, cap) {
// execute all events until e.isImmediatePropagationStopped()
var events = element.get(this, type, cap),
i = 0,
il = events.length,
elem = this;
do {
//events[i].call(this, e);
currentHandler = function () {
events[i].call(elem, e);
};
dispatchFakeEvent();
i += 1;
} while (i < il && !e.isImmediatePropagationStopped());
}
// According to modern specs, useCapture can be optional and it it's not
// supplied, assume it was false. Also, if evt is not a string, fn is not a
// function or cap is not a boolean, just do nothing but don't throw any
// errors. Chrome and Firefox throw errors if this is bound to something other
// than a node, saying "Illegal operation" so we'll match this.
// Luckily, IE's attachEvent will throw a similar error if something other than
// an HTMLElement is bound to it
if (!hasNativeAEL) {
extendDOM('addEventListener', function (evt, func, cap) {
if (is.string(evt) && is.callable(func) &&
(is.bool(cap) || cap === undef)) {
if (cap) {
evt = event.capture[evt] || evt;
}
element.store(this, evt, func, !!cap);
element.bind(this, evt, !!cap);
}
});
}
//}(window, document));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment