检查一个元素在逻辑上是阴影dom中另一个元素的孩子

发布于 2025-01-21 08:51:58 字数 3738 浏览 1 评论 0 原文

在我的现实世界中,我正在处理一个飞行组件,一旦外面的任何元素都能得到焦点,我就想关闭它。此飞行组件用作其他元素的阴影-DOM模板的一部分。当然,我的现实案例,还有更多的组成部分。我将案件降低到最低限度。

在下面的示例中,我们有一个外部和 Intry-Component 。内心仅仅是一个普通的插槽,也是一个重点列表,作为我问题的演示者。 外部组件包含一个未命名的插槽和一个内部组件,其中包含一个命名插槽。

因此,内部组件将使用其 Inner-plain-slot 从外部组件中选择插槽外部nest-slot 。最后,它将包含两个按钮的DIV。

现在是问题本身:这些按钮不是 Intry-Component 的真正孩子。但是,由于将它们插入 Inner-Component ,因此收到了 focusOut event。是否有一种方法可以通过编程方式检查元素是否在逻辑上是另一个组件的孩子,即使光线中没有亲子关系?

当然, slot.signednodes({flatten:true})进入了我的薄荷。但这只会返回我的按钮周围的包装div。因此,我必须在所有返回的节点上迭代,以检查其中任何一个是否包含所讨论的元素。当然,如果分配的节点不是简单的 div ,而是webcomponent,那么一切都会变得荒谬的复杂。

因此,简而言之: 给定一个WebComponent wa 和一个节点 b ,是否有一种方法可以通过编程方式检查是否存在逻辑的父子关系(意味着事件可能会从中冒出来泡泡b wa )?

const stamp = (node, name) => {
  const template = document.getElementById(name);
  const templateContent = template.content;

  const shadowRoot = node.attachShadow({
    mode: 'open'
  });
  shadowRoot.appendChild(templateContent.cloneNode(true));
}


customElements.define('inner-element',
  class extends HTMLElement {
    constructor() {
      super();
      stamp(this, 'inner-element');

      this.addEventListener('focusout', this.onFocusOut);
    }

    onFocusOut(focusevent) {
      const newFocus = focusevent.relatedTarget;
      const oldFocus = focusevent.target;
      document.getElementById('log').innerHTML += `<div>${oldFocus?oldFocus.id:'null'} -> ${newFocus?newFocus.id:'null'}; oldFocus is Child? ${this.contains(oldFocus)}. newFocus is Child? ${this.contains(newFocus)}</div>`;
    }

  }
);

customElements.define('outer-element',
  class extends HTMLElement {
    constructor() {
      super();
      stamp(this, 'outer-element');
    }
  }
);
<template id="inner-element">
  <style>
    :host {
      border: 2px solid hotpink;
      margin:2px;
      padding: 2px;
    }
  </style>
  <slot id="inner-plain-slot"></slot>
</template>

<template id="outer-element">
  <style>
    :host {
      border: 2px solid rebeccapurple;
      margin:2px;
      padding: 2px;
      display:flex;
      flex-direction:row
    }
  </style>
  <inner-element>  <slot id="outer-nested-slot" name="nested"></slot>  </inner-element>
  <slot id="outer-plain-slot"></slot>
</template>


<outer-element>
  <div style="display:flex;flex-direction:row" slot="nested">
    <button id="nest1">nested-1</button>
    <button id="nest2">nested-2</button>
  </div>
  <button id="not-nest1">not-nested1</button>
  <button id="not-nest2">not-nested2</button>
</outer-element>

<div>
  <code id=log></code>
</div>

我发现了一个可能的解决方案。解决问题后,解决方案似乎很明显。但是我仍然更喜欢一种更本地的方法来执行该检查。

const checkChildParentRelation = (potentialChild, potentialParent) => {
  if (!potentialChild || !potentialParent) {
    return false;
  }
  let result = false;
  const listener = e => {
    result = true;
    e.stopImmediatePropagation();
  };

  const eventName = `parent-child-test-event-${Date.now()}`;

  potentialParent.addEventListener(eventName, listener);
  potentialChild.dispatchEvent(
    new CustomEvent(eventName, {
      bubbles: true,
      composed: true,
      cancelable: true,
    })
  );
  potentialParent.removeEventListener(eventName, listener);
  return result;
};

In my real world case, I'm dealing with a flyout-component, which I want to close once the any element outside of it receives focus. This flyout-component is used as part of the shadow-dom template of other elements. And of course, my real world case, there are a lot more components involved. I reduced the case to a bare minimum.

In the following example, we have an outer and an inner-component. The inner one is little more than a plain slot and a focusout-listener as a demonstrator for my problem.
The outer-component contains an unnamed slot and an inner-component, which contains a named slot.

So the inner-component will select the slot outer-nested-slot from the outer-component with its inner-plain-slot. Finally, it will contain the div with the two buttons.

Now here comes the question itself: The buttons are no real children of inner-component. Nevertheless the focusout-event is received since they are slotted into inner-component. Is there a way to programmatically check if a Element is logically a child of another component, even though there is no parent-child-relation in the light-dom?

Of course slot.assignedNodes({flatten:true}) came into my mint. But this would return only the wrapping div around my buttons. Thus I would have to iterate over all the returned nodes in order to check if any of them contains the element in question. And of course, if the assigned node was not a simple div, but again a webcomponent, everything gets ridiculous complex.

So, in a nutshell:
Given a webcomponent w-a and a node b, is there a way to programmatically check if there is a logical parent-child-relation (implying an event could bubble from b to w-a )?

const stamp = (node, name) => {
  const template = document.getElementById(name);
  const templateContent = template.content;

  const shadowRoot = node.attachShadow({
    mode: 'open'
  });
  shadowRoot.appendChild(templateContent.cloneNode(true));
}


customElements.define('inner-element',
  class extends HTMLElement {
    constructor() {
      super();
      stamp(this, 'inner-element');

      this.addEventListener('focusout', this.onFocusOut);
    }

    onFocusOut(focusevent) {
      const newFocus = focusevent.relatedTarget;
      const oldFocus = focusevent.target;
      document.getElementById('log').innerHTML += `<div>${oldFocus?oldFocus.id:'null'} -> ${newFocus?newFocus.id:'null'}; oldFocus is Child? ${this.contains(oldFocus)}. newFocus is Child? ${this.contains(newFocus)}</div>`;
    }

  }
);

customElements.define('outer-element',
  class extends HTMLElement {
    constructor() {
      super();
      stamp(this, 'outer-element');
    }
  }
);
<template id="inner-element">
  <style>
    :host {
      border: 2px solid hotpink;
      margin:2px;
      padding: 2px;
    }
  </style>
  <slot id="inner-plain-slot"></slot>
</template>

<template id="outer-element">
  <style>
    :host {
      border: 2px solid rebeccapurple;
      margin:2px;
      padding: 2px;
      display:flex;
      flex-direction:row
    }
  </style>
  <inner-element>  <slot id="outer-nested-slot" name="nested"></slot>  </inner-element>
  <slot id="outer-plain-slot"></slot>
</template>


<outer-element>
  <div style="display:flex;flex-direction:row" slot="nested">
    <button id="nest1">nested-1</button>
    <button id="nest2">nested-2</button>
  </div>
  <button id="not-nest1">not-nested1</button>
  <button id="not-nest2">not-nested2</button>
</outer-element>

<div>
  <code id=log></code>
</div>

I found one possible solution using events themself. The solution seems rather obvious after condensing the question. But I would still prefer a more native way to perform that check.

const checkChildParentRelation = (potentialChild, potentialParent) => {
  if (!potentialChild || !potentialParent) {
    return false;
  }
  let result = false;
  const listener = e => {
    result = true;
    e.stopImmediatePropagation();
  };

  const eventName = `parent-child-test-event-${Date.now()}`;

  potentialParent.addEventListener(eventName, listener);
  potentialChild.dispatchEvent(
    new CustomEvent(eventName, {
      bubbles: true,
      composed: true,
      cancelable: true,
    })
  );
  potentialParent.removeEventListener(eventName, listener);
  return result;
};

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

魔法唧唧 2025-01-28 08:51:58

阴影DOM及其与事件之间的关系的一个大问题是,当被阴影DOM带来的事件目标将是被捕获在该阴影DOM 之外的主机元素这就是“重新定位。 ,目标将像正常一样工作。如果将同一事件的侦听器附加到 window 之类的东西,则目标将是阴影DOM中任何元素的阴影主机元素。

我找不到使用DOM API进行操作的简单方法 - Node#contains 之类的东西总是会返回falses,当事件侦听器附加到外面的节点Shadow dom,您正在尝试确定阴影DOM的“目标”(将被重新定位)是否包含阴影DOM内部的另一个节点。因此,我写了这篇文章:

// Find the document-fragment or document node which contains the given node
export function findRootNode(n: Node): Node {
    if (n.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
        if (n instanceof ShadowRoot) {
            return n.host;
        }
        // I'm not sure when this could happen, but I don't expect it.
        throw ("found document fragment that is not a shadow root");
    }
    if (n.nodeType === Node.DOCUMENT_NODE) {
        return n;
    }
    return findRootNode(n.parentNode);
}

// findEventTarget will find what the event target (with respect to Window)
// should be for this element:
// - if in a shadow DOM then the shadow host element
// - otherwise, the element itself
export function findEventTarget(n: Node): Node {
    const rootNode = findRootNode(n);
    if (rootNode === document) {
        // not in shadow dom
        return n;
    }
    return rootNode;
}

这些可以用来查找您应该将事件的目标与哪个节点进行比较。如果您在Shadow dom s 中在节点 a 上有一些事件,则可以使用 const et = findeventtarget(a)并比较 ET 到活动的目标。这将在不同的阴影DOM中的组件之间起作用。如果要将其扩展到在阴影DOM和一个阴影DOM内部之间的工作使用 node.contains 节点 findeventtarget()返回。

One big issue with the shadow DOM and how it relates with events is that the event target for events with a shadow DOM will be the host element when caught outside of that shadow DOM. This is "retargeting." That means that if you attach an event listener inside of the Shadow DOM, the target will work like normal. If you attach the same event listener to something like window, then the target will be the shadow host element for any element within the shadow DOM.

I couldn't find an easy way to do what you're doing using just the DOM API -- something like Node#contains will always return false when the event listener is attached to a Node outside of the shadow DOM and you're trying to determine if the "target" (which will be retargeted) of the shadow DOM contains another node inside of the shadow DOM. So I wrote this:

// Find the document-fragment or document node which contains the given node
export function findRootNode(n: Node): Node {
    if (n.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
        if (n instanceof ShadowRoot) {
            return n.host;
        }
        // I'm not sure when this could happen, but I don't expect it.
        throw ("found document fragment that is not a shadow root");
    }
    if (n.nodeType === Node.DOCUMENT_NODE) {
        return n;
    }
    return findRootNode(n.parentNode);
}

// findEventTarget will find what the event target (with respect to Window)
// should be for this element:
// - if in a shadow DOM then the shadow host element
// - otherwise, the element itself
export function findEventTarget(n: Node): Node {
    const rootNode = findRootNode(n);
    if (rootNode === document) {
        // not in shadow dom
        return n;
    }
    return rootNode;
}

These can be used to find which node you should compare your event's target to. If you had some event on node A inside a shadow DOM S, then you can use const et = findEventTarget(A) and compare et to the target of your event. This will work in between components in different shadow DOMs. If you want to extend it to work both between shadow DOMs and inside of one shadow DOM, then you can use a combination of Node.contains with the "real" nodes and, if that returns null, then use Node.contains with the Node returned from findEventTarget().

怎樣才叫好 2025-01-28 08:51:58

对于松散的耦合事件是最好的。

在我看来,您想从执行 event.composedpath()函数中获取您从执行中获得的DOM元素数组

For loose coupling Events are best.

Sounds to me like you want to proces the array of DOM Elements you get from executing the event.composedPath() function

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文