/ Published in: JavaScript
Enhanced cross-browser event handling
Expand |
Embed | Plain Text
Copy this code and paste it in your HTML
/***************************************** * Events Max v10.0 * Enhanced cross-browser event handling * * This work is licensed under a Creative Commons Attribution 3.0 Unported License * http://creativecommons.org/licenses/by/3.0/ * * Author: Andy Harrison, http://dragonzreef.com/ * Date: 28 June 2012 *****************************************/ //Cross-browser event registration functions //Supports both capture and bubble phases //Handlers execute in FIFO order //Supports mouseenter and mouseleave events //Custom event attributes: // event.mouse.button: the mouse button value (1, 2, or 4) for mousedown, mouseup, click, and dblclick events // event.mouse.position: object containing the mouse positions within the screen, window, document, and layer (e.g., evt.mouse.position.document.x) // event.mouse.wheelDelta: distance ("clicks") the mouse wheel rolled; negative means it rolled up // event.keyboard.charCode: Unicode character code that was generated on a keypress event // event.keyboard.char: Unicode character that was generated on a keypress event // //addEventHandler(obj, type, handler, useCapture) //removeEventHandler(obj, type, handler_or_guid, useCapture) // //Event types are case-sensitive and should not include "on" (e.g., use "click", not "onclick") // //Usage examples: // addEventHandler(element, "click", handlerFunction, true); // removeEventHandler(element, "click", handlerFunction, true); //or: // var guid = addEventHandler(element, "click", function(evt){doSomething()}); // removeEventHandler(element, "click", guid); // //This script uses a custom event attribute for the mouse button value: event.mouse.button //This goes by the Microsoft model, where left==1, right==2, and middle==4 //In IE lte 8, the value may be incorrect on mousedown since we can't reliably keep track of the event.button value between events. // (e.g., if the mouse leaves the window, buttons are changed by the user without the browser's detection, and the mouse comes back) // //There are a few custom attributes used by this script that must not be manipulated: // ._handlerGUID on functions passed as handlers // ._eventHandlers on DOM objects that have handlers assigned to them // //Be aware that browsers act differently when the DOM is manipulated. Much of it seems to have to do with when the propagation path is // determined for events (e.g., as soon as they're added to the event loop vs. just before they're dispatched). Some fire mouseover/out // events when an element is added/removed/positioned under the mouse, some stop bubbling an event if the currentTarget has been removed // from the DOM, etc. // //Techniques and inspiration largely from: // http://dean.edwards.name/weblog/2005/10/add-event2/ // http://outofhanwell.wordpress.com/2006/07/03/cross-window-events/ // http://blog.metawrap.com/2005/10/24/ie-closures-leaks/ // http://www.quirksmode.org/js/events_properties.html //addEventHandler(obj, type, handler, useCapture) //returns the GUID assigned to the handler var addEventHandler; //removeEventHandler(obj, type, handler_or_guid, useCapture) var removeEventHandler; (function (){ "use strict"; /*** event handler registration ***/ var newGUID = 1; //GUID to assign to the next event handler function without one addEventHandler = function (obj, type, handler, useCapture){ type = ""+type; if(!(obj instanceof Object || (!document.addEventListener && typeof(obj) === "object"))){ throw new TypeError("Invalid argument: obj"); } if(!(/^[0-9a-z]+$/i).test(type)){ throw new TypeError("Invalid argument: type"); } if(typeof(handler) !== "function"){ throw new TypeError("Invalid argument: handler"); } var ownerWindow = getOwnerWindow(obj); //make sure the object's window flushes handlers when the page unloads to avoid memory leaks if(!flushAllHandlers.adding && !handlerIsAssigned(ownerWindow, "unload", "bubble", flushAllHandlers._handlerGUID)){ flushAllHandlers.adding = true; addEventHandler(ownerWindow, "unload", flushAllHandlers); flushAllHandlers.adding = false; } if(type === "mouseenter"){ //make sure the object's window handles the mouseover type so that custom events can be triggered after the bubble phase of mouseenter addTypeHandler(ownerWindow, "mouseover"); //add global handlers for the mouseover event type for the object (if they don't already exist) addTypeHandler(obj, "mouseover"); if(!obj._eventHandlers) obj._eventHandlers = {}; if(!obj._eventHandlers["mouseenter"]) obj._eventHandlers["mouseenter"] = { capture: [], bubble: [] }; } else if(type === "mouseleave"){ //make sure the object's window handles the mouseout type so that custom events can be triggered after the bubble phase of mouseleave addTypeHandler(ownerWindow, "mouseout"); //add global handlers for the mouseout event type for the object (if they don't already exist) addTypeHandler(obj, "mouseout"); if(!obj._eventHandlers) obj._eventHandlers = {}; if(!obj._eventHandlers["mouseleave"]) obj._eventHandlers["mouseleave"] = { capture: [], bubble: [] }; } else{ //add global handlers for the event type for the object (if they don't already exist) addTypeHandler(obj, type); } if(isNaN(handler._handlerGUID) || handler._handlerGUID < 1 || handler._handlerGUID === Infinity){ handler._handlerGUID = newGUID++; //assign a GUID to the handler if it doesn't have one (or if the user messed with it) } var phase = useCapture ? "capture" : "bubble"; if(!handlerIsAssigned(obj, type, phase, handler._handlerGUID)){ //if this handler isn't already assigned to this object, event type, and phase obj._eventHandlers[type][phase].push({ guid: handler._handlerGUID, handler: handler }); //add the handler to the list } return handler._handlerGUID; }; //get the window in which the object resides; this is not necessarily the same window where a function is defined function getOwnerWindow(obj){ return (obj.ownerDocument || obj.document || obj).parentWindow || window; /* obj==element obj==window obj==document */ } //add global handlers for an event type for an object (if they don't already exist) function addTypeHandler(obj, type){ if(!obj._eventHandlers) obj._eventHandlers = {}; if(!obj._eventHandlers[type]){ obj._eventHandlers[type] = { capture: [], bubble: [] }; if(document.addEventListener){ //not IE lte 8 obj.addEventListener(type, patchHandler(obj, handleEvent, true), true); obj.addEventListener(type, patchHandler(obj, handleEvent), false); } else if(obj.attachEvent){ //IE lte 8 obj.attachEvent("on"+type, patchHandler(obj, handleEvent)); } else{ //just in case if(obj["on"+type]){ //if there is already a handler assigned obj["on"+type]._handlerGUID = newGUID; obj._eventHandlers[type]["bubble"][0] = { guid: newGUID++, handler: obj["on"+type] }; } obj["on"+type] = patchHandler(obj, handleEvent); } } } function patchHandler(obj, handler, capturing){ return function (evt){ //In IE lte 8, if this patched handler is assigned to an event attribute on a DOM node instead of using attachEvent(), // we need to get the event object from the window the node resides in (which is not necessarily where the handler was defined). var evtWindow = getOwnerWindow(obj); evt = evt || evtWindow.event; if(!evt.view) evt.view = evtWindow; patchEvent.call(obj, evt, capturing); handler.call(obj, evt); //applies the correct value for the `this` keyword and passes the patched event object }; } //is the handler for this object, event type, and phase already in the list? function handlerIsAssigned(obj, type, phase, guid){ if(!obj._eventHandlers || !obj._eventHandlers[type] || !guid) return false; var handlerList = obj._eventHandlers[type][phase]; for(var i=0; i<handlerList.length; i++){ if(handlerList[i].guid === guid) return true; //handler is already in the list } return false; } /*** event handling ***/ var _evtStatus = {}; //.capturing, .propagationStopped, .propagationStoppedAtTarget, .propagationStoppedImmediately var mouseELQueue = []; //propagation path for mouseenter and mouseleave events function handleEvent(evt){ var returnValue, evtClone, path, handlers, i, j; returnValue = evt.type==="mouseover" ? false : evt.type!=="beforeunload" ? true : returnValue; //test whether an object is still in the DOM or if it has been removed by a previous handler function inDOM(obj){ if(!obj) return false; if(obj === evt.view || obj === evt.view.document) return true; var tmp = obj; do{ if(!tmp.parentNode) return false; //object is not in the DOM tmp = tmp.parentNode; }while(tmp !== evt.view.document) return true; } function updateReturnValue(evt, newValue){ if(evt.type === "mouseover"){ returnValue = returnValue === true || newValue === true || evt.defaultPrevented; } else if(evt.type === "beforeunload"){ //in this implementation, only the first defined return value will be used; return values from subsequent handlers will be ignored returnValue = typeof(returnValue) !== "undefined" ? returnValue : newValue; } else{ returnValue = !evt.cancelable || (returnValue && newValue !== false && !evt.defaultPrevented); } } /*** mouseenter & mouseleave preparations ***/ function fillMouseELQueue(evt){ //note: this can get screwed up if elements are moved/removed from the DOM var obj, obj2; mouseELQueue = []; if(evt.target === evt.relatedTarget){ //do nothing } else if(evt.target === evt.view){ //related is a child of window; did not enter or leave window; do nothing } else if(evt.relatedTarget === null){ //entered/left window; events will be fired obj = evt.target; while(obj){ mouseELQueue.push(obj); obj = obj.parentNode; } mouseELQueue.push(evt.view); } else{ obj = evt.relatedTarget; while(obj && obj !== evt.target){ obj = obj.parentNode } if(obj === evt.target){ //related is a child of target; did not enter or leave target; do nothing } else{ //related is not a child of target (but target is not necessarily a child of related); // entered/left target; possibly left/entered related; events will be fired obj = evt.target; while(obj && obj !== evt.relatedTarget){ obj2 = evt.relatedTarget; while(obj2 && obj2 !== obj){ obj2 = obj2.parentNode; } if(obj === obj2){ //common ancestor of target & related (mouse left/entered related) break; } mouseELQueue.push(obj); //obj is a child of related obj = obj.parentNode; } } } } //at beginning of capture phase, fill mouseELQueue (if applicable) if((evt.type === "mouseover" || evt.type === "mouseout") && _evtStatus.capturing && this === evt.view){ fillMouseELQueue(evt); } /*** manually run capture phase, if required ***/ //for IE lte 8, run capture phase manually if(!document.addEventListener && evt.eventPhase === 2){ //create copy of event object (so we can modify read-only properties) evtClone = createEventClone(evt); evtClone.eventPhase = 1; //at beginning of capture phase, fill mouseELQueue (if applicable) if(evt.type === "mouseover" || evt.type === "mouseout"){ fillMouseELQueue(evtClone); } path = getPropagationPath(evt); //array of objects with related event handler (not including the target) for(i=path.length-1; i>=0; i--){ //for each element in the propagation path array if(_evtStatus.propagationStopped) break; //update event object evtClone.currentTarget = path[i]; //execute the capture handlers (in FIFO order) handlers = path[i]._eventHandlers[evtClone.type]["capture"]; for(j=0; j<handlers.length; j++){ //execute the handler and update the return value updateReturnValue(evtClone, handlers[j].handler.call(path[j], evtClone)); if(_evtStatus.propagationStoppedImmediately) break; } } //execute the capture handlers on the target (in FIFO order) if(!_evtStatus.propagationStopped){ evtClone.eventPhase = 2; evtClone.currentTarget = this; handlers = this._eventHandlers[evtClone.type]["capture"]; for(i=0; i<handlers.length; i++){ //execute the handler and update the return value updateReturnValue(evtClone, handlers[i].handler.call(this, evtClone)); if(_evtStatus.propagationStoppedImmediately) break; } } evtClone = null; } /*** process handlers for currentTarget ***/ //execute the handlers for this phase (in FIFO order) if(!_evtStatus.propagationStopped || (evt.eventPhase===2 && _evtStatus.propagationStoppedAtTarget)){ handlers = this._eventHandlers[evt.type][_evtStatus.capturing ? "capture" : "bubble"]; for(i=0; i<handlers.length; i++){ if(_evtStatus.propagationStoppedImmediately) break; //execute the handler and update the return value updateReturnValue(evt, handlers[i].handler.call(this, evt)); } } if((evt.type === "mouseover" && returnValue === true) || (evt.type !== "mouseover" && returnValue === false)){ evt.preventDefault(); } /*** finalize event ***/ //if done handling this event if(!_evtStatus.capturing && (this === evt.view || !evt.bubbles)){ //trigger mouseenter events, if applicable if(evt.type === "mouseover" && mouseELQueue.length > 0){ evtClone = createEventClone(evt); while(mouseELQueue.length > 0){ evtClone.target = mouseELQueue.pop(); triggerCustomEvent(evtClone, "mouseenter", false); } } //trigger mouseleave events, if applicable else if(evt.type === "mouseout" && mouseELQueue.length > 0){ evtClone = createEventClone(evt); while(mouseELQueue.length > 0){ evtClone.target = mouseELQueue.shift(); triggerCustomEvent(evtClone, "mouseleave", false); } } //reset event status _evtStatus = {}; } evt.returnValue = returnValue; return returnValue; } //get hierarchical array of objects with handlers for a specific event; first item is object closest to target (target is not included) function getPropagationPath(evt){ var path = []; var obj = evt.target; var handlers; while(obj.parentNode){ obj = obj.parentNode; if(!obj._eventHandlers || !obj._eventHandlers[evt.type]) continue; handlers = obj._eventHandlers[evt.type]; if(handlers["capture"].length > 0 || handlers["bubble"].length > 0){ path.push(obj); } } if(evt.target !== evt.view && evt.view._eventHandlers && evt.view._eventHandlers[evt.type]){ handlers = evt.view._eventHandlers[evt.type]; if(handlers["capture"].length > 0 || handlers["bubble"].length > 0){ path.push(evt.view); } } return path; } function patchEvent(evt, capturing){ if(!evt.target) evt.target = evt.srcElement; if(!evt.srcElement) evt.srcElement = evt.target; if(!evt.relatedTarget) try{ evt.relatedTarget = evt.target===evt.toElement ? evt.fromElement : evt.toElement; }catch(e){} if(!evt.currentTarget) evt.currentTarget = this; if(!evt.eventPhase) evt.eventPhase = evt.target===this ? 2 : 3; //capturing==1 (not supported), at_target==2, bubbling==3 _evtStatus.capturing = capturing; //to determine, when evt.eventPhase===2, whether we need to execute capture or bubble handlers on the target var originalPreventDefault = evt.preventDefault; evt.preventDefault = function (){ if(this.cancelable){ if(originalPreventDefault) originalPreventDefault(); this.returnValue = false; if(!this.defaultPrevented) this.defaultPrevented = true; } }; evt.stopPropagation = function (){ if(this.eventPhase === 2 && !_evtStatus.propagationStopped) _evtStatus.propagationStoppedAtTarget = true; _evtStatus.propagationStopped = true; }; evt.stopImmediatePropagation = function (){ _evtStatus.propagationStopped = true; _evtStatus.propagationStoppedImmediately = true; }; /*** mouse event attributes ***/ if(!evt.fromElement && !evt.toElement){ try{ if(evt.type === "mouseover" || evt.type === "mouseenter"){ evt.fromElement = evt.relatedTarget; evt.toElement = evt.target; } else if(evt.type === "mouseout" || evt.type === "mouseleave"){ evt.fromElement = evt.target; evt.toElement = evt.relatedTarget; } }catch(e){} } //add custom mouse attributes evt.mouse = {}; //mouse button //this is the button that was pressed to trigger this event: 1==left, 2==right, 4==middle if(evt.type === "mousedown" || evt.type === "mouseup" || evt.type === "click" || evt.type === "dblclick"){ if(evt.which){ evt.mouse.button = evt.which===1 ? 1 : evt.which===2 ? 4 : 2; } else{ //IE lte 8 var mb = patchEvent.mouseButtons; if(evt.target === this){ //update mb.button if(evt.type === "mousedown"){ mb.button = (evt.button ^ mb.pressed) & evt.button; if((mb.button & evt.button) === 0) mb.button = evt.button; mb.pressed = evt.button; //note: mb.button may be incorrect on mousedown since we can't reliably keep track of IE's event.button // value (i.e., which buttons are pressed when the event is fired) between events (e.g., if the mouse // leaves the window, buttons are changed by the user without the browser's detection, and the mouse comes back) } else if(evt.type === "mouseup"){ mb.button = evt.button; mb.pressed = ~evt.button & mb.pressed; } } evt.mouse.button = mb.button; } } else{ evt.mouse.button = patchEvent.mouseButtons.button = 0; } //mouse wheel distance //this is the distance ("clicks") the mouse wheel rolled; negative means it rolled up if((evt.type==="mousewheel" || evt.type==="wheel" || evt.type==="DOMMouseScroll") && (evt.wheelDelta || evt.detail)){ evt.mouse.wheelDelta = evt.wheelDelta ? -evt.wheelDelta/120 : evt.detail ? evt.detail/3 : 0; } //mouse position if(evt.type.slice(0,5) === "mouse" || evt.type==="wheel" || evt.type==="DOMMouseScroll" || evt.type.slice(0,4)==="drag" || evt.type==="drop"){ evt.mouse.position = {}; evt.mouse.position.screen = { x:evt.screenX, y:evt.screenY, left:evt.screenX, top:evt.screenY }; evt.mouse.position.window = evt.mouse.position.frame = { x:evt.clientX, y:evt.clientY, left:evt.clientX, top:evt.clientY }; evt.mouse.position.document = (function(){ if(isNaN(evt.pageX) || isNaN(evt.pageY)){ var left, top; //scroll position of document if(window.pageYOffset) //all except IE { left = window.pageXOffset; top = window.pageYOffset; } else if(document.documentElement && !isNaN(document.documentElement.scrollTop)) //IE standards compliance mode { left = document.documentElement.scrollLeft; top = document.documentElement.scrollTop; } else //IE quirks mode { left = document.body.scrollLeft; top = document.body.scrollTop; } return { x:left+evt.clientX, y:top+evt.clientY, left:left+evt.clientX, top:top+evt.clientY }; } else return { x:evt.pageX, y:evt.pageY, left:evt.pageX, top:evt.pageY }; })(); evt.mouse.position.layer = { x:evt.layerX||evt.x||0, y:evt.layerY||evt.y||0, left:evt.layerX||evt.x||0, top:evt.layerY||evt.y||0 }; try{ evt.pageX = evt.mouse.position.document.x; evt.pageY = evt.mouse.position.document.y; }catch(e){} try{ evt.layerX = evt.mouse.position.layer.x; evt.layerY = evt.mouse.position.layer.y; }catch(e){} } /*** keyboard event attributes ***/ //add custom key attributes evt.keyboard = {}; //see http://unixpapa.com/js/key.html // http://www.quirksmode.org/js/keys.html if(evt.type === "keypress"){ if(isNaN(evt.which)){ //IE lte 8 evt.keyboard.charCode = evt.keyCode; evt.keyboard.char = String.fromCharCode(evt.keyCode); } else if(evt.which !== 0 && evt.charCode !== 0){ //other browsers (note: sometimes special keys still give a non-zero value) evt.keyboard.charCode = evt.which; evt.keyboard.char = String.fromCharCode(evt.which); } else{ //special key evt.keyboard.charCode = 0; evt.keyboard.char = evt.key; //evt.key only works in IE gte 9; it gives "Control", "Shift", "Down", etc. for special keys } try{ evt.which = evt.keyboard.charCode; }catch(e){} try{ evt.keyCode = evt.keyboard.charCode; }catch(e){} try{ evt.charCode = evt.keyboard.charCode; }catch(e){} try{ evt.key = evt.keyboard.char; }catch(e){} } else if(evt.type === "keydown" || evt.type === "keyup"){ evt.keyboard.char = evt.key; //evt.key only works in IE gte 9 } } patchEvent.mouseButtons = { button:0, pressed:0 }; //for IE lte 8; keeps track of which mouse buttons are pressed (not always accurate) function triggerCustomEvent(evt, type, bubbles){ var path, handlers, i, j; //reset event status _evtStatus = {}; //create custom event object evt = createCustomEventClone(evt); evt.type = type; evt.bubbles = !!bubbles; path = getPropagationPath(evt); //array of objects with related event handler (not including the target) //run capture phase for(i=path.length-1; i>=0; i--){ //for each element in the propagation path array if(_evtStatus.propagationStopped) break; //update event object evt.eventPhase = 1; evt.currentTarget = path[i]; //execute the capture handlers (in FIFO order) handlers = path[i]._eventHandlers[evt.type]["capture"]; for(j=0; j<handlers.length; j++){ handlers[j].handler.call(path[j], evt); //execute the handler if(_evtStatus.propagationStoppedImmediately) break; } } //run target phase if(!_evtStatus.propagationStopped && evt.target._eventHandlers && evt.target._eventHandlers[evt.type]){ //update event object evt.eventPhase = 2; evt.currentTarget = evt.target; //execute capture & bubble handlers (in FIFO order) handlers = evt.currentTarget._eventHandlers[evt.type]["capture"].concat(evt.currentTarget._eventHandlers[evt.type]["bubble"]); for(i=0; i<handlers.length; i++){ handlers[i].handler.call(evt.target, evt); //execute the handler if(_evtStatus.propagationStoppedImmediately) break; } } //if this event can bubble if(evt.bubbles){ //run bubble phase for(i=0; i<path.length; i++){ //for each element in the propagation path array if(_evtStatus.propagationStopped) break; //update event object evt.eventPhase = 3; evt.currentTarget = path[i]; //execute the bubble handlers (in FIFO order) handlers = path[i]._eventHandlers[evt.type]["bubble"]; for(j=0; j<handlers.length; j++){ handlers[j].handler.call(path[j], evt); //execute the handler if(_evtStatus.propagationStoppedImmediately) break; } } } //reset event status _evtStatus = {}; } //creates a custom object to use as an event object (e.g., based on a mouseover event to use for a mouseenter event) function createEventClone(eventToClone){ var evt = {}; for(var i in eventToClone) evt[i] = eventToClone[i]; return evt; } //creates a custom object to use as an event object (e.g., based on a mouseover event to use for a mouseenter event) function createCustomEventClone(eventToClone){ var evt = createEventClone(eventToClone); evt.cancelable = false; evt.returnValue = true; evt.defaultPrevented = false; _evtStatus.propagationStopped = false; _evtStatus.propagationStoppedImmediately = false; return evt; } /*** avoid memory leaks ***/ //remove circular references and avoid memory leaks when the window unloads (especially for IE) //nulls event attributes and handler collection function flushEventHandlers(obj){ obj._eventHandlers = null; for(var prop in obj){ if(prop.slice(0, 2) === "on") obj[prop] = null; } } function flushAllHandlers(evt){ var elems = evt.view.document.getElementsByTagName("*"); for(var i=0; i<elems.length; i++){ flushEventHandlers(elems[i]); } flushEventHandlers(evt.view.document); flushEventHandlers(evt.view); } flushAllHandlers.adding = false; /*** event handler removal ***/ removeEventHandler = function (obj, type, handler_or_guid, useCapture){ if(!(obj instanceof Object || (!document.addEventListener && typeof(obj) === "object"))){ throw new TypeError("Invalid argument: obj"); } if(!(/^[0-9a-z]+$/i).test(type)){ throw new TypeError("Invalid argument: type"); } if(( isNaN(handler_or_guid) && typeof(handler_or_guid) !== "function") || handler_or_guid < 1 || handler_or_guid === Infinity){ throw new TypeError("Invalid argument: handler_or_guid"); } var guid = typeof(handler_or_guid)==="function" ? handler_or_guid._handlerGUID : handler_or_guid; if(isNaN(guid) || guid < 1 || guid === Infinity){ //in case the user messed with ._handlerGUID throw new TypeError("Handler GUID is invalid"); } if(obj._eventHandlers && obj._eventHandlers[type]){ var handlers = obj._eventHandlers[type][useCapture ? "capture" : "bubble"]; for(var i=0; i<handlers.length; i++){ if(handlers[i].guid === guid){ //handler is in the list handlers.splice(i, 1); //remove the handler from the list break; } } } }; })();