Last active
September 20, 2016 02:06
-
-
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
This file contains hidden or 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
| /** | |
| * @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