More Accurate Element Measurement through jQuery

D

David Mark

Defending the recent decision to further tangle up the attr method by
hard-wiring it to jQuery method calls for attribute names that match
jQuery method names (e.g. height), Resig offered this explanation:-

"...for example, we definitely do want to get the height value as
reported by.height(), especially since it's likely to be more accurate
than trying to get elem.height."

http://groups.google.com/group/jquery-dev/browse_thread/thread/baef5e91bd714033

More accurate than what? The value returned by jQuery's height method
is going to mean something quite different than - for example - the
height property of an image element.

<img height="32" width="32" src="..." style="border:solid 10px;padding:
10px">

There are a number of contexts where you would want to retrieve the
image height of 32. For instance, if you wanted to zoom 2X, you could
easily calculate the zoomed height as:-

img.height * 2 // 64

It used to be that jQuery could do this with its attr method:-

attr('height') * 2 // 64

Simple, concise, 100% accurate, works in virtually any agent, but
apparently not "cool" enough for the code monkeys.

I imagine one of their "thoughts" is "what if the display style is
none?". Wouldn't it be cool if they added it to the layout real fast,
measured and removed it? Well, of course not. That would just step
on more significant details and create more confusion (at least for
those paying attention).

So what will this new "more accurate" rendition do besides cross more
wires under the hood?

This is the height method:-

jQuery.fn[ type ] = function( size ) {
// Get window width or height
var elem = this[0];
if ( !elem ) { return null; }
return ("scrollTo" in elem && elem.document) ? // does it walk and
quack like a window?

There's the result of the ridiculous "overload"-happy design, which
doesn't really work in JS. So this thing measures windows too. I
suppose it is predictable as this object model uses one object to
represent a single element, multiple elements, documents, windows,
etc. :)

// Everyone else use document.documentElement or document.body
depending on Quirks vs Standards mode


What?


elem.document.compatMode === "CSS1Compat" &&
elem.document.documentElement[ "client" + name ] ||
elem.document.body[ "client" + name ] :

Well, we know that's bullshit.

http://www.cinsoft.net/viewport.asp


// Get document width or height
(elem.nodeType === 9) ? // is it a document
// Either scroll[Width/Height] or offset[Width/Height], whichever
is greater
Math.max(
elem.documentElement["client" + name],
elem.body["scroll" + name], elem.documentElement["scroll" +
name],
elem.body["offset" + name], elem.documentElement["offset" + name]
) :


Sure, scrollHeight and offsetHeight mean about the same thing for
documents objects. Whichever is bigger. ;)


// Get or set width or height on the element

Finally.

size === undefined ?
// Get width or height on the element
jQuery.css( elem, type ) :


Dammit, another detour (and I don't like where this is going).


css: function( elem, name, force, extra ) {
if ( name === "width" || name === "height" ) {
var val, props = { position: "absolute", visibility: "hidden",
display:"block" }, which = name === "width" ? [ "Left", "Right" ] :
[ "Top", "Bottom" ];

function getWH() {
val = name === "width" ? elem.offsetWidth : elem.offsetHeight;

if ( extra === "border" ) { return; }


Didn't pass any "extra", so proceed...


jQuery.each( which, function() {
if ( !extra ) {
val -= parseFloat(jQuery.curCSS( elem, "padding" + this, true))
|| 0;
}

Subtracts padding.


if ( extra === "margin" ) {
val += parseFloat(jQuery.curCSS( elem, "margin" + this, true))
|| 0;
} else {
val -= parseFloat(jQuery.curCSS( elem, "border" + this +
"Width", true)) || 0;

Subtracts border.


}
});
}

if ( elem.offsetWidth !== 0 ) {
getWH();

There it is.


} else {
jQuery.swap( elem, props, getWH );
}

return Math.max(0, Math.round(val));
}

return jQuery.curCSS( elem, name, force );
},

Okay, that was a wild ride (and imagine a UA doing it on every call to
attr). I want to duplicate the following design, which reads the
height of an image and then puts it back. The image's height should
remain the same.

var height = img.height; // 32
img.height = var; .// 32 (all is well)

There's one glaring issue with the jQuery way and that's curCSS. A
quote from the inline comments of that method says it all:-

// From the awesome hack by Dean Edwards

(!)

// If we're not dealing with a regular pixel number
// but a number that has a weird ending, we need to convert it to
pixels

This is from the IE fork. The hack used is complete bullshit, but
doesn't matter for this example.

Still, this is definitely going to be a "weird ending". :)

In standards mode, in most browsers, Resig's concoction will spit out:
32. Cool! And only took 10,000 times the effort. It's even (sort
of) symmetrical as the reverse process ends up setting the CSS height
to '32px'.

<img height="32" width="32" src="..." style="border:solid 10px;padding:
10px;height:32px">

In quirks mode, in some browsers (e.g. IE), or if the box model for
the image has been changed through CSS, the result is also 32.
Unfortunately, setting the CSS height to '32px' will be a catastrophe.
In fact, it's not even possible as the borders and padding take up
40px (and that's all that will be left of the image).

The confusion is evident in these methods as well:-

// innerHeight and innerWidth
jQuery.fn["inner" + name] = function(){
return this[0] ?
jQuery.css( this[0], type, false, "padding" ) :
null;
};

// outerHeight and outerWidth
jQuery.fn["outer" + name] = function(margin) {
return this[0] ?
jQuery.css( this[0], type, false, margin ? "margin" : "border" ) :
null;
};

What's wrong with those pictures?

So, even if there were no such things as quirks mode or alternate box
models and every agent supported computed styles (lets out IE), this
would sort of work, but would still be a ridiculous design.

What's the answer to all of this? It's very simple. Use more than
one function. One gets the CSS dimensions, one gets the offset
dimensions (and neither is tangled up with attribute or property
wrappers). A symmetrical setter for the former is trivial and the
latter is do-able as well. There is no need to manually subtract
borders and padding at all and the box model is irrelevant (GIGO).
See My Library (or the upcoming "B is for Box Model" article) for
examples.

Can't measure windows, document or elements; can't query elements by
attribute, can't get/set attributes or properties in any sort of
predictable way and as a bonus, if you act now...

From the creators of isArray and isFunction comes...

isObjectLiteral: function( obj ) {


Yes friends, you too can detect object literals. :)


if ( toString.call(obj) !== "[object Object]" || typeof obj.nodeType
=== "number" ) {
return false;
}


LOL. I've got to figure they know they screwed up massively with the
initial "overloaded" design, but I guess they are stuck with it.


// not own constructor property must be Object
if ( obj.constructor
&& !hasOwnProperty.call(obj, "constructor")


Oops. So Safari 2 and the like just blow up in the middle of
this. :)


&& !hasOwnProperty.call(obj.constructor.prototype,
"isPrototypeOf") ) {
return false;
}

//own properties are iterated firstly,


Says who?


//so to speed up, we can test last one if it is own or not

var key;
for ( key in obj ) {}

return key === undefined || hasOwnProperty.call( obj, key );
},


WTF? I've got a great idea to "speed up" this mess. Lose it
altogether. ;)
 
D

David Mark

In fact, it's not even possible as the borders and padding take up
40px (and that's all that will be left of the image).

Left out that after the set:-

img.height // 32 (survived the jQuery DOM assault)
attr('height') // 0 (jQuery fooled itself again)
 
M

Michael Haufe (\TNO\)

Defending the recent decision to further tangle up the attr method by
hard-wiring it to jQuery method calls for attribute names that match
jQuery method names (e.g. height), Resig offered this explanation:-

"...for example, we definitely do want to get the height value as
reported by.height(), especially since it's likely to be more accurate
than trying to get elem.height."

Generally when I call a function I don't want it to "likely" give me
the correct result. Either give me a 100% accurate answer, or throw an
error due to invalid arguments, nothing in between.
 
D

David Mark

Generally when I call a function I don't want it to "likely" give me
the correct result. Either give me a 100% accurate answer, or throw an
error due to invalid arguments,  nothing in between.

Unfortunately, there's a lot in-between with this thing. The biggest
problem seems to be that the authors haven't understood the questions,
so they can't possibly give accurate answers. Pressed on this, they
quickly launch into "can you give a specific example" mode, which is
tiresome as it just leads to more patching without a clue.
 
G

Gregor Kofler

David Mark meinte:

[snip]
if ( toString.call(obj) !== "[object Object]" || typeof obj.nodeType
=== "number" ) {
[..]

Is there any particular advantage with using this
toString.call(obj) !== "[object Object]"
check?

To me it just looks completely inefficient and perhaps error-prone (and
ugly, too), but I might be wrong. Are there *any* advantages when
compared to, say, a simple

typeof obj !=== "object" (maybe an extra check for NULL added). (Well,
it might be a shaky try to weed out arrays.)
Does every browser return this very string with toString() applied to an
object?

Gregor
 
D

David Mark

David Mark meinte:

[snip]
   if ( toString.call(obj) !== "[object Object]" || typeof obj.nodeType
=== "number" ) {

[..]

Is there any particular advantage with using this
toString.call(obj) !== "[object Object]"
check?

Depends on what obj is allowed to be.
To me it just looks completely inefficient and perhaps error-prone (and
ugly, too), but I might be wrong. Are there *any* advantages when
compared to, say, a simple

typeof obj !=== "object" (maybe an extra check for NULL added). (Well,
it might be a shaky try to weed out arrays.)

The null value passes that. Arrays will not. So it all depends on
what you are trying to do.
Does every browser return this very string with toString() applied to an
object?

To a native Object object? I believe they have to per the specs.
 
G

Gregor Kofler

David Mark meinte:
Is there any particular advantage with using this
toString.call(obj) !== "[object Object]"
check?
So it all depends on what you are trying to do.

Data type checking? And for that purpose, calling toString() and
"analyzing" the returned string seems pretty arkward to me.
To a native Object object? I believe they have to per the specs.

According to the ECMAScript specifications Null should return "null",
Undefined which should return "undefined". The specs state neither
anything about square brackets, nor the leading "object" string.

<script type="text/javascript">

var x;
console.log(toString.call(x)); // [object Window]
x = undefined;
console.log(toString.call(x)); // [object Window]
x = document;
console.log(toString.call(x)); // [object HTMLDocument]
x = new Object();
console.log(toString.call(x)); // [xpconnect wrapped native prototype]
x = {};
console.log(toString.call(x)); // [xpconnect wrapped native prototype]

if(
toString.call(x) !== "[object Object]" ||
typeof x.nodeType === "number"
) {
window.alert("Bang!"); // "Bang!"
}

</script>

Looks pretty reliable to me.
(And doubt that host objects in any browser never return "[object Object]".)

Gregor
 
R

Richard Cornford

David Mark meinte:
<script type="text/javascript">

var x;
console.log(toString.call(x)); // [object Window]
x = undefined;
console.log(toString.call(x)); // [object Window]
x = document;
console.log(toString.call(x)); // [object HTMLDocument]
x = new Object();
console.log(toString.call(x)); // [xpconnect wrapped native prototype]
x = {};
console.log(toString.call(x)); // [xpconnect wrapped native prototype]

if(
toString.call(x) !== "[object Object]" ||
typeof x.nodeType === "number"
) {
window.alert("Bang!"); // "Bang!"

}

</script>

Looks pretty reliable to me.

Remember (or observe) that the unqualified Identifier - toString - in
JQuery is a local variable in the containing scope, and has been
assigned a reverence to - Object.prototype.toString -. In isolation
the Identifier - toString - is likely to resolve as a non-standardised
- toString - property of the global object, with no specified
behaviour.
(And doubt that host objects in any browser never
return "[object Object]".)

So do I.

Richard.
 
G

Gregor Kofler

Richard Cornford meinte:
Remember (or observe) that the unqualified Identifier - toString - in
JQuery is a local variable in the containing scope, and has been
assigned a reverence to - Object.prototype.toString -. In isolation
the Identifier - toString - is likely to resolve as a non-standardised
- toString - property of the global object, with no specified
behaviour.

Makes sense. (Would be a nice addition to the discussion about shadowing
window.alert().)


Still: While

var x = {};
console.log(Object.prototype.toString.call(x));

now yields "[object Object]",

var x;
console.log(Object.prototype.toString.call(x));

and

var x = null;
console.log(Object.prototype.toString.call(x));

both return "[object Window]".

Gregor
 
R

Richard Cornford

Richard Cornford meinte:
^^^^^^^^^
Should have been "reference".
isolation the Identifier - toString - is likely to resolve as
a non-standardised - toString - property of the global object,
with no specified behaviour.

Makes sense. (Would be a nice addition to the discussion about shadowing
window.alert().)

Still: While

var x = {};
console.log(Object.prototype.toString.call(x));

now yields "[object Object]",

var x;
console.log(Object.prototype.toString.call(x));

and

var x = null;
console.log(Object.prototype.toString.call(x));

both return "[object Window]".

That is understandable. It is an effect of the defaulting of the -
this - value when the argument to - call - is null or undefined.

<quote cite="ECMA 262 3rd Ed.>
| 15.3.4.4 Function.prototype.call (thisArg [ , arg1 [ , arg2, ...]] )
|
| ...
| If thisArg is null or undefined, the called function is passed the
| global object as the this value. Otherwise, the called function is
| passed ToObject(thisArg) as the this value.
| ...
</quote>

So the ECMAScript global object is used and it is its [[Class]]
property that is being reported.

However, there is no reason for the [[Class]] property of the
ECMAScript global object not to be "Object" (and, at the very least,
there were many version of Opera where it was), so the result of -
Object.prototype.toString.call(null) - being "[object Object]" is
entirely expected and normal in ES3.

Richard.
 
N

NickFitz

The specs state neither
anything about square brackets, nor the leading "object" string.

It's in ECMAScript 3rd Edition, section 15.2.4.2:

Object.prototype.toString ( )
When the toString method is called, the following steps are taken:
1. Get the [[Class]] property of this object.
2. Compute a string value by concatenating the three strings
"[object ", Result(1), and "]".
3. Return Result(2).

Referring back to 15.2.4 for Result(1):

Properties of the Object Prototype Object
The value of the internal [[Prototype]] property of the Object
prototype
object is null and the value of the internal [[Class]] property is
"Object".


Regards,

Nick.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,580
Members
45,055
Latest member
SlimSparkKetoACVReview

Latest Threads

Top