javascript - 为什么点击事件不总是触发?

标签 javascript events d3.js

如果您正在重新讨论此问题,我已将所有更新移至底部,因此它实际上是个更好的问题。

问题

使用D3处理浏览器事件时,我遇到了一个奇怪的问题。不幸的是,这存在于一个很大的应用程序中,并且由于我完全不知道原因是什么,所以我一直在努力寻找一个小的可复制示例,因此我将尽可能多地提供有用的信息。

所以我的问题是,对于某些DOM元素,click事件似乎无法可靠地触发。我有两组不同的元素填充的圈子和白色圈子。您可以在下面的屏幕快照中看到1002和1003是白色圆圈,而Suppliers是实心圆圈。

现在这个问题只有出现在我不了解的白色圆圈上。下面的屏幕截图显示了当我单击圆圈时会发生什么。点击顺序通过红色数字显示,并与它们相关联的日志记录。本质上,您看到的是:

  • 鼠标按下
  • mouseup
  • 有时单击

  • 这个问题有点零星。我设法找到了可实现的再现,但是在刷新浏览器几次之后,现在很难再现了。如果我交替单击1002和1003,那么我会不断收到mousedownmouseup事件,但从未收到click。如果我第二次单击其中一个,则会收到click事件。如果我继续单击同一按钮(此处未显示),则仅每隔一次单击就会触发click事件。

    如果我用供应商这样的实心圆圈重复相同的过程,那么它将正常工作,并且每次都触发click

    如何创建圆

    因此,圆(在我的代码中也称为行星)已被创建为模块化组件。在那里循环数据并为每个数据创建一个实例
    data.enter()
        .append("g")
        .attr("class", function (d) { return d.promoted ? "collection moon-group" : "collection planet-group"; })
        .call(drag)
        .attr("transform", function (d) {
            var scale = d.size / 150;
            return "translate(" + [d.x, d.y] + ") scale(" + [scale] + ")";
        })
        .each(function (d) {
    
            // Create a new planet for each item
            d.planet = new d3.landscape.Planet()
                                  .data(d, function () { return d.id; })
                                  .append(this, d);
        });
    

    这并不能告诉您太多信息,在Force Directed图的下方用于计算位置。 Planet.append()函数中的代码如下:
    d3.landscape.Planet.prototype.append = function (target) {
        var self = this;
    
        // Store the target for later
        self.__container = target;
        self.__events = new custom.d3.Events("planet")
                                        .on("click", function (d) { self.__setSelection(d, !d.selected); })
                                        .on("dblclick", function (d) { self.__setFocus(d, !d.focused); self.__setSelection(d, d.focused); });
    
        // Add the circles
        var circles = d3.select(target)
                        .append("circle")
                        .attr("data-name", function (d) { return d.name; })
                        .attr("class", function(d) { return d.promoted ? "moon" : "planet"; })
                        .attr("r", function () { return self.__animate ? 0 : self.__planetSize; })
                        .call(self.__events);
    

    在这里,我们可以看到附加的圆圈(请注意,每个Planet实际上只是一个圆圈)。 custom.d3.Events的构造和调用是针对刚刚添加到DOM中的圆。该代码用于实心圆和白色圆,唯一的区别是类中的细微差异。为每个对象生成的DOM看起来像:

    填充

    <g class="collection planet-group" transform="translate(683.080338895066,497.948470463691) scale(0.6666666666666666,0.6666666666666666)">   
      <circle data-name="Suppliers" class="planet" r="150"></circle>
      <text class="title" dy=".35em" style="font-size: 63.1578947368421px;">Suppliers</text>   
    </g>
    

    白色的

    <g class="collection moon-group" transform="translate(679.5720546510213,92.00957926233855) scale(0.6666666666666666,0.6666666666666666)">      
      <circle data-name="1002" class="moon" r="150"></circle>   
      <text class="title" dy=".35em" style="font-size: 75px;">1002</text>
    </g>
    

    custom.d3.events有什么作用?

    其背后的想法是提供比默认情况下更丰富的事件系统。例如,允许双击(不会触发单击)和长按等。

    当使用circle容器调用事件时,将执行以下操作,并使用D3设置一些raw事件。这些与Planet.append()函数中连接的对象不同,因为events对象公开了它自己的自定义分派(dispatch)。这些是我用于调试/记录的事件;
    custom.d3.Events = function () {
    
       var dispatch = d3.dispatch("click", "dblclick", "longclick", "mousedown", "mouseup", "mouseenter", "mouseleave", "mousemove", "drag");
    
       var events = function(g) {
           container = g;
    
           // Register the raw events required
           g.on("mousedown", mousedown)
            .on("mouseenter", mouseenter)
            .on("mouseleave", mouseleave)
            .on("click", clicked)
            .on("contextmenu", contextMenu)
            .on("dblclick", doubleClicked);
    
           return events;
       };
    
       // Return the bound events
       return d3.rebind(events, dispatch, "on");
    }
    

    因此,在这里,我将介绍一些事件。以相反的顺序查看它们:

    点击

    将click函数设置为仅记录我们正在处理的值
     function clicked(d, i) {
        console.log("clicked", d3.event.srcElement);
        // don't really care what comes after
     }
    

    鼠标向上

    mouseup函数实际上记录并清除了一些全局窗口对象,这将在下面讨论。
     function mouseup(d, i) {
        console.log("mouseup", d3.event.srcElement);
        dispose_window_events();
     }
    

    按下鼠标

    mousedown功能稍微复杂一点,我将包括它的全部内容。它可以做很多事情:
  • 将鼠标记录到控制台上
  • 设置窗口事件(在窗口对象上连接mousemove/mouseup),因此即使鼠标不再位于触发mousedown的圆圈内,也可以触发mouseup
  • 查找鼠标位置并计算一些阈值
  • 设置计时器以触发长按
  • 触发位于custom.d3.event对象上的mousedown调度
    function mousedown(d, i) {
       console.log("mousedown", d3.event.srcElement);
    
       var context = this;
       dragging = true;
       mouseDown = true;
    
       // Wire up events on the window
       setup_window_events();
    
       // Record the initial position of the mouse down
       windowStartPosition = getWindowPosition();
       position = getPosition();
    
       // If two clicks happened far apart (but possibly quickly) then suppress the double click behaviour
       if (windowStartPosition && windowPosition) {
           var distance = mood.math.distanceBetween(windowPosition.x, windowPosition.y, windowStartPosition.x, windowStartPosition.y);
           supressDoubleClick = distance > moveThreshold;
       }
       windowPosition = windowStartPosition;
    
       // Set up the long press timer only if it has been subscribed to - because
       // we don't want to suppress normal clicks otherwise.
       if (events.on("longclick")) {
           longTimer = setTimeout(function () {
               longTimer = null;
               supressClick = true;
               dragging = false;
               dispatch.longclick.call(context, d, i, position);
           }, longClickTimeout);
       }
    
       // Trigger a mouse down event
       dispatch.mousedown.call(context, d, i);
       if(debug) { console.log(name + ": mousedown"); }
    }
    


  • 更新1

    我应该补充一点,我在Chrome,IE11和Firefox中已经经历过这一过程(尽管这似乎是最可靠的浏览器)。

    不幸的是,经过一些刷新和代码更改/恢复后,我一直在努力获得可靠的再现。但是我发现奇怪的是,以下顺序可以产生不同的结果:
  • F5刷新浏览器
  • 单击1002

  • 有时,这会触发mousedownmouseupclick。有时它会错过click。在同一页面的两个不同负载之间偶尔会发生此问题,这似乎很奇怪。

    我还应该补充说,我已经尝试了以下方法:
  • 导致mousedown失败,并验证click是否仍然触发,以确保mousedown中的偶发错误不会引起此问题。如果click中有错误,我可以确认mousedown将触发事件。
  • 尝试检查计时问题。我是通过在mousedown中插入一个长的阻塞循环来完成此操作的,并且可以确认mouseupclick事件将在相当长的延迟后触发。因此,事件确实确实按预期执行。


  • 更新2

    @CoolBlue发表评论后的快速更新是,向我的事件处理程序添加 namespace 似乎没有任何区别。以下仍然偶尔遇到此问题:
    var events = function(g) {
        container = g;
    
        // Register the raw events required
        g.on("mousedown.test", mousedown)
         .on("mouseenter.test", mouseenter)
         .on("mouseleave.test", mouseleave)
         .on("click.test", clicked)
         .on("contextmenu.test", contextMenu)
         .on("dblclick.test", doubleClicked);
    
        return events;
    };
    

    另外,css是我尚未提到的东西。两种不同类型之间的CSS应该相似。完整的集合如下所示,尤其是仅将圆圈中间的标签的point-events设置为none。我已经避免在某些测试中单击它,但据我所知,它似乎并没有太大区别。
    /* Mixins */
    /* Comment here */
    .collection .planet {
      fill: #8bc34a;
      stroke: #ffffff;
      stroke-width: 2px;
      stroke-dasharray: 0;
      transition: stroke-width 0.25s;
      -webkit-transition: stroke-width 0.25s;
    }
    .collection .title {
      fill: #ffffff;
      text-anchor: middle;
      pointer-events: none;
      -webkit-touch-callout: none;
      -webkit-user-select: none;
      -moz-user-select: none;
      -ms-user-select: none;
      user-select: none;
      font-weight: normal;
    }
    .collection.related .planet {
      stroke-width: 10px;
    }
    .collection.focused .planet {
      stroke-width: 22px;
    }
    .collection.selected .planet {
      stroke-width: 22px;
    }
    
    .moon {
      fill: #ffffff;
      stroke: #8bc34a;
      stroke-width: 1px;
    }
    .moon-container .moon {
      transition: stroke-width 1s;
      -webkit-transition: stroke-width 1s;
    }
    .moon-container .moon:hover circle {
      stroke-width: 3px;
    }
    .moon-container text {
      fill: #8bc34a;
      text-anchor: middle;
    }
    .collection.moon-group .title {
      fill: #8bc34a;
      text-anchor: middle;
      pointer-events: none;
      font-weight: normal;
    }
    .collection.moon-group .moon {
      stroke-width: 3px;
      transition: stroke-width 0.25s;
      -webkit-transition: stroke-width 0.25s;
    }
    .collection.moon-group.related .moon {
      stroke-width: 10px;
    }
    .collection.moon-group.focused .moon {
      stroke-width: 22px;
    }
    .collection.moon-group.selected .moon {
      stroke-width: 22px;
    }
    .moon:hover {
      stroke-width: 3px;
    }
    

    更新3

    因此,我尝试排除其他问题。一种是更改CSS,以使white圈子1002和1003现在使用与Suppliers相同的类,因此使用与CSS相同的CSS,而CSS是起作用的类。您可以看到下面的图像和CSS作为证明:

    <g class="collection planet-group" transform="translate(1132.9999823040162,517.9999865702812) scale(0.6666666666666666,0.6666666666666666)">
       <circle data-name="1003" class="planet" r="150"></circle>
       <text class="title" dy=".35em" style="font-size: 75px;">1003</text>
    </g>
    

    我还决定修改custom.d3.event代码,因为这是事件处理中最复杂的部分。我将其剥离回去只是简单地记录:
    var events = function(g) {
        container = g;
    
        // Register the raw events required
        g.on("mousedown.test", function (d) { console.log("mousedown.test"); })
         .on("click.test", function (d) { console.log("click.test"); });
    
        return events;
    };
    

    现在看来这仍然无法解决问题。下面是一个痕迹(现在我不确定为什么每次都会触发两个click.test事件-如果有人能解释它,请欣赏……但是现在将其作为规范)。您会看到,在突出显示的位置上,没有记录click.test,我不得不再次单击-因此在单击之前注册了两次mousedown.test



    更新4

    因此,在收到@CoolBlue的建议后,我尝试研究设置好的d3.behavior.drag。我尝试删除拖动行为的举报,但这样做后看不到任何问题-这可能表明存在问题。这样设计的目的是允许在力向图中拖动圆。因此,我在拖动中添加了一些日志记录,以便随时关注发生的情况:
    var drag = d3.behavior.drag()
                 .on("dragstart", function () { console.log("dragstart"); self.__dragstart(); })
                 .on("drag", function (d, x, y) { console.log("drag", d3.event.sourceEvent.x, d3.event.sourceEvent.y); self.__drag(d); })
                 .on("dragend", function (d) { console.log("dragend"); self.__dragend(d); });
    

    我还指出了drag eventd3代码库,其中有suppressClick标志。因此,我对此做了些微修改,以查看这是否抑制了我期望的点击次数。
    return function (suppressClick) {
         console.log("supressClick = ", suppressClick);
         w.on(name, null);
         ...
    }
    

    这样的结果有点奇怪。我将所有日志记录合并在一起,以说明4个不同的示例:
  • 蓝色:单击正确触发,我注意到suppressClick是错误的。
  • 红色:该点击没有触发,似乎是我不小心触发了移动,但suppressClick仍然为假。
  • 黄色:点击确实触发了,suppressClick仍然为假,但有意外 Action 。我不知道为什么这不同于以前的红色。
  • 绿色:单击时我故意移动了一点,将suppressClick设置为true,但单击没有触发。



  • 更新5

    因此,再深入研究D3代码,我真的无法解释我在更新4中详述的行为中看到的不一致之处。我只是尝试了一些不同的尝试,以查看它是否做了我所希望的预期的。基本上,我是在将D3强制设为,而从禁止取消点击。所以在drag event
    return function (suppressClick) {
        console.log("supressClick = ", suppressClick);
        suppressClick = false;
        w.on(name, null);
        ...
    }
    

    完成此操作后,我仍然设法失败了,这引发了一个问题,即是否确实是引起它的preventClick标志。这也可能通过更新#4解释了控制台中的不一致情况。我也尝试在其中增加setTimeout(off, 0),但这并不能阻止所有点击都像我期望的那样触发。

    因此,我认为这表明suppressClick实际上并不是问题所在。这是一个控制台日志作为证明(并且我还进行了同事双重检查以确保我在这里不丢失任何内容):



    更新6

    我发现可能与此问题有关的另一段代码(但我不确定100%)。在连接到d3.behavior.drag的地方,我使用以下代码:
     var drag = d3.behavior.drag()
                 .on("dragstart", function () { self.__dragstart(); })
                 .on("drag", function (d) { self.__drag(d); })
                 .on("dragend", function (d) { self.__dragend(d); });
    

    因此,我一直在研究self.__dragstart()函数,并注意到了d3.event.sourceEvent.stopPropagation();。这些功能没有更多的东西(通常只是开始/停止力导向图和更新线的位置)。

    我想知道这是否会影响点击行为。如果我取出此stopPropagation,则我的整个表面开始平移,这是不希望的,因此这可能不是答案,但可能是另一种调查途径。

    更新7

    我忘记添加到原始问题中的一种可能的明显排放。可视化还支持缩放/平移。
     self.__zoom = d3.behavior
                            .zoom()
                            .scaleExtent([minZoom, maxZoom])
                            .on("zoom", function () { self.__zoomed(d3.event.translate, d3.event.scale); });
    

    现在要实现此目的,实际上所有内容的顶部都有一个大矩形。所以我的顶层svg实际上看起来像:

    <svg class="galaxy">
       <g width="1080" height="1795">
          <rect class="zoom" width="1080" height="1795" style="fill: none; pointer-events: all;"></rect>
       <g class="galaxy-background" width="1080" height="1795" transform="translate(-4,21)scale(1)"></g>
       <g class="galaxy-main" width="1080" height="1795" transform="translate(-4,21)scale(1)">
       ... all the circles are within here
       </g>
    </svg>
    

    当我在d3.event.sourceEvent.stopPropagation();上的drag事件的回调中关闭d3.behaviour.drag时,我记得这一点。这阻止了进入我的圈子的任何单击事件,这使我有些困惑,然后我在检查DOM时记得那个大矩形。我不太确定为什么现在重新启用传播会阻止点击。

    最佳答案

    我最近又遇到了这个问题,幸运的是,我们设法隔离了问题并解决了问题。

    实际上,这是由于mousedown事件中注册了某些内容,该事件将DOM元素svg:circle根据z顺序移至顶部。它通过将其取出DOM并在适当的位置重新插入来实现。

    这将产生如下所示的内容:

  • mouseenter
  • mousedown
  • (移动DOM元素,但保留相同的事件包装器)
  • mouseup

  • 问题在于,就浏览器而言,mousedownmouseup几乎发生在DOM中的不同元素上,移动它已使事件模型困惑。

    因此,在我的情况下,如果原始click发生在同一元素中,我就通过手动在mouseup上触发mousedown事件来应用此修复程序。

    关于javascript - 为什么点击事件不总是触发?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31243959/

    相关文章:

    javascript - 使用 d3 画笔选择折线图中的元素

    javascript - d3.js:异常错误

    javascript - JS pageX和pageY取坐标加上相对位置div的margin?

    javascript - Node.js:无法将 node.js 应用程序部署到 heroku

    java - JSP onclick 事件在传递链接中更新的变量之前

    javascript - FileList 中的 Safari 错误?

    c# - 返回键的Key Down事件不起作用

    java - 如何发布仅包含键盘字符(无修饰符)的 SWT 键事件?

    javascript - 使用d3绘制散点图

    javascript - 如何在 Jsrender 模板中使用 javascript 变量