简体   繁体   English

为什么不点击事件总是火?

[英]Why doesn't click event always fire?

If you're revisiting this question I've moved all the updates to the bottom so it actually reads better as a question. 如果您正在重新审视此问题,我已将所有更新移至底部,因此它实际上更好地作为一个问题。

The Problem 问题

I've got a bit of a strange problem handling browser events using D3 . 我使用D3处理浏览器事件时遇到了一些奇怪的问题。 Unfortunately this sits in quite a large application, and because I'm completely lost on what the cause is I'm struggling to find a small reproduceable example so I'm going to provide as much hopefully useful information as I can. 不幸的是,这是一个相当大的应用程序,因为我完全迷失了原因,我正在努力寻找一个可重复的小例子,所以我将提供尽可能多的希望有用的信息。

So my problem is that click events don't seem to fire reliably for certain DOM Elements. 所以我的问题是click事件似乎不能为某些DOM元素可靠地触发。 I have two different sets of elements Filled circles and White circles. 我有两组不同的元素填充圆圈和白色圆圈。 You can see in the screenshot below 1002 and 1003 are white circles, while Suppliers is a filled circle. 您可以在下面的屏幕截图中看到10021003是白色圆圈,而供应商是一个填充圆圈。

在此输入图像描述

Now this problem only occurs for the white circles which I don't understand. 现在这个问题发生在我不理解的白色圆圈上。 The screenshot below shows what happens when I click the circles. 下面的屏幕截图显示了点击圈子时会发生什么。 The order of clicks is shown via the red numbers, and the logging associated with them. 点击顺序通过红色数字和与之关联的日志记录显示。 Essentially what you see is: 基本上你看到的是:

  • mousedown 鼠标按下
  • mouseup 鼠标松开
  • sometimes a click 有时点击

The issue is a bit sporadic. 这个问题有点零星。 I had managed to track down a realiable reproduction but after a few refreshes of the browser it's now much harder to reproduce. 我曾设法追踪一个可靠的复制品,但经过浏览器的一些刷新后,它现在更难以重现。 If I alternate click on 1002 and 1003 then I keep getting mousedown and mouseup events but never a click . 如果我替换单击10021003然后我继续获得mousedownmouseup事件但从不click If I click on one of them a second time then I do get a click event. 如果我再次点击其中一个,那么我会收到click事件。 If I keep clicking on the same one (not shown here) only every other click fires the click event. 如果我一直点击同一个(此处未显示),则只有每次点击都会触发click事件。

If I repeat the same process with a filled circle like Suppliers then it works fine and click is fired every single time. 如果我像供应商那样用填充的圆圈重复相同的过程,那么它工作正常并且每次click都会被click


How the Circles are created 如何创建圈子

So the circles (aka Planets in my code) have been created as a modular component. 所以圈子(我的代码中的行星也称为行星)已经被创建为模块化组件。 There for the data is looped through and an instance for each is created 数据循环通过,并为每个数据创建一个实例

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

This doesn't tell you all that much, underneath a Force Directed graph is being used to calculate positions. 这并没有告诉你那么多,在Force Directed图表下方用于计算位置。 The code within the Planet.append() function is as follows: 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);

Here we can see the circles being appended (note each Planet is actually just a single circle). 在这里我们可以看到附加的圆圈(注意每个星球实际上只是一个圆圈)。 The custom.d3.Events is constructed and called for the circle that has just been added to the DOM. 构造并调用custom.d3.Events用于刚刚添加到DOM的圆。 This code is used for both the filled and the white circles, the only difference is a slight variation in the classes. 此代码用于填充和白色圆圈,唯一的区别是类中的轻微变化。 The DOM produced for each looks like: 为每个产生的DOM看起来像:

Filled 填充

<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>

White 白色

<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>

What does custom.d3.events do? custom.d3.events有什么作用?

The idea behind this is to provide a richer event system than you get by default. 这背后的想法是提供比默认情况下更丰富的事件系统。 For example allowing double-clicks (that don't trigger single clicks) and long clicks etc. 例如,允许双击(不会触发单击)和长按等。

When events is called with the circle container is executes the following, setting up some raw events using D3. 当使用circle容器调用事件时,执行以下操作,使用D3设置一些raw事件。 These aren't the same ones that have been hooked up to in the Planet.append() function, because the events object exposes it's own custom dispatch. 这些与Planet.append()函数中的连接不同,因为events对象公开了它自己的自定义调度。 These are the events however that I'm using for debugging/logging; 这些是我用于调试/记录的事件;

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

So in here, I hook up to a few events. 所以在这里,我联系了几个事件。 Looking at them in reverse order: 以相反的顺序查看它们:

click 点击

The click function is set to simply log the value that we're dealing with click函数设置为只记录我们正在处理的值

 function clicked(d, i) {
    console.log("clicked", d3.event.srcElement);
    // don't really care what comes after
 }

mouseup 鼠标松开

The mouseup function essentially logs, and clear up some global window objects, that will be discussed next. mouseup函数基本上记录并清除一些全局窗口对象,这将在下面讨论。

 function mouseup(d, i) {
    console.log("mouseup", d3.event.srcElement);
    dispose_window_events();
 }

mousedown 鼠标按下

The mousedown function is a little more complex and I'll include the entirety of it. mousedown函数稍微复杂一点,我将包含它的全部内容。 It does a number of things: 它做了很多事情:

  • Logs the mousedown to console 将mousedown记录到控制台
  • Sets up window events (wires up mousemove/mouseup on the window object) so mouseup can be fired even if the mouse is no longer within the circle that triggered mousedown 设置窗口事件(在窗口对象上连接mousemove / mouseup),这样即使鼠标不再在触发mousedown的圆圈内也可以触发mouseup
  • Finds the mouse position and calculates some thresholds 查找鼠标位置并计算一些阈值
  • Sets up a timer to trigger a long click 设置计时器以触发长按
  • Fires the mousedown dispatch that lives on the custom.d3.event object 触发生成在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"); } } 

Update 1 更新1

I should add that I have experienced this in Chrome, IE11 and Firefox (although this seems to be the most reliable of the browsers). 我应该补充一点,我在Chrome,IE11和Firefox中经历过这种情况(虽然这似乎是最可靠的浏览器)。

Unfortunately after some refresh and code change/revert I've struggled getting the reliable reproduction. 不幸的是,在一些刷新和代码更改/恢复之后,我一直在努力获得可靠的再现。 What I have noticed however which is odd is that the following sequence can produce different results: 我注意到的是奇怪的是,以下序列可以产生不同的结果:

  • F5 Refresh the Browser F5刷新浏览器
  • Click on 1002 点击1002

Sometimes this triggeres mousedown , mouseup and then click . 有时候这个mousedown mouseup ,然后click Othertimes it misses off the click . Othertimes它错过了click It seems quite strange that this issue can occur sporadically between two different loads of the same page. 这个问题可能偶尔发生在同一页面的两个不同负载之间,这似乎很奇怪。

I should also add that I've tried the following: 我还要补充一点,我尝试过以下方法:

  • Caused mousedown to fail and verify that click still fires, to ensure a sporadic error in mousedown could not be causing the problem. 导致mousedown失败并确认click仍然触发,以确保mousedown的零星错误无法导致问题。 I can confirm that click will fire event if there is an error in mousedown . 如果mousedown有错误,我可以确认click会触发事件。
  • Tried to check for timing issues. 试图检查时间问题。 I did this by inserting a long blocking loop in mousedown and can confirm that the mouseup and click events will fire after a considerable delay. 我通过在mousedown插入一个长阻塞循环来做到这一点,并且可以确认mouseupclick事件会在相当长的延迟后触发。 So the events do look to be executing sequentially as you'd expect. 所以事件看起来像你期望的那样顺序执行。

Update 2 更新2

A quick update after @CoolBlue's comment is that adding a namespace to my event handlers doesn't seem to make any difference. @ CoolBlue评论之后的快速更新是,为我的事件处理程序添加命名空间似乎没有任何区别。 The following still experiences the problem sporadically: 以下仍然偶尔会遇到这个问题:

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

Also the css is something that I've not mentioned yet. css也是我尚未提及的东西。 The css should be similar between the two different types. 两种不同类型的CSS应该相似。 The complete set is shown below, in particular the point-events are set to none just for the label in the middle of the circle. 完整集如下所示,特别是point-events仅针对圆圈中间的标签设置为none I've taken care to avoid clicking on that for some of my tests though and it doesn't seem to make much difference as far as I can tell. 我已经注意避免点击我的一些测试,但据我所知它似乎没有太大的区别。

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

Update 3 更新3

So I've tried ruling different things out. 所以我试着把不同的东西排除在外。 One is to change the CSS such that the white circles 1002 and 1003 now use the same class and therefore same CSS as Suppliers which is the one that worked. 一种是改变CSS,使得white圆圈10021003现在使用相同的类,因此与供应商相同的CSS是有效的。 You can see the image and CSS below as proof: 您可以在下面看到图像和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>

I also decided to modify the custom.d3.event code as this is the most complex bit of eventing. 我还决定修改custom.d3.event代码,因为这是最复杂的事件。 I stripped it right back down to simply just logging: 我把它剥离回来只是简单地记录:

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

Now it seems that this still didn't solve the problem. 现在看来这仍然没有解决问题。 Below is a trace (now I'm not sure why I get two click.test events fired each time - appreciate if anyone can explain it... but for now taking that as the norm). 下面是一个跟踪(现在我不知道为什么我每次都会发出两次click.test事件 - 如果有人能够解释它,那就明白了......但现在把它作为常态)。 What you can see is that on the ocassion highlighted, the click.test did not get logged, I had to click again - hence the double mousedown.test before the click was registered. 你可以看到,在click.test突出显示, click.test没有被记录,我不得不再次点击 - 因此在mousedown.test之前双mousedown.test

在此输入图像描述


Update 4 更新4

So after a suggestion from @CoolBlue I tried looking into the d3.behavior.drag that I've got set up. 所以在得到@CoolBlue的建议后,我试着查看我已经设置的d3.behavior.drag I've tried removing the wireup of the drag behaviour and I can't see any issues after doing so - which could indicate a problem in there. 我已经尝试删除拖动行为的连线,这样做后我看不到任何问题 - 这可能表明存在问题。 This is designed to allow the circles to be dragged within a force directed graph. 这旨在允许在力导向图内拖动圆。 So I've added some logging in the drag so I can keep an eye on whats going on: 所以我在拖动中添加了一些日志,所以我可以关注最新情况:

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

I was also pointed to the d3 code base for the drag event which has a suppressClick flag in there. 我还指向了拖拽事件d3代码库,其中有一个suppressClick标志。 So I modified this slightly to see if this was suppressing the click that I was expecting. 所以我稍微修改了一下,看看这是否会抑制我期待的点击。

return function (suppressClick) {
     console.log("supressClick = ", suppressClick);
     w.on(name, null);
     ...
}

The results of this were a bit strange. 结果有点奇怪。 I've merged all the logging together to illustrate 4 different examples: 我已将所有日志记录合并在一起,以说明4个不同的示例:

  • Blue: The click fired correctly, I noted that suppressClick was false. 蓝色:点击正确,我注意到suppressClick为false。
  • Red: The click didn't fire, it looks like I'd accidentally triggered a move but suppressClick was still false. 红色:点击没有触发,看起来我不小心触发了一次移动,但是suppressClick仍然是假的。
  • Yellow: The click did fire, suppressClick was still false but there was an accidental move. 黄色:点击确实触发, suppressClick仍然是假的但是有一个意外的移动。 I don't know why this differs from the previous red one. 我不知道为什么这与之前的红色不同。
  • Green: I deliberately moved slightly when clicking, this set suppressClick to true and the click didn't fire. 绿色:点击时我故意轻微移动,此设置suppressClick为true并且点击没有触发。

在此输入图像描述


Update 5 更新5

So looking in depth at the D3 code a bit more, I really can't explain the inconsistencies that I see in the behavior that I detailed in update 4. I just tried something different on the off-chance to see if it did what I expected. 所以深入研究D3代码,我真的无法解释我在更新4中详述的行为中看到的不一致。我只是尝试了一些不同的机会,看看它是否做了我预期。 Basically I'm forcing D3 to never suppress the click. 基本上我强迫D3 永远不会抑制点击。 So in the drag event 所以在拖动事件中

return function (suppressClick) {
    console.log("supressClick = ", suppressClick);
    suppressClick = false;
    w.on(name, null);
    ...
}

After doing this I still managed to get a fail, which raises questions as to whether it really is the suppressClick flag that is causing it. 在这样做之后,我仍然设法失败,这引发了关于它是否真的是导致它的suppressClick标志的问题。 This might also explain the inconsistencies in the console via update #4. 这也可以通过更新#4解释控制台中的不一致。 I also tried upping the setTimeout(off, 0) in there and this didn't prevent all of the clicks from firing like I'd expect. 我也尝试在那里增加setTimeout(off, 0) ,这并没有阻止所有的点击像我期望的那样发射。

So I believe this suggests maybe the suppressClick isn't actually the problem. 所以我相信这表明suppressClick实际上可能不是问题所在。 Here's a console log as proof (and I also had a colleague double check to ensure that I'm not missing anything here): 这是一个控制台日志作为证据(我也有一个同事仔细检查,以确保我在这里没有遗漏任何东西):

在此输入图像描述


Update 6 更新6

I've found another bit of code that may well be relevant to this problem (but I'm not 100% sure). 我发现了另一些可能与这个问题相关的代码(但我不是100%肯定)。 Where I hook up to the d3.behavior.drag I use the following: 我连接到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); });

So I've just been looking into the self.__dragstart() function and noticed a d3.event.sourceEvent.stopPropagation(); 所以我一直在研究self.__dragstart()函数并注意到了一个d3.event.sourceEvent.stopPropagation(); . There isn't much more in these functions (generally just starting/stopping the force directed graph and updating positions of lines). 这些函数中没有更多(通常只是启动/停止力导向图和更新线的位置)。

I'm wondering if this could be influencing the click behavior. 我想知道这是否会影响点击行为。 If I take this stopPropagation out then my whole surface begins to pan, which isn't desirable so that's probably not the answer, but could be another avenue to investigate. 如果我把这个停止stopPropagation ,那么我的整个表面开始平移,这是不可取的,所以这可能不是答案,但可能是另一个调查的途径。


Update 7 更新7

One possible glaring emissions that I forgot to add to the original question. 一个可能的明显排放,我忘了添加到原始问题。 The visualization also supports zooming/panning. 可视化还支持缩放/平移。

 self.__zoom = d3.behavior
                        .zoom()
                        .scaleExtent([minZoom, maxZoom])
                        .on("zoom", function () { self.__zoomed(d3.event.translate, d3.event.scale); });

Now to implement this there is actually a large rectangle over the top of everything. 现在要实现这一点,实际上在所有东西的顶部都有一个大矩形。 So my top level svg actually looks like: 所以我的顶级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>

I remembered this when I turned off the d3.event.sourceEvent.stopPropagation(); 当我关闭d3.event.sourceEvent.stopPropagation();时,我记得这个d3.event.sourceEvent.stopPropagation(); in the callback for the drag event on d3.behaviour.drag . d3.behaviour.drag上的drag事件的回调中。 This stopped any click events getting through to my circles which confused me somewhat, then I remembered the large rectangle when inspecting the DOM. 这阻止了任何点击事件进入我的圈子,这让我有些困惑,然后在检查DOM时我记得那个大矩形。 I'm not quite sure why re-enabling the propagation prevents the click at the moment. 我不太确定为什么重新启用传播会阻止此刻的点击。

I recently came across this again, and fortunately have managed to isolate the problem and work around it. 我最近又遇到了这个问题,幸运的是,我已经成功地解决了这个问题并解决了这个问题。

It was actually due to something being registered in the mousedown event, which was moving the DOM element svg:circle to the top based on a z-order. 这实际上是由于在mousedown事件中注册了一些东西,它正在基于z顺序将DOM元素svg:circle移动到顶部。 It does this by taking it out the DOM and re-inserting it at the appropriate place. 它通过将DOM取出并在适当的位置重新插入来完成此操作。

This produces something that flows like this: 这产生了像这样流动的东西:

  • mouseenter 的mouseenter
  • mousedown 鼠标按下
    • (move DOM element but keep same event wrapper) (移动DOM元素但保留相同的事件包装器)
    • mouseup 鼠标松开

The problem is, as far as the browser is concerned the mousedown and mouseup occurred almost on different elements in the DOM, moving it has messed up the event model. 问题是,就浏览器而言, mousedownmouseup几乎出现在DOM中的不同元素上,移动它已经弄乱了事件模型。

Therefore in my case I've applied a fix by firing the click event manually on mouseup if the original mousedown occured within the same element. 因此,在我的情况下,如果原始mousedown在同一元素中发生,我通过在mouseup手动触发click事件来应用修复。

var events = function(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 g;
};

Returning g instead of events might resolve the problem. 返回g而不是事件可能会解决问题。

The problem could be with the rectangle layer too or with the way you call your event suppressor. 问题可能在于矩形图层,也可能与您调用事件抑制器的方式有关。 It may simply cause your keyup event to hang instead of being cancelled so your first key up event returns after the second click. 它可能只会导致您的keyup事件挂起而不是被取消,因此您的第一个按键事件会在第二次单击后返回。 You should implement a counter on your events to verify this theory and return the value of the counter with the event name in the log. 您应该在事件上实现一个计数器来验证此理论,并使用日志中的事件名称返回计数器的值。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM