Reputation: 1
How to remove event listener of an element when I remove the corresponding element in the midst of the event being triggered in D3?
Right now, after I call .remove()
, it seems like the event is still lingering.
For example, I have an event listener that listens for mousedown, if I mousedown all the time and remove the corresponding element using my keyboard, the mousedown event still lingers. Is this expected?
More specifically, I am dealing with the d3.drag() function which my element calls.
If so, is there a way to clear all of them at once or do I need to remove them manually? How to remove the lingering event?
The following is the link to reproduce: https://jsfiddle.net/38wtj4y0/33/
Try dragging the rectangle around and not release your mousedown. Press a key and the rectangle will disappear but when you move your mouse around (still mousedown), you will see that the console is still printing "dragging"
Update
@Gerardo Furtado's answer below is still a workaround hack, which is to make all triggered function return using the if statement. In other words, if the user does not release mousedown, behind the scene, this drag function will still be triggered forever, just that they will all do nothing since they will return. This is still an utter waste of resource.
The only explanation that I can think of that causes this problem is that D3 can only remove an event listener if it is no longer active, i.e it has to wait for user to release. Therefore, I need a way to free all triggered user interaction.
Upvotes: 2
Views: 628
Reputation: 21578
This is an interesting question, although I have to admit, that I have never come across this problem in a real world application. The reason why this happens is to be found in the inner workings of D3's drag implementation.
First, it is worth mentioning, that, if you remove an element from the DOM tree, it can no longer become a target when performing hit-testing for a pointer event. Thus, no event listener registered on that element will be executed any more. This is the behavior one would expect and it is the reason for your confusion, because in your JSFiddle the listener seems to executed even though the element was successfully removed.
To understand what is going on, you have to dig into the source code of d3.drag()
. On initialization the drag behavior registers various event handlers on the selection's elements:
function drag(selection) {
selection
.on("mousedown.drag", mousedowned)
//...
}
This handler listening for mousedown
events will not set up the rest of the drag behavior before such an event is fired on the respective element. Once an element of the drag behavior receives a mousedown
event, the internal mousedowned()
handler will be executed:
function mousedowned() {
//...
select(event.view).on("mousemove.drag", mousemoved, true).on("mouseup.drag", mouseupped, true);
//...
}
Within this handler a "mousemove.drag"
and a "mouseup.drag"
listeners are registered on event.view
. This view
property of the MouseEvent
is inherited from the UIEvent
interface and—at least in browsers—points to the Window
object the event happened in. Those drag handlers on the global window
are used by d3-drag to do its work. And those handlers are responsible for the seemingly confusing behavior you witnessed. We will come to this soon, first let us check how the listeners are subsequently removed.
When the drag gesture eventually ends by firing a mouseup
event, those handlers are removed from the window
object in the function mouseupped()
:
function mouseupped() {
select(event.view).on("mousemove.drag mouseup.drag", null);
}
Now, let us have another look at your code. Even though you removed the target for the drag behavior triggered by a keydown
event, the aforementioned handlers on the window
still exist because you are keeping the mouse button pressed whereby suppressing a mouseup
event to be fired. Hence, the mouseupped()
handler has not yet been executed. This will keep the drag behavior alive as mousemove
events are still captured by the drag's internal handlers on window
. Additionally, those internal handlers will also keep delegating to your own dragged
handler causing the console output you are witnessing.
As mentioned at the very beginning of this post, I have never seen this causing any real world trouble. If you nonetheless want to avoid this behavior you could remove the internal handlers once you remove the target:
d3.select(window).on("keydown", function() {
d3.select(".draggable-rect").remove();
d3.select(d3.event.view) // Remove global (internal) drag handlers
.on("mousemove.drag", null)
.on("mouseup.drag", null);
})
As is always the case when it comes to fiddling with the inner workings of some library, you have to be cautious not to break other things and to keep in mind that this is in danger of breaking silently with any future release of D3.
Have a look at this working demo:
d3.select("svg").append('rect').attr('class', 'draggable-rect');
d3.select(window).on("keydown", function() {
d3.select(".draggable-rect").remove();
d3.select(d3.event.view)
.on("mousemove.drag", null)
.on("mouseup.drag", null);
})
d3.select(".draggable-rect")
.call(d3.drag().on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
function dragstarted(d) {
d3.select(this).raise().classed("active", true);
}
function dragged(d) {
console.log("dragging")
d3.select(this).attr("x", d3.event.x - 40).attr("y", d3.event.y - 40);
}
function dragended(d) {
d3.select(this).classed("active", false);
}
.test-area {
width: 400px;
height: 400px;
border: 1px solid black;
}
svg {
width: 400px;
height: 400px;
}
.draggable-rect {
width: 80px;
height: 80px;
fill: green;
}
<script src="https://d3js.org/d3.v4.js"></script>
<div class="test-area">
<svg>
</svg>
</div>
Upvotes: 2