Элемент SVG теряет обработчики событий при перемещении по DOM
Я использую этот фрагмент D3 для перемещения элементов SVG g
в начало элемента rest, поскольку порядок визуализации SVG зависит от порядка элементов в DOM, и нет индекса z:
d3.selection.prototype.moveToFront = function () {
return this.each(function () {
this.parentNode.appendChild(this);
});
};
Я запускаю его так:
d3.select(el).moveToFront()
Моя проблема в том, что если я добавлю прослушиватель событий D3, например d3.select(el).on('mouseleave',function(){})
, переместите элемент перед деревом DOM с помощью кода выше, все прослушиватели событий будут потеряны в Internet Explorer 11, все еще отлично работая в других браузерах,
Как я могу обойти это?
Ответы
Ответ 1
Одним из решений может быть использование делегирования событий. Эта довольно простая парадигма является обычным явлением в jQuery (который дал мне идею попробовать ее здесь.)
Расширяя прототип d3.selection
с помощью делегированного прослушивателя событий, мы можем прослушивать события в родительском элементе, но применять обработчик, только если цель события также является нашей желаемой целью.
Итак, вместо:
d3.select('#targetElement').on('mouseout',function(){})
Вы бы использовали:
d3.select('#targetElementParent').delegate('mouseout','#targetElement',function(){})
Теперь не имеет значения, теряются ли события при перемещении элементов или даже при добавлении/редактировании/удалении элементов после создания слушателей.
Здесь демо. Протестировано на Chrome 37, IE 11 и Firefox 31. Я приветствую конструктивную обратную связь, но, пожалуйста, обратите внимание, что я совсем не знакомый с d3.js, мог легко пропустить что-то фундаментальное;)
//prototype. delegated events
d3.selection.prototype.delegate = function(event, targetid, handler) {
return this.on(event, function() {
var eventTarget = d3.event.target.parentNode,
target = d3.select(targetid)[0][0];
if (eventTarget === target) {//only perform event handler if the eventTarget and intendedTarget match
handler.call(eventTarget, eventTarget.__data__);
}
});
};
//add event listeners insead of .on()
d3.select('#svg').delegate('mouseover','#g2',function(){
console.log('mouseover #g2');
}).delegate('mouseout','#g2',function(){
console.log('mouseout #g2');
})
//initial move to front to test that the event still works
d3.select('#g2').moveToFront();
http://jsfiddle.net/f8bfw4y8/
Обновлено и улучшено...
Вслед за Makyen полезной обратной связью я сделал несколько улучшений, чтобы позволить делегированному слушателю применяться к всем связанным детям. EG "прослушивание мыши на каждом g в svg"
Здесь скрипка. Снимок ниже.
//prototype. move to front
d3.selection.prototype.moveToFront = function () {
return this.each(function () {
this.parentNode.appendChild(this);
});
};
//prototype. delegated events
d3.selection.prototype.delegate = function(event, targetselector, handler) {
var self = this;
return this.on(event, function() {
var eventTarget = d3.event.target,
target = self.selectAll(targetselector);
target.each(function(){
//only perform event handler if the eventTarget and intendedTarget match
if (eventTarget === this) {
handler.call(eventTarget, eventTarget.__data__);
} else if (eventTarget.parentNode === this) {
handler.call(eventTarget.parentNode, eventTarget.parentNode.__data__);
}
});
});
};
var testmessage = document.getElementById("testmessage");
//add event listeners insead of .on()
//EG: onmouseover/out of ANY <g> within #svg:
d3.select('#svg').delegate('mouseover','g',function(){
console.log('mouseover',this);
testmessage.innerHTML = "mouseover #"+this.id;
}).delegate('mouseout','g',function(){
console.log('mouseout',this);
testmessage.innerHTML = "mouseout #"+this.id;
});
/* Note: Adding another .delegate listener REPLACES any existing listeners of this event on this node. Uncomment this to see.
//EG2 onmouseover of just the #g3
d3.select('#svg').delegate('mouseover','#g3',function(){
console.log('mouseover of just #g3',this);
testmessage.innerHTML = "mouseover #"+this.id;
});
//to resolve this just delegate the listening to another parent node eg:
//d3.select('body').delegate('mouseover','#g3',function(){...
*/
//initial move to front for testing. OP states that the listener is lost after the element is moved in the DOM.
d3.select('#g2').moveToFront();
svg {height:300px; width:300px;}
rect {fill: pink;}
#g2 rect {fill: green;}
#testmessage {position:absolute; top:50px; right:50px;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg id="svg">
<g id="g1"><rect x="0px" y="0px" width="100px" height="100px" /></g>
<g id="g2"><rect x="50px" y="50px" width="100px" height="100px" /></g>
<g id="g3"><rect x="100px" y="100px" width="100px" height="100px" /></g>
</svg>
<div id="testmessage"></div>
Ответ 2
Одиночный прослушиватель событий на родительском элементе или выше предка DOM:
Существует относительно простое решение, о котором я изначально не упоминал, потому что предположил, что вы отклонили его как невыполнимое в вашей ситуации. Это решение состоит в том, что вместо нескольких слушателей каждый из одного дочернего элемента у вас есть один прослушиватель элемента-предка, который вызывается для всех событий типа на его дочерних элементах. Он может быть разработан для быстрого выбора дальнейшего процесса на основе event.target
, event.target.id
или, лучше, event.target.className
(с определенным классом вашего создания, назначенным, если элемент является допустимой целью для обработчика события). В зависимости от того, что делают обработчики событий и процент элементов под предком, на котором вы уже используете слушателей, возможно, лучшим решением является один обработчик событий. Наличие одного слушателя потенциально снижает накладные расходы на обработку событий. Тем не менее, любая фактическая разница в производительности в зависимости от того, что вы делаете в обработчиках событий, и о том, какой процент детей-предков, на которых вы могли бы разместить слушателей.
Слушатели событий по интересующим элементам
В вашем вопросе спрашивают о слушателях, которые ваш код разместил на перемещаемом элементе. Учитывая, что вас не интересуют слушатели, размещенные на элементе, кодом, который вы не контролируете, тогда метод грубой силы работы над этим - это сохранить список слушателей и элементы, на которые вы их разместили.
Наилучший способ реализации этого обходного метода грубой силы во многом зависит от того, как вы размещаете слушателей по элементам, разнообразию, которое вы используете, и т.д. Это вся информация, которая недоступна нам из вопроса. Без этой информации невозможно сделать хорошо известный выбор того, как это реализовать.
Использование только одиночных слушателей каждого типа/пространства имен, добавленных через selection.on()
:
Если у вас есть один прослушиватель каждого типа .namespace, и вы добавили их через метод d3.selection.on(), и вы не используете прослушиватели типа Capture, то на самом деле это относительно легко.
При использовании только одного слушателя каждого типа метод selection.on()
позволяет вам читать слушателя, который назначен этому элементу и его тип.
Таким образом, ваш метод moveToFront()
может стать:
var isIE = /*@[email protected]*/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]);
}
});
}
});
};
Вам не обязательно проверять наличие IE, так как это не помешает переместить слушателей в другие браузеры. Однако это будет стоить времени, и лучше не делать этого.
Вы должны использовать это, даже если вы используете несколько слушателей одного типа, просто указав пространство имен в списке слушателей. Например:
var typesOfListenersUsed = [ "click", "click.foo", "click.bar"
, "command", "mouseover", "mouseleave", ...];
Общие, несколько слушателей одного типа:
Если вы используете прослушиватели, которые вы добавляете не через d3
, тогда вам нужно будет реализовать общий метод записи слушателей, добавленных в элемент.
Как записать функцию, добавляемую в качестве слушателя, вы можете просто добавить метод к прототипу, который записывает событие, которое вы добавляете в качестве слушателя. Например:
d3.selection.prototype.recOn = function (type, func) {
recordEventListener(this, type, func);
d3.select(this).on(type,func);
};
Затем используйте d3.select(el).recOn('mouseleave',function(){})
вместо d3.select(el).on('mouseleave',function(){})
.
Учитывая, что вы используете общее решение, потому что вы добавляете некоторые слушатели не через d3
, вам нужно будет добавить функции для обертывания вызовов, однако вы добавляете слушателя (например, addEventListener()
).
Затем вам понадобится функция, которую вы вызываете после appendChild
в moveToFront()
. Он может содержать оператор if только для восстановления слушателей если браузер IE11 или IE.
d3.selection.prototype.restoreRecordedListeners = function () {
if(isIE) {
...
}
};
Вам нужно будет выбрать, как сохранить записанную информацию слушателя. Это сильно зависит от того, как вы реализовали другие области вашего кода, о которых мы не знаем. Вероятно, самый простой способ записать, какие слушатели находятся на элементе, - это создать индекс в списке слушателей, который затем записывается как класс. Если количество действительных различных функций слушателя, которое вы используете, невелико, это может быть статически определенный список. Если число и многообразие велики, то это может быть динамический список.
Я могу расширить это, но насколько надежным является то, что это действительно зависит от вашего кода. Это может быть так же просто, как придерживаться только 5-10 фактически различных функций, которые вы используете в качестве слушателей. Возможно, он должен быть таким же надежным, чтобы быть полным общим решением для записи любого возможного количества слушателей. Это зависит от информации, которую мы не знаем о вашем коде.
Моя надежда заключается в том, что кто-то еще сможет предоставить вам простое и легкое решение для IE11, где вы просто установите какое-либо свойство, или вызовите какой-то метод, чтобы заставить IE не отбрасывать слушателей. Однако метод грубой силы решит проблему.
Ответ 3
Это также происходит в IE до 11. Моя ментальная модель, почему эта ошибка возникает, заключается в том, что если вы нависаете над элементом, а затем перемещаете его вперед, снимая и снова присоединяя его, mouseout
события выиграли 't fire, потому что IE теряет состояние, в котором a mouseover
произошло в прошлом и, таким образом, не запускает событие mouseout
.
Похоже, что это работает отлично, если вы перемещаете все остальные элементы, кроме той, над которой вы нависаете. И это то, чего вы легко можете достичь, используя selection.sort(comparatorFunction)
. См. Документацию d3 по сортировке и selection.sort и selection.order для получения более подробной информации.
Вот простой пример:
// myElements is a d3 selection of, for example, circles that overlap each other
myElements.on('mouseover', function(hoveredDatum) {
// On mouseover, the currently hovered element is sorted to the front by creating
// a custom comparator function that returns "1" for the hovered element and "0"
// for all other elements to not affect their sort order.
myElements.sort(function(datumA, datumB) {
return (datumA === hoveredDatum) ? 1 : 0;
});
});