Yann-Erwan Perio said:
Hmm, in modern browsers (including IE) clientHeight
does return the client height, and scrollHeight
returns the whole document height.
<URL:
http://jibbering.com/faq/#FAQ4_9>
What version of IE, and platform?
I am wondering whether there isn't a mismatch in terminology here, and
it is the IE equivalents of outerWidth/Height that is wanted by the OP.
<F ... Y>
In 4_9 the variable redefinition in the different branchs
is misleading, variable definitions occur once, when
execution scopes are created, their initialization come
later within the corresponding statement execution
(Ecma-263-12.2).
</F ... Y>
Yes, it is not syntactically incorrect but it is certainly misleading.
Personally, I don't like the answer to 4.9 much anyway. Preferring the
innerWidth/Height properties certainly simplifies the code considerably
but they are not the equivalents of the clientWidth/Height properties on
the root element in IE browsers as they represent dimensions that
includes the thickness of the scroll bars (if present), while the
'client' values return the dimensions inside the scroll bar.
In most practical circumstances I have found that the dimensions inside
the scroll bars are the dimensions that I would prefer to be using. If,
for example, a page has a vertical scroll bar but no horizontal scroll
bar (the common case in a fluid layout) I don't want to be positioning
anything behind the vertical scroll bar because that is likely to
introduce a horizontal scroll bar. And that horizontal scroll bar might
flicker on an off if I am moving something that might partly slip behind
the vertical scroll bar. So to control movement, or clip the offending
elements appropriately, to the maximum extent possible, I need (want) to
know the dimensions inside the scroll bars and only use
innerWidth/Height when it is the only mechanism available.
Unfortunately the very simple rules that apply to determining which
element should be read to retrieve the viewport/visible client-region
(inside any scroll bars) dimensions on IE browsers are not really
applicable to any other DOM browsers (except by coincidence on some).
You simply cannot apply the - document.compatMode - test and be left
with a reference to the correct object (Opera is a particular offender
here). Logic that prefers to use the innerWidth/Height side-steps that
problem, but preferring to use the correct clientWidth/Height properties
brings you face to face with it.
So the problem is that the clientWidth/Height dimensions reported by
either of - document.body - and/or - document.documentElement - might be
either the dimensions of the viewport/client-region (that are wanted) or
they might be the client area of the document (which might be bigger
than the viewport, or smaller). However, given a browser that also
supports innerWidth/Height, some logic can be applied towards deciding
which should be used. For example, the real viewport/client-region
dimensions will never exceed the corresponding innerWidth/Height
dimensions (only the document dimensions can do that, though they may
not), and if a page is scrolled it is reasonable to assume that it has a
scroll bar, which means that in that case the 'client' dimension inside
the scroll bar must be smaller than the corresponding 'innner' value.
The problem with this testing strategy is that you cannot always make a
final decision as to which object is reporting the true 'client'
dimensions as the result of one test. A provisional decision can be made
and its result will be 'safe' (in that they will never be worse than the
'inner' values). However, conditions might change. A page that was not
scrolled might be scrolled, or the page might be re-sized, altering the
'inner' values but not necessarily in the same way as the document
dimensions. Those changes would allow the provisional decision to be
reviewed and possibly a final decisions to be made at that point.
This brings me to the progressively self re-configuring ("Russian doll")
object/system that I said I would publish a few months back. That object
was intended to address this particular problem. I still haven't had
time to fully test it (at least to the extent that I would consider
necessarily), it hasn't yet been shown to any Mac browsers, and I don't
think Konqueror has seen this current version yet either. But here it is
anyway:-
/* The - getWindowState - function can be regarded a factory that
returns a simple interface object, though that interface is in
reality always a reference to the same object (a 'singleton
class').
var windowInterface = getWindowState();
NOTE: the first call to the - getWindowState - function should
not happen until after the browser has passed the opening BODY
tag (else the corresponding element will not be available in
the DOM).
Code is expected to retrieve a reference to the interface and
store it locally for re-use.
The interface has 4 methods that return pixel dimensions as
numbers (or NaN if the browser does not support any mechanism
for reading the corresponding information):-
var clientWidth = windowInterface.getWidth();
var clientHeight = windowInterface.getHeight();
var verticalScroll = windowInterface.getScrollY();
var horizontalScroll = windowInterface.getScrollX();
Wherever possible non-NaN returned values for width and height
will be the vewport/client-region dimensions _not_ including
the scroll bar thicknesses. They will always be equal to, or
less than the innerWidth/Height values on browsers that support
those properties.
*/
function getWindowState(){
var global = this;
var lastSt,lastSl,lastIW,lastIH,bodyRank = null,docElRank = null;
var twoTestCount = 0;
/* When the system reports dimension and scroll values it is
reading those values form one of a number of objects,
depending on which the browser supports, and which seems
to be providing the best information. When decisions are
made about which object to read a reference to that abject
is assigned to one of the following variables. The initial
object assignments represent defaults and will result in
the interface returning NaN values of the browser does not
support any mechanism for reporting the relevant
information:-
*/
var readScroll = {scrollLeft:NaN,scrollTop:NaN};
var readSizeC = {clientWidth:NaN,clientHeight:NaN};
var readSizeI = {innerWidth:NaN,innerHeight:NaN};
/* Default property names used when reading values form the
object assigned to the - readScroll - variable. Their
values are changed to - pageXOffset - and -pageYOffset -
respectively whenever the global object is assigned to -
readScroll -, which happens whenever it supports those
properties as numeric values:-
*/
var readScrollX = 'scrollLeft';
var readScrollY = 'scrollTop';
/* This object represents the interface to the scrolling and
viewport/client-region dimensions. Calls to - getWindowState
- always return a reference to this one object. Its methods
are dynamically changed during the configuration of this
system to provide the best information available in the
shortest time possible:-
*/
var theInterface = {
getScrollX:getScrlXMain,
getScrollY:getScrlYMain,
getWidth:function(){return initWidthHeight('getWidth');},
getHeight:function(){return initWidthHeight('getHeight');}
};
/* When the system makes a provisional decision about which of
- global -, - document.body - or - document.documentElement
- it should be reporting dimensions from, but cannot entirely
eliminate the other contenders, that provisional decision does
not need to be reviewed until one of the criteria on which is
was based is changed (as the code will draw the same conclusion
otherwise). So these functions are used to monitor for changes
in the scroll of the browser window and only re-examine the
provisional decision if the window has changed scroll (as that
may have altered the decisions making criteria in a way that
allows a more final conclusion).
The scroll values actually returned are not influenced by this
testing, it is only done because it may allow the resolution of
the viewpot/client-region dimension reporting issue:-
*/
function getScrlXTest(){hasChangedScroll();return getScrlXMain();}
function getScrlYTest(){hasChangedScroll();return lastSl;}
/* The following two functions are the final scroll position
reporting functions. They read a value from the - readScroll -
object. That object may be the NaN returning default (if no
mechanism is available for reporting scroll positions on the
current browser), It may have been assigned - document.body - or
- document.documentElement -, or (and whenever - pageYOffset
- and -pageXOffset - are available as numeric properties) a
reference to the global object. The property names held in the
variables - readScrollX - and - readScrollY - will be either
scrollTop/Left or pageX/YOffset, depending on which object is
being used to report the values.
Once the viewport/client-region dimension reporting issues have
been finalised these two functions are assigned as method of
the interface. They are also the interface's default methods,
only swapped for the testing method if the dimension reporting
cannot be resolved at the first attempt:-
*/
function getScrlXMain(){return readScroll[readScrollX];}
function getScrlYMain(){return readScroll[readScrollY];}
/* These two functions return the innerHeight/Width properties
from the object referred to by - readSizeI -. That object is
initially a NaN returning default but is replaced with a
reference to the global object if that object is found to have
numeric innerHeight/Width properties. In the event that this
system decides that the innerHeight/Width properties of the
global object are the most appropriate these two functions
will be assigned to the - getWidth - and - getHeight - methods
of the interface object. Otherwise they are employed in the
decision making process to compare innerHeight/Width values
with clientHeight/width values of the object being considered
as the optimum candidate for returning browser window inner
dimensions:-
*/
function getWidthI(){return readSizeI.innerWidth;}
function getHeightI(){return readSizeI.innerHeight;}
/* The - readSizeC - variable refers to one of - document.body -
or - document.documentElement - (or the default), and these
two method return clientWidth/Height values from that object.
In the event that either the - body - or - documentElement -
are settled upon as the best source for the
viewport/client-region dimensions then these functions are
assigned to the corresponding methods of the interface:-
*/
function getWidthC(){return readSizeC.clientWidth;}
function getHeightC(){return readSizeC.clientHeight;}
/* When the system makes a provisional decision about which of
- global -, - document.body - or - document.documentElement
- it should be reporting dimensions from, but cannot entirely
eliminate the other contenders, that provisional decision does
not need to be reviewed until one of the criteria on which is
was based is changed (as the code will draw the same conclusion
otherwise). So these functions are used to monitor for changes
in the size of the browser window and only re-examine the
provisional decision if the window has changed size (as that may
have altered the decisions making criteria in a way that allows
a more final conclusion):-
*/
function getHeightSmart(){
return (hasChangedSize()?theInterface.getHeight:getHeightC)();
}
function getWidthSmart(){
return (hasChangedSize()?theInterface.getWidth:getWidthC)();
}
/* In the event that both - document.body - or -
document.documentElement - can be eliminated from consideration
(or are not available on the browsers (Net 4), the system
resorts to using the innerHeight/Width functions for the
interface methods (which may be returning default NaN values if
they are also unsupported). This may happen in several places
so the code that does the method swapping is wrapped in a single
inner function:-
*/
function setInnerWH(){
theInterface.getWidth = getWidthI;
theInterface.getHeight = getHeightI;
}
/* Whenever the client-region/viewport dimension reporting system
is able to make its final decision changes in the scroll position
of the window are not longer interesting and the interface
methods
that return scroll values can be swapped to the simplest and
quickest versions available. This may happen in several places so
the code that does the swapping is wrapped in a single inner
function:-
*/
function setMinimumScroll(){
theInterface.getScrollX = getScrlXMain;
theInterface.getScrollY = getScrlYMain;
}
/* The next two functions check for changes in the dimensions of a
window (as reported by innerHeight/width) or the scroll position
of the window, and re-execute the viewport/client-region test
function (as assigned to - threeObjectTest -) if those values
have changed:-
*/
function hasChangedSize(){
if(
(lastIH != (lastIH = getHeightI()))||
(lastIW != (lastIW = getWidthI()))
){
threeObjectTest();
return true;
}
return false;
}
function hasChangedScroll(){
if(
(lastSl != (lastSl = getScrlYMain()))||
(lastSt != (lastSt = getScrlXMain()))
){
threeObjectTest();
}
}
/* The next function is the one that does the tests that decide
whether one of - document.body - or - document.documentElement
- can be eliminated form consideration as candidates for
reporting the viewport/client-region dimensions, or it returns
a value that is used to decide with of the two is providing the
information most likely to be correct.
The parameters are two objects: testObj - is one of -
document.body - or document.documentElement - and - rankObj -
is an object that holds information resulting form this test,
or previous tests on the same - testObj - object.
When this function returns NaN the corresponding - testObj -
is eliminated from consideration as a candidate for
viewport/client-region dimension reading. Otherwise the object
with the _lowest_ - rank - value is used provisionally:-
*/
function rankObj_inner(testObj, rankObj){
/* Variables that will hold vertical and horizontal differences
between client and inner values:-
*/
var dv,dh;
/* If client dimensions exceed inner dimensions then this
object cannot be reporting viewport/client-region
dimensions. So values of either dv or dh that are less
than zero result in this function returning NaN:-
*/
if(
((dv = (global.innerHeight - testObj.clientHeight)) >= 0)&&
((dh = (global.innerWidth - testObj.clientWidth)) >= 0)&&
/* If the page is scrolled - getScrlX/YMain() - will return
a non-zero value. A scrolled page will have a
corresponding scroll bar so a non-zero vertical scroll
value implies a vertical scroll bar and so the - dh -
value will be non-zero on an object reporting
viewport/client-region dimensions. Similarly with
horizontal scrolling. Objects are eliminated if a
non-zero scroll value is not accompanied by a non-zero
difference between inner and client values in the
perpendicular axis:-
*/
(!(getScrlXMain()&&!dv))&&
(!(getScrlYMain()&&!dh))&&
/* Non-zero values for - dh - and dv - will represent the
thickness of the scroll bars. While scrollbar dimensions
are user-configurable and OS dependent, it is unlikely
that they will change while this script is running. The
- rankObj - has - hDiff - and - vDiff - properties that
are assigned non-zero values only once. If this is the
second+ time that this object has been tested and it has
non-zero dh/v values that are not identical to non-zero
h/vDiff properties of the - rankObj - then those values
are assumed to not represent scrollbar thickness and
the - testObj - can be eliminated form consideration:-
*/
(
!(
(
dh&&(rankObj.hDiff||(rankObj.hDiff = dh))&&
(dh != rankObj.hDiff)
)||(
dv&&(rankObj.vDiff||(rankObj.vDiff = dv))&&
(dv != rankObj.vDiff)
)
)
)
){
/* If the - dv - and - dh - values are equal then either
the browser is not displaying scroll bars at all or
both the horizontal and vertical scroll bars are being
displayed (or this object is not reporting the
viewport/client-region dimensions and the values are
pure coincidence):-
*/
if(dh == dv){
/* If both dh and dv are zero then the likelihood is
very good that this object is reporting
viewport/client-region dimensions (and if it is not
then the values it is reporting can be used without
trouble anyway), so a zero value is assigned to the
- rank - property of - rankObj. If the differences
are non-zero then the likelihood is that both scroll
bars are showing and have the same dimensions.
***
It is problematic to be assuming that vertical and
horizontal scrollbars should have the same
thickness, even though it appears to be true on all
of the OSs and window managers examined to date
(this assumption is a significant problem).
***
However, this cannot give the same certainty as zero
differences so the value of one is assigned.
*/
rankObj.rank = Number(Boolean(dh));
/* The assumption that scroll bars will be of the same
thickens leads to the rule that if either is non-zero
and dv and dh are not identical then the differences
cannot represent scrollbars thicknesses and so the
object cannot be reporting viewport/client-region
dimensions:-
*/
}else if((dh&&!dv)||(dv&&!dh)){
/* When one of the values is zero the sum of the values
will be the magnitude of the other. This value is
assigned to the - rank - property of - rankObj -
and the - testObj - with the smallest corresponding
- rank - value will be provisionally preferred over
the other:-
*/
rankObj.rank = (dh+dv);
}else{
/* Otherwise NaN is returned and will result in -
testObj - being eliminated form consideration as a
candidate for reading the viewport/client-region
dimensions:-
*/
rankObj.rank = NaN;
}
}else{
rankObj.rank = NaN;
}
return rankObj;
}
/* Provided with an object as its first parameter, and an
indefinite number of following string arguments, this function
returns true if all the string arguments represent numeric
properties of the - testObj - object, and false otherwise.
It is used to check whether various objects support relevant
numeric properties before considering them as candidates for
the reporting of the values of those properties:-
*/
function propsAreNumbers(testObj){
for(var c = arguments.length;--c
{
if(typeof testObj[arguments[c]] != 'number'){
return false;
}
}
return true;
}
/* Initially the following function wraps the preceding -
rankObj_inner - function, testing that the - testObj -
parameter supports the relevant properties. However, the -
rankObj - identifier is assigned a reference to -
rankObj_inner - after it has been called on each object of
interest because if they support the relevant properties on
the first call to - rankObj - they will still support the
properties on any subsequent calls. There is no need to
re-execute the code within this function:-
*/
function rankObj(testObj, rankObj){
if(
testObj&&
propsAreNumbers(testObj, 'clientWidth','clientHeight')
){
rankObj_inner(testObj, rankObj)
}
return rankObj;
}
/* The default methods of the interface object uses functions that
call this function as the default values of - getHeight - and -
getWidth - but this function replaces those functions, with the
effect that this function is only called once. It does the
initial set-up for the testing that is used to determine which
object the viewport/client-region dimensions should be read
from. This strategy defers the configuration/set-up decisions
until the first use of one of those methods:-
*/
function initWidthHeight(callOn){
lastIW = getWidthI();
lastIH = getHeightI();
lastSt = getScrlYMain();
lastSl = getScrlXMain();
/* The objects used to hold the decision making information
that is used to decide between the use of - document.body
- and - document.documentElement - have their initial -
rank - property defaulted to NaN so that the corresponding
object will be eliminated form consideration if it is either
not implemented or does not support numeric
clientheight/Width properties:-
*/
bodyRank = {vDiff:0,hDiff:0,rank:NaN};
docElRank = {vDiff:0,hDiff:0,rank:NaN};
/* Replacing the dimension retrieval methods prevents this
function from being re-called on subsequent uses of the
interface methods. These replacement functions re-evaluate
the decision making criteria whenever the browser window is
innerHeight/Width values change:-
*/
theInterface.getWidth = getWidthSmart;
theInterface.getHeight = getHeightSmart;
/* The scroll value reporting method are swapped for versions
that test for changes in the scroll values as those changes
allow an opportunity to re-evaluate the decision making
criteria:-
*/
theInterface.getScrollX = getScrlXTest;
theInterface.getScrollY = getScrlYTest;
/* The - threeObjectTest - function attempts to make the
decision between reading the clientHeight/Width values form
either the - document.body - or the -
document.documentElement -, or the innnerHeight/Width
properties from the global object:-
*/
threeObjectTest();
/* The first use of - threeObjectTest - calls - rankObj -,
which tests for the existence of - document.body - and -
document.documentElement -. But if the objects are not
available on the browser, or do not support the required
properties they will be eliminated form future
consideration. as a result it is unecessery to re-test
their existence, and re-testing is avoided by swapping -
rankObj - for the function that it calls to acquire the
actual ranking values:-
*/
rankObj = rankObj_inner;
/* Having initialised the configuration process this function
must return a value. It does this by calling whichever
corresponding method has been assigned to the interface at
this point:-
*/
return theInterface[callOn]();
}
/* The strategy used to determine which object is to be used to
report viewport/client-region dimensions is to generate a
"ranking" for the body and the documentElement. If that ranking
is NaN the object can immediately be removed form consideration.
Otherwise the object with the _lowest_ ranking is used
provisionally, and the tests repeated if the browser window is
re-sized or scrolled (as that means that the decision making
criteria have changed and it might have become possible to make
a more final decision):-
*/
function threeObjectTest(){
/* Whichever function is currently assigned to - rankObj - is
called and a boolean value set based on whether the - rank
- property of the returned object (bodyRank or docElRank)
is NaN:-
*/
var bodyNaN = isNaN(rankObj(document.body, bodyRank).rank);
var docElNaN =
isNaN(rankObj(document.documentElement, docElRank).rank);
/* If either value is true (a - rank - was NaN) at least one of
the objects can be removed from further consideration:-
*/
if(bodyNaN||docElNaN){
/* If both values were NaN then only reading the
innerHeight/Width values of the global object is viable
so the interface methods are re-assigned to functions
that do that job directly an do no further testing:-
*/
if(bodyNaN&&docElNaN){
setMinimumScroll();
setInnerWH();
/* Else only one object can be dropped from consideration
(leaving that object and reading the innerHeight/Width
from the global object as the only remaining options):-
*/
}else{
/* The non-NaN returning object is assigned to -
readSizeC - and its corresponding 'ranking' object
assigned to - bodyRank -:-
*/
readSizeC = ((docElNaN)?document.body:
document.documentElement);
bodyRank = ((docElNaN)?bodyRank:docElRank);
docElRank = null;
/* Having eliminated one of the objects it is only
necessary to make decisions between the two
remaining candidates. This is done by swapping the
- threeObjectTest - function for one that only makes
decisions between whichever object has been assigned
to - readSizeC - and using innerHeight/Width
values:-
*/
threeObjectTest = twoObjectTest;
}
/* If neither object can be rejected at this stage one of them
must be chosen provisionally to be used to return
veiwport/client-region dimensions. The object with the
lowest 'ranking' is used, and that decision re-evaluated
whenever the browser has been re-sized or scrolled:-
*/
}else{
readSizeC = ((bodyRank.rank < docElRank.rank)?
document.body:
document.documentElement);
}
}
/* Once one object has been eliminated it is only necessary to test
the remaining object to see if it possible to either eliminate
it and use the innerHeight/width values of the global object, or
to settle on that object as the best candidate and so stop
further testing and remove the global object from consideration.
This function does those tests:-
*/
function twoObjectTest(){
/* If the - rank - is Nan after this test then the remaining on
of body or documentElement can be eliminated and only the
innerHeight/Width values reported in future:-
*/
if(isNaN(rankObj(readSizeC, bodyRank).rank)){
bodyRank = null;
setMinimumScroll();
setInnerWH();
/* Else if the one remaining object has seen non-zero
differences in clientWidth/Height and innerWidth/Height
(scroll bar thicknesses) and has survived this test twice
then it is a fair bet that it does represent the best object
to be reading viewport/client-region dimensions from. So
further consideration of the innerHeight/Width values is
eliminated, along with any additional testing:-
*/
}else if(
(bodyRank.vDiff)&&(bodyRank.hDiff)&&
(++twoTestCount > 2)
){
bodyRank = null;
theInterface.getWidth = getWidthC;
theInterface.getHeight = getHeightC;
setMinimumScroll();
}
}
/* The following code is executed when the outermost function is
called. It is only executed once as its final act is to assign
an inner function to - getWindowState -, that inner function
then acts as the 'factory' function for the interface:-
*/
/* If the global object does not support innerHeight/Width
properties then the preceding decision making mechanism cannot
be used. Instead a decision is made at this point based on the
usual - document.compatMode - tests that apply to IE browser:-
*/
if(!propsAreNumbers(global, 'innerHeight', 'innerWidth')){
readSizeC = compatModeTest(readSizeC);
theInterface.getWidth = getWidthC;
theInterface.getHeight = getHeightC;
}else{
/* As the global object does supports innerHeight/Width the
default - readSizeI - object is replaced with a reference
to the global object:-
*/
readSizeI = global;
}
/* If the global object supports pageXYOffset properties then
they are the simplest mechanism for reporting scroll values:-
*/
if(propsAreNumbers(global, 'pageYOffset', 'pageXOffset')){
readScroll = global;
readScrollY = 'pageYOffset';
readScrollX = 'pageXOffset';
/* Else the - document.compatMode - tests are used again:-
*/
}else{
readScroll = compatModeTest(readScroll);
}
/* The global reference to - getWindowState - is replaced with a
reference to an inner function that just returns the interface
object, and that inner function is called and its return value
returned by this initial (and only) cal to the current -
getWindowState - (outermost) function:-
*/
return (getWindowState = function(){return theInterface;})();
}
/* The preceding function is dependent upon this compatMode
test function (at least in IE and IE-like browsers):-
*/
function compatModeTest(obj){
if(
(document.compatMode)&&
(document.compatMode.indexOf('CSS') != -1)&&
(document.documentElement)
){
return (compatModeTest = function(){
return document.documentElement;
})((obj = null));
}else if(document.body){
return (compatModeTest = function(){
return document.body;
})((obj = null));
}else{
return obj;
}
}
OK that is a huge mass of (convoluted
code, made worse by being
extensively commented. However, properly minimised it reduces to <
2.4Kb, and its complexity is internal, the public interface is small and
simple, making it easy to use without any interest in its internal
complexity.
Comments/feedback/Improvements/etc welcome.
Richard.