/* event_handler, version 1.1 * Requires Prototype 1.6.0 * Copyright (c) 2008 Motionbox, Inc. * * event_handler is freely distributable under * the terms of an MIT-style license. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * For details, see the web site: http://code.google.com/p/motionbox * * * Authors: Richard Allaway, Topping Bowers, Baldur Gudbjornsson, Matt Royal * * MBX.event_handler is an implementation of event delegation based on the * prototype library. * Readme: http://code.google.com/p/motionbox/wiki/EventHandlerDocumentation * API: * * MBX.event_handler has the following three public functions: * * subscribe(specifiers, eventTypes, functionsToCall) * specifiers = a string (or optional array of strings) specifying either a class or an id to subscribe to * eventTypes = a string (or optional array of strings) specifying the name of the events to subscribe to * functionsToCall = a function (or optional array of functions) to call upon receiving the event. * These functions should accept an Event as their first argument. * -- * returns: handlerObj * * unsubscribe(handlerObj) * handlerObj = the object returned by the subscribe function * * fireCustom(target, eventName, opts) * target = DOM element * eventName = the name of the event to fire (eg, "click" or "my_custom_event") * opts = optional object with which to extend the event that is fired *--------------------------------------------------------------------------*/ if (!("MBX" in window)) { MBX = {}; } MBX.EventHandler = (function () { //private functions below // all the standard events we want to listen to on document. // please note that 'change' and 'blur' DO NOT BUBBLE in IE - so you will need to do something // extra for the Microsoft browsers var stdEvents = ["mouseout", "click", "mouseover", "keypress", "change", "blur", "focus"]; // an object with all the event listeners we have listed by eventType // gets filled in on init var eventListeners = {}; // Event bubbles up to the document where the listeners are and fires this function // if the target element matches anything in the subscribers object then the functions fire // it continues to go all the way up the tree var handleEvent = function (evt) { //for debug uncomment out the below //console.dir(evt); //console.log(Event.element(evt)); //evt.isConsumed = false; var targetElement; //the below fixes an intermittent prototype JS error if (Event && evt) { try { targetElement = Event.element(evt); } catch (e) { } } if (targetElement) { functionsFromElementAndEvent(targetElement, evt); } }; //subscribe to the listeners stdEvents.each(function (evtType) { eventListeners[evtType] = document.observe(evtType, handleEvent); }); //this holds the actual subscriptions in the form // ids: { // myId: { // myEventType: [function, function, function] // } // } // same for classes // rules however is the opposite (for speed sake) // so: // rules: { // eventType: { // "my rule": [function, function, function] // } // } var subscriptions = { ids: {}, classes: {}, rules: {} }; //executes an array of functions sending the event to the function var callFunctions = function (functionsToCall, evt) { while (functionsToCall.length > 0) { functionsToCall.pop()(evt); } }; // if there is a listener defined for the evtType, then // loop through those rules and compare them to target // bad CSS selectors can throw up really bad JS errors, var functionsFromRules = function (target, evtType) { if (!subscriptions.rules[evtType]) { return []; } var functionsToCall = []; for (prop in subscriptions.rules[evtType]) { if (subscriptions.rules[evtType].hasOwnProperty(prop) && target.match(prop)) { functionsToCall = functionsToCall.concat(subscriptions.rules[evtType][prop]); } } return functionsToCall; }; // go to the subscriptions.ids object and grab an array of all the functions that are subscribed to // the eventType evtType... so subscriptions.ids[targetId][evtType] which will be an array of functions var functionsFromId = function (targetId, evtType) { var returnArray = []; if (subscriptions.ids[targetId] && subscriptions.ids[targetId][evtType]) { returnArray = returnArray.concat(subscriptions.ids[targetId][evtType]); } return returnArray; }; //same as functionsFromId, but uses all classes on the target object and looks in subscriptions.classes object var functionsFromClasses = function (targetClasses, evtType) { var functionsToCall = []; var classObject; targetClasses = $A(targetClasses); for (var index = 0, classLen = targetClasses.length; index < classLen; ++index) { classObject = subscriptions.classes[targetClasses[index]] if (classObject && classObject[evtType]) { functionsToCall = functionsToCall.concat(classObject[evtType]); } } return functionsToCall; }; // given an element and an event type, call the functions held in the // subscriptions object var functionsFromElementAndEvent = function (targetElement, evt) { var evtType = evt.type; if (!targetElement) { return; } if (targetElement.id) { callFunctions(functionsFromId(targetElement.id, evtType), evt); } if (targetElement.className) { var targetClasses = Element.classNames(targetElement); callFunctions(functionsFromClasses(targetClasses, evtType), evt); } callFunctions(functionsFromRules(targetElement, evtType), evt); //recursively call self walking up the tree if (targetElement != window && targetElement != document && targetElement.parentNode) { var upTreeNode = targetElement.parentNode; if (upTreeNode && upTreeNode.tagName && upTreeNode.tagName != "HTML") { functionsFromElementAndEvent($(upTreeNode), evt); } } }; // handle the creation of ID or class based subscriptions for a single specifier arrays of types and functions var createIdOrClassSubscription = function(specifierType, specifier, evtTypes, funcs) { var subscriptionArray = []; if (!subscriptions[specifierType][specifier]) { subscriptions[specifierType][specifier] = {}; } var specifierObject = subscriptions[specifierType][specifier]; evtTypes.each(function (evtType) { funcs.each(function (func) { if (!specifierObject[evtType]) { specifierObject[evtType] = [func]; } else { specifierObject[evtType].push(func); } subscriptionArray.push({'specifierType': specifierType, 'eventType': evtType, 'func': func, 'specifier': specifier}); }); }); return subscriptionArray; }; // handle a CSS selector based subscription for a single specifier and arrays of types and functions var createRulesSubscription = function(specifier, evtTypes, funcs) { var subscriptionArray = []; evtTypes.each(function (evtType) { if (!subscriptions.rules[evtType]) { subscriptions.rules[evtType] = {}; } var specifierObject = subscriptions.rules[evtType]; funcs.each(function (func) { if (!specifierObject[specifier]) { specifierObject[specifier] = [func]; } else { specifierObject[specifier].push(func); } subscriptionArray.push({'specifierType': 'rules', 'eventType': evtType, 'func': func, 'specifier': specifier}); }); //each function }); // each event type return subscriptionArray; }; // utility functions var isArray = function (obj) { if (obj) { return obj.constructor == Array; } }; var isId = function(specifierString) { return /^\#[\w-]+$/.test(specifierString); }; var isClass = function(specifierString) { return /^\.[\w-]+$/.test(specifierString); }; var browserLikeEventExtender = { preventDefault: function () {}, stopPropagation: function () {}, pageX: 0, pageY: 0, clientX: 0, clientY: 0 }; var CustomEvent = function (theTarget, evt, opts) { this.type = evt; this.target = theTarget; this.srcElement = theTarget; this.eventName = evt; this.memo = {}; Object.extend(this, opts); for (prop in browserLikeEventExtender) { if (browserLikeEventExtender.hasOwnProperty(prop)) { if (!this[prop]) { this[prop] = browserLikeEventExtender[prop]; } } } if (Prototype.Browser.IE) { Event.extend(this); } }; if (!Prototype.Browser.IE) { (function () { var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { m[name] = Event.Methods[name].methodize(); return m; }); CustomEvent.prototype = CustomEvent.prototype || document.createEvent("HTMLEvents").__proto__; Object.extend(CustomEvent.prototype, methods); })(); } if (Prototype.Browser.IE) { var destroyObservers = function () { stdEvents.each(function (evtType) { document.stopObserving(evtType, handleEvent); }); }; window.attachEvent('onunload', destroyObservers); } return { //public functions // institue the subscriber: '#' indicates an id, "." indicates a class, anything else is considered // a CSS Selector // subscribe with: // MBX.EventHandler.subscribe(".myClass", "click", function (){ alert('hi'); }); // or: // MBX.EventHandler.subscribe("p#blah.cool", "click", function(evt) {console.dir(evt);}); // events may be custom events (see fireCustom). // returns an object you can use to unsubscribe subscribe: function (specifiers, evtTypes, funcs) { if (!isArray(specifiers)) { specifiers = [specifiers]; } if (!isArray(evtTypes)) { evtTypes = [evtTypes]; } if (!isArray(funcs)) { funcs = [funcs]; } var referenceArray = []; specifiers.each(function (specifier) { var specifierType; if (isId(specifier)) { specifier = specifier.sub(/#/, ""); specifierType = "ids"; } if (isClass(specifier)) { specifierType = "classes"; specifier = specifier.sub(/\./, ""); } //check if it matched id or class if (specifierType) { referenceArray = referenceArray.concat(createIdOrClassSubscription(specifierType, specifier, evtTypes, funcs)); } else { // we assume that anything not matching a class or id is a css selector rule referenceArray = referenceArray.concat(createRulesSubscription(specifier, evtTypes, funcs)); } //end to rules handling }); // each specifier // return the array that can be used to unsubscribe return referenceArray; }, unsubscribe: function (handlerObjects) { handlerObjects.each(function (handlerObject) { if (!(handlerObject.specifierType && handlerObject.eventType && handlerObject.specifier) || typeof handlerObject.func != 'function') { throw new Error('bad unsubscribe object passed to EventHandler.unsubscribe'); } if (handlerObject.specifierType != "rules") { var locator = subscriptions[handlerObject.specifierType][handlerObject.specifier][handlerObject.eventType]; for (var i = 0, funcLen = locator.length; i < funcLen; ++i) { if (locator[i] == handlerObject.func) { subscriptions[handlerObject.specifierType][handlerObject.specifier][handlerObject.eventType].splice(i, 1); } } } else { var locator = subscriptions[handlerObject.specifierType][handlerObject.eventType][handlerObject.specifier]; for (var i = 0, funcLen = locator.length; i < funcLen; ++i) { if (locator[i] == handlerObject.func) { subscriptions[handlerObject.specifierType][handlerObject.eventType][handlerObject.specifier].splice(i, 1); } } } }); return true; }, // fire a custom event of your choosing. Will notify any subscribers to that evt // MBX.EventHandler.fireCustom($('element'), 'mycustomeevent'); fireCustom: function (theTarget, evt, opts) { opts = opts || {}; var theEvent = new CustomEvent(theTarget, evt, opts); Event.extend(theEvent); handleEvent(theEvent); }, //TEST FUNCTION ONLY! dirSubscriptions: function () { console.dir(subscriptions); }, dirEventListeners: function () { console.dir(eventListeners); }, debugSubscriptions: function () { return subscriptions; } }; })();