2

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.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
mareoraft
  • 3,474
  • 4
  • 26
  • 62

2 Answers2

1

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!

  • Are you suggesting that I should use the line `d3.event.dataTransfer.setData('text/plain', 'anything')` instead? Also, where exactly do you suggest I insert this? – mareoraft Mar 31 '16 at 01:36
  • When I place it at the beginning of mouseup or mousedown, it cancels the click entirely. If I put it at the end of one of the functions, it has no effect at all. – mareoraft Mar 31 '16 at 01:45
1

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.

meetamit
  • 24,727
  • 9
  • 57
  • 68
  • If it helps, you can view the whole code at https://github.com/ProveMath/prove-math – mareoraft Mar 31 '16 at 21:06
  • The horizontal bars that appear after clicking a node are in fact new DOM elements (added via jQuery's `.append()` method). – mareoraft Mar 31 '16 at 21:12
  • I have included more code and an UPDATE above. I am hoping we can find a less hacky solution. – mareoraft Mar 31 '16 at 21:38
  • It seems that wrapping the setTimeout around the blinds.open() AND the show() (and the two lines in between) works. However, wrapping the setTimeout around the blinds.open() ONLY or the show() ONLY does not work. – mareoraft Mar 31 '16 at 21:44
  • Also, the `delete` lines are not relevant. – mareoraft Mar 31 '16 at 21:45
  • 1
    @mareoraft I'm just now realizing that you got into this `mouseup` `mousedown` stuff because you wanted a way to differentiate between a click and a drag, so you ended up re-implementing click behavior. Is that correct? If so, you can switch back to subscribing to `'click'` events, and inside the handler function, test if `d3.event.defaultPrevented` is `true`, indicating node was dragged, or `false` indicating it wasn't. See [this SO post](http://stackoverflow.com/questions/19931307/d3-differentiate-between-click-and-drag-for-an-element-which-has-a-drag-behavior). Would that work? – meetamit Mar 31 '16 at 23:27
  • 1
    Also check out [this jsfiddle](http://jsfiddle.net/bfavqu03/) and click or drag nodes and see the alert message – meetamit Mar 31 '16 at 23:30
  • This is impressive. However, if the mouse moves only 1 pixel, it counts as a "drag", not a "click". I don't think this is desirable behavior. – mareoraft Apr 01 '16 at 01:48
  • So this `node.on("click", function(d) { alert(d3.event.defaultPrevented ? "was dragged" : "was clicked") })` idea sounds like the right way to go, except that very minor movements still count as drags and not clicks. I could imagine users with a touchy mouse getting "drag"s when they intended "clicks"s. If you can think of another implementation or a way to modify this behavior, I would really appreciate it. But I will award you the bounty at this point because you really deserve it!! – mareoraft Apr 01 '16 at 01:59
  • 1
    Thanks @mareoraft!!! As far as I know, there is no way to tweak `d3.drag` to make it less "drag eager". I suppose you can use a hybrid of the `"click"` event (i.e. checking `d3.event.defaultPrevented`) together with the existing way you use `mousedown` and `mouseup` to record the drag distance and duration. But the triggering of the `'node-click'` event should be done from the `'click'` event handler — not from `mouseup` — to prevent the firefox bug. And this still avoids using `setTimeout`. – meetamit Apr 01 '16 at 04:33