我正在尝试制作各种SVG编辑器。长话短说,我需要将鼠标事件附加到给定SVG内特定深度的<g>
元素。由于各种原因,我无法提前知道身份证。 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");
});
此代码有效,但它需要很长时间才能开始。假设我有十个这样的元素将匹配特定的选择。似乎在页面加载后的每一秒中,另外一个实际上获得了附加的鼠标事件。我想知道每次d3附加一个事件时我是否可以打印一个控制台事件,或者我怎么知道d3是否完成了附加所需的一切。
基本上这个JSFiddle需要更快地加载鼠标事件。如果等待几秒钟,您会看到越来越多的盒子在工作。
这是一个非常有趣的问题,我设法让它工作,但我没有解释为什么这个工作。如果有深入了解的人会解释这一点,我将不胜感激。
慢:
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");
});
快速:
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");
});
不同之处仅在于在将事件处理程序附加到它们之前对元素应用填充 - .style("fill", "white").on("mouseover",
小提琴玩 - https://jsfiddle.net/v8e4hnff/1/
注意:还试图在SVG元素上使用JS本机选择器和事件处理程序附件实现,这比D3快得多。 IE11和Chrome上的行为相同。
如上所述,如果有人能解释这种行为,请做!
事实证明,这是臭名昭着的pointer-events
与fill
麻烦的错综复杂的变化。事件处理程序实际上是立即附加到<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");
});
rect {
stroke: red;
stroke-width: 0.2;
stroke-dasharray: 1.5 1.5;
fill:none;
}
<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>
当浏览器确定哪个元素将成为指针事件的目标时,它将执行一个称为命中测试的事情:
16.5.1 Hit-testing
确定指针事件是否导致正命中测试取决于指针的位置,graphics element的大小和形状,以及元素上‘pointer-events’属性的计算值。
上面的句子包含两个重要信息:
<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");
})
正如你在这个JSFiddle中看到的,这些总会有所不同。这与您的方案相关,因为您在组上注册了处理程序函数。这样,只有当组中的图形子元素成为指针事件的目标并且事件冒泡到组本身时,才会执行处理程序。这本身并不是一个问题,但是,结合下一点,这就解释了为什么你的设置不起作用。pointer-events
属性确定“元素是否或何时可能是鼠标事件的目标”。因为从未在整个代码中设置此属性,所以默认启动,即visiblePainted
定义如下(强调我的):
当visibility属性设置为visible并且鼠标光标位于元素的内部(即'fill')并且fill属性设置为none以外的值时,该元素只能是鼠标事件的目标,或当鼠标光标位于元素的周边(即“笔划”)上时,笔划属性设置为非none值。
正如其他人在评论中指出的那样,你们组中的相关<path>
元素都具有定义st8
的类fill: none
,从而防止它们在悬停其内部时成为事件目标,即填充。当这些路径不能成为指针事件的目标时,没有事件可能会冒泡到您的组,这会使事件侦听器无效。
如果第一次在一个元素上执行了一个监听器(为什么会发生这种情况在下面解释,所以请暂时跟我一起),这个问题通过在路径上设置fill
属性来解决,从而使它成为指针的合法目标事件。这就是为什么处理人员在他们第一次复活时会继续运作的原因。
附注:此效果非常强大,甚至会影响开发工具在Chrome和Firefox中处理这些元素的方式。当您尝试检查一个已设置为none的元素时,通过右键单击它,开发工具将打开引用根<svg>
元素而不是您单击的元素,因为后者不是事件的目标。相比之下,尝试使用事件处理程序已经运行的元素,可以这么说,它将为这个元素打开开发工具。对此的简单解决方案是通过将属性显式设置为all
来允许指针事件在路径的内部(即填充)上发生:
当指针位于元素的内部(即填充)或周边(即笔划)上时,该元素只能是鼠标事件的目标。
fill
,stroke
和visibility
属性的值不会影响事件处理。
这可以在注册事件处理程序之前完成,就像在我更新的JSFiddle中一样:
d3.selectAll("svg > g > g").select("g").select("g")
.attr("pointer-events", "all")
.on("mouseover", function() {
//...
}
上面提供了一个适当的分析和工作解决方案,但是,如果你给它一些时间沉入,仍然存在一个问题,为什么处理程序似乎是注册的,或者至少是这样的延迟激活。关于这一点的更多思考,事实证明,理解这个问题的所有信息已经包含在我的解释中。
正如我上面所说,<path>
元素实际上将是事件目标,而不是组。由于pointer-events
属性默认为visiblePainted
,因此可以看到重新阅读上述规范时,指针事件并非完全无法访问:
[...]或当鼠标光标位于元素的周边(即“笔划”)上时,笔划属性设置为非none值。
尽管臭名昭着的类st8
设置stroke: ff0000
(显然不是没有),它指定stroke-width:0.24
这是一个非常细的线。事实证明,事实证明,很难完全击中线路。但是,如果你真的打了它,它将导致路径成为事件目标,事件冒泡到组,最终执行事件处理程序。通过将stroke-width
设置为更大的值可以更容易地触及路径来演示此效果:
.st8 {
fill:none;
stroke:#ff0000;
stroke-dasharray:1.68,1.2;
stroke-linecap:round;
stroke-linejoin:round;
stroke-width:2 /* Set to 2 to make it easier to hit */
}
看看这个JSFiddle的工作演示。
即使没有设置pointer-events: all
,这也行,因为这些线现在足够宽,可以被指针击中。因为胖线是丑陋的并且会破坏精细的布局,但这更像是一个演示而不是一个真正的解决方案。