Perfection kills

Exploring Javascript by example

Detecting event support without browser sniffing

April 1st, 2009 by kangax (Permalink)

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 23 Comments »

Comments (23)

  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.

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

Leave a Comment

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

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