Skip to content

Commit 768a150

Browse files
A few fixes for layout.js:
* Use an internal function for getting styles instead of the public `Element.getStyle`. * Fix measurement of hidden `position: fixed` elements in IE. * Add comments for clarity.
1 parent 54cadda commit 768a150

File tree

1 file changed

+119
-38
lines changed

1 file changed

+119
-38
lines changed

src/prototype/dom/layout.js

Lines changed: 119 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,58 @@
99
return (Number(match[1]) / 100);
1010
}
1111

12+
// A bare-bones version of Element.getStyle. Needed because getStyle is
13+
// public-facing and too user-friendly for our tastes. We need raw,
14+
// non-normalized values.
15+
//
16+
// Camel-cased property names only.
17+
function getRawStyle(element, style) {
18+
element = $(element);
19+
20+
// Try inline styles first.
21+
var value = element.style[style];
22+
if (!value || value === 'auto') {
23+
// Reluctantly retrieve the computed style.
24+
var css = document.defaultView.getComputedStyle(element, null);
25+
value = css ? css[style] : null;
26+
}
27+
28+
if (style === 'opacity') return value ? parseFloat(value) : 1.0;
29+
return value === 'auto' ? null : value;
30+
}
31+
32+
function getRawStyle_IE(element, style) {
33+
// Try inline styles first.
34+
var value = element.style[style];
35+
if (!value && element.currentStyle) {
36+
// Reluctantly retrieve the current style.
37+
value = element.currentStyle[style];
38+
}
39+
return value;
40+
}
41+
42+
// Quickly figures out the content width of an element. Used instead of
43+
// `element.measure('width')` in several places below; we don't want to
44+
// call back into layout code recursively if we don't have to.
45+
//
46+
// But this means it doesn't handle edge cases. Use it when you know the
47+
// element in question is visible and will give accurate measurements.
48+
function getContentWidth(element, context) {
49+
var boxWidth = element.offsetWidth;
50+
51+
var bl = getPixelValue(element, 'borderLeftWidth', context) || 0;
52+
var br = getPixelValue(element, 'borderRightWidth', context) || 0;
53+
var pl = getPixelValue(element, 'paddingLeft', context) || 0;
54+
var pr = getPixelValue(element, 'paddingRight', context) || 0;
55+
56+
return boxWidth - bl - br - pl - pr;
57+
}
58+
59+
if ('currentStyle' in document.documentElement) {
60+
getRawStyle = getRawStyle_IE;
61+
}
62+
63+
1264
// Can be called like this:
1365
// getPixelValue("11px");
1466
// Or like this:
@@ -17,10 +69,10 @@
1769
var element = null;
1870
if (Object.isElement(value)) {
1971
element = value;
20-
value = element.getStyle(property);
72+
value = getRawStyle(element, property);
2173
}
2274

23-
if (value === null) {
75+
if (value === null || Object.isUndefined(value)) {
2476
return null;
2577
}
2678

@@ -30,8 +82,8 @@
3082
if ((/^(?:-)?\d+(\.\d+)?(px)?$/i).test(value)) {
3183
return window.parseFloat(value);
3284
}
33-
34-
var isPercentage = value.include('%'), isViewport = (context === document.viewport);
85+
86+
var isPercentage = value.include('%'), isViewport = (context === document.viewport);
3587

3688
// When IE gives us something other than a pixel value, this technique
3789
// (invented by Dean Edwards) will convert it to pixels.
@@ -48,11 +100,16 @@
48100

49101
return value;
50102
}
51-
103+
52104
// For other browsers, we have to do a bit of work.
53105
// (At this point, only percentages should be left; all other CSS units
54106
// are converted to pixels by getComputedStyle.)
55107
if (element && isPercentage) {
108+
// The `context` argument comes into play for percentage units; it's
109+
// the thing that the unit represents a percentage of. When an
110+
// absolutely-positioned element has a width of 50%, we know that's
111+
// 50% of its offset parent. If it's `position: fixed` instead, we know
112+
// it's 50% of the viewport. And so on.
56113
context = context || element.parentNode;
57114
var decimal = toDecimal(value);
58115
var whole = null;
@@ -61,7 +118,7 @@
61118
var isHorizontal = property.include('left') || property.include('right') ||
62119
property.include('width');
63120

64-
var isVertical = property.include('top') || property.include('bottom') ||
121+
var isVertical = property.include('top') || property.include('bottom') ||
65122
property.include('height');
66123

67124
if (context === document.viewport) {
@@ -87,12 +144,12 @@
87144

88145
// Turns plain numbers into pixel measurements.
89146
function toCSSPixels(number) {
90-
if (Object.isString(number) && number.endsWith('px')) {
147+
if (Object.isString(number) && number.endsWith('px'))
91148
return number;
92-
}
93149
return number + 'px';
94150
}
95151

152+
// Shortcut for figuring out if an element is `display: none` or not.
96153
function isDisplayed(element) {
97154
var originalElement = element;
98155
while (element && element.parentNode) {
@@ -105,6 +162,8 @@
105162
return true;
106163
}
107164

165+
// In IE6-7, positioned elements often need hasLayout triggered before they
166+
// report accurate measurements.
108167
var hasLayout = Prototype.K;
109168
if ('currentStyle' in document.documentElement) {
110169
hasLayout = function(element) {
@@ -282,7 +341,7 @@
282341
**/
283342
get: function($super, property) {
284343
// Try to fetch from the cache.
285-
var value = $super(property);
344+
var value = $super(property);
286345
return value === null ? this._compute(property) : value;
287346
},
288347

@@ -291,14 +350,24 @@
291350
// when hidden), elements need a "preparation" phase that ensures
292351
// accuracy of measurements.
293352
_begin: function() {
294-
if (this._prepared) return;
295-
353+
if (this._isPrepared()) return;
354+
296355
var element = this.element;
297356
if (isDisplayed(element)) {
298-
this._prepared = true;
357+
this._setPrepared(true);
299358
return;
300359
}
301360

361+
// If we get this far, it means this element is hidden. To get usable
362+
// measurements, we must remove `display: none`, but in a manner that
363+
// isn't noticeable to the user. That means we also set
364+
// `visibility: hidden` to make it invisible, and `position: absolute`
365+
// so that it won't alter the document flow when displayed.
366+
//
367+
// Once we do this, the element is "prepared," and we can make our
368+
// measurements. When we're done, the `_end` method cleans up our
369+
// changes.
370+
302371
// Remember the original values for some styles we're going to alter.
303372
var originalStyles = {
304373
position: element.style.position || '',
@@ -307,45 +376,48 @@
307376
display: element.style.display || ''
308377
};
309378

310-
// We store them so that the `_end` function can retrieve them later.
379+
// We store them so that the `_end` method can retrieve them later.
311380
element.store('prototype_original_styles', originalStyles);
312381

313-
var position = element.getStyle('position'),
314-
width = element.getStyle('width');
315-
316-
if (width === "0px" || width === null) {
317-
// Opera won't report the true width of the element through
382+
var position = getRawStyle(element, 'position'), width = element.offsetWidth;
383+
384+
if (width === 0 || width === null) {
385+
// Opera/IE won't report the true width of the element through
318386
// `getComputedStyle` if it's hidden. If we got a nonsensical value,
319387
// we need to show the element and try again.
320388
element.style.display = 'block';
321-
width = element.getStyle('width');
389+
width = element.offsetWidth;
322390
}
323391

324392
// Preserve the context in case we get a percentage value.
325393
var context = (position === 'fixed') ? document.viewport :
326394
element.parentNode;
327395

328-
element.setStyle({
329-
position: 'absolute',
396+
var tempStyles = {
330397
visibility: 'hidden',
331398
display: 'block'
332-
});
399+
};
333400

334-
var positionedWidth = element.getStyle('width');
401+
// If the element's `position: fixed`, it's already out of the document
402+
// flow, so it's both unnecessary and inaccurate to set
403+
// `position: absolute`.
404+
if (position !== 'fixed') tempStyles.position = 'absolute';
405+
406+
element.setStyle(tempStyles);
335407

336-
var newWidth;
408+
var positionedWidth = element.offsetWidth, newWidth;
337409
if (width && (positionedWidth === width)) {
338410
// If the element's width is the same both before and after
339411
// we set absolute positioning, that means:
340412
// (a) it was already absolutely-positioned; or
341413
// (b) it has an explicitly-set width, instead of width: auto.
342414
// Either way, it means the element is the width it needs to be
343415
// in order to report an accurate height.
344-
newWidth = getPixelValue(element, 'width', context);
416+
newWidth = getContentWidth(element, context);
345417
} else if (position === 'absolute' || position === 'fixed') {
346418
// Absolute- and fixed-position elements' dimensions don't depend
347419
// upon those of their parents.
348-
newWidth = getPixelValue(element, 'width', context);
420+
newWidth = getContentWidth(element, context);
349421
} else {
350422
// Otherwise, the element's width depends upon the width of its
351423
// parent.
@@ -360,18 +432,20 @@
360432
this.get('margin-right');
361433
}
362434

435+
// Whatever the case, we've now figured out the correct `width` value
436+
// for the element.
363437
element.setStyle({ width: newWidth + 'px' });
364438

365439
// The element is now ready for measuring.
366-
this._prepared = true;
440+
this._setPrepared(true);
367441
},
368442

369443
_end: function() {
370444
var element = this.element;
371445
var originalStyles = element.retrieve('prototype_original_styles');
372-
element.store('prototype_original_styles', null);
446+
element.store('prototype_original_styles', null);
373447
element.setStyle(originalStyles);
374-
this._prepared = false;
448+
this._setPrepared(false);
375449
},
376450

377451
_compute: function(property) {
@@ -383,6 +457,14 @@
383457
return this._set(property, COMPUTATIONS[property].call(this, this.element));
384458
},
385459

460+
_isPrepared: function() {
461+
return this.element.retrieve('prototype_element_layout_prepared', false);
462+
},
463+
464+
_setPrepared: function(bool) {
465+
return this.element.store('prototype_element_layout_prepared', bool);
466+
},
467+
386468
/**
387469
* Element.Layout#toObject([keys...]) -> Object
388470
* - keys (String): A space-separated list of keys to include.
@@ -459,8 +541,6 @@
459541
if (Element.Layout.COMPOSITE_PROPERTIES.include(key)) return;
460542

461543
var value = this.get(key);
462-
// Unless the value is null, add 'px' to the end and add it to the
463-
// returned object.
464544
if (value != null) css[cssNameFor(key)] = value + 'px';
465545
}, this);
466546
return css;
@@ -523,9 +603,8 @@
523603

524604
var pLeft = this.get('padding-left'),
525605
pRight = this.get('padding-right');
526-
606+
527607
if (!this._preComputing) this._end();
528-
529608
return bWidth - bLeft - bRight - pLeft - pRight;
530609
},
531610

@@ -766,10 +845,11 @@
766845
* `Element.getLayout` again only when the values in an existing
767846
* `Element.Layout` object have become outdated.
768847
*
769-
* Remember that instances of `Element.Layout` compute values the first
770-
* time they're asked for and remember those values for later retrieval.
771-
* If you want to compute all an element's measurements at once, pass
772-
*
848+
* If the `preCompute` argument is `true`, all properties will be measured
849+
* when the layout object is instantiated. If you plan to measure several
850+
* properties of an element's dimensions, it's probably worth it to get a
851+
* pre-computed hash.
852+
*
773853
* ##### Examples
774854
*
775855
* var layout = $('troz').getLayout();
@@ -1512,8 +1592,9 @@
15121592
do {
15131593
valueT += element.offsetTop || 0;
15141594
valueL += element.offsetLeft || 0;
1515-
if (element.offsetParent == document.body)
1595+
if (element.offsetParent == document.body) {
15161596
if (Element.getStyle(element, 'position') == 'absolute') break;
1597+
}
15171598

15181599
element = element.offsetParent;
15191600
} while (element);

0 commit comments

Comments
 (0)