Element Positioning: .css( ‘left’ ) vs .position().left (Or: Making Element Positioning Play Nice Across Browsers)

css() vs position()

I sit in front of my trusty computer, coding-away right-to-left popup-location fixes in anticipation of the new VisualEditor deployment in the Hebrew Wikipedia. The hard part, I tell myself (and with good reason) is calculating the mirroring coordinates; which object to I use as parent to mirror my coordinates against?

As I’ve explained in a previous post, the positioning and mirroring of nested elements can be a real challenge — the RTL/LTR Vertigo.

This time, the element whose position I was trying to mirror popped up inside another widget — a frame inside a frame — which made positioning all that much trickier, since it’s all relative to one another. In cases like these, I find myself following up on the relative positioning of several elements in the nesting chain; even Einstein’s head would’ve spun.

The principle, however, is the same — flip the ‘left’ position with the mirrored ‘right’ position, relative to the parent container.

Similarly to last time, the fixed code looked like this:

// ('dimensions' variable was set above according to ltr positions)
// Fix for RTL 
if ( this.$.css( 'direction' ) === 'rtl' ) {
	dimensions.right = parseFloat( this.$.parent().css( 'left' ) ) - dimensions.width - dimensions.left;
	// Erase the value for 'left':
	delete dimensions.left;
this.$.css( dimensions );

In VisualEditor, this.$ represents the jQuery object of the object we’re in (‘this’). Since I am calling for a jQuery function ‘css()’, I must refer to the jQuery object, rather than the VisualEditor object.

I can test the RTL direction property this.$.css( ‘direction’ ) on my popup widget because the direction is an inherited property; even if I didn’t set “direction:rtl” explicitly, the element will inherit it from the parent, who will inherit it from its parent.. etc etc forever and ever. Well, at least until an element with defined directionality is found, and if none is found, then, as far as it should go, the default is LTR.

So this should have worked. And in Firefox, it did. But not in Chrome.

Firefox and Chrome

The coordinate mirroring worked beautifully in Firefox.

It also failed beautifully in Chrome. But it’s all about math, isn’t it? Parent position minus some left value, minus some width minus– math is math. Is math. So why, I begged my screen. “Whyyyy aren’t you working??”

The Real Problem

The problem with Chrome wasn’t the cause, it was a symptom. The real problem was the way I read through the positioning of the elements. In specific, the problem was this line:

this.$.parent().css( 'left' )

This line reads the parent object’s css “left” property, which was defined (I checked!)  so it should have worked. In theory, I should have gotten the ‘left’ position of the parent. Right?

Sure. I did get the left property — but that’s exactly the problem. What I got was the CSS property of the element, I didn’t really get the actual coordinates the element was positioned. Not quite, not exactly, and not entirely.

css(‘left’) vs position().left vs offset().left

There are several ways to read the position of an element, and each one of those gives you a slightly different value. They’re all true, it just depends what, exactly, you mean to use.

css( ‘left’ ) returns the calculated value of the CSS property. It is the “left:10px” that exists either in the css stylesheet or in the style=” property of the element.

position().left returns the position of the element relative to the offset parent.

offset().left returns the position of the element relative to the document.

Differences in Browsers and Screens

There may be subtle differences in the way browsers render result differently, the value of the css(‘left’) property can be different across browsers. position() and offset() are actual x/y values, so requesting these values will give me the actual position of the element (in whatever browser it was rendered) rather than the ‘requested’ value the CSS style contains.

Since the positions are relative to one another, it won’t do me much good to have the position in relation to the document. What i really need is the x/y positions of my parent element in relation to its own offset element. That is, I need position() value.

Notice one more thing: css(‘left’) will return the css value – a number and units – like ‘100px’, which is why I needed to encapsulate it with “parseFloat” before I continued with the calculations. On top of that, I run the risk of the css style returning ‘auto’ which would be parsed into 0 and render the entire mirroring action completely faulty.

position() and offset(), however, return x/y values, which are numerical, and represent the actual distances and positions relative to their parent and the screen.

VisualEditor Supercalifragilisti-Nested Divs

VisualEditor has lots of nested divs. And nested iFrames inside divs, that also have divs in them. And many of those get some position property along the chain; a Widget may get to be positioned at the center of the screen, while its panels are positioned with some percentage according to their size, and they, in turn, will have an input that has a popup attached.

And they’re all relative, and most of them has their positions injected dynamically, since the position most of the time depends on either where your marker is, or where your mouse is, or where whatever element you intend to edit is located, etc etc.

All of this makes flipping coordinates in VisualEditor challenging.

But the bigger challenge comes when we do that while calculating the offset and/or position() value of another element. We actually do that quite a lot, especially with popup widgets. For instance, inside the Page Settings window, you can add new Categories. Typing the category name in the input pops a “suggestion menu” that appears right under it. That means that for the suggestion menu to appear correctly, we need to check where the input element (which is it’s “sibling” in the nesting chain) is located, and mirror the coordinates. But we need to be careful and check that we don’t mirror mirror coordinates (because that won’t make sense) or mirror already mirrored mirror mirror coordina— well, you get my drift. It can be confusing.

On top of that, VisualEditor popups are iframes, and iframes are problematic with jQuery as it is, especially when they pop up already inside other iframes (re category popup inside the  Page Settings widget).

The more we depend on other dynamically positioned elements, the more we run the risk of having inconsistencies when mirroring those coordinates.

Example position() vs css()

Check out this StackOverflow answer. It shows an example of how position() and css() values are equal and another where they are be different.

The solution

Since my popup widget appeared inside another widget, the offset and general different in rendering was probably more emphasized than if the element appeared as part of the main VisualEditor surface. So, the solution was rather simple:

// ('dimensions' variable was set above according to ltr positions)
// Fix for RTL 
if ( this.$.css( 'direction' ) === 'rtl' ) {
	dimensions.right = this.$.parent().position().left - dimensions.width - dimensions.left;
	// Erase the value for 'left':
	delete dimensions.left;
this.$.css( dimensions );

And that worked like a charm.

Many thanks go to Timo Tijhof (Krinkle) who found this problem by spotting it in Chrome, and to Roan Kattouw (Catrope) for figuring out the solution is to use position().left 

More Resources

Tags: , ,

Trackback from your site.