diff options
author | Richard Gibson <richard.gibson@gmail.com> | 2017-04-24 12:15:39 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-04-24 12:15:39 -0400 |
commit | 1d2df772b4d6e5dbf91df6e75f4a1809f7879ab0 (patch) | |
tree | 96caec8d8d4c290a828d02c39a2b915bad5c5970 | |
parent | e1b1b2d7fe5aff907a9accf59910bc3b7e4d1dec (diff) | |
download | jquery-1d2df772b4d6e5dbf91df6e75f4a1809f7879ab0.tar.gz jquery-1d2df772b4d6e5dbf91df6e75f4a1809f7879ab0.zip |
Offset: Use correct offset parents; include all border/scroll values
Thanks @anseki
Fixes gh-3080
Fixes gh-3107
Closes gh-3096
Closes gh-3487
-rw-r--r-- | src/offset.js | 57 | ||||
-rw-r--r-- | test/data/offset/boxes.html | 99 | ||||
-rw-r--r-- | test/unit/offset.js | 211 |
3 files changed, 340 insertions, 27 deletions
diff --git a/src/offset.js b/src/offset.js index c1ab85787..563c6e8cd 100644 --- a/src/offset.js +++ b/src/offset.js @@ -7,13 +7,12 @@ define( [ "./css/curCSS", "./css/addGetHookIf", "./css/support", - "./core/nodeName", "./core/init", "./css", "./selector" // contains ], function( jQuery, access, document, documentElement, rnumnonpx, - curCSS, addGetHookIf, support, nodeName ) { + curCSS, addGetHookIf, support ) { "use strict"; @@ -70,6 +69,8 @@ jQuery.offset = { }; jQuery.fn.extend( { + + // offset() relates an element's border box to the document origin offset: function( options ) { // Preserve chaining for setter @@ -81,7 +82,7 @@ jQuery.fn.extend( { } ); } - var doc, docElem, rect, win, + var rect, win, elem = this[ 0 ]; if ( !elem ) { @@ -96,50 +97,54 @@ jQuery.fn.extend( { return { top: 0, left: 0 }; } + // Get document-relative position by adding viewport scroll to viewport-relative gBCR rect = elem.getBoundingClientRect(); - - doc = elem.ownerDocument; - docElem = doc.documentElement; - win = doc.defaultView; - + win = elem.ownerDocument.defaultView; return { - top: rect.top + win.pageYOffset - docElem.clientTop, - left: rect.left + win.pageXOffset - docElem.clientLeft + top: rect.top + win.pageYOffset, + left: rect.left + win.pageXOffset }; }, + // position() relates an element's margin box to its offset parent's padding box + // This corresponds to the behavior of CSS absolute positioning position: function() { if ( !this[ 0 ] ) { return; } - var offsetParent, offset, + var offsetParent, offset, doc, elem = this[ 0 ], parentOffset = { top: 0, left: 0 }; - // Fixed elements are offset from window (parentOffset = {top:0, left: 0}, - // because it is its only offset parent + // position:fixed elements are offset from the viewport, which itself always has zero offset if ( jQuery.css( elem, "position" ) === "fixed" ) { - // Assume getBoundingClientRect is there when computed position is fixed + // Assume position:fixed implies availability of getBoundingClientRect offset = elem.getBoundingClientRect(); } else { + offset = this.offset(); - // Get *real* offsetParent - offsetParent = this.offsetParent(); + // Account for the *real* offset parent, which can be the document or its root element + // when a statically positioned element is identified + doc = elem.ownerDocument; + offsetParent = elem.offsetParent || doc.documentElement; + while ( offsetParent && + ( offsetParent === doc.body || offsetParent === doc.documentElement ) && + jQuery.css( offsetParent, "position" ) === "static" ) { - // Get correct offsets - offset = this.offset(); - if ( !nodeName( offsetParent[ 0 ], "html" ) ) { - parentOffset = offsetParent.offset(); + offsetParent = offsetParent.parentNode; + } + if ( offsetParent && offsetParent !== elem && offsetParent.nodeType === 1 ) { + + // Incorporate borders into its offset, since they are outside its content origin + parentOffset = jQuery( offsetParent ).offset(); + parentOffset = { + top: parentOffset.top + jQuery.css( offsetParent, "borderTopWidth", true ), + left: parentOffset.left + jQuery.css( offsetParent, "borderLeftWidth", true ) + }; } - - // Add offsetParent borders - parentOffset = { - top: parentOffset.top + jQuery.css( offsetParent[ 0 ], "borderTopWidth", true ), - left: parentOffset.left + jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true ) - }; } // Subtract parent offsets and element margins diff --git a/test/data/offset/boxes.html b/test/data/offset/boxes.html new file mode 100644 index 000000000..dbc7a15c0 --- /dev/null +++ b/test/data/offset/boxes.html @@ -0,0 +1,99 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html id="documentElement" class="box"> + <head> + <meta http-equiv="Content-type" content="text/html; charset=utf-8"> + <meta name="description" content="horizontal values 2^N; vertical doubled"> + <title>Nonempty margin/border/padding/position</title> + <style type="text/css" media="screen"> + /* work with convenient classes, units, and dimensions */ + .static { position: static; } + .relative { position: relative; } + .absolute { position: absolute; } + .fixed { position: fixed; } + .box { + font-size: 4px; + border-style: solid; + min-width: 300px; + } + + /* start the exponential scales, reserving the first bit for scroll position */ + .box { + border-width: 1em 0.5em; + top: 2em; left: 1em; + margin: 4em 2em; + padding: 8em 4em; + } + #documentElement { + margin: 16em 8em; + border-width: 32em 16em; + padding: 64em 32em; + } + #body { + margin: 128em 64em; + border-width: 256em 128em; + padding: 512em 256em; + } + #documentElement { + top: 1024em; left: 512em; + } + #body { + top: 2048em; left: 1024em; + } + + /* style for humans */ + :not(.box) { + font-size: 20px; + } + html { + border-color: hsl(20, 100%, 70%); + background-color: hsl(110, 100%, 70%); + } + body { + border-color: hsl(200, 100%, 70%); + background-color: hsl(290, 100%, 70%); + } + html::after, + body::after { + font: italic 16px sans-serif; + content: attr(id); + } + div.box { + background-color: hsla(0, 0%, 70%, 0.5); + opacity: 0.7; + } + div.box div.box { + background-color: hsla(60, 100%, 70%, 0.5); + } + </style> + <script src="../../jquery.js"></script> + <script src="../iframeTest.js"></script> + <script type="text/javascript" charset="utf-8"> + jQuery( function() { + window.scrollTo( 1, 2 ); + startIframeTest(); + } ); + </script> + </head> + <body id="body" class="box"> + <div id="relative" class="relative box"> + <div id="relative-relative" class="relative box"><code + >relative > relative</code></div> + <div id="relative-absolute" class="absolute box"><code + >relative > absolute</code></div> + </div> + <div id="absolute" class="absolute box"> + <div id="absolute-relative" class="relative box"><code + >absolute > relative</code></div> + <div id="absolute-absolute" class="absolute box"><code + >absolute > absolute</code></div> + </div> + <div id="fixed" class="fixed box"> + <div id="fixed-relative" class="relative box"><code + >fixed > relative</code></div> + <div id="fixed-absolute" class="absolute box"><code + >fixed > absolute</code></div> + </div> + <p id="positionTest" class="absolute">position:absolute with no top/left values</p> + </body> +</html> diff --git a/test/unit/offset.js b/test/unit/offset.js index 5b73ede60..622a7ba90 100644 --- a/test/unit/offset.js +++ b/test/unit/offset.js @@ -503,6 +503,215 @@ QUnit.test( "chaining", function( assert ) { assert.equal( jQuery( "#absolute-1" ).offset( undefined ).jquery, jQuery.fn.jquery, "offset(undefined) returns jQuery object (#5571)" ); } ); +// Test complex content under a variety of <html>/<body> positioning styles +( function() { + var POSITION_VALUES = [ "static", "relative", "absolute", "fixed" ], + + // Use shorthands for describing an element's relevant properties + BOX_PROPS = + ( "top left marginTop marginLeft borderTop borderLeft paddingTop paddingLeft" + + " style parent" ).split( /\s+/g ), + props = function() { + var propObj = {}; + supportjQuery.each( arguments, function( i, value ) { + propObj[ BOX_PROPS[ i ] ] = value; + } ); + return propObj; + }, + + // Values must stay synchronized with test/data/offset/boxes.html + divProps = function( position, parentId ) { + return props( 8, 4, 16, 8, 4, 2, 32, 16, position, parentId ); + }, + htmlProps = function( position ) { + return props( position === "static" ? 0 : 4096, position === "static" ? 0 : 2048, + 64, 32, 128, 64, 256, 128, position ); + }, + bodyProps = function( position ) { + return props( position === "static" ? 0 : 8192, position === "static" ? 0 : 4096, + 512, 256, 1024, 512, 2048, 1024, position, + position !== "fixed" && "documentElement" ); + }, + viewportScroll = { top: 2, left: 1 }, + + alwaysScrollable = false; + + // Support: iOS <=7 + // Detect viewport scrollability for pages with position:fixed document element + ( function() { + var $iframe = jQuery( "<iframe/>" ) + .css( { position: "absolute", width: "50px", left: "-60px" } ) + .attr( "src", url( "./data/offset/boxes.html" ) ); + + // Hijack the iframe test infrastructure + window.iframeCallback = function( $, win, doc ) { + doc.documentElement.style.position = "fixed"; + alwaysScrollable = win.pageXOffset !== 0; + window.iframeCallback = undefined; + $iframe.remove(); + return; + }; + + $iframe.appendTo( document.body ); + return; + } )(); + + function getExpectations( htmlPos, bodyPos ) { + + // Initialize data about page elements + var expectations = { + "documentElement": htmlProps( htmlPos ), + "body": bodyProps( bodyPos ), + "relative": divProps( "relative", "body" ), + "relative-relative": divProps( "relative", "relative" ), + "relative-absolute": divProps( "absolute", "relative" ), + "absolute": divProps( "absolute", "body" ), + "absolute-relative": divProps( "relative", "absolute" ), + "absolute-absolute": divProps( "absolute", "absolute" ), + "fixed": divProps( "fixed" ), + "fixed-relative": divProps( "relative", "fixed" ), + "fixed-absolute": divProps( "absolute", "fixed" ) + }; + + // Define position and offset expectations for page elements + supportjQuery.each( expectations, function( id, props ) { + var parent = expectations[ props.parent ], + + // position() relates an element's margin box to its offset parent's padding box + pos = props.pos = { + top: props.top, + left: props.left + }, + + // offset() relates an element's border box to the document origin + offset = props.offset = { + top: pos.top + props.marginTop, + left: pos.left + props.marginLeft + }; + + // Account for ancestors differently by element position + // fixed: ignore them + // absolute: offset includes offsetParent offset+border + // relative: position includes parent padding (and also position+margin+border when + // parent is not offsetParent); offset includes parent offset+border+padding + // static: same as relative + for ( ; parent; parent = expectations[ parent.parent ] ) { + // position:fixed + if ( props.style === "fixed" ) { + break; + } + + // position:absolute bypass + if ( props.style === "absolute" && parent.style === "static" ) { + continue; + } + + // Offset update + offset.top += parent.offset.top + parent.borderTop; + offset.left += parent.offset.left + parent.borderLeft; + if ( props.style !== "absolute" ) { + offset.top += parent.paddingTop; + offset.left += parent.paddingLeft; + + // position:relative or position:static position update + pos.top += parent.paddingTop; + pos.left += parent.paddingLeft; + if ( parent.style === "static" ) { + pos.top += parent.pos.top + parent.marginTop + parent.borderTop; + pos.left += parent.pos.left + parent.marginLeft + parent.borderLeft; + } + } + break; + } + + // Viewport scroll affects position:fixed elements, except when the page is + // unscrollable. + if ( props.style === "fixed" && + ( alwaysScrollable || expectations.documentElement.style !== "fixed" ) ) { + + offset.top += viewportScroll.top; + offset.left += viewportScroll.left; + } + } ); + + // Support: IE<=10 only + // Fudge the tests to work around <html>.gBCR() erroneously including margins + if ( /MSIE (?:9|10)\./.test( navigator.userAgent ) ) { + expectations.documentElement.pos.top -= expectations.documentElement.marginTop - + viewportScroll.top; + expectations.documentElement.offset.top -= expectations.documentElement.marginTop - + viewportScroll.top; + expectations.documentElement.pos.left -= expectations.documentElement.marginLeft - + viewportScroll.left; + expectations.documentElement.offset.left -= expectations.documentElement.marginLeft - + viewportScroll.left; + if ( htmlPos !== "static" ) { + delete expectations.documentElement; + delete expectations.body; + delete expectations.relative; + delete expectations.absolute; + } + } + + return expectations; + } + + // Cover each combination of <html> position and <body> position + supportjQuery.each( POSITION_VALUES, function( _, htmlPos ) { + supportjQuery.each( POSITION_VALUES, function( _, bodyPos ) { + var label = "nonzero box properties - html." + htmlPos + " body." + bodyPos; + testIframe( label, "offset/boxes.html", function( assert, $, win, doc ) { + + // Define expectations at runtime so alwaysScrollable is correct + var expectations = getExpectations( htmlPos, bodyPos ); + + assert.expect( 3 * Object.keys( expectations ).length ); + + // Setup documentElement and body styles + doc.documentElement.style.position = htmlPos; + doc.body.style.position = bodyPos; + + // Verify expected document offset + supportjQuery.each( expectations, function( id, descriptor ) { + assert.deepEqual( + supportjQuery.extend( {}, $( "#" + id ).offset() ), + descriptor.offset, + "jQuery('#" + id + "').offset()" ); + } ); + + // Verify expected relative position + supportjQuery.each( expectations, function( id, descriptor ) { + assert.deepEqual( + supportjQuery.extend( {}, $( "#" + id ).position() ), + descriptor.pos, + "jQuery('#" + id + "').position()" ); + } ); + + // Verify that values round-trip + supportjQuery.each( Object.keys( expectations ).reverse(), function( _, id ) { + var $el = $( "#" + id ), + pos = supportjQuery.extend( {}, $el.position() ); + + $el.css( { top: pos.top, left: pos.left } ); + if ( $el.css( "position" ) === "relative" ) { + + // $relative.position() includes parent padding; switch to absolute + // positioning so we don't double its effects. + $el.css( { position: "absolute" } ); + } + assert.deepEqual( supportjQuery.extend( {}, $el.position() ), pos, + "jQuery('#" + id + "').position() round-trips" ); + + // TODO Verify .offset(...) + // assert.deepEqual( $el.offset( offset ).offset(), offset ) + // assert.deepEqual( $el.offset( adjustedOffset ).offset(), adjustedOffset ) + // assert.deepEqual( $new.offset( offset ).offset(), offset ) + } ); + } ); + } ); + } ); +} )(); + QUnit.test( "offsetParent", function( assert ) { assert.expect( 13 ); @@ -578,7 +787,7 @@ QUnit.test( "iframe scrollTop/Left (see gh-1945)", function( assert ) { // the iframe but only its parent element. // It seems (not confirmed) in android 4.0 it's not possible to scroll iframes from the code. if ( - /iphone os/i.test( navigator.userAgent ) || + /iphone os|ipad/i.test( navigator.userAgent ) || /android 4\.0/i.test( navigator.userAgent ) ) { assert.equal( true, true, "Can't scroll iframes in this environment" ); |