如何获取 "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 是根据哪个元素计算的?假设您没有对包含块的引用并且您不知道它向上有多少层。如果没有设置 transformperspectivefilter 属性的祖先,它甚至可能是 documentElement。

对于 absoluterelative 定位的元素,我们有 elem.offsetParent 这给了我们这个参考。但是,对于 fixed 个元素,它被设置为 null。

当然,我可以查找 dom 并找到具有 transformperspectivefilter 样式 属性 的第一个元素设置,但这似乎很老套,而且不是未来的证据。

谢谢!

已知行为和规范合规。不过规格可能应该更改。
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,但它是 jQueryhttps://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>