简体   繁体   中英

d3 dragging event does not terminate in Firefox

I am using the d3 force directed graph animation.

Steps to reproduce problem:

  1. Fire up Firefox browser
  2. visit provemath.org
  3. click the x in the top right or log in (nodes should appear at this point)
  4. click on any node
  5. click on the back arrow on the top left

The result is that the node you clicked is still attached to your mouse as if you are dragging it around. The desired result is that this doesn't happen :)

Insights:

This only occurs in Firefox.

d3 relevant code: When data is bound to nodes, we use .call(gA.drag) where gA.drag = gA.force.drag() , and in the d3 library itself, we have:

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

Also when data is bound to nodes, I use .on('mousedown', mousedown) and .on('mouseup', mouseup) . I wrote those functions and they are:

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

I have tried using both d3.event.stopPropagation() and d3.event.dataTransfer.setData('text', 'anything') as suggested in this question at various points in my code, to no avail. The setData code seems to have the effect of halting events dead in their tracks as soon as the line is run, which doesn't make sense to me.

One possible, but not entirely satisfactory solution might be to manually find and destroy the drag event when a user clicks the back arrow.

UPDATE: I AM INCLUDING SOME MORE CODE EXCERPTS:

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's suggestion of using a timeout, even with a time of "0":

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

is in fact working, so I think his theory is correct.

Did you use d3.event.dataTransfer.setData('text', 'anything') as is? Firefox breaks when you set text as the mime-type, you need to use text/plain .

PSA: In IE11, it's the other way around. In fact, IE11 breaks when you set anything but 'Text' as the mime-type!

It's hard to diagnose this issue without access to the full code and the ability to insert debugging calls and testing potential fixes. Ideally you'd have a jsFiddle that reproduces this problem while isolating things just the relevant code (with fake hardcoded data if needed). If you can create that jsFiddle, I'll happily try to fix it there and revise my answer here. Otherwise, here goes:

I suspect that the problem is that in Firefox d3 completely misses the dragend event , because mouseup is triggered prior to it and from mouseup you're triggering node-click . I can't see further, but I'm guessing that triggering node-click immediately (meaning synchronously) results in changes to the DOM, making another element appear in front of the dragged node, and hence causing the missed dragend . It's just a theory, and it could be that it's only partially accurate and that the details of why dragend is missed are somewhat more nuanced.

There's probably a proper fix, but as mentioned, that requires a jsFiddle isolating the problem. However, I'm guessing that there's also the following hack that would workaround this problem: Wrapping the call to $.event.trigger in a setTimeout , something like

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

}

Using setTimeout will delay the node-click event a bit, giving the browser and/or d3 a chance to finish up the drag business prior to modifying the DOM. It's not pretty, and there are usually better way to fix synchronization issues that don't involve setTimeout , which tends to pile on new problems rather than avoid it. But maybe you'll be lucky and this will fix it without causing new problems ¯\\_(ツ)_/¯

The 2nd argument to setTimeout (shown as 100 ) is something you should experiment with. Could be that 0 would work or it might need to be even greater than 100.

Also, it could be that the delete statements need to be moved into the setTimeout function handler as well. Not sure, because it's unclear what they do.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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