John Vinyard
John Vinyard

Reputation: 13485

Click Event Not Firing After Drag (sometimes) in d3.js

Observed Behavior

I'm using d3.js, and I'm in a situation where I'd like to update some data based on a drag event, and redraw everything after the dragend event. The draggable items also have some click behavior.

Draggable items can only move along the x-axis. When an item is dragged, and the cursor is directly above the draggable item on dragend/mouseup, the item must be clicked twice after it is re-drawn for the click event to fire. When an item is dragged, but dragend/mouseup does not occur directly above the item, the click event fires as expected (on the first try) after the redraw.

Desired Behavior

I'd like the click event to always fire on the first click after dragging, regardless of where the cursor is.

If I replace the click event on the draggable items with a mouseup event, everything works as expected, but click is the event I'd really like to handle.

A Demonstration

Here is a self-contained example: http://jsfiddle.net/RRCyq/2/

And here is the relevant javascript code:

var data, click_count,did_drag;
// this is the data I'd like to render
data = [
    {x : 100, y : 150},
    {x : 200, y : 250}
];
// these are some elements I'm using for debugging
click_count = d3.select('#click-count');
did_drag = d3.select('#did-drag');

function draw() {
    var drag_behavior,dragged = false;

    // clear all circles from the svg element
    d3.select('#test').selectAll('circle')
        .remove();

    drag_behavior = d3.behavior.drag()
        .origin(Object)
        .on("drag", function(d) {
            // indicate that dragging has occurred
            dragged = true;
            // update the data
            d.x = d3.event.x;
            // update the display
            d3.select(this).attr('cx',d.x);
        }).on('dragend',function() {
            // data has been updated. redraw.
            if(dragged) { draw(); }
        });

    d3.select('#test').selectAll('circle')
        .data(data)
        .enter()
        .append('circle')
        .attr('cx',function(d) { return d.x; })
        .attr('cy',function(d) { return d.y; })
        .attr('r',20)
        .on('click',function() {
            did_drag.text(dragged.toString());
            if(!dragged) {
                // increment the click counter
                click_count.text(parseInt(click_count.text()) + 1);
            }
        }).call(drag_behavior);
}

draw();

Upvotes: 3

Views: 6017

Answers (2)

Langdon
Langdon

Reputation: 20063

A little late to the party, buuuut...

The documentations suggests that you use d3.event.defaultPrevented in your click event to know whether or not the element was just dragged. If you combine that with your drag and dragend events, a much cleaner approach is to call the exact function you want when necessary (see when and how flashRect is called):

http://jsfiddle.net/langdonx/fE5gN/

var container,
    rect,
    dragBehavior,
    wasDragged = false;

container = d3.select('svg')
    .append('g');

rect = container.append('rect')
    .attr('width', 100)
    .attr('height', 100);

dragBehavior = d3.behavior.drag()
    .on('dragstart', onDragStart)
    .on('drag', onDrag)
    .on('dragend', onDragEnd);

container
    .call(dragBehavior)
    .on('click', onClick);

function flashRect() {
    rect.attr('fill', 'red').transition().attr('fill', 'black');
}

function onDragStart() {
    console.log('onDragStart');
}

function onDrag() {
    console.log('onDrag');

    var x = (d3.event.sourceEvent.pageX - 50);

    container.attr('transform', 'translate(' + x + ')');

    wasDragged = true;
}

function onDragEnd() {
    if (wasDragged === true) {
        console.log('onDragEnd');

        // always do this on drag end
        flashRect();
    }

    wasDragged = false;
}

function onClick(d) {
    if (d3.event.defaultPrevented === false) {
        console.log('onClick');

        // only do this on click if we didn't just finish dragging
        flashRect();
    }
}

I didn't like the global variable, so I made a revision to use data: http://jsfiddle.net/langdonx/fE5gN/1/

Upvotes: 6

John Vinyard
John Vinyard

Reputation: 13485

After observing that the click required before my svg circles would start responding to click events again could happen anywhere in the document, I settled on a hack whereby I simulate a click event on the document (thanks to https://stackoverflow.com/a/2706236/1015178) after the drag ends. It's ugly, but it works.

Here's the function to simulate an event (again, thanks to https://stackoverflow.com/a/2706236/1015178)

function eventFire(el, etype){
  if (el.fireEvent) {
    (el.fireEvent('on' + etype));
  } else {
    var evObj = document.createEvent('Events');
    evObj.initEvent(etype, true, false);
    el.dispatchEvent(evObj);
  }
}

And here's the updated drag behavior:

drag_behavior = d3.behavior.drag()
    .origin(Object)
    .on("drag", function(d) {
        // indicate that dragging has occurred
        dragged = true;
        // update the data
        d.x = d3.event.x;
        // update the display
        d3.select(this).attr('cx',d.x);
    }).on('dragend',function() {
        // data has been updated. redraw.
        if(dragged) { draw(); }
        // simulate a click anywhere, so the svg circles
        // will start responding to click events again
        eventFire(document,'click');
    });

Here's the full working example of my hackish "fix":

http://jsfiddle.net/RRCyq/3/

Upvotes: 2

Related Questions