Detecting event support without browser sniffing
One of the pain points of feature testing in client-side scripting is that for event support. DOM doesn’t really specify any means to detect exactly which events browser understands and can work with. If you’d like to know if a browser supports, say, “dblclick” event, you’re pretty much out of luck. This is probably the reason why so many scripts on the web employ unreliable browser sniffing in such cases. One of the most common events that people sniff for are IE’s proprietary mouseenter/mouseleave, Opera’s impotent contextmenu, and input-related onbeforepaste, onbeforecut, etc. which are present in IE and WebKit, but not in Mozilla-based browsers.
Since browser sniffing is completely unreliable (as well as unmaintainable and fragile), we need a better way to detect events.
An obvious solution might be to employ an actual testing – create an element, attach an event listener, fire an event from that element and check if event listener gets executed. This solution is unfortunately quite brittle and is often too cumbersome. Simulating key events, for example, is currently hardly supported across browsers. Moreover, many events can be considered too obtrusive and interfere with user experience. It is also possible that events such as “scroll” and “resize” are prevented by popup-blockers and so can not be reliably tested.
It’s not widely known, but there actually is a quite robust way to detect most of the DOM L2 events. The trick is that many modern browsers report property corresponding to an event name as being existent in an element:
'onclick' in document.documentElement; // true 'onclick2' in document.documentElement; // false
Unfortunately, this is not the case with Firefox. Besides, browsers that do support this, sometimes don’t allow to test an arbitrary event on an arbitrary element. An event must be checked on an element that could actually originate that event:
'onreset' in document.documentElement; // false 'onreset' in document.createElement('input'); // true
To work around Firefox, we can employ a slightly different strategy (recommended by David Mark). The workaround is based on the fact that some of the browsers, including Firefox, actually create methods on an element when an attribute with the name corresponding to a “known” event is set on that element:
var el = document.createElement('div'); el.setAttribute('onclick', 'return;'); typeof el.onclick; // "function" el.setAttribute('onclick2', 'return;'); typeof el.onclick2; // "undefined"
Combining these two approaches, we can create a somewhat robust way to detect an event support. A generic isEventSupported function would look like:
var isEventSupported = (function(){ var TAGNAMES = { 'select':'input','change':'input', 'submit':'form','reset':'form', 'error':'img','load':'img','abort':'img' } function isEventSupported(eventName) { var el = document.createElement(TAGNAMES[eventName] || 'div'); eventName = 'on' + eventName; var isSupported = (eventName in el); if (!isSupported) { el.setAttribute(eventName, 'return;'); isSupported = typeof el[eventName] == 'function'; } el = null; return isSupported; } return isEventSupported; })();
You can now check for “contextmenu” support with – isEventSupported("contextmenu") – instead of an inferior – navigator.userAgent.indexOf('Opera') > -1. If Opera “fixes” “contextmenu” event in its future versions, there’s a big chance that isEventSupported will just evaluate to true and you won’t need to change a single line of code (to make whatever relies on “contextmenu” event – work)
You can also use a stripped down version of this function, for detecting, say, only Mouse events:
function isMouseEventSupported(eventName) { var el = document.createElement('div'); eventName = 'on' + eventName; var isSupported = (eventName in el); if (!isSupported) { el.setAttribute(eventName, 'return;'); isSupported = typeof el[eventName] == 'function'; } el = null; return isSupported; }
And then use – isMouseEventSupported("mouseenter") instead of a horrendous – (!!window.attachEvent && !window.opera) :)
The only oddity I noticed with this method was IE reporting false for “unload” event. “unload” can still be easily checked in a global window object – "unload" in window – returns true in all versions of IE that I tested (6-8). That expression can, of course, produce false positives if there’s a global “unload” variable, but, as a workaround, you can always try deleting the variable and see if in still returns true.
A minor downside to this test is that it doesn’t allow to detect Mutation Events. Fortunately, detecting those is not very complex. You can find an example of a perfect feature test for DOMAttrModified in Diego Perini’s NWMatcher.
I made a simple test case, listing all of the events specified in DOM L2 and corresponding results of running isEventSupported on them.
Give it a try, and enjoy a feature testing!
Radoslav Stankov said:
#I didn’t know about this technique, it always great to learn new stuff. 10x for sharing
Lea Verou said:
#Awsome article!!
I’ve tried to detect DOM L2 events myself in the past, but always got stuck in the firefox issue.
Thanks!
Diego Perini said:
#Very useful stuff indeed, as always here…
FYI Mozilla, Firefox, Safari, Konqueror (and derivated browsers) have had event names enumerated on their own “Event” object for a while, I believe it is a W3C recommendation/specification, maybe useful for your testing and shorter code too.
kangax (article author) said:
#@Radoslav, @Lea – Thanks, guys!
@Diego
Interesting. I wasn’t aware of this. Looking at Firefox,
Eventdoes indeed have keys corresponding to uppercased events –"MOUSEMOVE" in Event; // true, and the values seem to be – 2^n – 1,2,4,8,16…Interesting that some events – e.g. “contextmenu” – are not present.
I also couldn’t find any mention of such properties in DOM L2 Event specs. Could you point me to where you’ve seen them?
Diego Perini said:
#@Juriy,
“ContextMenu” like “DOMContentLoaded”, “DOMActivate”, “DOMFocusIn”, DOMFocusOut” and Mutation Events are not exposed as properties like other DOM0 events.
I believe it will be hard to find such old informations since this fact was known already in early versions of Netscape 4, available when setting up capturing of events with the old API:
window.captureEvents( Event.MOUSEMOVE | Event.MOUSEOVER | Event.CLICK );
This is a link which contains a few info on the subject, also not exhaustive:
https://developer.mozilla.org/En/DOM/Window.captureEvents
So if we leave out for a moment “contextmenu” and Mutation/DOM events the code can probably be shortened a bit.
John Resig said:
#An interesting technique. It’s also interesting to note that this technique also works for determining if event bubbling will work correctly. For example checking for the change event on a div returns false in IE but true in Firefox.
kangax (article author) said:
#I uploaded another test page which checks names through both – original and Diego’s versions – for comparison.
It appears that Gecko 1.8 (e.g. Firefox 2) doesn’t have uppercased event names in global
Eventobject, so all tests return false :/@John
Interesting, indeed. Will play with it more.
Diego Perini said:
#@kangax,
isn’t that just the “Gecko” based browser you use that doesn’t support event names on the Event Prototype ?
All Firefox and Netscape versions I could test up to Mozilla 1.7 have support for that bit of info !
Opera is what makes our testing harder here.
David Mark said:
#“The only oddity I noticed with this method was IE reporting false for “unload” event”
Not that odd as “unload” causes a test on a DIV.
Also, you are assuming that all browsers that fail the initial – in – test have a working setAttribute method. Looks like a good inference for now and the foreseeable future, but it is not the best approach.
Tiong said:
#This is definitely one of the best tutorial i ever came across with. Nice tip, thanks for shaing with us.
Rob Reid said:
#Great function! Just what I was looking for the other day.
I have tweaked it slightly to add a test in for resize/unload so that IE/Webkit report on this correctly.
http://www.strictly-software.com/eventsupport.htm
Doing a test for resize/unload and then setting the element variable to a window reference instead of creating an element dynamically didn’t seem to work in Opera causing failure reports. So I just check for unload/resize after your initial test and that seems to work across browsers.
var isEventSupported = (function(){ var win=this, //ta john! cache={}, //cache results TAGNAMES = { 'select':'input','change':'input', 'submit':'form','reset':'form', 'error':'img','load':'img','abort':'img' }; function isEventSupported(eventName) { var key = (TAGNAMES[eventName] || (eventName=="unload"||eventName=="resize")?"window":'div') + "_" + eventName; if(cache[key])return cache[key]; var el = document.createElement(TAGNAMES[eventName] || 'div'); var oneventName = 'on' + eventName.toLowerCase(); // this test should work in most cases apart from IE/Webkit with unload/resize var isSupported = (oneventName in el); // we cannot use createElement to create a window object so to get a correct test for IE/Webkit on resize/unload check the window if(!isSupported && (eventName=="unload" || eventName=="resize")){ isSupported = (oneventName in win); } // fallback to setAttribute if supported if (!isSupported && el.setAttribute) { el.setAttribute(oneventName, 'return;'); isSupported = typeof el[oneventName] == 'function'; } // the above tests should work in majority of cases but this test checks the EVENT object // haven't seen an example where this is required yet so may not be needed. if(!isSupported && win.Event && typeof(win.Event)=="object"){ isSupported = (eventName.toUpperCase() in win.Event); } el = null; cache[key]=isSupported; return isSupported; } return isEventSupported; })();marcis said:
#What about “beforeunload” event for window object?
jpv said:
#thanks for this detection tip, I used it to detect if the browser does support the XHR onprogress event, here is the code :
if(sFeatureName == 'xhr-event-progress') { var oEl = new XMLHttpRequest(); if(!oEl) // if standard XHR does not exist, we're probably on IE6 that do not support event progress anyway return false; return ('onprogress' in oEl); }Note that the test on the function type for FF is not mandatory here since it’s not a DOM element
usage is situation (french) : http://jpv.typepad.com/blog/2009/11/barre-de-chargement-dune-image.html
kangax (article author) said:
#@marcis
“beforeunload” can also be inferred with
in(in supporting browsers) —'onbeforeunload' in windowistruein my WebKit (nightly). Firefox doesn’t support that kind of inference, but allows to detect “beforeunload” be setting corresponding attribute on an element — just like described in a post (even though it might seem weird):var el = document.createElement('div'); el.setAttribute('onbeforeunload', ''); typeof el.onbeforeunload; // "function"kangax (article author) said:
#@Rob Reid
The reason
isEventSupportedwas failing “unload”, is because it had to be tested onwindowobject (as you rightfully noticed). In Mozilla, however,"onunload" in windowisfalse, so fallback inference of setting an attribute has to be used (which works reliably so far):var el = document.createElement('div'); el.setAttribute('onunload', ''); typeof el.onunload == 'function'; // trueI modified a snippet slightly. It now falls back on performing setAttribute-based check on generic element.
Thanks for bringing it up.
Anton Stoychev said:
#Can anyone tell me whether it can detect proprietary events? I’m trying to develop a slightly future proof web application to support multi-touch interactivity but to do so I have to recognise the machine as such. So far I think only firefox 3.7 support multi-touch events.
Can anyone with multi-touch device can confirm that
isEventSupportedreturn true on these events, with firefox 3.7 multi-touch build? It returns false with windows XP and FF 3.7 multi-touch build. I couldn’t find any multi-touch emulator to test it on my machine.I’ve browsed through the patch’s code and so far I can only test whether such event exist in a browser but not if it is supported by the device:
This tests whether the event model exists.
Mark “Tarquin” Wilton-Jones states that models as well may be tested via
document.implementation.hasFeature('EventModelName','2.0') || window.EventModelNameHaving looked at examples of firefox multitouch these events are attached to
document. It may be that they can be attached only onto document node but there’s no clear specification.