如何获取 "fixed" 定位元素的包含块 javascript?
How can I get the containing block of a "fixed" positioned element with javascript?
假设我们有以下设置:
#header {
background-color: #ddd;
padding: 2rem;
}
#containing-block {
background-color: #eef;
padding: 2rem;
height: 70px;
transform: translate(0, 0);
}
#button {
position: fixed;
top: 50px;
}
<div id="header">header</div>
<div id="containing-block">
containing-block
<div>
<div>
<div>
<button id="button" onclick="console.log('offsetParent', this.offsetParent)">click me</button>
</div>
</div>
</div>
</div>
按钮的位置为 fixed
,包含块的位置为 transform
属性。
这可能会让人感到意外,但按钮的位置是相对于 #containing-block
,而不是视口(正如人们在使用 fixed
时所期望的那样)。那是因为 #containing-block
元素具有 transform
属性 集。有关说明,请参阅 https://developer.mozilla.org/en-US/docs/Web/CSS/position#fixed。
有没有简单的方法可以找出哪个是按钮的包含块? top: 50px
是根据哪个元素计算的?假设您没有对包含块的引用并且您不知道它向上有多少层。如果没有设置 transform
、perspective
或 filter
属性的祖先,它甚至可能是 documentElement。
对于 absolute
或 relative
定位的元素,我们有 elem.offsetParent
这给了我们这个参考。但是,对于 fixed
个元素,它被设置为 null。
当然,我可以查找 dom 并找到具有 transform
、perspective
或 filter
样式 属性 的第一个元素设置,但这似乎很老套,而且不是未来的证据。
谢谢!
已知行为和规范合规。不过规格可能应该更改。
https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
我已经包含了来自各种库的一些解决方法。
解决方法取自 dom-helpers(似乎最一致,使用 offsetParent 遍历意味着它应该只真正遍历一次或两次。):
https://github.com/react-bootstrap/dom-helpers/blob/master/src/offsetParent.ts
// taken from popper.js
function getStyleComputedProperty(element, property) {
if (element.nodeType !== 1) {
return [];
}
// NOTE: 1 DOM access here
const window = element.ownerDocument.defaultView;
const css = window.getComputedStyle(element, null);
return property ? css[property] : css;
}
getOffsetParent = function(node) {
const doc = (node && node.ownerDocument) || document
const isHTMLElement = e => !!e && 'offsetParent' in e
let parent = node && node.offsetParent
while (
isHTMLElement(parent) &&
parent.nodeName !== 'HTML' &&
getComputedStyle(parent, 'position') === 'static'
) {
parent = parent.offsetParent
}
return (parent || doc.documentElement)
}
#header {
background-color: #ddd;
padding: 2rem;
}
#containing-block {
background-color: #eef;
padding: 2rem;
height: 70px;
transform: translate(0, 0);
}
#button {
position: fixed;
top: 50px;
}
<div id="header">header</div>
<div id="containing-block">
containing-block
<div>
<div>
<div>
<button id="button" onclick="console.log('offsetParent', getOffsetParent(this),this.offsetParent)">click me</button>
</div>
</div>
</div>
</div>
从 jQuery 来源获取的解决方法代码。不处理非元素,也不处理 TABLE TH TD,但它是 jQuery。
https://github.com/jquery/jquery/blob/master/src/offset.js
// taken from popper.js
function getStyleComputedProperty(element, property) {
if (element.nodeType !== 1) {
return [];
}
// NOTE: 1 DOM access here
const window = element.ownerDocument.defaultView;
const css = window.getComputedStyle(element, null);
return property ? css[property] : css;
}
getOffsetParent = function(elem) {
var doc = elem.ownerDocument;
var offsetParent = elem.offsetParent || doc.documentElement;
while (offsetParent &&
(offsetParent !== doc.body || offsetParent !== doc.documentElement) &&
getComputedStyle(offsetParent, "position") === "static") {
offsetParent = offsetParent.parentNode;
}
return offsetParent;
}
#header {
background-color: #ddd;
padding: 2rem;
}
#containing-block {
background-color: #eef;
padding: 2rem;
height: 70px;
transform: translate(0, 0);
}
#button {
position: fixed;
top: 50px;
}
<div id="header">header</div>
<div id="containing-block">
containing-block
<div>
<div>
<div>
<button id="button" onclick="console.log('offsetParent', getOffsetParent(this),this.offsetParent)">click me</button>
</div>
</div>
</div>
</div>
解决方法代码取自 popper.js。 doc.body 似乎不正确。唯一专门针对 TH TD TABLE 的。 dom-helpers 应该工作只是因为它使用 offsetParent 遍历。
https://github.com/popperjs/popper-core/blob/master/src/dom-utils/getOffsetParent.js
var isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined' && typeof navigator !== 'undefined';
const isIE11 = isBrowser && !!(window.MSInputMethodContext && document.documentMode);
const isIE10 = isBrowser && /MSIE 10/.test(navigator.userAgent);
function isIE(version) {
if (version === 11) {
return isIE11;
}
if (version === 10) {
return isIE10;
}
return isIE11 || isIE10;
}
function getStyleComputedProperty(element, property) {
if (element.nodeType !== 1) {
return [];
}
// NOTE: 1 DOM access here
const window = element.ownerDocument.defaultView;
const css = window.getComputedStyle(element, null);
return property ? css[property] : css;
}
function getOffsetParent(element) {
if (!element) {
return document.documentElement;
}
const noOffsetParent = isIE(10) ? document.body : null;
// NOTE: 1 DOM access here
let offsetParent = element.offsetParent || null;
// Skip hidden elements which don't have an offsetParent
while (offsetParent === noOffsetParent && element.nextElementSibling) {
offsetParent = (element = element.nextElementSibling).offsetParent;
}
const nodeName = offsetParent && offsetParent.nodeName;
if (!nodeName || nodeName === 'BODY' || nodeName === 'HTML') {
return element ? element.ownerDocument.documentElement : document.documentElement;
}
// .offsetParent will return the closest TH, TD or TABLE in case
// no offsetParent is present, I hate this job...
if (['TH', 'TD', 'TABLE'].indexOf(offsetParent.nodeName) !== -1 && getStyleComputedProperty(offsetParent, 'position') === 'static') {
return getOffsetParent(offsetParent);
}
return offsetParent;
}
#header {
background-color: #ddd;
padding: 2rem;
}
#containing-block {
background-color: #eef;
padding: 2rem;
height: 70px;
transform: translate(0, 0);
}
#button {
position: fixed;
top: 50px;
}
<div id="header">header</div>
<div id="containing-block">
containing-block
<div>
<div>
<div>
<button id="button" onclick="console.log('offsetParent', getOffsetParent(this))">click me</button>
</div>
</div>
</div>
</div>
我最近设计了一个我认为是对这个不那么小的、长期存在的怪癖的相当优雅的解决方法。我设计了一个 CustomElement 可以自动检测它是否已在包含块内部使用,如果是,则将其自身从 DOM 中的当前位置移动到 body 元素的末尾。
感谢这个对类似问题的回答,为我指明了正确的方向。
<!DOCTYPE html>
<title> Breakout Fixed </title>
<script type="module">
customElements.define(
'breakout-fixed',
class BreakoutFixed extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode : 'open' });
this.shadowRoot.innerHTML = this.template;
}
get template() {
return `
<style> :host { position: fixed; } </style>
<slot></slot>
`;
}
breakout() {
const el = this;
if (this.fixed !== true) {
window.addEventListener('resize', el.fix);
this.fixed = true;
}
if (el.parentNode == document.body) { return; }
function shift() {
getContainingBlock(el) &&
document.body.append(el);
}
function getContainingBlock(node) {
if (node.parentElement) {
if (node.parentElement == document.body) {
return document.body;
} else if (testNode(node.parentElement) == false) {
return getContainingBlock(node.parentElement);
} else { return node.parentElement; }
} else { return null; }
function testNode(node) {
let test; let cs = getComputedStyle(node);
test = cs.getPropertyValue('position'); if ([
'absolute', 'fixed'
].includes(test)) { return true; }
test = cs.getPropertyValue('transform'); if (test != 'none') { return true; }
test = cs.getPropertyValue('perspective'); if (test != 'none') { return true; }
test = cs.getPropertyValue('perspective'); if (test != 'none') { return true; }
test = cs.getPropertyValue('filter'); if (test != 'none') { return true; }
test = cs.getPropertyValue('contain'); if (test == 'paint') { return true; }
test = cs.getPropertyValue('will-change'); if ([
'transform', 'perspective', 'filter'
].includes(test)) { return true; }
return false;
}
}
}
connectedCallback() {
this.breakout();
}
}
);
</script>
<style>
body { background: dimgrey; }
#container {
height: 300px;
width: 50%;
background: dodgerblue;
transform: scale(2);
}
div#test {
position: fixed;
right: 0;
bottom: 0;
padding: 1rem;
background: red;
}
breakout-fixed {
top: 0; right: 0;
padding: 1rem;
background: limegreen;
transform: scale(3);
transform-origin: top right;
}
</style>
<div id="container">
<div id="test"> This element will be fixed to it's containing block. </div>
<breakout-fixed>
<div> This element will be fixed to the viewport. </div>
</breakout-fixed>
</div>
假设我们有以下设置:
#header {
background-color: #ddd;
padding: 2rem;
}
#containing-block {
background-color: #eef;
padding: 2rem;
height: 70px;
transform: translate(0, 0);
}
#button {
position: fixed;
top: 50px;
}
<div id="header">header</div>
<div id="containing-block">
containing-block
<div>
<div>
<div>
<button id="button" onclick="console.log('offsetParent', this.offsetParent)">click me</button>
</div>
</div>
</div>
</div>
按钮的位置为 fixed
,包含块的位置为 transform
属性。
这可能会让人感到意外,但按钮的位置是相对于 #containing-block
,而不是视口(正如人们在使用 fixed
时所期望的那样)。那是因为 #containing-block
元素具有 transform
属性 集。有关说明,请参阅 https://developer.mozilla.org/en-US/docs/Web/CSS/position#fixed。
有没有简单的方法可以找出哪个是按钮的包含块? top: 50px
是根据哪个元素计算的?假设您没有对包含块的引用并且您不知道它向上有多少层。如果没有设置 transform
、perspective
或 filter
属性的祖先,它甚至可能是 documentElement。
对于 absolute
或 relative
定位的元素,我们有 elem.offsetParent
这给了我们这个参考。但是,对于 fixed
个元素,它被设置为 null。
当然,我可以查找 dom 并找到具有 transform
、perspective
或 filter
样式 属性 的第一个元素设置,但这似乎很老套,而且不是未来的证据。
谢谢!
已知行为和规范合规。不过规格可能应该更改。
https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
我已经包含了来自各种库的一些解决方法。
解决方法取自 dom-helpers(似乎最一致,使用 offsetParent 遍历意味着它应该只真正遍历一次或两次。):
https://github.com/react-bootstrap/dom-helpers/blob/master/src/offsetParent.ts
// taken from popper.js
function getStyleComputedProperty(element, property) {
if (element.nodeType !== 1) {
return [];
}
// NOTE: 1 DOM access here
const window = element.ownerDocument.defaultView;
const css = window.getComputedStyle(element, null);
return property ? css[property] : css;
}
getOffsetParent = function(node) {
const doc = (node && node.ownerDocument) || document
const isHTMLElement = e => !!e && 'offsetParent' in e
let parent = node && node.offsetParent
while (
isHTMLElement(parent) &&
parent.nodeName !== 'HTML' &&
getComputedStyle(parent, 'position') === 'static'
) {
parent = parent.offsetParent
}
return (parent || doc.documentElement)
}
#header {
background-color: #ddd;
padding: 2rem;
}
#containing-block {
background-color: #eef;
padding: 2rem;
height: 70px;
transform: translate(0, 0);
}
#button {
position: fixed;
top: 50px;
}
<div id="header">header</div>
<div id="containing-block">
containing-block
<div>
<div>
<div>
<button id="button" onclick="console.log('offsetParent', getOffsetParent(this),this.offsetParent)">click me</button>
</div>
</div>
</div>
</div>
从 jQuery 来源获取的解决方法代码。不处理非元素,也不处理 TABLE TH TD,但它是 jQuery。 https://github.com/jquery/jquery/blob/master/src/offset.js
// taken from popper.js
function getStyleComputedProperty(element, property) {
if (element.nodeType !== 1) {
return [];
}
// NOTE: 1 DOM access here
const window = element.ownerDocument.defaultView;
const css = window.getComputedStyle(element, null);
return property ? css[property] : css;
}
getOffsetParent = function(elem) {
var doc = elem.ownerDocument;
var offsetParent = elem.offsetParent || doc.documentElement;
while (offsetParent &&
(offsetParent !== doc.body || offsetParent !== doc.documentElement) &&
getComputedStyle(offsetParent, "position") === "static") {
offsetParent = offsetParent.parentNode;
}
return offsetParent;
}
#header {
background-color: #ddd;
padding: 2rem;
}
#containing-block {
background-color: #eef;
padding: 2rem;
height: 70px;
transform: translate(0, 0);
}
#button {
position: fixed;
top: 50px;
}
<div id="header">header</div>
<div id="containing-block">
containing-block
<div>
<div>
<div>
<button id="button" onclick="console.log('offsetParent', getOffsetParent(this),this.offsetParent)">click me</button>
</div>
</div>
</div>
</div>
解决方法代码取自 popper.js。 doc.body 似乎不正确。唯一专门针对 TH TD TABLE 的。 dom-helpers 应该工作只是因为它使用 offsetParent 遍历。 https://github.com/popperjs/popper-core/blob/master/src/dom-utils/getOffsetParent.js
var isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined' && typeof navigator !== 'undefined';
const isIE11 = isBrowser && !!(window.MSInputMethodContext && document.documentMode);
const isIE10 = isBrowser && /MSIE 10/.test(navigator.userAgent);
function isIE(version) {
if (version === 11) {
return isIE11;
}
if (version === 10) {
return isIE10;
}
return isIE11 || isIE10;
}
function getStyleComputedProperty(element, property) {
if (element.nodeType !== 1) {
return [];
}
// NOTE: 1 DOM access here
const window = element.ownerDocument.defaultView;
const css = window.getComputedStyle(element, null);
return property ? css[property] : css;
}
function getOffsetParent(element) {
if (!element) {
return document.documentElement;
}
const noOffsetParent = isIE(10) ? document.body : null;
// NOTE: 1 DOM access here
let offsetParent = element.offsetParent || null;
// Skip hidden elements which don't have an offsetParent
while (offsetParent === noOffsetParent && element.nextElementSibling) {
offsetParent = (element = element.nextElementSibling).offsetParent;
}
const nodeName = offsetParent && offsetParent.nodeName;
if (!nodeName || nodeName === 'BODY' || nodeName === 'HTML') {
return element ? element.ownerDocument.documentElement : document.documentElement;
}
// .offsetParent will return the closest TH, TD or TABLE in case
// no offsetParent is present, I hate this job...
if (['TH', 'TD', 'TABLE'].indexOf(offsetParent.nodeName) !== -1 && getStyleComputedProperty(offsetParent, 'position') === 'static') {
return getOffsetParent(offsetParent);
}
return offsetParent;
}
#header {
background-color: #ddd;
padding: 2rem;
}
#containing-block {
background-color: #eef;
padding: 2rem;
height: 70px;
transform: translate(0, 0);
}
#button {
position: fixed;
top: 50px;
}
<div id="header">header</div>
<div id="containing-block">
containing-block
<div>
<div>
<div>
<button id="button" onclick="console.log('offsetParent', getOffsetParent(this))">click me</button>
</div>
</div>
</div>
</div>
我最近设计了一个我认为是对这个不那么小的、长期存在的怪癖的相当优雅的解决方法。我设计了一个 CustomElement 可以自动检测它是否已在包含块内部使用,如果是,则将其自身从 DOM 中的当前位置移动到 body 元素的末尾。
感谢这个对类似问题的回答,为我指明了正确的方向。
<!DOCTYPE html>
<title> Breakout Fixed </title>
<script type="module">
customElements.define(
'breakout-fixed',
class BreakoutFixed extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode : 'open' });
this.shadowRoot.innerHTML = this.template;
}
get template() {
return `
<style> :host { position: fixed; } </style>
<slot></slot>
`;
}
breakout() {
const el = this;
if (this.fixed !== true) {
window.addEventListener('resize', el.fix);
this.fixed = true;
}
if (el.parentNode == document.body) { return; }
function shift() {
getContainingBlock(el) &&
document.body.append(el);
}
function getContainingBlock(node) {
if (node.parentElement) {
if (node.parentElement == document.body) {
return document.body;
} else if (testNode(node.parentElement) == false) {
return getContainingBlock(node.parentElement);
} else { return node.parentElement; }
} else { return null; }
function testNode(node) {
let test; let cs = getComputedStyle(node);
test = cs.getPropertyValue('position'); if ([
'absolute', 'fixed'
].includes(test)) { return true; }
test = cs.getPropertyValue('transform'); if (test != 'none') { return true; }
test = cs.getPropertyValue('perspective'); if (test != 'none') { return true; }
test = cs.getPropertyValue('perspective'); if (test != 'none') { return true; }
test = cs.getPropertyValue('filter'); if (test != 'none') { return true; }
test = cs.getPropertyValue('contain'); if (test == 'paint') { return true; }
test = cs.getPropertyValue('will-change'); if ([
'transform', 'perspective', 'filter'
].includes(test)) { return true; }
return false;
}
}
}
connectedCallback() {
this.breakout();
}
}
);
</script>
<style>
body { background: dimgrey; }
#container {
height: 300px;
width: 50%;
background: dodgerblue;
transform: scale(2);
}
div#test {
position: fixed;
right: 0;
bottom: 0;
padding: 1rem;
background: red;
}
breakout-fixed {
top: 0; right: 0;
padding: 1rem;
background: limegreen;
transform: scale(3);
transform-origin: top right;
}
</style>
<div id="container">
<div id="test"> This element will be fixed to it's containing block. </div>
<breakout-fixed>
<div> This element will be fixed to the viewport. </div>
</breakout-fixed>
</div>