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.Daniel Friesen said:
#Interesting however I noticed that Firefox 3.5 actually supports html5′s oninput however so far every detecion method I try here fails at detecting support for it.
Diego Perini said:
#Daniel,
you haven’t tried hard enough … it is actually possible to detect if the browser can trigger “input” events for keystrokes in “input” controls.
To simulate real user interaction on the control you have to combine setting “.focus()” and then “.dispatch()” a “keypress” event on the element you want to test.
The difficult step will be recognizing browsers that implements “KeyEvents” or “KeyboardEvents” interfaces and create/init the correct event appropriately.
Daniel Friesen said:
#@Diego Perini
Interesting, I never thought of trying to indirectly trigger events that way.
I did do some experimentation with dispatchEvent when I was trying to come up with a method myself. I was dispatching input events directly, then realized that would work whether or not input events were supported.
I was of the impression that there was no strange magic behind the scenes that would cause events to implicitly trigger other events when an event was faked by user code.
I’m already checking what browsers support oninput (it looks like Chrome supports it as well, normal tests work too) I’ll come up with the least intrusive way to use your trick to check for support and probably make a blog post of my findings as well.
Timothy Stone said:
#You should be able fix this “problem” with a theoretical, but typical of cut-and-paste javascript, global “unload” property by calling “hasOwnProperty” in the check of the element.
Deleteing the variable could lead to other problems that could be silent killers of code.
Paul Grenier said:
#I’ve found a problem with dispatching several events in Chrome and as a result I also have to sniff the browser as you suggested (see http://code.google.com/p/proto-q/)
In case you’re interested, I think the problem stems from Chrome’s JIT compilation of the JS into machine code–it must see a string of events dispatching each other as a single call stack, and as a result, the call stack size is exceeded. In every other browser, dispatching an event clears the stack. My workaround right now is to have Chrome dispatch the event using setTimeout() which clears the stack as expected (but takes longer).
Robin said:
#There are now some extra events included with HTML5, like “input” [http://goo.gl/UiE19] which fires as soon as information is input into a form element.
Unfortunately while “input” works in Chrome 8 and Fx 4b7, it seems very hard to detect support for. It is not included in the Event object in either browser:
In Chrome, the event can be detected by the script, I assume because it *does* register a “oninput” method on the element. However, Firefox doesn’t seem to, so even though Firefox supports “input” it can’t be detected by any method we have here.
If anyone has any ideas on this please comment, I’d really like to solve this.
Mathias Bynens said:
#Robin, see A Browser Maze:
oninputsupport.Diego Perini said:
#Robin,
if you read a few messages above you see I suggested a method to detect “oninput” event support on Firefox.
Later I observed that all new HTML5 events and many others can be tested in all browsers by creating a disconnected “BODY” element, assigning attributes to it, then check if the same-name property on the “window” object is a “function” reference instead of a “string”:
var test = “oninput”;
var body = document.createElement(“body”);
body.setAttribute(test, “return”);
if (window[test] && window[test].call) {
alert(“Event is supported !”);
}
this kind of testing could destroy previously installed handlers, so save/restore them before/after testing.
Unfortunately I believe this is just a cross-browser bug that has to be fixed, so relying on it seems risky at best.
André said:
#Hello,
I tried your function to detect support for “webkitTransitionEnd”, “oTransitionEnd” and “transitionend”. In all browsers I tested your function the result was “false”, although they support css3 transitions.
Greetings
-André-
Johan Sundström said:
#More data on a corner case of this: IE 9 supposedly supports the “onload” event for <script src=”…”> tags, in IE9 Standards Mode (at least according to EricLaw -MSFT-), but on pages that don’t live up to whatever that means, it does not fire. Unfortunately, however, testing for the event support with the tricks outlined here always detects it as supported:
hasEvent(document.createElement(‘script’), ‘onload’); // always true in IE9 :-(
function hasEvent(el, onFoo) {
if (onFoo in el) return true;
// Gecko fallback:
el.setAttribute(onFoo, ”);
var ok = ‘function’ === typeof el[onFoo];
el.removeAttribute(onFoo);
return ok;
}
I haven’t come up with any tidy hack to detect this without resorting to user agent sniffing, I’m afraid.
Troy III said:
#Contrary to other old static NN broods
IE was designed with dynamics in mind, and IE event model is amazing as always, if only other static born browsers would dare to follow – your code could become as short (simple, clean, efficient and powerfull) as this:
isEventSupported =
function(tag, event){
return document.createElement(tag)[event]===null;
};
example:
alert( isEventSupported(“DIV”, “onclick”) ); //TRUE
alert( isEventSupported(“DIV”, “atclick”) ); //FALSE
example:
if( isEventSupported(“DIV”, “onclick”) ){..[if true code]..}else{..[if false code]..}
(For a range of other Window events not covered by Document object, one could use the IFRAME element)
This (code) works on Explorer, because the default value of supported events is “null” and of course “undefined” for those which are not.
[the cons.] Regretfully:
- only Explorer provides the means for this.
- It will also return TRUE for events it can recieve – but cannot fire.
————————————
On Firefox[5], the finite list of event names (and properties) exposed on “Event object” are the following:
CAPTURING_PHASE
AT_TARGET
BUBBLING_PHASE
MOUSEDOWN
MOUSEUP
MOUSEOVER
MOUSEOUT
MOUSEMOVE
MOUSEDRAG
CLICK
DBLCLICK
KEYDOWN
KEYUP
KEYPRESS
DRAGDROP
FOCUS
BLUR
SELECT
CHANGE
RESET
SUBMIT
SCROLL
LOAD
UNLOAD
XFER_DONE
ABORT
ERROR
LOCATE
MOVE
RESIZE
FORWARD
HELP
BACK
TEXT
ALT_MASK
CONTROL_MASK
SHIFT_MASK
META_MASK
to close useless, one might say.
Troy III said:
#This one, (considering the situation),is not that bad either:
isEventSupported =function(El, ev){
El=document.createElement(El);El.setAttribute(ev,'');
El=typeof El[ev]=="function"; return El;
};
Yet creating elements just for the sake of the test is dirty as hell!
-Not sure if Divs support the “onreset” event…
isEventSupported("DIV", "onreset") //TRUE
…but, – it will at least mean that the browser supports it! :)
Have fun.
wes said:
#is there a way to check if DOMContentLoaded is supported?
there is no html equivalent (.ondomcontentloaded) in standard browsers!
Daniel Friesen said:
#@wes DOMContentLoaded is one of the events you don’t need to bother testing if it exists. Since what you really want is to fire as soon as you can depending on what the browser supports, you just try to register everything there is and add some sort of window.isAlreadyLoaded when one has fired so that the later ones don’t do anything.
Karthikeyan said:
#Is there any way we could check the composition events i.e compositionstart , compositionend.