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

时间:2014-09-24 14:12:26

标签: javascript html internet-explorer svg d3.js

我使用此D3片段将SVG g元素移动到其余元素的顶部,因为SVG渲染顺序取决于order of elements in 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中丢失,仍然正常工作其它浏览器。 我该如何解决它?

3 个答案:

答案 0 :(得分:4)

父元素或更高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()

然后,您需要在appendChild moveToFront()之后调用的函数。它可以包含if语句,仅恢复监听器if the browser is IE11, or IE

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

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

我可以对此进行扩展,但使真正的强大程度取决于您的代码。它可以简单到只用5-10个实际上不同的函数,你用作听众。它可能需要非常强大,才能成为记录任何可能数量的侦听器的完整通用解决方案。这取决于我们不了解您的代码的信息。

我希望其他人能够为IE11提供一个简单易用的修复程序,你只需要设置一些属性,或调用一些方法让IE不要放弃听众。然而,蛮力方法将解决问题。

答案 1 :(得分:3)

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

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

所以而不是:

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

你会使用:

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

现在,移动元素时是否丢失事件无关紧要,或者即使在创建侦听器后添加/编辑/删除元素也无关紧要。

Here's the demo.在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/

更新和改进......

根据Makyen的有用反馈,我做了一些改进,允许委派的监听器应用于所有匹配的孩子。 EG“在svg中的每个g上监听鼠标悬停

Here's the fiddle。下面的代码段。

//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标签的事件,因为你永远不会把孩子移到那个之外。 EG:

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

答案 2 :(得分:3)

这也发生在11之前的IE中。我发生此错误的原因模型是,如果您将鼠标悬停在某个元素上,然后通过分离并重新附加它将其移到前面,{{1因为IE失去了mouseout过去发生的状态而没有触发mouseover事件,所以事件不会被触发。

这似乎是为什么如果你移动所有其他元素你正在悬停的元素,它可以正常工作。这是使用mouseout轻松实现的目标。有关详细信息,请参阅d3 documentation on sort以及selection.sortselection.order源代码。

这是一个简单的例子:

selection.sort(comparatorFunction)