事件监听器如何/何时在d3.js中附加?

Ben*_*Ben 5 javascript svg dom-events d3.js

我正在尝试制作各种SVG编辑器.长话短说,我需要将鼠标事件附加到<g>给定SVG内特定深度的元素.由于各种原因,我无法提前知道身份证.SVG非常庞大,即使不是数千个元素也会有数百个.

d3.selectAll("svg > g > g > g").select("g").on("mouseover", function() {
    console.log("mouseover");
  }).on("mouseout", function() {
    console.log("mouseout");          
  }).on("click", function() {
    console.log("clicked");
  });
Run Code Online (Sandbox Code Playgroud)

此代码有效,但它需要很长时间才能开始.假设我有十个这样的元素将匹配特定的选择.似乎在页面加载后的每一秒中,另外一个实际上获得了附加的鼠标事件.我想知道每次d3附加一个事件时我是否可以打印一个控制台事件,或者我怎么知道d3是否完成了附加所需的一切.

基本上这个JSFiddle需要更快地加载鼠标事件.如果等待几秒钟,您会看到越来越多的盒子在工作.

alt*_*lus 7

tl; dr

事实证明,这是臭名昭著pointer-eventsfill麻烦之间的错综复杂的变化。实际上,事件处理程序会立即附加到<g>元素上。但是,它们会在一段时间内不执行,因为事件在大多数情况下不会进入这些元素。设置pointer-events: all确实可以轻松解决此问题。

除了技术问题之外,这还是一个很好的例子,说明了为什么您应该提供一个最小的例子,即把事情简化到最低限度。大量的代码使不必要的攻击变得困难。以下代码片段仅包含足以说明问题的代码:

d3.select("g").on("mouseover", function() {
  // The difference between below log entries shows, that the event was
  // targeted at another element and bubbled up to this handler's element.
  console.dir(d3.event.target.tagName);   // <rect>: actual target for this event
  console.dir(this.tagName);              // <g>:    element this handler is attached to

  d3.select(this).select("rect")
    .style("fill", "orange");
});
Run Code Online (Sandbox Code Playgroud)
rect {
  stroke: red;
  stroke-width: 0.2;
  stroke-dasharray: 1.5 1.5;
  fill:none;
}
Run Code Online (Sandbox Code Playgroud)
<script src="https://d3js.org/d3.v4.js"></script>
<svg width="300" height="300">
  <g>
    <rect x="20" y="20" width="200" height="200"/>
  </g>
</svg>
Run Code Online (Sandbox Code Playgroud)


分析

当浏览器确定哪个元素将成为指针事件的目标时,它将执行称为命中测试的操作:

16.5.1命中测试

确定指针事件是否导致肯定的命中测试取决于指针的位置,图形元素的大小和形状以及元素'pointer-events'属性的计算值。

上面的句子包含有关您的问题的两个重要信息:

  1. 仅图形元素可以成为指针事件的直接目标,而仅<g>元素本身不能单独成为这些事件的目标。但是,这些事件可能会冒出泡沫并最终到达该组。在事件处理程序中,您可以记录d3.event.target以及中引用的事件的实际目标,该目标this指向该处理程序附加到的元素:

    .on("mouseover", function() {
      // The difference between below log entries shows, that the event was
      // targeted at another element and bubbled up to this handler's element.
      console.log(d3.event.target);   // <path>: actual target for this event
      console.log(this);              // <g>:    element this handler is attached to
    
      d3.select(this).select("path")
        .style("fill", "orange");
    })
    
    Run Code Online (Sandbox Code Playgroud)

    如您在此JSFiddle中所看到的,它们将始终不同。这与您的方案有关,因为您在组上注册了处理程序功能。这样,仅当组的图形子元素成为指针事件的目标且事件冒泡到组本身时,处理程序才会执行。就其本身而言,这不是什么大问题,但是结合下一点,这可以解释为什么您的设置无法正常工作。

  2. pointer-events属性确定“元素是否或何时成为鼠标事件的目标”。因为在整个代码中都没有设置此属性,所以默认值会插入,其visiblePainted定义如下(强调我的意思):

    仅当可见性属性设置为visible且鼠标光标位于元素的内部(即“填充”)并且fill属性设置为除none之外的其他值时,该元素才可以是鼠标事件的目标。,或者当鼠标光标悬停在元素的边界(即“笔触”)上并且笔触属性设置为除none之外的其他值时。

    正如其他人在评论中指出的那样,<path>您组中的所有相关元素均具有st8定义的类fill: none,从而防止它们在悬停其内部(即填充)时成为事件目标。当这些路径不能成为指针事件的目标时,就没有任何事件可能会扩展到您的组,这会使事件侦听器无用。

    如果侦听器是第一次在某个元素上执行的(为什么会发生这种情况,下面对此进行说明,请耐心等待),此问题将通过fill在路径上设置该属性来解决,从而使其成为指针事件的合法目标。这就是为什么处理程序在初次出现时将继续运行的原因。

    旁注:此效果非常强大,甚至会影响开发工具在Chrome和Firefox中处理这些元素的方式。当您尝试检查填充设置为none的元素时,通过右键单击它,开发工具将打开引用根<svg>元素而不是您单击的元素的参考,因为后者不是事件的目标。相比之下,可以尝试使用事件处理程序已经在工作的元素,可以这样说,它将为该元素完全打开开发工具。

一个简单的解决方案是通过将属性显式设置为,使指针事件发生在路径的内部(即,填充)all

仅当指针位于元素的内部(即填充)或外围(即笔触)上方时,该元素才能成为鼠标事件的目标。该值fillstroke并且visibility属性不影响事件处理。

最好像在我更新的JSFiddle中那样在注册事件处理程序之前立即完成此操作:

d3.selectAll("svg > g > g").select("g").select("g")
  .attr("pointer-events", "all")
  .on("mouseover", function() {
    //...
  }
Run Code Online (Sandbox Code Playgroud)

为什么有时会起作用,为什么会延迟呢?

上面提供了适当的分析和可行的解决方案,但是,如果您给它一些时间沉浸其中,那么仍然存在一个问题,为什么在地面上似乎已注册处理程序,或者至少在这样的延迟下激活了这些处理程序。我对此进行了更多的思考,事实证明,所有了解该问题的信息已包含在我的解释中。

如上所述,<path>元素实际上是事件目标,而不是组。使用pointer-events默认属性时,visiblePainted对于指针事件它们并不是完全不可访问的,可以重新阅读上述规范:

[…]或当鼠标光标悬停在元素的外围(即“笔触”)并且笔触属性设置为除none之外的值时。

尽管臭名昭著的类st8stroke: ff0000(显然不是),但它指定stroke-width:0.24了一条很细的线。事实证明,如果加上破折号,根本很难达到目标。但是,如果您确实点击了它,它将导致该路径成为事件目标,并且事件冒泡到该组,最终执行事件处理程序。可以通过将设置stroke-width为更大的值来证明这一效果,使其更容易上路:

.on("mouseover", function() {
  // The difference between below log entries shows, that the event was
  // targeted at another element and bubbled up to this handler's element.
  console.log(d3.event.target);   // <path>: actual target for this event
  console.log(this);              // <g>:    element this handler is attached to

  d3.select(this).select("path")
    .style("fill", "orange");
})
Run Code Online (Sandbox Code Playgroud)

看看这个JSFiddle上的工作演示。

即使没有设置,pointer-events: all它也将起作用,因为行现在足够宽,可以被指针击中。由于粗线很难看并且会破坏精细的布局,因此,这更多的是演示,而不是实际的解决方案。


tib*_*blu 3

这是一个非常有趣的问题,我设法使它起作用,但我没有解释为什么它起作用。如果有深入知识的人能解释这一点,我将不胜感激。

慢的:

var targetElements = d3.selectAll("svg > g > g").select("g").select("g").select("path");
targetElements.on("mouseover", function() {
  d3.select(this)
    .style("fill", "orange");
}).on("mouseout", function() {
  d3.select(this)
    .style("fill", "BLUE");
}).on("click", function() {
  d3.select(this)
    .style("fill", "green");
});
Run Code Online (Sandbox Code Playgroud)

快速地:

var targetElements = d3.selectAll("svg > g > g").select("g").select("g").select("path");
targetElements.style('fill', 'white'); // Black magic - comment this out and the event handler attachment is delayed alot
targetElements.on("mouseover", function() {
  d3.select(this)
    .style("fill", "orange");
}).on("mouseout", function() {
  d3.select(this)
    .style("fill", "BLUE");
}).on("click", function() {
  d3.select(this)
    .style("fill", "green");
});
Run Code Online (Sandbox Code Playgroud)

区别仅在于在将事件处理程序附加到元素之前对元素应用填充 -.style("fill", "white").on("mouseover",

玩的小提琴 - https://jsfiddle.net/v8e4hnff/1/

注意:还尝试在 SVG 元素上使用 JS 本机选择器和事件处理程序附件来实现,这比 D3 快一点点。IE11 和 Chrome 上的行为相同。

如上所述,如果有人可以解释这种行为,请这样做!

  • 尝试更新样式:“.st8 {fill:#FFF;”,它应该可以修复它。您不能将鼠标悬停在空白处 (2认同)