Perfection Kills

by kangax

Exploring Javascript by example

Detecting event support without browser sniffing

April 1st, 2009

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!

Categories: sniff busting

Comments (44)

  1. Gravatar

    Radoslav Stankov said:

    I didn’t know about this technique, it always great to learn new stuff. 10x for sharing

  2. Gravatar

    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!

  3. Gravatar

    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.

  4. Gravatar

    kangax (article author) said:

    @Radoslav, @Lea – Thanks, guys!

    @Diego
    Interesting. I wasn’t aware of this. Looking at Firefox, Event does 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?

  5. Gravatar

    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.

  6. Gravatar

    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.

  7. Gravatar

    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 Event object, so all tests return false :/

    @John
    Interesting, indeed. Will play with it more.

  8. Gravatar

    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.

  9. Gravatar

    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.

  10. Gravatar

    Tiong said:

    This is definitely one of the best tutorial i ever came across with. Nice tip, thanks for shaing with us.

  11. Gravatar

    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;
    })();
    
  12. Gravatar

    marcis said:

    What about “beforeunload” event for window object?

  13. Gravatar

    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

  14. Gravatar

    kangax (article author) said:

    @marcis

    “beforeunload” can also be inferred with in (in supporting browsers) — 'onbeforeunload' in window is true in 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"
    
  15. Gravatar

    kangax (article author) said:

    @Rob Reid

    The reason isEventSupported was failing “unload”, is because it had to be tested on window object (as you rightfully noticed). In Mozilla, however, "onunload" in window is false, 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'; // true
    

    I modified a snippet slightly. It now falls back on performing setAttribute-based check on generic element.

    Thanks for bringing it up.

  16. Gravatar

    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.

    onMozMagnifyGestureStart
    onMozMagnifyGestureUpdate
    onMozMagnifyGesture
    onMozRotateGestureStart
    onMozRotateGestureUpdate
    onMozRotateGesture
    onMozTapGesture
    onMozPressTapGesture
    onMozTouchDown
    onMozTouchMove
    onMozTouchRelease
    

    Can anyone with multi-touch device can confirm that isEventSupported return 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:

    typeof MozTouchEvent != 'undefined'
    typeof SimpleGestureEvent != 'undefined'
    

    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.EventModelName 

    Having 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.

  17. Gravatar

    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.

  18. Gravatar

    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.

  19. Gravatar

    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.

  20. Gravatar

    Timothy Stone said:

    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.

    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.

  21. Gravatar

    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).

  22. Gravatar

    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:

    'INPUT' in Event // returns false

    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.

  23. Gravatar
  24. Gravatar

    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.

  25. Gravatar

    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é-

  26. Gravatar

    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.

  27. Gravatar

    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.

  28. Gravatar

    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.

  29. Gravatar

    wes said:

    is there a way to check if DOMContentLoaded is supported?
    there is no html equivalent (.ondomcontentloaded) in standard browsers!

  30. Gravatar

    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.

  31. Gravatar

    Karthikeyan said:

    Is there any way we could check the composition events i.e compositionstart , compositionend.

Trackbacks

  1. Ajaxian » Detecting event support in browsers said:

    [...] has a really nice article on testing for event support in browsers in which he delves into the quirks and work-arounds needed to get ‘er done, coming up with a nice [...]

  2. Social Vitamin JS detecting event support in browsers said:

    [...] there is no easy way of detecting which elements supports which events across browsers. Kangax from Perfection Kills has come up with a very clever way of detecting [...]

  3. Detectar los eventos disponibles en el navegador de tu usuario | aNieto2K said:

    [...] Por norma general, siempre debemos de pensar, a la hora de desarrollar nuestros scripts, en ofrecer al usuario una alternativa, ya sea para realizar una opción o para interactuar con la página. Conocer los eventos que tenemos disponibles en el navegador del usuario que nos está visitando es una idea realmente interesante y en Perfection kills han desarrollado un sistema muy interesante con el que conocer de antemano las cap…. [...]

  4. Detecting event support in browsers | Guilda Blog said:

    [...] has a really nice article on testing for event support in browsers in which he delves into the quirks and work-arounds needed to get ‘er done, coming up with a [...]

  5. Ryan Morr - Javascript, CSS, and Web Apps » Env: Feature Testing said:

    [...] for the techniques utilized within this method goes to Juriy Zaytsev and his recent post concerning event support detection along with the talented developers involved in the discussion over at Ajaxian for all the [...]

  6. Просто делегиране не submit събития | NeXt said:

    [...] Затова направих нова версия, която засича дали submit се делегира ( за начина по който разбирам по-подробно пише в тази статия) [...]

  7. links for 2009-07-11 | NeXt said:

    [...] perfection kills » Blog Archive » Detecting event support without browser sniffing (tags: programming javascript development event browsers) [...]

  8. links for 2010-02-24 | Digitalistic - Mashup or die trying said:

    [...] Detecting event support without browser sniffing One of the pain points of feature testing in client-side scripting is that for event support (tags: javascript event touch eventsupport browser js) AKPC_IDS += "406,";Popularity: unranked [?] [...]

  9. Detecting touch-based browsing | AlastairC said:

    [...] solution came from this article on Detecting event support without browser sniffing, however, it does more than I was looking for, and wasn’t aimed at touch based [...]

  10. 细说浏览器特性检测(1)-通用事件检测 | 宅居 said:

    [...] Detecting event support without browser sniffing为本文提供了大量的思路。 [...]

  11. window 的 hashchange (onhashchange) 事件 | Gea-Suan Lin's BLOG said:

    [...] iframe 做類似的效果…另外在偵測瀏覽器是否有支援 hashchange 可以利用「Detecting event support without browser sniffing」這篇說明的方式偵測是否有支援特定的 event,可以避免使用 browser [...]

  12. Mathias Bynens said:

    Using the oninput event with onkeydown as its fallback…

    HTML5 standardizes the oninput event handler, which should be used to detect user input in JavaScript. Sure, you could use onkeydown or onkeyup instead, but those were never really designed for this particular use case, and it shows….

  13. Decoding jQuery – jQuery.support and DOM Level 2 Events | Shi Chuan's blog said:

    [...] people have been using browser sniffing, but such method can be quite unreliable. Juriy Zaytsev blogged about a method that could detect most of the DOM L2 events. The trick is that many modern browsers [...]

Leave a Comment

Please, don't forget to escape your input (<, > and &). Wrap code sections with <pre>

Allowed tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>