From 749badbd4d118066f15660a617540ee468ae014a Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 30 Oct 2009 04:19:17 -0500 Subject: [PATCH 01/12] Revised pass on `Element.Layout` and `Element.Offset` classes. Unit tests are incomplete. --- src/dom.js | 1 + src/dom/layout.js | 575 +++++++++++++++++++++++++++++++++ test/unit/fixtures/layout.html | 105 ++++++ test/unit/layout_test.js | 75 +++++ 4 files changed, 756 insertions(+) create mode 100644 src/dom/layout.js create mode 100644 test/unit/fixtures/layout.html create mode 100644 test/unit/layout_test.js diff --git a/src/dom.js b/src/dom.js index f9ff3ff30..31f7605f4 100644 --- a/src/dom.js +++ b/src/dom.js @@ -21,6 +21,7 @@ //= require "dom/dom" +//= require "dom/layout" //= require "dom/selector" //= require "dom/form" //= require "dom/event" diff --git a/src/dom/layout.js b/src/dom/layout.js new file mode 100644 index 000000000..2547cd434 --- /dev/null +++ b/src/dom/layout.js @@ -0,0 +1,575 @@ + + +(function() { + + // Converts a CSS percentage value to a decimal. + // Ex: toDecimal("30%"); // -> 0.3 + function toDecimal(pctString) { + var match = pctString.match(/^(\d+)%?$/i); + if (!match) return null; + return (Number(match[1]) / 100); + } + + // Can be called like this: + // getPixelValue("11px"); + // Or like this: + // getPixelValue(someElement, 'paddingTop'); + function getPixelValue(value, property) { + if (Object.isElement(value)) { + element = value; + value = element.getStyle(property); + } + if (value === null) { + return null; + } + + // Non-IE browsers will always return pixels. + if ((/^\d+(px)?$/i).test(value)) { + return window.parseInt(value, 10); + } + + // When IE gives us something other than a pixel value, this technique + // (invented by Dean Edwards) will convert it to pixels. + if (element.runtimeStyle) { + var style = element.style.left, rStyle = element.runtimeStyle.left; + element.runtimeStyle.left = element.currentStyle.left; + element.style.left = value || 0; + value = element.style.pixelLeft; + element.style.left = style; + element.runtimeStyle.left = rStyle; + + return value; + } + + // For other browsers, we have to do a bit of work. + if (value.include('%')) { + var decimal = toDecimal(value); + var whole; + if (property.include('left') || property.include('right') || + property.include('width')) { + whole = $(element.parentNode).measure('width'); + } else if (property.include('top') || property.include('bottom') || + property.include('height')) { + whole = $(element.parentNode).measure('height'); + } + + return whole * decimal; + } + + // If we get this far, we should probably give up. + return null; + } + + function toCSSPixels(number) { + if (Object.isString(number) && number.endsWith('px')) { + return number; + } + return number + 'px'; + } + + function isDisplayed(element) { + var originalElement = element; + while (element && element.parentNode) { + var display = element.getStyle('display'); + if (display === 'none') { + return false; + } + element = $(element.parentNode); + } + return true; + } + + /** + * class Element.Layout < Hash + * + * A set of key/value pairs representing measurements of various + * dimensions of an element. + **/ + Element.Layout = Class.create(Hash, { + initialize: function($super, element, preCompute) { + $super(); + this.element = $(element); + // The 'preCompute' boolean tells us whether we should fetch all values + // at once. If so, we should do setup/teardown only once. We set a flag + // so that we can ignore calls to `_begin` and `_end` elsewhere. + if (preCompute) { + this._preComputing = true; + this._begin(); + } + Element.Layout.PROPERTIES.each( function(property) { + if (preCompute) { + this._compute(property); + } else { + this._set(property, null); + } + }, this); + if (preCompute) { + this._end(); + this._preComputing = false; + } + }, + + _set: function(property, value) { + return Hash.prototype.set.call(this, property, value); + }, + + set: function(property, value) { + if (Element.Layout.COMPOSITE_PROPERTIES.include(property)) { + throw "Cannot set a composite property."; + } + + return this._set(property, toCSSPixels(value)); + }, + + get: function($super, property) { + // Try to fetch from the cache. + var value = $super(property); + return value === null ? this._compute(property) : value; + }, + + // `_begin` and `_end` are two functions that are called internally + // before and after any measurement is done. In certain conditions (e.g., + // when hidden), elements need a "preparation" phase that ensures + // accuracy of measurements. + _begin: function() { + if (this._prepared) return; + + var element = this.element; + if (isDisplayed(element)) { + this._prepared = true; + return; + } + + // Remember the original values for some styles we're going to alter. + var originalStyles = { + position: element.style.position || '', + width: element.style.width || '', + visibility: element.style.visibility || '', + display: element.style.display || '' + }; + + // We store them so that the `_end` function can retrieve them later. + element.store('prototype_original_styles', originalStyles); + + var position = element.getStyle('position'), + width = element.getStyle('width'); + + var layout = element.getLayout(); + element.setStyle({ + position: 'absolute', + visibility: 'visible', + display: 'block' + }); + + var positionedWidth = element.getStyle('width'); + + var newWidth; + if (width && (positionedWidth === width)) { + // If the element's width is the same both before and after + // we set absolute positioning, that means: + // (a) it was already absolutely-positioned; or + // (b) it has an explicitly-set width, instead of width: auto. + // Either way, it means the element is the width it needs to be + // in order to report an accurate height. + newWidth = window.parseInt(width, 10); + } else if (width && (position === 'absolute' || position === 'fixed')) { + newWidth = window.parseInt(width, 10); + } else { + // If not, that means the element's width depends upon the width of + // its parent. + var parent = element.up(), pLayout = parent.getLayout(); + + newWidth = pLayout.get('width') - + layout.get('margin-left') - + layout.get('border-left') - + layout.get('padding-left') - + layout.get('padding-right') - + layout.get('border-right') - + layout.get('margin-right'); + } + + element.setStyle({ width: newWidth + 'px' }); + + // The element is now ready for measuring. + this._prepared = true; + }, + + _end: function() { + var element = this.element; + var originalStyles = element.retrieve('prototype_original_styles'); + element.store('prototype_original_styles', null); + element.setStyle(originalStyles); + this._prepared = false; + }, + + _compute: function(property) { + var COMPUTATIONS = Element.Layout.COMPUTATIONS; + if (!(property in COMPUTATIONS)) { + throw "Property not found."; + } + + var value = COMPUTATIONS[property].call(this, this.element); + this._set(property, value); + return value; + } + }); + + Object.extend(Element.Layout, { + // All measurable properties. + PROPERTIES: $w('height width top left right bottom border-left border-right border-top border-bottom padding-left padding-right padding-top padding-bottom margin-top margin-bottom margin-left margin-right padding-box-width padding-box-height border-box-width border-box-height margin-box-width margin-box-height'), + + // Sums of other properties. Can be read but not written. + COMPOSITE_PROPERTIES: $w('padding-box-width padding-box-height margin-box-width margin-box-height border-box-width border-box-height'), + + COMPUTATIONS: { + 'height': function(element) { + if (!this._preComputing) this._begin(); + + var bHeight = this.get('border-box-height'); + if (bHeight <= 0) return 0; + + var bTop = this.get('border-top'), + bBottom = this.get('border-bottom'); + + var pTop = this.get('padding-top'), + pBottom = this.get('padding-bottom'); + + if (!this._preComputing) this._end(); + + return bHeight - bTop - bBottom - pTop - pBottom; + }, + + 'width': function(element) { + if (!this._preComputing) this._begin(); + + var bWidth = this.get('border-box-width'); + if (bWidth <= 0) return 0; + + var bLeft = this.get('border-left'), + bRight = this.get('border-right'); + + var pLeft = this.get('padding-left'), + pRight = this.get('padding-right'); + + if (!this._preComputing) this._end(); + + return bWidth - bLeft - bRight - pLeft - pRight; + }, + + 'padding-box-height': function(element) { + var height = this.get('height'), + pTop = this.get('padding-top'), + pBottom = this.get('padding-bottom'); + + return height + pTop + pBottom; + }, + + 'padding-box-width': function(element) { + var width = this.get('width'), + pLeft = this.get('padding-left'), + pRight = this.get('padding-right'); + + return width + pLeft + pRight; + }, + + 'border-box-height': function(element) { + return element.offsetHeight; + }, + + 'border-box-width': function(element) { + return element.offsetWidth; + }, + + 'margin-box-height': function(element) { + var bHeight = this.get('border-box-height'), + mTop = this.get('margin-top'), + mBottom = this.get('margin-bottom'); + + if (bHeight <= 0) return 0; + + return bHeight + mTop + mBottom; + }, + + 'margin-box-width': function(element) { + var bWidth = this.get('border-box-width'), + mLeft = this.get('margin-left'), + mRight = this.get('margin-right'); + + if (bWidth <= 0) return 0; + + return bWidth + mLeft + mRight; + }, + + 'top': function(element) { + return getPixelValue(element, 'top'); + }, + + 'bottom': function(element) { + return getPixelValue(element, 'bottom'); + }, + + 'left': function(element) { + return getPixelValue(element, 'left'); + }, + + 'right': function(element) { + return getPixelValue(element, 'right'); + }, + + 'padding-top': function(element) { + return getPixelValue(element, 'paddingTop'); + }, + + 'padding-bottom': function(element) { + return getPixelValue(element, 'paddingBottom'); + }, + + 'padding-left': function(element) { + return getPixelValue(element, 'paddingLeft'); + }, + + 'padding-right': function(element) { + return getPixelValue(element, 'paddingRight'); + }, + + 'border-top': function(element) { + return element.clientTop || + getPixelValue(element, 'borderTopWidth'); + }, + + 'border-bottom': function(element) { + return element.clientBottom || + getPixelValue(element, 'borderBottomWidth'); + }, + + 'border-left': function(element) { + return element.clientLeft || + getPixelValue(element, 'borderLeftWidth'); + }, + + 'border-right': function(element) { + return element.clientRight || + getPixelValue(element, 'borderRightWidth'); + }, + + 'margin-top': function(element) { + return getPixelValue(element, 'marginTop'); + }, + + 'margin-bottom': function(element) { + return getPixelValue(element, 'marginBottom'); + }, + + 'margin-left': function(element) { + return getPixelValue(element, 'marginLeft'); + }, + + 'margin-right': function(element) { + return getPixelValue(element, 'marginRight'); + } + } + }); + + /** + * class Element.Offset + * + * A representation of the top- and left-offsets of an element relative to + * another. + **/ + Element.Offset = Class.create({ + /** + * new Element.Offset(left, top) + * + * Instantiates an [[Element.Offset]]. You shouldn't need to call this + * directly. + **/ + initialize: function(left, top) { + this.left = left.round(); + this.top = top.round(); + + // Act like an array. + this[0] = this.left; + this[1] = this.top; + }, + + /** + * Element.Offset#relativeTo(offset) -> Element.Offset + * - offset (Element.Offset): Another offset to compare to. + * + * Returns a new [[Element.Offset]] with its origin at the given + * `offset`. Useful for determining an element's distance from another + * arbitrary element. + **/ + relativeTo: function(offset) { + return new Element.Offset( + this.left - offset.left, + this.top - offset.top + ); + }, + + /** + * Element.Offset#inspect() -> String + **/ + inspect: function() { + return "# Array + **/ + toArray: function() { + return [this.left, this.top]; + } + }); + + /** + * Element.getLayout(@element) -> Element.Layout + * + * Returns an instance of [[Element.Layout]] for measuring an element's + * dimensions. + * + * Note that this method returns a _new_ `Element.Layout` object each time + * it's called. If you want to take advantage of measurement caching, + * retain a reference to one `Element.Layout` object, rather than calling + * `Element.getLayout` whenever you need a measurement. You should call + * `Element.getLayout` again only when the values in an existing + * `Element.Layout` object have become outdated. + **/ + function getLayout(element) { + return new Element.Layout(element); + } + + /** + * Element.measure(@element, property) -> Number + * + * Gives the pixel value of `element`'s dimension specified by + * `property`. + * + * Useful for one-off measurements of elements. If you find yourself + * calling this method frequently over short spans of code, you might want + * to call [[Element.getLayout]] and operate on the [[Element.Layout]] + * object itself (thereby taking advantage of measurement caching). + **/ + function measure(element, property) { + return $(element).getLayout().get(property); + } + + /** + * Element.cumulativeOffset(@element) -> Element.Offset + * + * Returns the offsets of `element` from the top left corner of the + * document. + **/ + function cumulativeOffset(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return new Element.Offset(valueL, valueT); + } + + /** + * Element.positionedOffset(@element) -> Element.Offset + * + * Returns `element`'s offset relative to its closest positioned ancestor + * (the element that would be returned by [[Element.getOffsetParent]]). + **/ + function positionedOffset(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + if (element.tagName.toUpperCase() == 'BODY') break; + var p = Element.getStyle(element, 'position'); + if (p !== 'static') break; + } + } while (element); + return new Element.Offset(valueL, valueT); + } + + /** + * Element.cumulativeScrollOffset(@element) -> Element.Offset + * + * Calculates the cumulative scroll offset of an element in nested + * scrolling containers. + **/ + function cumulativeScrollOffset(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return new Element.Offset(valueL, valueT); + } + + /** + * Element.viewportOffset(@element) -> Array + * + * Returns the X/Y coordinates of element relative to the viewport. + **/ + function viewportOffset(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + // Safari fix + if (element.offsetParent == document.body && + Element.getStyle(element, 'position') == 'absolute') break; + } while (element = element.offsetParent); + + element = forElement; + var tagName = element.tagName, O = Prototype.Browser.Opera; + do { + if (!O || tagName && tagName.toUpperCase() === 'BODY') { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } + } while (element = element.parentNode); + return new Element.Offset(valueL, valueT); + } + + Element.addMethods({ + getLayout: getLayout, + measure: measure, + cumulativeOffset: cumulativeOffset, + positionedOffset: positionedOffset, + cumulativeScrollOffset: cumulativeScrollOffset, + viewportOffset: viewportOffset + }); + + // If the browser supports the nonstandard `getBoundingClientRect` + // (currently only IE and Firefox), it becomes far easier to obtain + // true offsets. + if ('getBoundingClientRect' in document.documentElement) { + Element.addMethods({ + viewportOffset: function(element) { + element = $(element); + var rect = element.getBoundingClientRect(); + return new Element.Offset(rect.left, rect.top); + }, + + cumulativeOffset: function(element) { + element = $(element); + var docOffset = $(document.documentElement).viewportOffset(), + elementOffset = element.viewportOffset(); + return elementOffset.relativeTo(docOffset); + }, + + positionedOffset: function(element) { + element = $(element); + var parent = element.getOffsetParent(); + var isBody = (parent.nodeName.toUpperCase() === 'BODY'); + var eOffset = element.viewportOffset(), + pOffset = isBody ? viewportOffset(parent) : parent.viewportOffset(); + return eOffset.relativeTo(pOffset); + } + }); + } +})(); \ No newline at end of file diff --git a/test/unit/fixtures/layout.html b/test/unit/fixtures/layout.html new file mode 100644 index 000000000..918e20c6c --- /dev/null +++ b/test/unit/fixtures/layout.html @@ -0,0 +1,105 @@ +
+

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+
+ + + + +
+

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

+
+ + + +
+
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +
+
+ + + +
+
+
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +
+
+
+ + + +
+
+
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +
+
+
+ + \ No newline at end of file diff --git a/test/unit/layout_test.js b/test/unit/layout_test.js new file mode 100644 index 000000000..3cbb0518c --- /dev/null +++ b/test/unit/layout_test.js @@ -0,0 +1,75 @@ +function isDisplayed(element) { + var originalElement = element; + + while (element && element.parentNode) { + var display = element.getStyle('display'); + if (display === 'none') { + return false; + } + element = $(element.parentNode); + } + return true; +} + +new Test.Unit.Runner({ + setup: function() { + }, + + 'test layout on absolutely-positioned elements': function() { + var layout = $('box1').getLayout(); + + this.assertEqual(242, layout.get('width'), 'width' ); + this.assertEqual(555, layout.get('height'), 'height'); + + this.assertEqual(3, layout.get('border-left'), 'border-left'); + this.assertEqual(10, layout.get('padding-top'), 'padding-top'); + this.assertEqual(1020, layout.get('top'), 'top'); + + this.assertEqual(25, layout.get('left'), 'left'); + }, + + 'test layout on elements with display: none and exact width': function() { + var layout = $('box2').getLayout(); + + this.assertEqual(500, layout.get('width'), 'width'); + this.assertEqual(3, layout.get('border-right'), 'border-right'); + this.assertEqual(10, layout.get('padding-bottom'), 'padding-bottom'); + }, + + 'test layout on elements with display: none and width: auto': function() { + var layout = $('box3').getLayout(); + + // Ensure that we cleaned up after ourselves. + this.assert(!isDisplayed($('box3')), 'box should still be hidden'); + + + this.assertEqual(364, layout.get('width'), 'width'); + this.assertEqual(400, layout.get('margin-box-width'), 'margin-box-width'); + this.assertEqual(3, layout.get('border-right'), 'border-top'); + this.assertEqual(10, layout.get('padding-bottom'), 'padding-right'); + }, + + 'test layout on elements with display: none ancestors': function() { + var layout = $('box4').getLayout(); + + // Ensure that we cleaned up after ourselves. + this.assert(!isDisplayed($('box4')), 'box should still be hidden'); + + // Width and height values are nonsensical for deeply-hidden elements. + this.assertEqual(0, layout.get('width'), 'width of a deeply-hidden element should be 0'); + this.assertEqual(0, layout.get('margin-box-height'), 'height of a deeply-hidden element should be 0'); + + // But we can still get meaningful values for other measurements. + this.assertEqual(0, layout.get('border-right'), 'border-top'); + this.assertEqual(13, layout.get('padding-bottom'), 'padding-right'); + }, + + 'test positioning on absolutely-positioned elements': function() { + var layout = $('box5').getLayout(); + + this.assertEqual(30, layout.get('top'), 'top'); + this.assertEqual(60, layout.get('right'), 'right (percentage value)'); + + this.assertNull(layout.get('left'), 'left (should be null because none was set)'); + } +}); \ No newline at end of file From 7aa195b650d6c415a6c90753b19f409e3ec15c3a Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 2 Nov 2009 20:03:17 -0600 Subject: [PATCH 02/12] Update layout code. Roughly compatible with IE 6-8... so far. --- src/dom/layout.js | 32 ++++++++++++++++---------------- test/unit/layout_test.js | 13 +++++++++---- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/dom/layout.js b/src/dom/layout.js index 2547cd434..204fcbca8 100644 --- a/src/dom/layout.js +++ b/src/dom/layout.js @@ -30,7 +30,7 @@ // When IE gives us something other than a pixel value, this technique // (invented by Dean Edwards) will convert it to pixels. - if (element.runtimeStyle) { + if (/\d/.test(value) && element.runtimeStyle) { var style = element.style.left, rStyle = element.runtimeStyle.left; element.runtimeStyle.left = element.currentStyle.left; element.style.left = value || 0; @@ -38,7 +38,7 @@ element.style.left = style; element.runtimeStyle.left = rStyle; - return value; + return value; } // For other browsers, we have to do a bit of work. @@ -57,7 +57,7 @@ } // If we get this far, we should probably give up. - return null; + return 0; } function toCSSPixels(number) { @@ -154,10 +154,9 @@ var position = element.getStyle('position'), width = element.getStyle('width'); - var layout = element.getLayout(); element.setStyle({ position: 'absolute', - visibility: 'visible', + visibility: 'hidden', display: 'block' }); @@ -177,15 +176,16 @@ } else { // If not, that means the element's width depends upon the width of // its parent. - var parent = element.up(), pLayout = parent.getLayout(); + var parent = element.parentNode, pLayout = $(parent).getLayout(); + newWidth = pLayout.get('width') - - layout.get('margin-left') - - layout.get('border-left') - - layout.get('padding-left') - - layout.get('padding-right') - - layout.get('border-right') - - layout.get('margin-right'); + this.get('margin-left') - + this.get('border-left') - + this.get('padding-left') - + this.get('padding-right') - + this.get('border-right') - + this.get('margin-right'); } element.setStyle({ width: newWidth + 'px' }); @@ -333,22 +333,22 @@ }, 'border-top': function(element) { - return element.clientTop || + return Object.isNumber(element.clientTop) ? element.clientTop : getPixelValue(element, 'borderTopWidth'); }, 'border-bottom': function(element) { - return element.clientBottom || + return Object.isNumber(element.clientBottom) ? element.clientBottom : getPixelValue(element, 'borderBottomWidth'); }, 'border-left': function(element) { - return element.clientLeft || + return Object.isNumber(element.clientLeft) ? element.clientLeft : getPixelValue(element, 'borderLeftWidth'); }, 'border-right': function(element) { - return element.clientRight || + return Object.isNumber(element.clientRight) ? element.clientRight : getPixelValue(element, 'borderRightWidth'); }, diff --git a/test/unit/layout_test.js b/test/unit/layout_test.js index 3cbb0518c..5b057925a 100644 --- a/test/unit/layout_test.js +++ b/test/unit/layout_test.js @@ -31,29 +31,34 @@ new Test.Unit.Runner({ 'test layout on elements with display: none and exact width': function() { var layout = $('box2').getLayout(); + this.assert(!isDisplayed($('box3')), 'box should be hidden'); + this.assertEqual(500, layout.get('width'), 'width'); this.assertEqual(3, layout.get('border-right'), 'border-right'); this.assertEqual(10, layout.get('padding-bottom'), 'padding-bottom'); + + this.assert(!isDisplayed($('box3')), 'box should still be hidden'); }, 'test layout on elements with display: none and width: auto': function() { var layout = $('box3').getLayout(); - // Ensure that we cleaned up after ourselves. - this.assert(!isDisplayed($('box3')), 'box should still be hidden'); + this.assert(!isDisplayed($('box3')), 'box should be hidden'); - this.assertEqual(364, layout.get('width'), 'width'); this.assertEqual(400, layout.get('margin-box-width'), 'margin-box-width'); this.assertEqual(3, layout.get('border-right'), 'border-top'); this.assertEqual(10, layout.get('padding-bottom'), 'padding-right'); + + // Ensure that we cleaned up after ourselves. + this.assert(!isDisplayed($('box3')), 'box should still be hidden'); }, 'test layout on elements with display: none ancestors': function() { var layout = $('box4').getLayout(); // Ensure that we cleaned up after ourselves. - this.assert(!isDisplayed($('box4')), 'box should still be hidden'); + this.assert(!isDisplayed($('box4')), 'box should be hidden'); // Width and height values are nonsensical for deeply-hidden elements. this.assertEqual(0, layout.get('width'), 'width of a deeply-hidden element should be 0'); From 97ea37d3d55a1d45793bda208ef918e4f06199fc Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 2 Nov 2009 23:52:07 -0600 Subject: [PATCH 03/12] A bunch of fixes for offsets. --- src/dom/layout.js | 57 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/src/dom/layout.js b/src/dom/layout.js index 204fcbca8..854f56622 100644 --- a/src/dom/layout.js +++ b/src/dom/layout.js @@ -79,6 +79,18 @@ return true; } + var hasLayout = Prototype.K; + + if ('currentStyle' in document.documentElement) { + hasLayout = function(element) { + if (!element.currentStyle.hasLayout) { + element.style.zoom = 1; + } + return element; + }; + } + + /** * class Element.Layout < Hash * @@ -411,7 +423,14 @@ * Element.Offset#inspect() -> String **/ inspect: function() { - return "#".interpolate(this); + }, + + /** + * Element.Offset#toString() -> String + **/ + toString: function() { + return "[#{left}, #{top}]".interpolate(this); }, /** @@ -477,17 +496,24 @@ * (the element that would be returned by [[Element.getOffsetParent]]). **/ function positionedOffset(element) { + // Account for the margin of the element. + var layout = element.getLayout(); + var valueT = 0, valueL = 0; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; element = element.offsetParent; if (element) { - if (element.tagName.toUpperCase() == 'BODY') break; + if (isBody(element)) break; var p = Element.getStyle(element, 'position'); if (p !== 'static') break; } } while (element); + + valueL -= layout.get('margin-top'); + valueT -= layout.get('margin-left'); + return new Element.Offset(valueL, valueT); } @@ -544,6 +570,10 @@ viewportOffset: viewportOffset }); + function isBody(element) { + return $w('BODY HTML').include(element.nodeName.toUpperCase()); + } + // If the browser supports the nonstandard `getBoundingClientRect` // (currently only IE and Firefox), it becomes far easier to obtain // true offsets. @@ -565,11 +595,26 @@ positionedOffset: function(element) { element = $(element); var parent = element.getOffsetParent(); - var isBody = (parent.nodeName.toUpperCase() === 'BODY'); + + // When the BODY is the offsetParent, IE6 mistakenly reports the + // parent as HTML. Use that as the litmus test to fix another + // annoying IE6 quirk. + if (parent.nodeName.toUpperCase() === 'HTML') { + return positionedOffset(element); + } + var eOffset = element.viewportOffset(), - pOffset = isBody ? viewportOffset(parent) : parent.viewportOffset(); - return eOffset.relativeTo(pOffset); + pOffset = isBody(parent) ? viewportOffset(parent) : + parent.viewportOffset(); + var retOffset = eOffset.relativeTo(pOffset); + + // Account for the margin of the element. + var layout = element.getLayout(); + var top = retOffset.top - layout.get('margin-top'); + var left = retOffset.left - layout.get('margin-left'); + + return new Element.Offset(left, top); } - }); + }); } })(); \ No newline at end of file From ac451d6d8fe9dfaa20066984e7c39c1dbcaa4c35 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 2 Nov 2009 23:52:19 -0600 Subject: [PATCH 04/12] Tweaks to unit tests. --- test/unit/layout_test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/unit/layout_test.js b/test/unit/layout_test.js index 5b057925a..eec7187b3 100644 --- a/test/unit/layout_test.js +++ b/test/unit/layout_test.js @@ -57,7 +57,6 @@ new Test.Unit.Runner({ 'test layout on elements with display: none ancestors': function() { var layout = $('box4').getLayout(); - // Ensure that we cleaned up after ourselves. this.assert(!isDisplayed($('box4')), 'box should be hidden'); // Width and height values are nonsensical for deeply-hidden elements. @@ -67,6 +66,9 @@ new Test.Unit.Runner({ // But we can still get meaningful values for other measurements. this.assertEqual(0, layout.get('border-right'), 'border-top'); this.assertEqual(13, layout.get('padding-bottom'), 'padding-right'); + + // Ensure that we cleaned up after ourselves. + this.assert(!isDisplayed($('box3')), 'box should still be hidden'); }, 'test positioning on absolutely-positioned elements': function() { @@ -75,6 +77,6 @@ new Test.Unit.Runner({ this.assertEqual(30, layout.get('top'), 'top'); this.assertEqual(60, layout.get('right'), 'right (percentage value)'); - this.assertNull(layout.get('left'), 'left (should be null because none was set)'); + this.assertEqual(340, layout.get('left'), 'left'); } }); \ No newline at end of file From 093c0cce4bbb89b88b0af20356faa83906b3aafa Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 2 Nov 2009 23:54:25 -0600 Subject: [PATCH 05/12] Optimize retrieving of top|left|right|bottom properties. Add some documentation. Disable setting of properties for now. --- src/dom/layout.js | 188 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 178 insertions(+), 10 deletions(-) diff --git a/src/dom/layout.js b/src/dom/layout.js index 854f56622..3152ed6b9 100644 --- a/src/dom/layout.js +++ b/src/dom/layout.js @@ -96,8 +96,115 @@ * * A set of key/value pairs representing measurements of various * dimensions of an element. + * + *

Overview

+ * + * The `Element.Layout` class is a specialized way to measure elements. + * It helps mitigate: + * + * * The convoluted steps often needed to get common measurements for + * elements. + * * The tendency of browsers to report measurements in non-pixel units. + * * The quirks that lead some browsers to report inaccurate measurements. + * * The difficulty of measuring elements that are hidden. + * + *

Usage

+ * + * Instantiate an `Element.Layout` class by passing an element into the + * constructor: + * + * var layout = new Element.Layout(someElement); + * + * You can also use [[Element.getLayout]], if you prefer. + * + * Once you have a layout object, retrieve properties using [[Hash]]'s + * familiar `get` and `set` syntax. + * + * layout.get('width'); //-> 400 + * layout.get('top'); //-> 180 + * + * The following are the CSS-related properties that can be retrieved. + * Nearly all of them map directly to their property names in CSS. (The + * only exception is for borders — e.g., `border-width` instead of + * `border-left-width`.) + * + * * `height` + * * `width` + * * `top` + * * `left` + * * `right` + * * `bottom` + * * `border-left` + * * `border-right` + * * `border-top` + * * `border-bottom` + * * `padding-left` + * * `padding-right` + * * `padding-top` + * * `padding-bottom` + * * `margin-top` + * * `margin-bottom` + * * `margin-left` + * * `margin-right` + * + * In addition, these "composite" properties can be retrieved: + * + * * `padding-box-width` (width of the content area, from the beginning of + * the left padding to the end of the right padding) + * * `padding-box-height` (height of the content area, from the beginning + * of the top padding to the end of the bottom padding) + * * `border-box-width` (width of the content area, from the outer edge of + * the left border to the outer edge of the right border) + * * `border-box-height` (height of the content area, from the outer edge + * of the top border to the outer edge of the bottom border) + * * `margin-box-width` (width of the content area, from the beginning of + * the left margin to the end of the right margin) + * * `margin-box-height` (height of the content area, from the beginning + * of the top margin to the end of the bottom margin) + * + *

Caching

+ * + * Because these properties can be costly to retrieve, `Element.Layout` + * behaves differently from an ordinary [[Hash]]. + * + * First: by default, values are "lazy-loaded" — they aren't computed + * until they're retrieved. To measure all properties at once, pass + * a second argument into the constructor: + * + * var layout = new Element.Layout(someElement, true); + * + * Second: once a particular value is computed, it's cached. Asking for + * the same property again will return the original value without + * re-computation. This means that **an instance of `Element.Layout` + * becomes stale when the element's dimensions change**. When this + * happens, obtain a new instance. + * + *

Hidden elements

+ * + * Because it's a common case to want the dimensions of a hidden element + * (e.g., for animations), it's possible to measure elements that are + * hidden with `display: none`. + * + * However, **it's only possible to measure a hidden element if its parent + * is visible**. If its parent (or any other ancestor) is hidden, any + * width and height measurements will return `0`, as will measurements for + * `top|bottom|left|right`. + * **/ Element.Layout = Class.create(Hash, { + /** + * new Element.Layout(element[, preCompute]) + * - element (Element): The element to be measured. + * - preCompute (Boolean): Whether to compute all values at once. + * + * Declare a new layout hash. + * + * The `preCompute` argument determines whether measurements will be + * lazy-loaded or not. If you plan to use many different measurements, + * it's often more performant to pre-compute, as it minimizes the + * amount of overhead needed to measure. If you need only one or two + * measurements, it's probably not worth it. + **/ initialize: function($super, element, preCompute) { $super(); this.element = $(element); @@ -125,14 +232,25 @@ return Hash.prototype.set.call(this, property, value); }, + + // TODO: Investigate. set: function(property, value) { - if (Element.Layout.COMPOSITE_PROPERTIES.include(property)) { - throw "Cannot set a composite property."; - } - - return this._set(property, toCSSPixels(value)); + throw "Properties of Element.Layout are read-only."; + // if (Element.Layout.COMPOSITE_PROPERTIES.include(property)) { + // throw "Cannot set a composite property."; + // } + // + // return this._set(property, toCSSPixels(value)); }, + /** + * Element.Layout#get(property) -> Number + * - property (String): One of the properties defined in + * [[Element.Layout.PROPERTIES]]. + * + * Retrieve the measurement specified by `property`. Will throw an error + * if the property is invalid. + **/ get: function($super, property) { // Try to fetch from the cache. var value = $super(property); @@ -228,9 +346,20 @@ Object.extend(Element.Layout, { // All measurable properties. + /** + * Element.Layout.PROPERTIES = Array + * + * A list of all measurable properties. + **/ PROPERTIES: $w('height width top left right bottom border-left border-right border-top border-bottom padding-left padding-right padding-top padding-bottom margin-top margin-bottom margin-left margin-right padding-box-width padding-box-height border-box-width border-box-height margin-box-width margin-box-height'), - // Sums of other properties. Can be read but not written. + /** + * Element.Layout.COMPOSITE_PROPERTIES = Array + * + * A list of all composite properties. Composite properties don't map + * directly to CSS properties — they're combinations of other + * properties. + **/ COMPOSITE_PROPERTIES: $w('padding-box-width padding-box-height margin-box-width margin-box-height border-box-width border-box-height'), COMPUTATIONS: { @@ -313,19 +442,37 @@ }, 'top': function(element) { - return getPixelValue(element, 'top'); + var offset = element.positionedOffset(); + return offset.top; }, 'bottom': function(element) { - return getPixelValue(element, 'bottom'); + var offset = element.positionedOffset(), + parent = element.getOffsetParent(), + pHeight = parent.measure('height'); + + var mHeight = this.get('border-box-height'); + + return pHeight - mHeight - offset.top; + // + // return getPixelValue(element, 'bottom'); }, 'left': function(element) { - return getPixelValue(element, 'left'); + var offset = element.positionedOffset(); + return offset.left; }, 'right': function(element) { - return getPixelValue(element, 'right'); + var offset = element.positionedOffset(), + parent = element.getOffsetParent(), + pWidth = parent.measure('width'); + + var mWidth = this.get('border-box-width'); + + return pWidth - mWidth - offset.left; + // + // return getPixelValue(element, 'right'); }, 'padding-top': function(element) { @@ -382,6 +529,27 @@ } }); + // An easier way to compute right and bottom offsets. + if ('getBoundingClientRect' in document.documentElement) { + Object.extend(Element.Layout.COMPUTATIONS, { + 'right': function(element) { + var parent = hasLayout(element.getOffsetParent()); + var rect = element.getBoundingClientRect(), + pRect = parent.getBoundingClientRect(); + + return (pRect.right - rect.right).round(); + }, + + 'bottom': function(element) { + var parent = hasLayout(element.getOffsetParent()); + var rect = element.getBoundingClientRect(), + pRect = parent.getBoundingClientRect(); + + return (pRect.bottom - rect.bottom).round(); + } + }); + } + /** * class Element.Offset * From 62d0430f3b98153af49b01f7972e75cf2264ce13 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 6 Nov 2009 10:56:01 -0600 Subject: [PATCH 06/12] Fix Element.viewportOffset on nested elements in Opera < 9.5 and remove browser sniff. (cherry picked from commit 0d2d18fb7297fb945ca6983b843c5bcf367c721c) --- src/dom/layout.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/dom/layout.js b/src/dom/layout.js index 3152ed6b9..d0d4dca13 100644 --- a/src/dom/layout.js +++ b/src/dom/layout.js @@ -707,21 +707,23 @@ * Returns the X/Y coordinates of element relative to the viewport. **/ function viewportOffset(forElement) { - var valueT = 0, valueL = 0; + var valueT = 0, valueL = 0, docBody = document.body; var element = forElement; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; // Safari fix - if (element.offsetParent == document.body && + if (element.offsetParent == docBody && Element.getStyle(element, 'position') == 'absolute') break; } while (element = element.offsetParent); - element = forElement; - var tagName = element.tagName, O = Prototype.Browser.Opera; + element = forElement; do { - if (!O || tagName && tagName.toUpperCase() === 'BODY') { + // Opera < 9.5 sets scrollTop/Left on both HTML and BODY elements. + // Other browsers set it only on the HTML element. The BODY element + // can be skipped since its scrollTop/Left should always be 0. + if (element != docBody) { valueT -= element.scrollTop || 0; valueL -= element.scrollLeft || 0; } From c89d6eac95f75f778ffea3411d1d27e99ede4976 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 6 Nov 2009 10:58:45 -0600 Subject: [PATCH 07/12] Fix incorrect offset in Element.viewportOffset on IE < 8. (cherry picked from commit 3afb0002cbd31726187338c5119657b76111f80c) --- src/dom/layout.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/dom/layout.js b/src/dom/layout.js index d0d4dca13..444da63d0 100644 --- a/src/dom/layout.js +++ b/src/dom/layout.js @@ -751,8 +751,13 @@ Element.addMethods({ viewportOffset: function(element) { element = $(element); - var rect = element.getBoundingClientRect(); - return new Element.Offset(rect.left, rect.top); + var rect = element.getBoundingClientRect(), + docEl = document.documentElement; + // The HTML element on IE < 8 has a 2px border by default, giving + // an incorrect offset. We correct this by subtracting clientTop + // and clientLeft. + return new Element.Offset(rect.top - docEl.clientTop, + rect.left - docEl.clientLeft); }, cumulativeOffset: function(element) { From 720ee56ae513cd3404a0d7c3eacd130259f426b1 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 6 Nov 2009 14:47:38 -0600 Subject: [PATCH 08/12] Fix bug introduced in rewrite of Nick's patch. I'm an idiot. --- src/dom/layout.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dom/layout.js b/src/dom/layout.js index 444da63d0..7f235f455 100644 --- a/src/dom/layout.js +++ b/src/dom/layout.js @@ -756,8 +756,8 @@ // The HTML element on IE < 8 has a 2px border by default, giving // an incorrect offset. We correct this by subtracting clientTop // and clientLeft. - return new Element.Offset(rect.top - docEl.clientTop, - rect.left - docEl.clientLeft); + return new Element.Offset(rect.left - docEl.clientLeft, + rect.top - docEl.clientTop); }, cumulativeOffset: function(element) { From 6ebfdd51d52c1227d471676586880079511e142d Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 4 Dec 2009 18:23:29 -0600 Subject: [PATCH 09/12] Move a few methods from dom.js to layout.js. --- src/dom/dom.js | 29 -------------- src/dom/layout.js | 100 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 98 insertions(+), 31 deletions(-) diff --git a/src/dom/dom.js b/src/dom/dom.js index 0f0e247c3..4ecb83f6e 100644 --- a/src/dom/dom.js +++ b/src/dom/dom.js @@ -1041,35 +1041,6 @@ Element.Methods = { return element; }, - /** - * Element.getDimensions(@element) -> Object - * - * Finds the computed width and height of `element` and returns them as - * key/value pairs of an object. - **/ - getDimensions: function(element) { - element = $(element); - var display = Element.getStyle(element, 'display'); - if (display != 'none' && display != null) // Safari bug - return {width: element.offsetWidth, height: element.offsetHeight}; - - // All *Width and *Height properties give 0 on elements with display none, - // so enable the element temporarily - var els = element.style; - var originalVisibility = els.visibility; - var originalPosition = els.position; - var originalDisplay = els.display; - els.visibility = 'hidden'; - if (originalPosition != 'fixed') // Switching fixed to absolute causes issues in Safari - els.position = 'absolute'; - els.display = 'block'; - var originalWidth = element.clientWidth; - var originalHeight = element.clientHeight; - els.display = originalDisplay; - els.position = originalPosition; - els.visibility = originalVisibility; - return {width: originalWidth, height: originalHeight}; - }, /** * Element.makePositioned(@element) -> Element diff --git a/src/dom/layout.js b/src/dom/layout.js index 7f235f455..aae2c2941 100644 --- a/src/dom/layout.js +++ b/src/dom/layout.js @@ -639,8 +639,42 @@ **/ function measure(element, property) { return $(element).getLayout().get(property); + } + + /** + * Element.getDimensions(@element) -> Object + * + * Finds the computed width and height of `element` and returns them as + * key/value pairs of an object. + **/ + function getDimensions(element) { + var layout = $(element).getLayout(); + return { + width: layout.measure('width'), + height: layout.measure('height') + }; } + /** + * Element.getOffsetParent(@element) -> Element + * + * Returns `element`'s closest _positioned_ ancestor. If none is found, the + * `body` element is returned. + **/ + function getOffsetParent(element) { + if (element.offsetParent) return $(element.offsetParent); + if (element === document.body) return $(element); + + while ((element = element.parentNode) && element !== document.body) { + if (Element.getStyle(element, 'position') !== 'static') { + return (element.nodeName === 'HTML') ? $(document.body) : $(element); + } + } + + return $(document.body); + } + + /** * Element.cumulativeOffset(@element) -> Element.Offset * @@ -731,17 +765,79 @@ return new Element.Offset(valueL, valueT); } + /** + * Element.absolutize(@element) -> Element + * + * Turns `element` into an absolutely-positioned element _without_ + * changing its position in the page layout. + **/ + function absolutize(element) { + element = $(element); + + if (Element.getStyle(element, 'position') === 'absolute') { + return element; + } + + var offsetParent = getOffsetParent(element); + var eOffset = element.viewportOffset(), pOffset = + offsetParent.viewportOffset(); + + var offset = eOffset.relativeTo(pOffset); + var layout = element.get('layout'); + + element.store('prototype_absolutize_original_styles', { + left: element.getStyle('left'), + top: element.getStyle('top'), + width: element.getStyle('width'), + height: element.getStyle('height') + }); + + element.setStyle({ + position: 'absolute', + top: offset.top + 'px', + left: offset.left + 'px', + width: layout.get('width') + 'px', + height: layout.get('height') + 'px' + }); + } + + /** + * Element.relativize(@element) -> Element + * + * Turns `element` into a relatively-positioned element without changing + * its position in the page layout. + * + * Used to undo a call to [[Element.absolutize]]. + **/ + function relativize(element) { + element = $(element); + if (Element.getStyle(element, 'position') === 'relative') { + return element; + } + + // Restore the original styles as captured by Element#absolutize. + var originalStyles = + element.retrieve('prototype_absolutize_original_styles'); + + if (originalStyles) element.setStyle(originalStyles); + return element; + } + Element.addMethods({ getLayout: getLayout, measure: measure, + getDimensions: getDimensions, + getOffsetParent: getOffsetParent, cumulativeOffset: cumulativeOffset, positionedOffset: positionedOffset, cumulativeScrollOffset: cumulativeScrollOffset, - viewportOffset: viewportOffset + viewportOffset: viewportOffset, + absolutize: absolutize, + relativize: relativize }); function isBody(element) { - return $w('BODY HTML').include(element.nodeName.toUpperCase()); + return element.nodeName.toUpperCase() === 'BODY'; } // If the browser supports the nonstandard `getBoundingClientRect` From b56e7e754b2b25a4a24ecb6bdb32ec943aceb29a Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 4 Dec 2009 18:23:47 -0600 Subject: [PATCH 10/12] Rewrite IE logic. --- src/dom/layout.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dom/layout.js b/src/dom/layout.js index aae2c2941..4234e0bd8 100644 --- a/src/dom/layout.js +++ b/src/dom/layout.js @@ -870,7 +870,8 @@ // When the BODY is the offsetParent, IE6 mistakenly reports the // parent as HTML. Use that as the litmus test to fix another // annoying IE6 quirk. - if (parent.nodeName.toUpperCase() === 'HTML') { + if (element.offsetParent && + element.offsetParent.nodeName.toUpperCase() === 'HTML') { return positionedOffset(element); } From 2efeb8e20dc8cbd45aa4daa4887aa0d42ac226d7 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 4 Dec 2009 18:24:19 -0600 Subject: [PATCH 11/12] Add Element.Layout#toCSS for exporting an object full of CSS key/value pairs. --- src/dom/layout.js | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/dom/layout.js b/src/dom/layout.js index 4234e0bd8..927fad5e2 100644 --- a/src/dom/layout.js +++ b/src/dom/layout.js @@ -89,6 +89,13 @@ return element; }; } + + // Converts the layout hash property names back to the CSS equivalents. + // For now, only the border properties differ. + function cssNameFor(key) { + if (key.includes('border')) return key + '-width'; + return key; + } /** @@ -341,7 +348,39 @@ var value = COMPUTATIONS[property].call(this, this.element); this._set(property, value); return value; - } + }, + + /** + * Element.Layout#toCSS([keys...]) -> Object + * + * Converts the layout hash to a plain object of CSS property/value + * pairs, optionally including only the given keys. + * + * Useful for passing layout properties to [[Element.setStyle]]. + **/ + toCSS: function() { + var keys = []; + for (var i = 0, j, argKeys, l = arguments.length; i < l; i++) { + argKeys = arguments[i].split(' '); + for (j = 0; j < argKeys.length; j++) { + keys.push(argKeys[j]); + } + } + if (keys.length === 0) keys = Element.Layout.PROPERTIES; + var css = {}; + keys.each( function(key) { + // Key needs to be a valid Element.Layout property... + if (!Element.Layout.PROPERTIES.include(key)) return; + // ...but not a composite property. + if (Element.Layout.COMPOSITE_PROPERTIES.include(key)) return; + + var value = this.get(key); + // Unless the value is null, add 'px' to the end and add it to the + // returned object. + if (value) css[cssNameFor(key)] = value + 'px'; + }); + return css; + } }); Object.extend(Element.Layout, { From 5149c1b0dfae9c52904ce1b2840cf91027f1d314 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 4 Dec 2009 18:24:45 -0600 Subject: [PATCH 12/12] Add a couple more layout tests. --- test/unit/fixtures/layout.html | 25 +++++++++++++++++++++++++ test/unit/layout_test.js | 7 +++++++ 2 files changed, 32 insertions(+) diff --git a/test/unit/fixtures/layout.html b/test/unit/fixtures/layout.html index 918e20c6c..5921e38ee 100644 --- a/test/unit/fixtures/layout.html +++ b/test/unit/fixtures/layout.html @@ -1,3 +1,5 @@ + +

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

@@ -16,6 +18,7 @@ +

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

@@ -30,6 +33,7 @@ } +
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. @@ -52,6 +56,7 @@ } +
@@ -79,6 +84,7 @@ } +
@@ -102,4 +108,23 @@ top: 30px; right: 10%; } + + + +
+
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +
+
+ + \ No newline at end of file diff --git a/test/unit/layout_test.js b/test/unit/layout_test.js index eec7187b3..46180c580 100644 --- a/test/unit/layout_test.js +++ b/test/unit/layout_test.js @@ -78,5 +78,12 @@ new Test.Unit.Runner({ this.assertEqual(60, layout.get('right'), 'right (percentage value)'); this.assertEqual(340, layout.get('left'), 'left'); + }, + + 'test positioning on absolutely-positioned element with top=0 and left=0': function() { + var layout = $('box6').getLayout(); + + this.assertEqual(0, layout.get('top'), 'top'); + this.assertIdentical($('box6_parent'), $('box6').getOffsetParent()); } }); \ No newline at end of file