如果在 DOM 周围移动,SVG 元素将丢失事件处理程序

SVG element loses event handlers if moved around the DOM

本文关键字:事件处理 程序 元素 SVG DOM 周围 移动 如果      更新时间:2023-09-26

我使用此 D3 代码段将 SVG g元素移动到其余元素的顶部,因为 SVG 渲染顺序取决于 DOM 中元素的顺序,并且没有 z 索引:

d3.selection.prototype.moveToFront = function () {
  return this.each(function () {
    this.parentNode.appendChild(this);
  });
};

我像这样运行它:

d3.select(el).moveToFront()

我的问题是,如果我添加一个 D3 事件侦听器,例如 d3.select(el).on('mouseleave',function(){}) ,然后使用上面的代码将元素移动到 DOM 树的前面,所有事件侦听器在 Internet Explorer 11 中都会丢失,在其他浏览器中仍然可以正常工作。如何解决它?

父元素上的单个事件侦听器,或更高的 DOM 祖先:

有一个相对简单的解决方案,我最初没有提到,因为我假设你认为它在你的情况下不可行。该解决方案是,不是每个侦听器在单个子元素上有多个侦听器,而是在祖先元素上有一个侦听器,该侦听器为其子元素上某个类型的所有事件调用。 它可以设计为快速选择基于event.targetevent.target.id或更好的event.target.className进一步处理(如果元素是事件处理程序的有效目标,则分配您创建的特定类(。根据事件处理程序正在执行的操作以及已在使用侦听器的祖先下的元素的百分比,单个事件处理程序可以说是更好的解决方案。使用单个侦听器可能会减少事件处理的开销。 但是,任何实际性能差异取决于您在事件处理程序中执行的操作以及您本来放置侦听器的祖先子项的百分比。

实际感兴趣的元素上的事件侦听器

您的

问题询问您的代码放置在要移动的元素上的侦听器。鉴于您似乎并不关心由您无法控制的代码放置在元素上的侦听器,那么解决此问题的蛮力方法是保留侦听器列表以及放置它们的元素。

实现此暴力解决方法的最佳方法在很大程度上取决于您将侦听器放置在元素上的方式、您使用的种类等。这是我们从问题中无法获得的所有信息。没有这些信息,就不可能对如何实现这一点做出已知的正确选择。

仅使用通过selection.on()添加的每个类型/命名空间的单个侦听器:

如果你有一个每个 type.namespace 的侦听器,并且你已经通过 d3.selection.on(( 方法将它们全部添加,并且你没有使用 Capture 类型的侦听器,那么它实际上相对容易。

当仅使用每种类型的单个侦听器时,selection.on() 方法允许您读取分配给元素和类型的侦听器。

因此,您的moveToFront()方法可能变为:

var isIE = /*@cc_on!@*/false || !!document.documentMode; // At least IE6
var typesOfListenersUsed = [ "click", "command", "mouseover", "mouseleave", ...];
d3.selection.prototype.moveToFront = function () {
  return this.each(function () {
    var currentListeners={};
    if(isIE) {
      var element = this;
      typesOfListenersUsed.forEach(function(value){
         currentListeners[value] = element.selection.on(value);
      });
    }
    this.parentNode.appendChild(this);
    if(isIE) {
      typesOfListenersUsed.forEach(function(value){
         if(currentListeners[value]) { 
           element.selection.on(value, currentListeners[value]);
         }
      });
    }
  });
};

您不一定需要检查 IE,因为将侦听器重新放置在其他浏览器中应该不会有什么坏处。但是,这会花费时间,最好不要这样做。

即使您使用相同类型的多个侦听器,也应该能够通过在侦听器列表中指定命名空间来使用它。例如:

var typesOfListenersUsed = [ "click", "click.foo", "click.bar"
                            , "command", "mouseover", "mouseleave", ...];

常规,同一类型的多个侦听器:

如果您使用的是不是通过 d3 添加的侦听器,则需要实现一种通用方法来记录添加到元素的侦听器。

如何记录作为侦

听器添加的函数,您只需向原型添加一个方法,该方法记录您作为侦听器添加的事件。例如:

d3.selection.prototype.recOn = function (type, func) {
  recordEventListener(this, type, func);
  d3.select(this).on(type,func);
};

然后使用 d3.select(el).recOn('mouseleave',function(){}) 而不是 d3.select(el).on('mouseleave',function(){})

鉴于您使用的是通用解决方案,因为您不是通过d3添加一些侦听器,您将需要添加函数来包装调用,但要添加侦听器(例如 addEventListener() (。

然后,您将需要一个函数,该函数在moveToFront()中的appendChild之后调用。它可以包含 if 语句,以便在浏览器是 IE11 或 IE 时仅还原侦听器。

d3.selection.prototype.restoreRecordedListeners = function () {
    if(isIE) {
        ...
    }
};

您需要选择如何存储录制的侦听器信息。这在很大程度上取决于您如何实现我们不知道的其他代码领域。记录哪些侦听器在元素上的最简单方法可能是在侦听器列表中创建一个索引,然后将其记录为一个类。如果您使用的实际不同侦听器函数的数量很少,则这可能是静态定义的列表。 如果数量和种类很大,那么它可能是一个动态列表。

我可以对此进行扩展,但是如何健壮地制作它实际上取决于您的代码。它可以像保持 5-10 个实际不同的功能一样简单,您用作侦听器。 它可能需要像一个完整的通用解决方案一样强大,以记录任何可能数量的侦听器。这取决于我们不知道的有关您的代码的信息。

我希望其他人能够为您提供IE11的简单易用的修复程序,您只需设置一些属性,或调用某些方法以使IE不丢弃侦听器。但是,蛮力方法将解决问题。

一种解决方案可能是使用事件委派。这个相当简单的范式在jQuery中很常见(这给了我在这里尝试的想法。

通过使用委托的事件侦听器扩展d3.selection原型,我们可以侦听父元素上的事件,但只有在事件的目标也是我们想要的目标时才应用处理程序。

所以代替:

d3.select('#targetElement').on('mouseout',function(){})

您将使用:

d3.select('#targetElementParent').delegate('mouseout','#targetElement',function(){})
现在,移动元素

时事件是否丢失,甚至在创建侦听器添加/编辑/删除元素都无关紧要。

这是演示。在Chrome 37,IE 11和Firefox 31上测试。我欢迎建设性的反馈,但请注意,我对 d3 一点也不熟悉.js所以很容易错过一些基本;)

//prototype. delegated events
d3.selection.prototype.delegate = function(event, targetid, handler) {
    return this.on(event, function() {
        var eventTarget = d3.event.target.parentNode,
            target = d3.select(targetid)[0][0];
        if (eventTarget === target) {//only perform event handler if the eventTarget and intendedTarget match
            handler.call(eventTarget, eventTarget.__data__);
        }
    });
};    
//add event listeners insead of .on() 
d3.select('#svg').delegate('mouseover','#g2',function(){
    console.log('mouseover #g2');
}).delegate('mouseout','#g2',function(){
    console.log('mouseout #g2');
})    
//initial move to front to test that the event still works
d3.select('#g2').moveToFront();

http://jsfiddle.net/f8bfw4y8/

更新和改进...

根据 Makyan 的有用反馈,我做了一些改进,以允许将委派的侦听器应用于所有匹配的子级。EG">侦听 svg 中每个 g 上的鼠标悬停">

这是小提琴。下面的代码片段。

//prototype. move to front
d3.selection.prototype.moveToFront = function () {
  return this.each(function () {
    this.parentNode.appendChild(this);
  });
};
//prototype. delegated events
d3.selection.prototype.delegate = function(event, targetselector, handler) {
    var self = this;
    return this.on(event, function() {
        var eventTarget = d3.event.target,
            target = self.selectAll(targetselector);
        target.each(function(){ 
            //only perform event handler if the eventTarget and intendedTarget match
            if (eventTarget === this) {
                handler.call(eventTarget, eventTarget.__data__);
            } else if (eventTarget.parentNode === this) {
                handler.call(eventTarget.parentNode, eventTarget.parentNode.__data__);
            }
        });
    });
};
var testmessage = document.getElementById("testmessage");
//add event listeners insead of .on() 
//EG: onmouseover/out of ANY <g> within #svg:
d3.select('#svg').delegate('mouseover','g',function(){
    console.log('mouseover',this);
    testmessage.innerHTML = "mouseover #"+this.id;
}).delegate('mouseout','g',function(){
    console.log('mouseout',this);
    testmessage.innerHTML = "mouseout #"+this.id;
});
/* Note: Adding another .delegate listener REPLACES any existing listeners of this event on this node. Uncomment this to see. 
//EG2 onmouseover of just the #g3
d3.select('#svg').delegate('mouseover','#g3',function(){
    console.log('mouseover of just #g3',this);
    testmessage.innerHTML = "mouseover #"+this.id;
});
//to resolve this just delegate the listening to another parent node eg:
//d3.select('body').delegate('mouseover','#g3',function(){...
*/
//initial move to front for testing. OP states that the listener is lost after the element is moved in the DOM.
d3.select('#g2').moveToFront();
svg {height:300px; width:300px;}
rect {fill: pink;}
#g2 rect {fill: green;}
#testmessage {position:absolute; top:50px; right:50px;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg id="svg">
    <g id="g1"><rect x="0px" y="0px" width="100px" height="100px" /></g>
    <g id="g2"><rect x="50px" y="50px" width="100px" height="100px" /></g>
    <g id="g3"><rect x="100px" y="100px" width="100px" height="100px" /></g>
</svg>
<div id="testmessage"></div>

与所有委派侦听器一样,如果将目标元素移到已委派侦听的父元素之外,那么该子元素的事件自然会丢失。但是,没有什么可以阻止您委托事件监听body标签,因为您永远不会将孩子移出该标签。例如:

d3.select('body').delegate('mouseover','g',function(){...

这也发生在 11 之前的 IE 中。对于发生此错误的原因,我的心理模型是,如果您将鼠标悬停在一个元素上,然后通过分离并重新附加它将其移动到前面,则mouseout事件不会触发,因为 IE 会失去过去发生mouseover的状态,因此不会触发mouseout事件。

这似乎就是为什么如果您移动所有其他元素,但您悬停在上面的元素之外,它可以正常工作的原因。这就是您可以使用轻松实现的 selection.sort(comparatorFunction) .有关更多详细信息,请参阅有关排序和 selection.sort 和 selection.order 源代码的 d3 文档。

下面是一个简单的示例:

// myElements is a d3 selection of, for example, circles that overlap each other
myElements.on('mouseover', function(hoveredDatum) {
  // On mouseover, the currently hovered element is sorted to the front by creating
  // a custom comparator function that returns “1” for the hovered element and “0”
  // for all other elements to not affect their sort order.
  myElements.sort(function(datumA, datumB) {
    return (datumA === hoveredDatum) ? 1 : 0;
  });
});