Created
July 12, 2012 21:41
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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