Raphaël (and its successor, Snap) both have a drag() method (Raphaël, Snap), and both claim:
Additionally following drag events will be triggered: drag.start.<id> on start, drag.end.<id> on end and drag.move.<id> on every move. When element will be dragged over another element drag.over.<id> will be fired as well.
The question is, those additional events are triggered on what? I've tried a document.addEventListener('drag.move.0') and paper.node.addEventListener('drag.move.0') and neither seems to be triggered. What object(s) do I need to listen to, to get those global drag events?
Okay, did my own source-diving and found it's a bit tangled, as Ian supposed. Firstly, in Snap, the event names changed to have a snap. prefix (source).
Looking at that source code, Snap is using its own event library ("Eve"), that doesn't use native listeners at all, so events aren't bound to any object, they're just namespaced for the thing you're interested in knowing about:
eve.on('snap.drag.start.0', function(el) {
console.log('Item 0 started dragging!');
});
The parameter passed to the listener is either the start_scope passed to the original drag() call, or the element being dragged if it wasn't set at that time.
The eve library does allow for wildcards in its scope, so something like:
eve.on('snap.drag.start.*', function(el) {
console.log('Item ' + el.id + ' started dragging!');
});
Should work to listen to all drag actions.
Related
Is there a way to get all elements that have a certain event listener attached to them?
I know I can get them if the event listener is defined as an attribute:
var allElemsInBodyWithOnclickAttr = $("body").find("*[onclick]");
But I have elements that have event listeners that are attached by code and thus have no onclick attribute.
So when I do (for example) this:
$("a").on("click", function(evt) {
alert("Hello");
});
..then using the code below doesn't fire the click on those anchor elements:
$("a[onclick]").trigger("click");
I guess I could loop through all the elements and check if they have the listener I'm looking for (using this SO question) but I can't imagine that's going to perform very well..
Can't add comments, but still - consider using https://github.com/ftlabs/fastclick for removing the delay.
That helped me, when i was developing app using cordova.
Also, didn't notice you have already mentioned this post, so i have written implementation for 'loop through all elements'-type-of-solution
Array.prototype.reduce.call(
$('body').children(),
(answer, node) => {
if (typeof $._data($(node)[0], 'events') != 'undefined') {
answer.push(node);
}
return answer;
},
[]
);
I think this is not possible, since it is not possible to test if a single element has an event listener attached to it.
Look here
So the only way to do that is to manage a map which contains a reference to each event handler for each event for each element.
Edit
With respect to the answer of #Ivan Shmidt, I must correct my answer: Obviously It seams to be possible with jQuery. That is because jQuery is holding a reference of attached event handlers, BUT events attached using good old .addEventListener() would bypass this and also not be found this way.
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?
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.
One solution could be to use event delegation. This fairly simple paradigm is commonplace in jQuery (which gave me the idea to try it here.)
By extending the d3.selection prototype with a delegated event listener we can listen for events on a parent element but only apply the handler if the event's target is also our desired target.
So instead of:
d3.select('#targetElement').on('mouseout',function(){})
You would use:
d3.select('#targetElementParent').delegate('mouseout','#targetElement',function(){})
Now it doesn't matter if the events are lost when you move elements or even if you add/edit/delete elements after creating the listeners.
Here's the demo. Tested on Chrome 37, IE 11 and Firefox 31. I welcome constructive feedback but please note that I am not at all familiar with d3.js so could easily have missed something fundamental ;)
//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/
Updated and Improved...
Following Makyen's useful feedback I have made a few improvements to allow the delegated listener to be applied to ALL matched children. EG "listen for mouseover on each g within svg"
Here's the fiddle. Snippet below.
//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>
As with all delegated listeners if you move the target element outside of the parent you have delegated the listening to then naturally the events for that child are lost. However, there is nothing to stop you delegating the event listening to the body tag as you'll never move a child outside of that. EG:
d3.select('body').delegate('mouseover','g',function(){...
This also happens in IE prior to 11. My mental model for why this error occurs is that if you're hovering over an element and then move it to the front by detaching and re-attaching it, mouseout events won't fire because IE loses the state that a mouseover occured in the past and thus doesn't fire a mouseout event.
This seems to be why it works fine if you move all other elements but the one you're hovering over. And this is what you can easily achieve by using selection.sort(comparatorFunction). See the d3 documentation on sort and the selection.sort and selection.order source code for further details.
Here's a simple example:
// 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;
});
});
I have a (deeply) nested structure of <div>s. With Javascript, I'd like to keep track of which element the mouse is currently over (i.e., 'has focus'), where more deeply nested elements have priority:
I've tried combinations of mouseover, mouseout, mouseenter, mouseleave and mousemove, but there seems to be no simple solution that gives me the expected behavior. I am able to receive mouse-events on the right divs, but often these events are subsequently received by higher level divs, which would then undeservedly take focus.
For example, in the transition above from a to c, the event might then be received by b last, unintentionally giving focus to b rather than c. Or the transition from c to b might not be registered at all for some reason.
I don't understand the underlying mechanism of mouse-event propagation well enough to come up with a reliable solution. It seems like it should be such a simple thing, but I can't figure it out.
I've been able to get it to work as follows: when a div receives a mouseenter/over event, flag it and search the entire DOM subtree. If any other flags are found deeper down, relinquish focus. But this is rather crude, and I can't help but think there must be an easier way.
Edit: Solution
The use of mouseover and mouseout, combined with a call to stopPropagation() seems to work very well. Here is a JSFiddle to show the working solution.
You can use the event stopPropagation() method:
https://developer.mozilla.org/en-US/docs/Web/API/event.stopPropagation
If using jQuery, try calling stopPropagation() on the event passed as the first parameter
http://api.jquery.com/event.stoppropagation/
$( "p" ).on('mouseover', function( event ) {
event.stopPropagation();
// Do something
});
Also, you can check which element is the target, as in event.target.
For non-IE browsers & IE >= 9 use,
evt.stopPropagation();
For Legacy IE browsers(IE<9) browsers use,
evt.cancelBubble = true;
This will prevent bubbling of events to parent elements.
The Agility.js documentation seems to say pretty clearly that Agility objects can accept any valid jQuery event type:
Usual DOM events such as click, dblclick, mouseenter, etc are supported through
jQuery's event API. Please consult jQuery's API for a list of events supported.
But in the docs and in all other examples I've seen, all the controller functions bound to events take no parameters. For example, the following snippet is taken straight out of the online docs:
var button = $$({msg:'Click me'}, '<button data-bind="msg"/>', {
'click &': function() {
this.model.set({msg:"I've been clicked!"});
}
});
$$.document.append(button);
Here the lambda attached to the click event obviously is parameterless. This works OK for ordinary jQuery events, but to use jQueryUI events, each event function has associated data. For example, if I want to use Draggable and Droppable widgets, the drop() function is called on the Droppable to indicate that a Draggable has been dropped onto it, and I need the parameters to drop(event, ui) to figure out which Draggable it was.
Is there any way to declare my controller event handlers so that I can get access to those parameters? Alternatively, is there some other way to do what I'm trying to do? Not getting any information about an event on a controller other than "it fired" seems like a real shortcoming here. I'm not even sure it is possible to extend Agility so that it can handle events other than the standard DOM ones, although I don't see any reason why it shouldn't be, from my reading of the code.
Agility does provide a trigger() function for client code to fire events, which allows for parameters other than the event type. I thought of wiring that up to the jQueryUI event somehow, but I don't see quite how. More to the point, why can you even pass parameters to trigger() if controllers can't see them?
That the example does not show it is unfortunate, but that doesn't mean the argument is not there.
var button = $$({msg:'Click me'}, '<button data-bind="msg"/>', {
'click &': function(jqEvent) {
this.model.set({msg:"I've been clicked!" + jqEvent.target.nodeName});
}
});
$$.document.append(button);
When in doubt just include a console.log(arguments) in your event handlers and see for yourself. You will get the same arguments jQuery gives you.
If you experiment with .trigger() and extra params you will find that there are no surprises there, either.
I tried to find an answer to my problem, but was not successful - apologies if the answer is very obvious:). Here is the premise of the issue I am trying to solve.
I have lots of UI elements (buttons, hyperlinks etc) that have native events attached to them (eg, clicking a link executes a function)
I do not have access to those event listeners, nor I know what functions/handlers need to be Invoked
I would like to do a generic function which would:
o transverse thru DOM, find the UI elements like button or hyperlink, and attach additional listeners to it that would execute the same handlers/functions (eg, I want to attach “touchend” listeners that would execute the same handlers/functions as “click” event)
Is there a way for me to somehow find out what event handler(s) is(are) used for a particular UI element, and then append the new listener for same handler via .on() method?
Hiya even though your quandary is bit open ended, hope this helps your cause :)
Read this - Things you may not know about JQuery = http://james.padolsey.com/javascript/things-you-may-not-know-about-jquery/
You can access all event handlers bound to an element (or any object) through jQuery’s event storage:
// List bound events:
console.dir( jQuery('#elem').data('events') );
// Log ALL handlers for ALL events:
jQuery.each($('#elem').data('events'), function(i, event){
jQuery.each(event, function(i, handler){
console.log( handler.toString() );
});
});
// You can see the actual functions which will occur
// on certain events; great for debugging!
P.S. - I would recommend to know your DOM better but above should help. B-)