【问题标题】:SVG element loses event handlers if moved around the DOM如果在 DOM 中移动,SVG 元素会丢失事件处理程序
【发布时间】:2014-11-19 01:52:36
【问题描述】:

我使用这个 D3 sn-p 将 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 中都会丢失,在其他浏览器中仍然可以正常工作. 我该如何解决?

【问题讨论】:

  • 我不熟悉d3的上下文,但你能选择像d3.select('body .selector').on(...这样的东西吗?
  • 尝试 insertBefore 以在该节点之前插入每个节点。也许它不是越野车。顺便提一句。如果你发现了一个 msie 错误,为什么不报告呢?
  • 你能提供一个测试用例吗?我已经在 IE 11.0.9600.17280 上尝试了 jsfiddle.net/7po8tdka/1jsfiddle.net/7po8tdka/2 并且无法重现该问题。
  • 我也尝试过使用 svg 矩形:jsfiddle.net/7po8tdka/4jsfiddle.net/7po8tdka/5 以及 <g> 元素 jsfiddle.net/7po8tdka/6,效果很好......
  • 你运行的是什么版本的IE?

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


【解决方案1】:

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

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

这是一个简单的例子:

// 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;
  });
});

【讨论】:

    【解决方案2】:

    一种解决方案是使用事件委托。这种相当简单的范例在 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 标签,因为您永远不会将孩子移出该标签。例如:

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

    【讨论】:

    • 鼠标悬停时小提琴中的方块是否应该高于其他方块?
    • 没有。该演示只是在 #g2 的鼠标悬停/退出时向控制台记录一条消息,但您可以在鼠标悬停时调用 moveToFront 而不是 jsfiddle.net/f8bfw4y8/5
    • 几个问题:A) 我们要监听的元素不能从它原来的父元素下面移出。 B)每个父级只允许一个被监视的元素(d3 selection.on() 只允许每个元素的每个事件类型一个侦听器。C)如果您使用命名空间,则可以绕过(B),但是如果您在every single listener 将为 every 事件调用相同的父级(您可能(取决于 d3 实现)将负载减少 event.stopPropagation()(在 IE
    • @Makyen 感谢您的反馈。因此,我已经解决了这些问题: A) 委托的侦听器可以是任何父级 - 甚至是 body。 B) 更新以允许所有匹配的子对象的侦听器
    • 更好。但是,在事件处理程序中,您不应该有额外的 DOM walk .selectAll() 需要。事件处理程序应该尽可能快,即使这会牺牲一点概括。这就是为什么像这样的东西还没有作为 d3 的一部分存在的原因;这是不好的做法。为活动目标分配唯一属性(例如类)并使用对event.target.className 的一次访问来选择执行/不执行要好得多。如果泛化,您还应该像selection.on()一样实现传递null(删除所有侦听器)和undefined(返回当前侦听器)。
    【解决方案3】:

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

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

    真正感兴趣的元素的事件监听器

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

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

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

    如果每个 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 语句以仅恢复侦听器if the browser is IE11, or IE

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

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

    我可以对此进行扩展,但使其真正的健壮程度取决于您的代码。它可以简单到只处理 5 到 10 个实际不同的函数,您可以将它们用作侦听器。它可能需要像一个完整的通用解决方案一样健壮,以记录任何可能数量的听众。这取决于我们不了解您的代码的信息。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-07-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多