判断用户是否在 Shadow DOM 外单击了页面

16

我正在尝试实现一个下拉框,你可以点击外面来关闭。这个下拉框是自定义日期输入的一部分,并且被封装在输入框的Shadow DOM里。

我想写出类似这样的代码:

window.addEventListener('mousedown', function (evt) {
  if (!componentNode.contains(evt.target)) {
    closeDropdown();
  }
});

然而,该事件被重新定向,因此evt.target始终是元素之外的对象。事件将穿越多个影子边界才能到达窗口,所以似乎无法真正确定用户是否在我的组件内部单击了。

注意:我没有在任何地方使用Polymer - 我需要一个适用于通用影子DOM的答案,而不是一个特定于Polymer的hack。


...将是window,因为我将事件监听器添加到窗口上。 - ovangle
我们可以有一个小例子/示例吗? - Rayon
这个链接 https://dev59.com/XnVC5IYBdhLWcg3w4Vf6#153047 会有所帮助! - Rayon
事件在到达窗口之前会穿越多个阴影边界,因此似乎无法确切知道用户是否单击了我的组件内部。如果点击,event.target 将是 host 元素。 - guest271314
4个回答

9
您可以尝试使用事件对象的“path”属性。尽管MDN还没有为其创建页面,但HTML5Rocks在其影子DOM教程中有一个小节介绍它。因此,我不知道这在各种浏览器中的兼容性情况。

关于事件路径,我发现了W3规范,但不确定它是否确切地适用于“Event.path”属性,但这是我能找到的最接近的参考。

如果有人知道有关“Event.path”的实际规范,请随时进行编辑(如果链接的规范页面不是它)。 它保存了事件经过的路径。其中包括在影子DOM中的元素。列表中的第一个元素(path[0])应该是实际被点击的元素。请注意,您需要从影子DOM引用中调用contains,例如shadowRoot.contains(e.path[0])或者影子DOM中的某个子元素。

Demo: 点击菜单展开,在菜单项以外的任何地方点击都将关闭菜单。

var host = document.querySelector('#host');
var root = host.createShadowRoot();
d = document.createElement("div");
d.id = "shadowdiv";

d.innerHTML = `
  <div id="menu">
    <div class="menu-item menu-toggle">Menu</div>
    <div class="menu-item">Item 1</div>
    <div class="menu-item">Item 2</div>
    <div class="menu-item">Item 3</div>
  </div>
  <div id="other">Other shadow element</div>
`;
var menuToggle = d.querySelector(".menu-toggle");
var menu = d.querySelector("#menu");
menuToggle.addEventListener("click",function(e){
  menu.classList.toggle("active");
});
root.appendChild(d)

//Use document instead of window
document.addEventListener("click",function(e){
  if(!menu.contains(e.path[0])){
    menu.classList.remove("active");
  }
});
#host::shadow #menu{
  height:24px;
  width:150px;
  transition:height 1s;
  overflow:hidden;
  background:black;
  color:white;
}
#host::shadow #menu.active {
  height:300px;
}
#host::shadow #menu .menu-item {
  height:24px;
  text-align:center;
  line-height:24px;
}

#host::shadow #other {
  position:absolute;
  right:100px;
  top:0px;
  background:yellow;
  width:100px;
  height:32px;
  font-size:12px;
  padding:4px;
}
<div id="host"></div>


请注意,由于 <div>width 是窗口的 width,如果单击发生在 <button> 右侧,则会在 console 中记录 true - guest271314
@guest271314,这是预期的,因为它是在影子根内部的“div”,所以shadowRoot.contains()会返回true。添加了一个实际元素的日志,以显示它是内部的div。 - Patrick Evans
1
@guest271314,修改了示例以展示我认为 OP 所想要的更多内容。在代码片段中,除菜单项外,单击任何内容都将关闭菜单。如果他们使用 event.target,因为它被重定向到主机,您将无法区分影子 DOM 中的菜单项和任何其他元素。 - Patrick Evans
4
现在,你可以使用Event.composedPath。请参阅DOM标准中的Event接口:“返回事件路径上的项目对象(将调用侦听器的对象),除了任何阴影树中的节点以外,这些节点的阴影根模式为“closed”,而且从事件的currentTarget不可到达。” - kleinfreund

6

因为声望不够,无法进行评论,但是我想分享一下使用composedPath的正确方式。请参考确定用户是否点击了影子DOM之外

document.addEventListener("click",function(e){
  if(!e.composedPath().includes(menu)){
    menu.classList.remove("active");
  }
});

这应该是新的被接受的答案。 - Jeffrey Nicholson Carré

1
shadowRootevent.target 将是 host 元素。如果 event.target 不是 host 元素,你可以使用 if (evt.target !== hostElement) 来关闭 shadowDOM 中的 <select> 元素,然后在 hostElement 上调用 .blur()。保留html标签。

var input = document.querySelector("input");
var shadow = input.createShadowRoot();
var template = document.querySelector("template");
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

window.addEventListener("mousedown", function (evt) {
  if (evt.target !== input) {
    input.blur();
  }
});
<input type="date" />
<template>
  <select>
    <option value="1999">1999</option>
    <option value="2000">2000</option>
  </select>
</template>


如果只有一层影子 DOM,则事件将被重新定向到宿主元素。但是,如果宿主元素位于另一个元素的影子 DOM 中,则在到达文档/窗口上的事件处理程序之前,事件将被重新定向两次。如果我表述不清楚,敬请谅解。 - ovangle

0
另一个选项是检查事件光标偏移量与目标元素的关系:
listener(event) {
    const { top, right, bottom, left } = targetElement.getBoundingClientRect();
    const { pageX, pageY } = event;
    const isInside = pageX >= left && pageX <= right && pageY >= top && pageY <= bottom;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接