Skip to content

Instantly share code, notes, and snippets.

@codyromano
Last active September 20, 2016 02:06
Show Gist options
  • Select an option

  • Save codyromano/f661af6263b8a0f88795d1518deeed7c to your computer and use it in GitHub Desktop.

Select an option

Save codyromano/f661af6263b8a0f88795d1518deeed7c to your computer and use it in GitHub Desktop.
An event listener that works even if the element hasn't been added to the page yet
/**
* @module GlobalEventListener
*/
(function(MutationObserver, exports) {
'use strict';
var proto = GlobalEventListener.prototype;
/**
* @this GlobalEventListener
*/
function initMutationObserver(MutationObserver) {
var observerConfig = {
childList: true,
attributes: this.watchAttributes,
characterData: false
};
if (MutationObserver) {
this.observer = new MutationObserver(this.updateListeners.bind(this));
this.observer.observe(this.root, observerConfig);
} else {
throw new Error("Your browser doesn't support the MutationObserver " +
"API. This probably means you're using IE10 or lower. For more info " +
+ "on compatibility, visit caniuse.com/#search=mutation%20observer. " +
"You can manually call 'updateListeners()' as a fallback.");
}
}
function GlobalEventListener(root, settings) {
this.root = root;
// Map {eventType => selectorString => callback} for O(1) lookup
this.selectorFnMap = {};
this.settings = extend({
/* When 'watchAttributes' is true, the GlobalEventListener will
manage events (adding or removing listeners as necessary) whenever a
subtree attribute changes.
Use this if you expect ID or class attributes to change frequently.
Otherwise, watching for insertion or removal should be fine. */
watchAttributes: false,
useMutationObserver: true
}, settings || {});
if (typeof root !== 'object' || !root instanceof HTMLElement) {
throw new Error("Root element not found. Please ensure that your " +
"specified root element exists before you instantiate the " +
"GlobalEventListener.");
}
if (this.settings.useMutationObserver) {
initMutationObserver.call(this, MutationObserver);
}
}
proto.updateListeners = function() {
// Iterate through event types such as click, hover, focus
forEachKeyValue(this.selectorFnMap, function(eventType, selectorList) {
// Iterate through the selectors associated with each event type
forEachKeyValue(selectorList,
manageEventListeners.bind(undefined, eventType));
});
};
function extend(objA, objB) {
for (var key in objB) {
if (objB.hasOwnProperty(key)) {
objA[key] = objB[key];
}
}
return objA;
}
function combineFunctions(fn1, fn2, context) {
context = context || null;
return function() {
var args = toArray(arguments);
fn1.apply(context, args);
return fn2.apply(context, args);
}
}
proto.listen = function(eventType, selector, callback) {
var existingCallback;
if (typeof this.selectorFnMap[eventType] !== 'object') {
this.selectorFnMap[eventType] = {};
}
existingCallback = this.selectorFnMap[eventType][selector];
if (typeof existingCallback === 'function') {
// Rather than pushing the new callback into an array that we would
// have to iterate through, we merge the callback with existing ones
this.selectorFnMap[eventType][selector] = combineFunctions(
existingCallback, callback);
} else {
this.selectorFnMap[eventType][selector] = callback;
}
}
function toArray(arrayLikeObject) {
return [].slice.call(arrayLikeObject);
}
function forEachKeyValue(obj, fn) {
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
fn(key, obj[key]);
}
}
}
function getNodesAsArray(selector) {
var elems = document.querySelectorAll(selector);
return (elems === null) ? [] : [].slice.call(elems);
}
/**
* @desc When the structure of the DOM changes, check if elements
* have been added or removed and add / remove listeners accordingly.
*/
function manageEventListeners(eventType, selector, callback) {
var nodes = getNodesAsArray(selector);
var operation = (nodes.length >= 1) ?
'addEventListener' : 'removeEventListener';
nodes.forEach(function(node) {
node[operation](eventType, callback, false);
});
}
exports.GlobalEventListener = GlobalEventListener;
})(window.MutationObserver, window)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment