Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SVG element loses event handlers if moved around the DOM

I use this D3 snippet to move SVG g elements to top of the rest element as SVG render order depends on the order of elements in DOM, and there is no z index:

d3.selection.prototype.moveToFront = function () {
  return this.each(function () {
    this.parentNode.appendChild(this);
  });
};

I run it like:

d3.select(el).moveToFront()

My issue is that if I add a D3 event listener, like d3.select(el).on('mouseleave',function(){}), then move the element to front of DOM tree using the code above, all event listeners are lost in Internet Explorer 11, still working fine in other browsers. How can I workaround it?

like image 778
Sergei Basharov Avatar asked Sep 24 '14 14:09

Sergei Basharov


People also ask

Does SVG support event handlers?

SVG is XML based, which means that every element is available within the SVG DOM. You can attach JavaScript event handlers for an element. In SVG, each drawn shape is remembered as an object. If attributes of an SVG object are changed, the browser can automatically re-render the shape.

Do event listeners get removed when element is removed?

According to the jquery Documentation when using remove() method over an element, all event listeners are removed from memory. This affects the element it selft and all child nodes. If you want to keep the event listners in memory you should use . detach() instead.

Does Dom use event handlers?

HTML DOM events allow JavaScript to register different event handlers on elements in an HTML document. Events are normally used in combination with functions, and the function will not be executed before the event occurs (such as when a user clicks a button).

How many ways can you attach an event handler to a DOM element?

There are three ways to assign an event handler: HTML event handler attribute, element's event handler property, and addEventListener() .


1 Answers

Single event listener on parent element, or higher DOM ancestor:

There is a relatively easy solution which I did not originally mention because I had assumed you had dismissed it as not feasible in your situation. That solution is that instead of multiple listeners each on a single child element, you have a single listener on an ancestor element which gets called for all events of a type on its children. It can be designed to quickly choose to further process based on the event.target, event.target.id, or, better, event.target.className (with a specific class of your creation assigned if the element is a valid target for the event handler). Depending on what your event handlers are doing and the percentage of elements under the ancestor on which you already are using listeners, a single event handler is arguably the better solution. Having a single listener potentially reduces the overhead of event handling. However, any actual performance difference depending on what you are doing in the event handlers and on what percentage of the ancestor's children on which you would have otherwise placed listeners.

Event listeners on elements actually interested in

Your question asks about listeners which your code has placed on the element being moved. Given that you do not appear concerned about listeners placed on the element by code which you do not control, then the brute-force method of working around this is for you to keep a list of listeners and the elements upon which you have placed them.

The best way to implement this brute-force workaround depends greatly on the way in which you place listeners on the elements, the variety that you use, etc. This is all information which is not available to us from the question. Without that information it is not possible to make a known-good choice of how to implement this.

Using only single listeners of each type/namespace all added through selection.on():

If you have a single listener of each type.namespace, and you have added them all through the d3.selection.on() method, and you are not using Capture type listeners, then it is actually relatively easy.

When using only a single listener of each type, the selection.on() method allows you to read the listener which is assigned to the element and type.

Thus, your moveToFront() method could become:

var isIE = /*@cc_on!@*/false || !!document.documentMode; // At least IE6
var typesOfListenersUsed = [ "click", "command", "mouseover", "mouseleave", ...];

d3.selection.prototype.moveToFront = function () {
  return this.each(function () {
    var currentListeners={};
    if(isIE) {
      var element = this;
      typesOfListenersUsed.forEach(function(value){
         currentListeners[value] = element.selection.on(value);
      });
    }
    this.parentNode.appendChild(this);
    if(isIE) {
      typesOfListenersUsed.forEach(function(value){
         if(currentListeners[value]) { 
           element.selection.on(value, currentListeners[value]);
         }
      });
    }
  });
};

You do not necessarily need to check for IE, as it should not hurt to re-place the listeners in other browsers. However, it would cost time, and is better not to do it.

You should be able to use this even if you are using multiple listeners of the same type by just specifying a namespace in the list of listeners. For example:

var typesOfListenersUsed = [ "click", "click.foo", "click.bar"
                            , "command", "mouseover", "mouseleave", ...];

General, multiple listeners of same type:

If you are using listeners which you are adding not through d3, then you would need to implement a general method of recording the listeners added to an element.

How to record the function being added as a listener, you can just add a method to the prototype which records the event you are adding as a listener. For example:

d3.selection.prototype.recOn = function (type, func) {
  recordEventListener(this, type, func);
  d3.select(this).on(type,func);
};

Then use d3.select(el).recOn('mouseleave',function(){}) instead of d3.select(el).on('mouseleave',function(){}).

Given that you are using a general solution because you are adding some listeners not through d3, you will need to add functions to wrap the calls to however you are adding the listener (e.g. addEventListener()).

You would then need a function which you call after the appendChild in your moveToFront(). It could contain the if statement to only restore the listeners if the browser is IE11, or IE.

d3.selection.prototype.restoreRecordedListeners = function () {
    if(isIE) {
        ...
    }
};

You will need to chose how to store the recorded listener information. This depends greatly on how you have implemented other areas of your code of which we have no knowledge. Probably the easiest way to record which listeners are on an element is to create an index into the list of listeners which is then recorded as a class. If the number of actual different listener functions you use is small, this could be a statically defined list. If the number and variety is large, then it could be a dynamic list.

I can expand on this, but how robust to make it really depends on your code. It could be as simple as keeping tack of only 5-10 actually different functions which you use as listeners. It might need to be as robust as to be a complete general solution to record any possible number of listeners. That depends on information we do not know about your code.

My hope is that someone else will be able to provide you with a simple and easy fix for IE11 where you just set some property, or call some method to get IE to not drop the listeners. However, the brute-force method will solve the problem.

like image 90
Makyen Avatar answered Oct 06 '22 00:10

Makyen