Perfection Kills

by kangax

Exploring Javascript by example

← back 751 words

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!

Did you like this? Donations are welcome

comments powered by Disqus