Click "Don't fire event" after dragging (sometimes) to d3.js
Observed behavior
I am using d3.js and I am in a situation where I want to update some data based on drag
and redraw everything after the event dragend
. Dragged items also have behavior click
.
Dragged items can only move along the x-axis. When an element is being dragged and the cursor is directly over the dragged element on dragend/mouseup
, the element must be double-clicked after re-drawing for the event click
to fire. When an element is being dragged but dragend/mouseup
not directly over the element, the event click
fires as expected (on the first try) after being redrawn.
Desired behavior
I would like the event to click
always fire on the first click after dragging, no matter where the cursor is.
If I replace the event click
on the dragged items with an event mouseup
, everything works as expected, but click
this is an event I'd really like to handle.
Demonstration
Here's an 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();
source to share
After we noticed that the click required before my SVG circles start responding to click events could again happen anywhere in the document, I settled into a hack whereby I simulate a click event on the document (thanks to fooobar. com / questions / 2828 / ... ) after the drag is complete. It's ugly, but it works.
Here's a function to simulate the event (again, thanks to fooobar.com/questions/2828 / ... )
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 and drop 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 is a complete working example of my hacky "fix":
source to share
A little late to the party, buuuut ...
The documentation suggests that you use d3.event.defaultPrevented
in your click
event to see if an element was just being dragged. If you combine this with events drag
and dragend
, a much simpler approach is to call the function you want (see When and how it is called flashRect
):
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 the data: http://jsfiddle.net/langdonx/fE5gN/1/
source to share