Showing posts with label event. Show all posts
Showing posts with label event. Show all posts

Friday, 6 November 2009

Testing for browser event support without sniffing

Browser Event Support by object detection

One 0f the things I have often wondered since I really got into Javascript a few years back was whether there was a way to check for event support without resorting to browser sniffing.

I had a task the other day which meant that I had to add some code to prevent a user from pasting in content into an email confirmation box. I used the onpaste event for most browsers but Opera doesn't support this so I had to use a keypress event to look for the CTRL+V key combo and then block it. This got me looking at ways of checking for browser event support without resorting to a sniff.

Then I came across this article by Ryan Morr: http://ryanmorr.com/archives/ondomready-no-browser-sniffing

A lot of the DOMReady functions I have seen used including my own use a browser sniff to check for old Opera, WebKit and KHTML and use a timer to check for loaded state, a call to DOMContentLoaded for DOM2 supported browsers, a cludge for IE using defer or a doScroll and then a fallback to window.onload to handle anything that doesn't fire by the time the window loads.

Ryan's solution is to do all of them without any browser checks. He adds a DOMContentLoaded listener for DOM2 browsers as well as a window.onload and then he sets a timer up for all browsers. All of these call a function which checks the appropriate event type and for IE does the doScroll check. Once a load has been confirmed the desired function is called and the timer is killed.

Its a shame that a timer has to be used for all browsers when in reality only a very small percentage of browsers will fall into the class that require it however its an example of thinking outside the box.

This somehow got me to another article by Kangax where he had a brilliant function for checking for event support in any browser using a combination of two methods:
var isSupported = ('onpaste' in element)
and for those that fail a creation of the event with a simple return as the function and then a check to make sure that the event is a typeof function e.g
el.setAttribute('onpaste', 'return;');
isSupported = typeof el['onpaste'] == 'function';

So I read some more and then checked out some similar articles and his test page which had a number of event tests and saw that in IE and Chrome/Safari that the unload and resize tests failed using the existing checks. These events are definitely supported so should result in a positive when tested for. Therefore I have amended the original function to use the window object for these checks if the first check fails. I have also added a little cache in to prevent the same event type being checked multiple times as well as combining another check by Diego Perini which checks the global Event object. I don't actually know if this last check is required as I haven't seen a browser where the first tests fail but its there anyway.

var isEventSupported = (function(){
var win=this,
cache={},
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();
var isSupported = (oneventName in el);
// cannot 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);
}
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
if(!isSupported && win.Event && typeof(win.Event)=="object"){
isSupported = (eventName.toUpperCase() in win.Event);
}
el = null;
cache[key]=isSupported;
return isSupported;
}
return isEventSupported;
})();


You can check out the test page here which compares a number of events against this function as well as Kangax's original and also a version by Diego Perini who I believe was the person who came up with the doScroll method for IE used in many a DOMReady function.

http://www.strictly-software.com/eventsupport.htm

Unfortunately this method of event testing doesn't work with the mutation events such as DOMContentLoaded but then the only way you can really test for these is by running them anyway. Therefore although this function was perfect for my onpaste check it wouldn't work in a DOMReady function to test whether DOMContentLoaded was supported or not.

Articles Mentioned:

http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing

http://ryanmorr.com/archives/ondomready-no-browser-sniffing

Wednesday, 21 October 2009

Window.Event support cross browser

Accessing window.event in Chrome, Safari and Opera


Something that I found out today, which you may or may not know related to events and browser support, is that the global event object used by Internet Explorer:

window.event

Is also supported in Chrome, Safari and Opera. I presumed Opera would support it as it also supports IE's event model as well as the DOM 2 model. By this I mean you can attach events using attachEvent or addEventListener as well of course using inline events e.g element.onclick=function(){}.

As addEventListener is the superior method this is why any custom addEvent function should always check for the standard option first before then checking for attachEvent and then reverting to DOM0 (if you require IE4 or NN4 support :) )

For Firefox when using inline Javascript event handlers you need to pass in the Event object as a parameter to any function you are calling. This is how I presumed the other standards compliant browsers would also work but it seems they support both methods. E.g in IE, Opera, Chrome and Safari both these methods work:

<input type="button" id="txtA" value="Click" onclick="Run();" />
<input type="button" id="txtA" value="Click" onclick="Run(event);" />


But in Firefox only the second method will work. The function called does a test for the window.event object to capture the event.type but in Firefox it uses the event parameter. On my example test you will notice the undefined messages when a test for window.event is carried out. 

For example you may have seen many a function like the following that handles both the window.event and event parameter:
function test(e){
// use the event parameter if passed otherwise
// use the global window.event object
e = e || window.event;
}
This may be old knowledge but it was new to me so I thought I would post it in case others didn't realise this event support. Also most developers have moved on from using inline event handlers to using unobtrusive Javascript and attaching events to the DOM after the HTML has loaded.

You can test this out here by running the following two test functions which will output some messages to a DIV container. This test should be carried out in multiple browsers especially Internet Explorer (IE), Chrome or Safari, Opera and Firefox. I haven't checked version support apart from IE 7,8, Chrome 3, Safari 4, Opera 10 and Firefox 3.5.









Sunday, 20 September 2009

Trouble with this keyword and namespaces

Namespaces, duplicate functions and more IE nightmares

I was working on some code the other night which belonged to a site similar to my own in which numerous scripts and "semi frameworks" were being included. By semi framework I mean an add-on script that includes numerous functions that will almost certainly be replicated elsewhere in the site (DOM manipulation, event handlers etc).

The code involved using an iframe and then running an onload event as soon as the content had loaded so that I could resize the iframe to the correct dimensions for the content within. I had a bit of code like this:

addEvent(document.getElementById('myIframe'),"load",resizeIframe);

// resize iframe content onload
function resizeIframe(){

var dc = S.getIframeDoc(this); // return iframe document
var h = dc.body.scrollHeight; // get height of document within iframe
this.height = h+30+"px"; // set height of iframe+30 to ensure no scrollbars
}

As you can see the function resizeIframe which is called when the iframe loads uses the this keyword to reference itself rather than getting a reference to itself using getElementById which is fine as long as you are using a proper browser that supports the DOM 2 event model. However as IE does not support this model it has a problem with the this keyword in that it references the global window object instead of the iframe.

However this is a well known problem and many a solution has been created to get round this issue in Internet Explorer. On the site in question I use the following function which as you can see handles DOM 2, IE's event model and the older DOM 0 event model. It also correctly handles the this keyword problem by storing a reference in the DOM to the function. Read up on PPKs event handler content for more details.


addEvent = function( obj, type, fn, cp )
{
if(obj){
if(obj.addEventListener){
cp = cp || false;
obj.addEventListener( type, fn, cp );
}else if ( obj.attachEvent ) {
obj[type+fn] = function(){fn.call(obj,window.event);}
obj.attachEvent( 'on'+type, obj[type+fn] );
}else{
var ev='on'+type;
var oldevent = obj[ev];
if (typeof oldevent != 'function'){
obj[ev]=fn;
}else{
obj[ev] = function(){ oldevent();fn();}
}
}
}
}


However when testing the site in Internet Explorer I was getting errors when trying to reference the contentWindow.document in my resizeIframe function saying its null or not an object.

The reason being that the this keyword was referencing the window object and there is no such object property on the global object.

I spent some time scratching my head and looking over the function at hand and then when I put some debug code into the addEvent function and noticed it not appearing it at all the problem suddenly made sense. After a quick search through the other JS files being included on the page in question I came across the following function in a script called sorttable.js used for sorting table contents.
function addEvent(elm, evType, fn, useCapture)
{
if (elm.addEventListener){
elm.addEventListener(evType, fn, useCapture);
return true;
}else if (elm.attachEvent){
var r = elm.attachEvent("on"+evType, fn);
return r;
} else {
var o = elm[evType];
if(typeof(o)=="function"){
elm[evType] = function(){o();fn();}
}else{
elm[evType] = function(){fn();};
}
}
}


As you can see its another addEvent function and one that doesn't handle the this keyword problem in IE.

As this script was being included after my own file it was overwriting the previous addEvent function and therefore was the cause of the error.

Now this self taught lesson reminded me of a recent post I wrote about the amount of duplicate frameworks and semi frameworks being included on websites at the moment. I reckon that on this page in question there was at least 5 different places where a function to add an event listener was being declared:
-My own addEvent function in my standard library strictly.js
-The addEvent function in the sorttable.js script
-JQueries own methods to bind events - different names but doing the same action.
-GooglesAJAX library would surely have its own event handlers.
-AddThis add-on also had a similar event handler.

Therefore 5 scripts all doing the same thing. I explain it more in my earlier post:

http://blog.strictly-software.com/2009/08/large-number-of-duplicate-frameworks.html

where I discuss the possibility of a standard global API for implementing these sort of functions that are nearly always replicated in add-on scripts. This would allow developers to choose the framework to handle this type of work and add-on developers wouldn't have to worry about implementing duplicate code.

Another solution would be if the add-ons would separate their code out into 2 files. The first file would contain the core functionality and then the other file would contain the functions that could/should be handled by the main framework used by the site.

This would allow the site developer to reduce the size of their codebase as they could choose to remove the 2nd file (if they wished) and allow their primary framework to handle the work that a lot of add-ons duplicate such as DOM manipulation and event handling.

Obviously this works only if the add-on is using the same naming convention as the many frameworks and if the frameworks don't even share the same naming convention then this would seem hard to achieve. However we could always use wrapper functions or aliases to point the add-on functions at the desired framework objects e.g for our addEvent function:
// addEvent used by this script
// params supplied
// obj = object, type = event, fn = function

// set alias so that add-on can use sites primary framework e.g jQuery/Prototype
A = addEvent = function(obj,type,fn){
$(obj).bind(type,fn);
}

Which allows the add-on to add event listeners with a call to A or addEvent with either call just being a pointer to the frameworks bind method. The add-on provider could either provide these alias wrappers themselves or just outline the interface required and allow the developer to implement the code.

And of course if the user of the add-on didn't use a framework then they could choose to utilise the 2nd files functions as is even if they were duplicated in 5 other places.

This is all just ideas at the moment but I am developing an add-on at the moment that is going to try this approach.


Why Namespaces are such a good idea

Another solution to the problem of duplicated functions is to always use namespaces to define your functions and objects so that there is little if no chance of two functions having the same name. The writer of the sorttable.js file didn't use a namespace to define his functions but if I had of defined my own addEvent function like so instead:
Strictly.addEvent = function(obj, type, fn){ ...

// call like so
Strictly.addEvent(document.getElementById('myIframe'),"load",Strictly.resizeIframe);


I would not have had any problem working out why my function wasn't working as expected unless one of the other add-ons also had used the namespace Strictly!

Obviously using namepaces may solve the problem of functions being overwritten but it doesn't solve the issue of duplicated functionality.


Object Comparisons in Internet Explorer

Now whilst I was scratching my head in relation to the this problem I looked into creating a little test function that would tell me whether this equated to the global object or not as I was going a bit offtrack and of course a simple this === window should do the trick. However whilst looking into the different methods of object comparision I came across some of the differences between browsers of how object comparison is carried out. I don't know why I thought IE wouldn't be the odd one out as it always is but you might find the results interesting or you may not.

Make sure you view the test page in IE and a standards compliant browser like Firefox for comparison so that you see what I mean:

www.strictly-software.com/thistest.htm

window === window.top is true in Firefox and false in IE.

window == document is false in Firefox and true in IE.

var _w=window;
_w === window.top is true in Firefox and false in IE.

When this relates the the global window object:

this === window.top is true in Firefox and false in IE

this === self is true in Firefox and false in IE

This may all be old news as far as JS developers are concerned but it was news to me so I thought I would post the link anyway.