jQuery functions provide a convenient shorthand for retrieving information from the DOM. There is no need to think about browser-specific quirks – mostly. Here is a case where it doesn't work out.
Retrieving the size of a document should be a straightforward affair: a simple $( document ).width() or $( document ).height() call, and you are done. Yet in IE, these calls often return a size just a little bit too large. Sometimes they are off by as little as 4px, sometimes the difference accounts for the width of the scroll bar, or the scrollbar plus 4px. This bug has been around for a long time. As of jQuery 1.6.1, it is still with us, and it looks like it's going to stay that way.
The actual size of the mismatch depends on the document layout and the version of IE. All IE browsers up to and including IE9 are affected, with the curious exception of IE7. The exact workings of this bug tell a little about the document properties used by jQuery and what they actually contain.
Where it went wrong
jQuery determines the document width and height by querying these properties and using the largest value:
The ones mentioned first, the d.dE.client* properties, contain the size of the viewport. The others reflect the content of the document. The bug occurs because the offset properties, d.dE.offset*, will occasionally return a value which is larger than both the viewport and the content area.
Under the hood
The d.dE.offset* property is based on the viewport size. In most versions of IE, it also includes elements of the browser chrome, most notably the scrollbar.
This is not an issue as long as there aren’t any scrollbars, of course. Both client* and offset* will return the size of the viewport, as they should. Equally, there won't be a problem if the document is significantly larger than the viewport, in width as well as height. Yes, scrollbars will interfere with d.dE.offset*. But that doesn’t matter because the document covers a much larger area, pushed out by the actual content of the page.
Document width in IE
But what if the page is spilling out of the viewport at the bottom, while still wrapping its content nicely inside the browser window horizontally? The vertical scrollbar is visible, adding to the width reported by d.dE.offsetWidth. The content doesn't exend underneath it. Suddenly d.dE.offsetWidth has become the largest value - and jQuery happily returns it as the document width, which now includes the scrollbar.
2px border in IE8
This happens in IE6, IE8 and IE9. In IE6, this situation is actually permanent. Even when the complete page is fitting nicely into the browser window, IE6 will display the vertical scrollbar element (though not the actual scrollbar slider itself - after all, there is nothing to scroll). To add to the confusion, IE6 and IE8 also include a small border around the viewport in the figure returned by the offset* property. It accounts for 2px on each side – thus adding a total of 4px in each dimension. For this reason, in IE8, d.dE.offset* and jQuery will be off by 4px even if scrollbars are absent.
It may be easier to see it in action, though. Here is a demo page to play around with. Just take any of the affected IE versions and watch the numbers change as you resize the window. Another, more basic illustration of the bug can be tweaked in jsFiddle.
As mentioned before, IE7 is not part of this particular mess. It appears that d.dE.offset* is just a synonym for d.dE.client* in IE7, so d.dE.offset* doesn’t introduce any new, weird numbers here.
In a nutshell: The d.dE.offset* property includes elements of the browser chrome in most versions of IE and is responsible for jQuery misreporting the document size. In addition, the implementation of this property has changed in every new version of IE. Don’t rely on it, ever.
The solution, then, is both simple and obvious. d.dE.offset* should not be queried in IE.
I am not sure about other browsers, though. While I'm not aware that using this property would be necessary, I didn’t really look into it, and there problably is a good reason for d.dE.offset* showing up in the jQuery source in the first place.
Anyway, a few lines wrapped in a jQuery extension can handle all that. They fix the issue in IE and leave the code for all other browsers intact. Use the extension by calling $(document).trueWidth() and $(document).trueHeight().
As you can see, the code is relying on browser detection. This technique is frowned upon by the majority of web developers, and with good reason. Yet here we seem to have one of the few cases where the far superior alternative, feature detection, doesn't lead anywhere:
Object detection doesn't work. The relevant objects are common to IE and other browsers, so they can’t be used to separate the lot.
It seems there is no easy way of simulating the behaviour on the fly, e.g. by creating some inline HTML and examining its properties. The problem here is that the bug doesn't occur with any DOM elements other than the document element itself.
Relying on $.browser.msie isn’t the most attractive thing to do, but another option isn't immediately obvious - that is, to me. Should you have an idea, I'd appreciate if you leave a comment. Anyway, sniffing out IE is still a huge improvement over the alternative, which is leaving the bug unfixed.
Yes, finally, the download
But perhaps you aren't interested in that kind of debate and just want to download the extension. When it is loaded, just call $(document).trueWidth() and $(document).trueHeight() instead of the native jQuery methods.
WOW! I "love" it (read the opposite pls) when you guys write great tutorials for gurus who know anyways how to solve these issues, but leave the rest of us without a clue as of how to implement all that ...
"When it is loaded, just call $(document).trueWidth() and $(document).trueHeight() instead of the native jQuery methods."
WOW, great ... and what do I do with that stuff now?
So difficult to think of the less fortunate ones who do not happen to be programmers?
What about the exact code for ".. just call $(document).trueWidth() and $(document).trueHeight() instead of the native jQuery methods." !??!
That would be reaaaaly great!
Thank you in the name of all non-programmer souls out there!
Thanks for elevating me to the level of a "guru" ;-)
Anyway, if you are just having trouble with the phrase you mentioned, "native jQuery methods" are the ones built into jQuery - width() and height() in this case. Whenever you would normally call $( document ).width() in your code, use $( document ).trueWidth() instead - that's all you need to change.
Hello. I English know bad, but I still decided to thank YOU for this perfect solution for fixing this problem.
After I read it, I came up with the brilliant idea of how exactly to define it is IE or another browser.
I think it's reliable way to pinpoint it IE or not:
var div = $('\').appendTo('body');
It is only my idea.
If this is to add to your plugin, finally function should look like this:
Thank you very much, Alexandr, for your suggestion, and for taking the time to struggle with the translation. Sorry for the garbled code in your comment. Some stuff doesn't make it past the filter in the comments section, so if anyone is interested, follow the link to jsFiddle.
In a nutshell, this is what you are suggesting:
a conditional comment, targeting IE, is added on the fly with jQuery
it contains a script tag which will only execute in IE
the script sets a flag telling us that we indeed are in IE.
And that is browser sniffing.
Browser sniffing doesn't necessarily have to happen by examining the UA string. The method doesn't really matter that much. The issue here is that the underlying logic is at fault. We see that we have a browser of type A, so we infer - but don't test directly - that it gets feature B wrong.
This approach is bad because it is indirect, based on an assumption, and might break with the next version of browser A. What we want to do is check, on the fly, if feature B is broken in the current environment. In case it is, we use a polyfill. Who cares about the browser type.
But feature detection is hard to pull off in this case, or maybe even impossible, for the reasons I have mentioned in the post. So it has to be browser sniffing for now. That might just as well be handled by the jQuery $.browser utility. Conditional comments will be dropped in IE10 anyway, and $.browser doesn't add a global variable (as does your flag).
But again, thanks for your suggestion, and for trying to solve this problem!
Oh, ok, that's a special case. Perhaps it will work for you then.
By the way, good point you make about the value being dependent on the zoom level. Again, it is rather unintuitive, the value growing when the zoom level is reduced (while the actual border size, in pixels, stays exactly the same).
But I'm sure you figured that out way before me :)
I'm not sure I've understood your question entirely, but I'd suggest you have a look at the jQuery source. Most of the browser inconsistencies are handled there, and you can take you cue from the way it's done.
(That doesn't apply to document height, of course, because it's broken in jQuery, but that's what this post is all about.)
If looking at the jQuery source doesn't get you anywhere, Stackoverflow is probably your best bet ;)
I can totally see why the jQuery team is going that route, but in the context of this bug, it sucks big time. As I have said before, browser sniffing is the only way to make it work (until proven otherwise).
So for now, I'd recommend reinstating jQuery.browser by including the jQuery.migrate plugin in your project. I might add the code for sniffing out IE to my extension later on, but I think that for now, jQuery.migrate probably is the best solution.