d3 拖动事件不会在 Firefox 中终止

d3 dragging event does not terminate in Firefox

我正在使用 d3 力定向图动画。

重现问题的步骤:

  1. 启动 Firefox 浏览器
  2. 访问provemath.org
  3. 单击右上角的 x 或登录(此时应出现节点)
  4. 点击任意节点
  5. 点击左上角的后退箭头

结果是您单击的节点仍然附着在您的鼠标上,就好像您在四处拖动它一样。期望的结果是这不会发生:)

见解:

这只发生在 Firefox 中。

d3相关代码: 当数据绑定到节点时,我们使用 .call(gA.drag) where gA.drag = gA.force.drag(),在 d3 库本身中,我们有:

    force.drag = function() {
      if (!drag) drag = d3.behavior.drag().origin(d3_identity).on("dragstart.force", d3_layout_forceDragstart).on("drag.force", dragmove).on("dragend.force", d3_layout_forceDragend);
      if (!arguments.length) return drag;
      this.on("mouseover.force", d3_layout_forceMouseover).on("mouseout.force", d3_layout_forceMouseout).call(drag);
    };
    function dragmove(d) {
      d.px = d3.event.x, d.py = d3.event.y;
      force.resume();
    }
    return d3.rebind(force, event, "on");
  };
  function d3_layout_forceDragstart(d) {
    d.fixed |= 2;
  }
  function d3_layout_forceDragend(d) {
    d.fixed &= ~6;
  }
  function d3_layout_forceMouseover(d) {
    d.fixed |= 4;
    d.px = d.x, d.py = d.y;
  }
  function d3_layout_forceMouseout(d) {
    d.fixed &= ~4;
  }

此外,当数据绑定到节点时,我使用 .on('mousedown', mousedown).on('mouseup', mouseup)。我写了这些函数,它们是:

function mousedown(node) {
    node.time_before = getShortTime(new Date())
    node.client_x_before = d3.event.clientX
    node.client_y_before = d3.event.clientY
    // d3.event.stopPropagation() // need cancelBubble for MS
}
function mouseup(node) {
    if( mod(getShortTime(new Date()) - node.time_before, 60) < 0.85
            && cartesianDistance([node.client_x_before, node.client_y_before], [d3.event.clientX, d3.event.clientY]) < 55
        ) {
        $.event.trigger({ type: 'node-click', message: node.id })
    }
    delete node.time_before
    delete node.client_x_before
    delete node.client_y_before
}
function getShortTime(date) {
  return date.getSeconds() + date.getMilliseconds()/1000
}
function mod(m, n) {
    return (m % n + n) % n;
}

我已经尝试在我的代码的不同点使用 d3.event.stopPropagation()d3.event.dataTransfer.setData('text', 'anything'),如 this 问题中所建议的,但无济于事。 setData 代码似乎具有在行为 运行 时立即停止事件的效果,这对我来说没有意义。

一种可能但不完全令人满意的解决方案可能是在用户单击后退箭头时手动查找并销毁拖动事件。

更新:我包含了更多代码摘录:

main.py

$(document).on('node-click', function(Event){
    current_node = graph.nodes[Event.message] // graph.nodes is a DICTIONARY of nodes
    updateNodeTemplateLearnedState()
    blinds.open({ // in this module, new DOM elements are added with jQuery's .append() method
        object: current_node,
    })
    hide('svg')
    hide('#overlay')
    show('#node-template') // This DOM element is the container that blinds.open() populated.  Event WITHOUT adding new DOM elements, it is possible that the mere putting of this guy in front of the vertices is causing the issue
    if( false /*mode !== 'learn'*/){
        ws.jsend({ command: "re-center-graph", central_node_id: current_node.id })
    }
})

function show(css_selector) { // this stuff fails for svg when using .addClass, so we can just leave show and hide stuff in the JS.
    let $selected = $(css_selector)
    if( !_.contains(css_show_hide_array, css_selector) ){
        $selected.css('height', '100%')
        $selected.css('width', '100%')
        $selected.css('overflow', 'scroll')
    }else{
        // $selected.removeClass('hidden')
        $selected.css('visibility', 'visible')
    }
}

meetamit 关于使用超时的建议,即使时间为“0”:

setTimeout(function() {
            $.event.trigger({ type: 'node-click', message: node.id })
        }, 0);

确实有效,所以我认为他的理论是正确的。

您是否按原样使用了 d3.event.dataTransfer.setData('text', 'anything')?当您将 text 设置为 mime 类型时,Firefox 会中断,您需要使用 text/plain.

PSA:在IE11中,情况正好相反。事实上,当您将 'Text' 以外的任何内容设置为 MIME 类型时,IE11 就会崩溃!

如果无法访问完整代码以及插入调试调用和测试潜在修复的能力,则很难诊断此问题。理想情况下,您将拥有一个重现此问题的 jsFiddle ,同时仅将相关代码 隔离(如果需要,使用伪造的硬编码数据)。如果您可以创建那个 jsFiddle,我会很乐意尝试在那里修复它并在此处修改我的答案。否则,这里是:

我怀疑问题是在 Firefox d3 中完全错过了 dragend 事件,因为 mouseup 在它之前被触发并且从 mouseup 你触发 node-click.我看不到更远的地方,但我猜测立即触发 node-click(意味着同步)会导致 DOM 发生变化,使另一个元素出现在被拖动节点的前面,从而导致错过了 dragend。这只是一个理论,它可能只是部分准确,并且 dragend 被遗漏的细节有些微妙。

可能有一个适当的修复,但如前所述,这需要一个 jsFiddle 来隔离问题。但是,我猜测还有以下 hack 可以解决此问题:将对 $.event.trigger 的调用包装在 setTimeout 中,例如

function mouseup(node) {
  if( mod(getShortTime(new Date()) - node.time_before, 60) < 0.85
        && cartesianDistance([node.client_x_before, node.client_y_before], [d3.event.clientX, d3.event.clientY]) < 55
    ) {
    setTimeout(function() {
      $.event.trigger({ type: 'node-click', message: node.id })
    }, 100);
  }
  delete node.time_before
  delete node.client_x_before
  delete node.client_y_before

}

使用 setTimeout 会稍微延迟 node-click 事件,使浏览器 and/or d3 有机会在修改 DOM 之前完成拖动业务。这不是很好,通常有更好的方法来修复不涉及 setTimeout 的同步问题,这往往会堆积新问题而不是避免它。但也许你会很幸运,这将解决它而不会引起新问题¯\_(ツ)_/¯

setTimeout 的第二个参数(显示为 100)是您应该尝试的东西。可能 0 会起作用,或者它可能需要大于 100。

此外,可能 delete 语句也需要移至 setTimeout 函数处理程序中。不确定,因为不清楚他们做什么。