mirror of
https://github.com/jquery/jquery.git
synced 2024-11-23 02:54:22 +00:00
Offset: Use correct offset parents; include all border/scroll values
Thanks @anseki Fixes gh-3080 Fixes gh-3107 Closes gh-3096 Closes gh-3487
This commit is contained in:
parent
e1b1b2d7fe
commit
1d2df772b4
@ -7,13 +7,12 @@ define( [
|
|||||||
"./css/curCSS",
|
"./css/curCSS",
|
||||||
"./css/addGetHookIf",
|
"./css/addGetHookIf",
|
||||||
"./css/support",
|
"./css/support",
|
||||||
"./core/nodeName",
|
|
||||||
|
|
||||||
"./core/init",
|
"./core/init",
|
||||||
"./css",
|
"./css",
|
||||||
"./selector" // contains
|
"./selector" // contains
|
||||||
], function( jQuery, access, document, documentElement, rnumnonpx,
|
], function( jQuery, access, document, documentElement, rnumnonpx,
|
||||||
curCSS, addGetHookIf, support, nodeName ) {
|
curCSS, addGetHookIf, support ) {
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
@ -70,6 +69,8 @@ jQuery.offset = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
jQuery.fn.extend( {
|
jQuery.fn.extend( {
|
||||||
|
|
||||||
|
// offset() relates an element's border box to the document origin
|
||||||
offset: function( options ) {
|
offset: function( options ) {
|
||||||
|
|
||||||
// Preserve chaining for setter
|
// Preserve chaining for setter
|
||||||
@ -81,7 +82,7 @@ jQuery.fn.extend( {
|
|||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
var doc, docElem, rect, win,
|
var rect, win,
|
||||||
elem = this[ 0 ];
|
elem = this[ 0 ];
|
||||||
|
|
||||||
if ( !elem ) {
|
if ( !elem ) {
|
||||||
@ -96,50 +97,54 @@ jQuery.fn.extend( {
|
|||||||
return { top: 0, left: 0 };
|
return { top: 0, left: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get document-relative position by adding viewport scroll to viewport-relative gBCR
|
||||||
rect = elem.getBoundingClientRect();
|
rect = elem.getBoundingClientRect();
|
||||||
|
win = elem.ownerDocument.defaultView;
|
||||||
doc = elem.ownerDocument;
|
|
||||||
docElem = doc.documentElement;
|
|
||||||
win = doc.defaultView;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
top: rect.top + win.pageYOffset - docElem.clientTop,
|
top: rect.top + win.pageYOffset,
|
||||||
left: rect.left + win.pageXOffset - docElem.clientLeft
|
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() {
|
position: function() {
|
||||||
if ( !this[ 0 ] ) {
|
if ( !this[ 0 ] ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var offsetParent, offset,
|
var offsetParent, offset, doc,
|
||||||
elem = this[ 0 ],
|
elem = this[ 0 ],
|
||||||
parentOffset = { top: 0, left: 0 };
|
parentOffset = { top: 0, left: 0 };
|
||||||
|
|
||||||
// Fixed elements are offset from window (parentOffset = {top:0, left: 0},
|
// position:fixed elements are offset from the viewport, which itself always has zero offset
|
||||||
// because it is its only offset parent
|
|
||||||
if ( jQuery.css( elem, "position" ) === "fixed" ) {
|
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();
|
offset = elem.getBoundingClientRect();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Get *real* offsetParent
|
|
||||||
offsetParent = this.offsetParent();
|
|
||||||
|
|
||||||
// Get correct offsets
|
|
||||||
offset = this.offset();
|
offset = this.offset();
|
||||||
if ( !nodeName( offsetParent[ 0 ], "html" ) ) {
|
|
||||||
parentOffset = offsetParent.offset();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add offsetParent borders
|
// Account for the *real* offset parent, which can be the document or its root element
|
||||||
parentOffset = {
|
// when a statically positioned element is identified
|
||||||
top: parentOffset.top + jQuery.css( offsetParent[ 0 ], "borderTopWidth", true ),
|
doc = elem.ownerDocument;
|
||||||
left: parentOffset.left + jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true )
|
offsetParent = elem.offsetParent || doc.documentElement;
|
||||||
};
|
while ( offsetParent &&
|
||||||
|
( offsetParent === doc.body || offsetParent === doc.documentElement ) &&
|
||||||
|
jQuery.css( offsetParent, "position" ) === "static" ) {
|
||||||
|
|
||||||
|
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 )
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subtract parent offsets and element margins
|
// Subtract parent offsets and element margins
|
||||||
|
99
test/data/offset/boxes.html
Normal file
99
test/data/offset/boxes.html
Normal file
@ -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>
|
@ -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)" );
|
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 ) {
|
QUnit.test( "offsetParent", function( assert ) {
|
||||||
assert.expect( 13 );
|
assert.expect( 13 );
|
||||||
|
|
||||||
@ -578,7 +787,7 @@ QUnit.test( "iframe scrollTop/Left (see gh-1945)", function( assert ) {
|
|||||||
// the iframe but only its parent element.
|
// 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.
|
// It seems (not confirmed) in android 4.0 it's not possible to scroll iframes from the code.
|
||||||
if (
|
if (
|
||||||
/iphone os/i.test( navigator.userAgent ) ||
|
/iphone os|ipad/i.test( navigator.userAgent ) ||
|
||||||
/android 4\.0/i.test( navigator.userAgent )
|
/android 4\.0/i.test( navigator.userAgent )
|
||||||
) {
|
) {
|
||||||
assert.equal( true, true, "Can't scroll iframes in this environment" );
|
assert.equal( true, true, "Can't scroll iframes in this environment" );
|
||||||
|
Loading…
Reference in New Issue
Block a user