]> source.dussan.org Git - jquery.git/commitdiff
Offset: Use correct offset parents; include all border/scroll values
authorRichard Gibson <richard.gibson@gmail.com>
Mon, 24 Apr 2017 16:15:39 +0000 (12:15 -0400)
committerGitHub <noreply@github.com>
Mon, 24 Apr 2017 16:15:39 +0000 (12:15 -0400)
Thanks @anseki

Fixes gh-3080
Fixes gh-3107
Closes gh-3096
Closes gh-3487

src/offset.js
test/data/offset/boxes.html [new file with mode: 0644]
test/unit/offset.js

index c1ab85787935c5c7d02c74202c38c1e6e25cd0ff..563c6e8cd9878e6d17802ff8212f6bff26624da3 100644 (file)
@@ -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 (file)
index 0000000..dbc7a15
--- /dev/null
@@ -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 &gt; relative</code></div>
+                       <div id="relative-absolute" class="absolute box"><code
+                               >relative &gt; absolute</code></div>
+               </div>
+               <div id="absolute" class="absolute box">
+                       <div id="absolute-relative" class="relative box"><code
+                               >absolute &gt; relative</code></div>
+                       <div id="absolute-absolute" class="absolute box"><code
+                               >absolute &gt; absolute</code></div>
+               </div>
+               <div id="fixed" class="fixed box">
+                       <div id="fixed-relative" class="relative box"><code
+                               >fixed &gt; relative</code></div>
+                       <div id="fixed-absolute" class="absolute box"><code
+                               >fixed &gt; absolute</code></div>
+               </div>
+               <p id="positionTest" class="absolute">position:absolute with no top/left values</p>
+       </body>
+</html>
index 5b73ede603b85946de944f4e1ee658e668a59476..622a7ba9027cceda5aad99d996fc125639b6c74a 100644 (file)
@@ -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" );