如果您正在重新审视此问题,我已将所有更新移至底部,以便它实际上更好地作为一个问题。
我使用D3
处理浏览器事件时遇到了一些奇怪的问题。不幸的是,这在一个相当大的应用程序中,并且因为我完全迷失了原因,我正在努力寻找一个可重复的小例子,所以我将尽可能多地提供有用的信息。我可以。
所以我的问题是,click
事件似乎无法为某些DOM元素可靠地触发。我有两组不同的元素填充圈子和白色圈子。您可以在下面的屏幕截图中看到 1002 和 1003 是白色圆圈,而供应商是一个圆圈。
现在这个问题仅发生在我不明白的白圈内。下面的屏幕截图显示了点击圈子时会发生什么。点击顺序通过红色数字和与之关联的日志记录显示。基本上你看到的是:
这个问题有点零星。我曾设法追踪一个可靠的复制品,但经过浏览器的一些更新后,它现在更难以复制。如果我替代点击 1002 和 1003 ,那么我会继续获取mousedown
和mouseup
个事件,但绝不会获得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);
在这里我们可以看到附加的圆圈(注意每个星球实际上只是一个圆圈)。构造并调用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>
这背后的想法是提供比默认情况下更丰富的事件系统。例如,允许双击(不会触发单击)和长按等等。
使用circle
容器调用事件时,执行以下操作,使用D3设置一些raw
事件。这些与Planet.append()
函数中已连接的相同,因为events
对象公开了它自己的自定义调度。这些是我用于调试/记录的事件;
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");
}
所以在这里,我联系了一些事件。以相反的顺序看着它们:
点击功能设置为只记录我们正在处理的值
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功能稍微复杂一点,我将包含整个功能。它做了很多事情:
触发生活在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中经历过这种情况(虽然这似乎是最可靠的浏览器)。
不幸的是,经过一些刷新和代码更改/还原后,我很难获得可靠的再现。我注意到的是奇怪的是,以下序列可以产生不同的结果:
1002
有时会触发mousedown
,mouseup
然后click
。 Othertimes它错过了click
。看起来很奇怪,这个问题偶尔会发生在同一页面的两个不同负载之间。
我还应该补充一点,我已尝试过以下方法:
mousedown
失败并验证click
仍然会触发,以确保mousedown
中的零星错误无法导致问题。如果click
中出现错误,我可以确认mousedown
会触发事件。mousedown
中插入一个长阻塞循环来做到这一点,并确认mouseup
和click
事件会在相当长的延迟后触发。因此事件看起来像你期望的那样按顺序执行。更新2
在@ CoolBlue评论之后的快速更新是,为我的事件处理程序添加命名空间似乎没有任何区别。以下仍然偶尔会遇到这个问题:
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 现在使用相同的类,因此与供应商相同的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 event的d3
代码库,其中有suppressClick
标志。所以我稍微修改了一下,看看这是否会抑制我期待的点击。
return function (suppressClick) {
console.log("supressClick = ", suppressClick);
w.on(name, null);
...
}
这个结果有点奇怪。我已将所有日志记录合并在一起,以说明4个不同的示例:
suppressClick
是假的。suppressClick
仍然是假的。suppressClick
仍然是假的,但是有一次意外移动。我不知道为什么这与之前的红色不同。suppressClick
为真,点击没有触发。
更新5
因此深入研究D3
代码,我真的无法解释我在更新4中详述的行为中看到的不一致性。我只是尝试了一些不同的关闭 - 看看它是否符合我的预期。基本上我强迫D3
从不压制点击。所以在drag event
return function (suppressClick) {
console.log("supressClick = ", suppressClick);
suppressClick = false;
w.on(name, null);
...
}
执行此操作后,我仍然设法失败,这引发了一个问题,即它是否真的是导致它的suppressClick标志。这也可以通过更新#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时我记得那个大矩形。我不太确定为什么重新启用传播会阻止此刻点击。
答案 0 :(得分:2)
我最近又遇到过这个问题,幸运的是,我已经设法解决问题并解决了这个问题。
实际上是由于在mousedown
事件中注册了某些内容,它正在根据z顺序将DOM元素svg:circle
移动到顶部。它通过将DOM取出并在适当的位置重新插入来实现此目的。
这产生了像这样流动的东西:
问题是,就浏览器而言,mousedown
和mouseup
几乎出现在DOM中的不同元素上,移动它会搞乱事件模型。
因此,在我的情况下,如果原始click
发生在同一元素中,我会在mouseup
上手动触发mousedown
事件来应用修复程序。
答案 1 :(得分:1)
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;
};
返回g代替事件可能会解决问题。
答案 2 :(得分:0)
问题可能在于矩形图层,也可能与您调用事件抑制器的方式有关。它可能只会导致您的keyup事件挂起而不是被取消,因此您的第一个按键事件会在第二次单击后返回。您应该在事件上实现一个计数器来验证这个理论,并在日志中返回带有事件名称的计数器值。