What would be the best way to implement a mouseenter/mouseleave like event in Javascript without jQuery? What's the best strategy for cross browser use? I'm thinking some kind of checking on the event.relatedTarget/event.toElement property in the mouseover/mouseout event handlers?
Like to hear your thoughts.
(Totally changed my terrible answer. Let's try again.)
Let's assume you have the following base, cross-browser event methods:
var addEvent = window.addEventListener ? function (elem, type, method) {
elem.addEventListener(type, method, false);
} : function (elem, type, method) {
elem.attachEvent('on' + type, method);
};
var removeEvent = window.removeEventListener ? function (elem, type, method) {
elem.removeEventListener(type, method, false);
} : function (elem, type, method) {
elem.detachEvent('on' + type, method);
};
(Pretty simple, I know.)
Whenever you implement mouseenter/mouseleave, you just attach events to the
normal mouseover/mouseout events, but then check for two important particulars:
The event's target is the right element (or a child of the right element)
The event's relatedTarget is not a child of the target
So we also need a function that checks whether one element is a child of
another:
function contains(container, maybe) {
return container.contains ? container.contains(maybe) :
!!(container.compareDocumentPosition(maybe) & 16);
}
The last "gotcha" is how we would remove the event listener. The quickest way
to implement it is by just returning the new function that we're adding.
So we end up with something like this:
function mouseEnterLeave(elem, type, method) {
var mouseEnter = type === 'mouseenter',
ie = mouseEnter ? 'fromElement' : 'toElement',
method2 = function (e) {
e = e || window.event;
var target = e.target || e.srcElement,
related = e.relatedTarget || e[ie];
if ((elem === target || contains(elem, target)) &&
!contains(elem, related)) {
method();
}
};
type = mouseEnter ? 'mouseover' : 'mouseout';
addEvent(elem, type, method2);
return method2;
}
Adding a mouseenter event would look like this:
var div = document.getElementById('someID'),
listener = function () {
alert('do whatever');
};
mouseEnterLeave(div, 'mouseenter', listener);
In order to remove the event, you'd have to do something like this:
var newListener = mouseEnterLeave(div, 'mouseenter', listener);
// removing...
removeEvent(div, 'mouseover', newListener);
It's hardly ideal, but all that's left is just implementation details. The
important part was the if clause: mouseenter/mouseleave is just
mouseover/mouseout, but checking if you're targeting the right element, and if
the related target is a child of the target.
The best way, imho, is to craft your own event system.
Dean Edwards wrote one some years ago that I've taken cues from in the past. His solution does work out of the box however.
http://dean.edwards.name/weblog/2005/10/add-event/
John Resig submitted his entry to a contest, in which his was judged the best (Note: Dean Edwards was one of the jury). So, I would say, check this one out too.
Also its doesn't hurt to go thru jQuery, DOJO source once in a while, to actually see the best practices they r using to make it work cross-browser.
another option is to distinguish true mouseout events from fake (child-generated) events using hit-testing. Like so:
elt['onmouseout']=function(evt){
if (!mouse_inside_bounding_box(evt,elt)) console.debug('synthetic mouseleave');
}
I've used something like this on chrome and, caveat emptor, it seemed to do the trick. Once you have a reliable mouseleave event mouseenter is trivial.
Related
I want to throttle function calls which are added as Event Listeners to the window.scroll function by a 3rd party library provided by an external supplied (cant be changed).
I figured out that the library causes some overhead by its scroll event listener, because if I remove the event handler, my page runs much smoother.
As I cannot directly control or change the external JS file, I thought to read the scroll-events attached to the Window and delete / rebind them again, but in a throttled format, as I have already the Underscore.js library in use.
I'm trying to read the Scroll events and than replace the function callback as a throttled version:
jQuery(document).ready(function($) {
let scrollEvents = $._data(window, 'events').scroll;
for(evt of scrollEvents ) {
evt.handler = _.throttle(evt.handler, 200)
}
});
Does not seem to bring the appropriate improvement. In the Webdeveloper Bar "Global Event Listeners" I still see the original event listeners attached, I do NOT see the Underscore Library (as intermediate layer) listed there.
What is potentially wrong with this code?
Thanks
EDIT
Those events are added globally to the Window, see WebDev Screenshot:
and I run the above code within the WebDev console, so it is ran only after those events exist already.
AND $._data(window, 'events').scroll; shows ALL those 5 events, so jQuery should be able to change them, isnt it?
Found a beautiful solution using Underscore.js, proxying the callback functions by a Throttler before adding it as Event Handler:
var f_add = EventTarget.prototype.addEventListener;
var f_remove = EventTarget.prototype.removeEventListener;
EventTarget.prototype.addEventListener = function(type, fn, capture) {
this.f = f_add;
if(type == 'scroll' && typeof _ === 'function')
fn = _.throttle(fn, 350);
this.f(type, fn, capture);
}
EventTarget.prototype.removeEventListener = function(type, fn, capture) {
this.f = f_remove;
if(type == 'scroll' && typeof _ === 'function')
fn = _.throttle(fn, 350);
this.f(type, fn, capture);
}
It overwrites the prototype for add/removeEventListener -> And if the event is a scroll event, it surrounds the Function fn with _.throttle().
Just as the title says I'm curious if I'm guaranteed to get an event object inside of a Javscript event handler. The main reason I'm asking is that I've seen onClick event handlers that look like this.
function(e) {
if(e && e.target) {
//Code in here
}
}
Which seems wrong to me, but I know Javascript can have minor variances across browsers. Is there some time at which it's appropriate to check for the event object? Or the event target? It seems like you'd have to have a target to fire off an event.
No. Older versions of windows don't pass the event argument to the event handler. They have it in a global variable window.eventand the target is in .srcElement. Other than that exception, you should always get an event structure.
A work-around for the older versions of IE is this:
function(e) {
if (!e) {
e = window.event;
e.target = e.srcElement;
}
// code that uses e here
}
But, usually, this is addressed at a higher level by the function that you use to install event handlers. For example:
// add event cross browser
function addEvent(elem, event, fn) {
if (elem.addEventListener) {
elem.addEventListener(event, fn, false);
} else {
elem.attachEvent("on" + event, function() {
// set the this pointer same as addEventListener when fn is called
window.event.target = window.event.srcElement;
return(fn.call(elem, window.event));
});
}
}
Depending on the browser compatibility they are looking to achieve, this may be an acceptable solution. However, for older version of IE, the event object is a part of the global window object. In order to get the target in that case you would want window.event.srcElement, as there is no target.
More info here on the event object for IE.
I'm sure we've all seen the site for vanilla-js (the fastest framework for JavaScript) ;D and I was just curious, exactly how much faster plain JavaScript was than jQuery at adding an event handler for a click. So I headed on over to jsPerf to test it out and I was quite surprised by the results.
jQuery outperformed plain JavaScript by over 2500%.
My test code:
//jQuery
$('#test').click(function(){
console.log('hi');
});
//Plain JavaScript
document.getElementById('test').addEventListener('click', function(){
console.log('hi');
});
I just can't understand how this would happen because it seems that eventually jQuery would end up having to use the exact same function that plain JavaScript uses. Can someone please explain why this happens to me?
As you can see in this snippet from jQuery.event.add it does only create the eventHandle once.
See more: http://james.padolsey.com/jquery/#v=1.7.2&fn=jQuery.event.add
// Init the element's event structure and main handler, if this is the first
events = elemData.events;
if (!events) {
elemData.events = events = {};
}
eventHandle = elemData.handle;
if (!eventHandle) {
elemData.handle = eventHandle = function (e) {
// Discard the second event of a jQuery.event.trigger() and
// when an event is called after a page has unloaded
return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? jQuery.event.dispatch.apply(eventHandle.elem, arguments) : undefined;
};
// Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
eventHandle.elem = elem;
}
And here we have the addEventListener:
// Init the event handler queue if we're the first
handlers = events[type];
if (!handlers) {
handlers = events[type] = [];
handlers.delegateCount = 0;
// Only use addEventListener/attachEvent if the special events handler returns false
if (!special.setup || special.setup.call(elem, data, namespaces, eventHandle) === false) {
// Bind the global event handler to the element
if (elem.addEventListener) {
elem.addEventListener(type, eventHandle, false);
} else if (elem.attachEvent) {
elem.attachEvent("on" + type, eventHandle);
}
}
}
I think it's because internally jQuery really only has to call addEventListener() once, for its own internal handler. Once that's set up, it just has to add your callback to a simple list. Thus most of the calls to .click() just do some bookkeeping and a .push() (or something like that).
Consider a basic addEventListener as
window.onload=function(){
document.getElementById("alert")
.addEventListener('click', function(){
alert("OK");
}, false);
}
where <div id="alert">ALERT</div> does not exist in the original document and we call it from an external source by AJAX. How we can force addEventListener to work for newly added elements to the documents (after initial scan of DOM elements by window.onload)?
In jQuery, we do this by live() or delegate(); but how we can do this with addEventListener in pure Javascript? As a matter of fact, I am looking for the equivalent to delegate(), as live() attaches the event to the root document; I wish to make a fresh event listening at the level of parent.
Overly simplified and is very far away from jQuery's event system but the basic idea is there.
http://jsfiddle.net/fJzBL/
var div = document.createElement("div"),
prefix = ["moz","webkit","ms","o"].filter(function(prefix){
return prefix+"MatchesSelector" in div;
})[0] + "MatchesSelector";
Element.prototype.addDelegateListener = function( type, selector, fn ) {
this.addEventListener( type, function(e){
var target = e.target;
while( target && target !== this && !target[prefix](selector) ) {
target = target.parentNode;
}
if( target && target !== this ) {
return fn.call( target, e );
}
}, false );
};
What you are missing on with this:
Performance optimizations, every delegate listener will run a full loop so if you add many on a single element, you will run all these loops
Writable event object. So you cannot fix e.currentTarget which is very important since this is usually used as a reference to some instance
There is no data store implementation so there is no good way to remove the handlers unless you make the functions manually everytime
Only bubbling events are supported, so no "change" or "submit" etc which you took for granted with jQuery
Many others which I'm simply forgetting about for now
document.addEventListener("DOMNodeInserted", evtNewElement, false);
function evtNewElement(e) {
try {
switch(e.target.id) {
case 'alert': /* addEventListener stuff */ ; break;
default: /**/
}
} catch(ex) {}
}
Note: according to the comment of #hemlock, it seems this family of events is deprecated. We have to head towards mutation observers instead.
I have a link that has a listener attached to it (I'm using YUI):
YAHOO.util.Event.on(Element, 'click', function(){ /* some functionality */});
I would like to the same functionality to happen in another scenario that doesn't involve a user-click. Ideally I could just simulate "clicking" on the Element and have the functionality automatically fire. How could I do this?
Too bad this doesn't work:
$('Element').click()
Thanks.
MDC has a good example of using dispatchEvent to simulate click events.
Here is some code to simulate a click on an element that also checks if something canceled the event:
function simulateClick(elm) {
var evt = document.createEvent("MouseEvents");
evt.initMouseEvent("click", true, true, window,
0, 0, 0, 0, 0, false, false, false, false, 0, null);
var canceled = !elm.dispatchEvent(evt);
if(canceled) {
// A handler called preventDefault
// uh-oh, did some XSS hack your site?
} else {
// None of the handlers called preventDefault
// do stuff
}
}
You're looking for fireEvent (IE) and dispatchEvent (others).
For YUI 3 this is all wrapped up nicely in Y.Event.simulate():
YUI().use("node", function(Y) {
Y.Event.simulate(document.body, "click", { shiftKey: true })
})
You can declare your function separately.
function DoThisOnClick () {
}
Then assign it to onclick event as you do right now, e.g.:
YAHOO.util.Event.on(Element, 'click', DoThisOnClick)
And you can call it whenever you want :-)
DoThisOnClick ()
In case anyone bumps into this looking for a framework agnostic way to fire any HTML and Mouse event, have a look here: How to simulate a mouse click using JavaScript?
1) FIRST SOLUTION
The article http://mattsnider.com/simulating-events-using-yui/ describes how to simulate a click using YAHOO:
var simulateClickEvent = function(elem) {
var node = YAHOO.util.Dom.get(elem);
while (node && window !== node) {
var listeners = YAHOO.util.Event.getListeners(node, 'click');
if (listeners && listeners.length) {
listeners.batch(function(o) {
o.fn.call(o.adjust ? o.scope : this, {target: node}, o.obj);
});
}
node = node.parentNode;
}
};
As you can see, the function loops over the node and its parents and for each of them gets the list of listeners and calls them.
2) SECOND SOLUTION
There is also another way to do it.
For example:
var elem = YAHOO.util.Dom.get("id-of-the-element");
elem.fireEvent("click", {});
where function is used as
3) THIRD SOLUTION
Version 2.9 of YUI2 has a better solution: http://yui.github.io/yui2/docs/yui_2.9.0_full/docs/YAHOO.util.UserAction.html
4) FORTH SOLUTION
YUI3 has also a better and clean solution: http://yuilibrary.com/yui/docs/event/simulate.html
Of course $('Element').click() won't work, but it can if you include jquery, it works well alongside yui.
As I untestand you need to do the following:
function myfunc(){
//functionality
}
YAHOO.util.Event.on(Element, 'click', myfunc);
Then call myfunc when something else needs to happen.
The inability to simulate a user-click on an arbitrary element is intentional, and for obvious reasons.