Properly keeping track of which (nested) div has mouse-over - javascript

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.

Related

How to capture all scrolling events on a page without attaching an onscroll handler to every single container

Consider the following Web page:
<html>
<body onscroll="alert('body scroll event')">
<div style='width:200px;height:200px;overflow:auto' onscroll="alert('div scroll event')">
<div style='height:400px'>
</div>
</div>
</body>
</html>
This html creates a div with a scrollbar. If you move the scrollbar, the onscroll event on the div element is triggered. However, the onscroll event on the body is NOT fired. This is expected, since the W3C states that element onscroll events do not "bubble".
However, I'm developing a client-side web framework that needs to know any time a scroll bar on ANY element of the page is scrolled. This would be easy to do if onscroll bubbled, but unfortunately it does not. Is there any other way to detect onscroll events across an entire page? (Right now I'm focusing mainly on Webkit, so a Webkit-specific solution would be fine...)
Here are some things I've tried:
Capturing DOMAttrModified (doesn't seem to fire for moving scrollbars.)
Using DOM Observers (also don't seem to fire for scrollbars)
Changing the onscroll event type to bubble (seems to not be possible)
It seems the ONLY way to capture onscroll events globally is to attach an onscroll event to EVERY element that may scroll, which is very ugly and is going to hurt the performance of my framework.
Anyone know a better way?
The simplest way to detect all scroll events in modern browser would be using 'capturing' rather than 'bubbling' when attaching the event:
window.addEventListener('scroll', function(){ code goes here }, true)
Unfortunately as I am aware there is no equivalent in older browser such as <= IE8
I had this same issue.
The easiest way of course is to use jQuery. Be aware that this method could potentially slow down your page significantly. Also it will not account for any new elements that are added after the event is bound.
$("*").scroll(function(e) {
// Handle scroll event
});
In vanilla JavaScript, you can set the useCapture boolean to true on your addEventListener call, and it will fire on all elements, including those added dynamically.
document.addEventListener('scroll', function(e) {
// Handle scroll event
}, true);
Note though that this will fire before the scroll event actually happens. As I understand it, there's two phases events go through. The capture phase happens first, and starts from the page root (ownerDocument?) and traverses down to the element where the event happened. After this comes the bubbling phase, which traverses from the element back up to the root.
Some quick testing too showed that this may be how jQuery handles it (for tracking scrolls on all page elements at least), but I'm not 100% sure.
Here's a JSFiddle showing the vanilla JavaScript method http://jsfiddle.net/0qpq8pcf/
*...crickets chirping... *
OK, I guess this question isn't going to get any stackoverflow love, so I might as well answer my own question as to the best solution I've found so far, in case another user stumbles across this question:
The best solution I've come up with is to capture "onmousedown" and "onkeydown" for the BODY element: These events bubble, and so if a user tries to move a scrollbar on the page these global functions will fire as a by-product. Then, in these functions, simply look up event.target and attach a temporary "onscroll" event to those objects until the mouse/key is "up" again. Using that method, you can avoid "handler bloat" and still globally capture all "onscroll" events. (I think this will work for "mouse wheel" scrolling as well, but my research on that final wrinkle is still pending.)
The following works fine when you want to i.e. close a dialog after anything in the background is scrolled:
var scrollListener = function(e) {
// TODO: hide dialog
document.removeEventListener('scroll', scrollListener, true);
};
document.addEventListener('scroll', scrollListener, true);
I needed to handle scrolling in any context in a complex of custom elements with scroll events in shadowRoots. This covers at least part of the problem in my scenario and generally borrows from the answers and comments here in the context of these newer web component APIs. Attaching and detaching the listener in the appropriate lifecycle callbacks works well so-far (once might not fit your use-case).
self.addEventListener('mousewheel', handler, {capture: true, once: true});
self.addEventListener('keydown', handler, {capture: true, once: true});
Note too the event.composedPath() provides the entire event path through all the shadowRoots with all the nodes if specifics about the scrolling context are needed. If this is the case it might make sense to use this approach and attach a new handler for that specific scenario--to the node of interest.
Like drcode said, capture on body tag is the trick. I just add touchmove to work on mobile.
document
.querySelector("body")
.addEventListener("mousewheel", e => {
console.log("scroll");
});
document
.querySelector("body")
.addEventListener("touchmove", e => {
console.log("scroll");
});
Regards,

very touchy onmouseout behaviour

I would like to design something similar to what can be seen on http://www.thedana.com/ by the "Check availability" button - I've used the jquery.js file from w3school.com and got the following so far: http://quaaoutlodge.com/drupal-7.14/ (Book Now tab). Now as you realize, it is very touchy and fades out sometimes when it shouldn't (when the cursor is still in the middle of the field) how can I make this nicer, more user friendly?
Thanks!
Ron
Update:
I tried to implement that but it doesn't quite work as I would like to show my "fade" div upon hovering over "book" and keep it up as the cursor moves down, over "fade" how do I accomplish this?
Url:http://quaaoutlodge.com/drupal-7.14/
Put the div#fade inside of the div#book, that will solve half of your problems. You will have to adapt the CSS as well for this change.
Another very important point to learners is that jQuery provides unobtrusive cross-browser event listeners attaching. That means, inline JS in the html as onmouseenter="handler()" is not just unnecessary and technically ugly - mixed structure with behavior -, it also pollutes the global scope with function names.
That's one of the reasons people advertise against W3School.
But back on topic here's a solution using the DOM Ready handler and a hover one:
Fiddle
HTML
<div id="book">
Book Now
<div id="fade">TEST</div>
</div>​
JS
​$(function() {
var fade = $('#fade');
$('#book').hover(function() {
fade.fadeIn();
}, function() {
fade.fadeOut();
});
});
Again, you will have to rework the CSS removing the position:absolute and margins from #fade.
Can you try with jquery's .mouseleave instead of the generic onmouseout?
http://api.jquery.com/mouseleave/
"The mouseleave event differs from mouseout in the way it handles event bubbling. If mouseout were used in this example, then when the mouse pointer moved out of the Inner element, the handler would be triggered. This is usually undesirable behavior. The mouseleave event, on the other hand, only triggers its handler when the mouse leaves the element it is bound to, not a descendant. So in this example, the handler is triggered when the mouse leaves the Outer element, but not the Inner element."

Is there no Event.fromElement in a dragenter/dragover event?

I was playing with drag n drop in full forms (so no instant upload). I though small part was gonna be highlighting a certain fieldset when hovered over with a file. Enter dragover and dragenter events (and dragleave etc).
Turns out it's not such a small part. The Fiddle: http://jsfiddle.net/rudiedirkx/epp74/
Try it out: drag over a fieldset and move around a bit. The first over triggers the fieldset's dragenter event (fieldset is yellow). The moving around after that (within the same fieldset) triggers dragenters and dragleaves (fieldset no more yellow), which is bad.
Which is why I wanted to make what IE made for mouseover and mouseout a long time ago: mouseenter and mouseleave (they trigger just once). For drag events, the exact same thing applies: they should trigger only once in the exact same way. JS libraries spoof these IE events by using Event.fromElement and Event.toElement (and compare them against the event owner element). (See jQuery or Mootools source for specifics.)
To make the same for drag events, I need the same fromElement and toElement. You can see in the Fiddle, I try, but I can't find them.
Anybody know where they are? Why they're not available?
I'm using Chrome primarily, which doesn't have a fromElement in the dragenter event, but does have a toElement in the dragleave event. In Firefox it's slightly worse (but more logical): both are empty.
Any and all ideas are so very welcome.
edit
After a little more debugging I've found out that Chrome's toElement in dragleave isn't always correct. It's never 'bigger' thanthis, but sometimes it should be: when I leave the fieldset (this) to its parent form (toElement). When I do that, both this and toElement are the fieldset (which is incorrect, right?).
edit Solution:
I ended up with something like this: http://jsfiddle.net/rudiedirkx/Lwd3md71/ which ignores elements in the event, and uses the event coordinates to find the element under the mouse. To make it trigger max once per animation frame, it uses requestAnimationframe, which results into 31-59 fps.
Firefox provides the relatedTarget event property, but Chrome and Safari don't. Sadly, this issue has been open for a couple years as this Chrome bug and this Webkit bug.
Edit: The issue has been fixed in Chrome.
There is a way of faking the relatedTarget for a "dragleave" event, which is to set a variable from the accompanying "dragenter" event -- since dragleave is always preceded by dragenter, a variable set in the latter will be available to the former:
var relatedTarget = null;
document.addEventListener('dragenter', function(e)
{
relatedTarget = e.target;
}, false);
document.addEventListener('dragleave', function(e)
{
console.log('target = ' + e.target + ' relatedTarget = ' + relatedTarget);
}, false);
It won't work the other way round, but you don't really need dragenter for anything else if you use it this way -- i.e. the dragleave alone is enough to tell you when the mouse is moving into, or entirely out of, a particular element.

how to prevent mutiple registration of a single event handler in jquery

I have a <div> box displaying search message and some radio button for recent message. There is link option for slide toggle.
When you click on that link it will show some input field and check box and radio button. And at the same time the text of link change to hide option. If you click on that it will hide all the input and checkbox option.
When I refreash the whole page its working properly but when that paticular box or part is refreashing then the box is hiding and imediately hides. If you refresh that portion n number of times the box is going on toggling continously. I think the problem is in registration of event handler. So please give me some solution.
CODE :
$(document).ready(function() {
$(".SideBar-blockheader1").click(function() {
e.preventDefault();
$(".SideBar-blockcontent1").slideToggle("fast");
});
$(".SideBar-optionheader").click(function() {
$(".SideBar-optioncontent").slideToggle("fast");
$(this).text($(this).text() == $("#hideopt").attr('value') ? $("#showopt").attr('value') : $("#hideopt").attr('value'));
});
$(".SideBar-optionheader").text($("#showopt").attr('value'));
$(".SideBar-optioncontent").hide();
});
jQuery has a method, called data() which can be used to extract the attached handler information of an HTML element. You can see if the element has already a click handler, and if it has, then stop re-attaching another handler to it.
if(typeof $('#id').data('events').click == 'object')
{
// A click handler is already attached
}
else
{
// No click handler; Attach one;
}
Although you haven't provided code, I suspect you are using .click(). For jQuery 1.7+ you should be using .on() in delegate mode (the element you bind to is an ancestor, not the clickable element itself) or .delegate() if pre jQ 1.7.
For example:
$('someAncestor').on('click', 'a.specialLink', function(event) {
event.preventDefault();
// the rest of your code for the click handler
})
"someAncestor" is any valid selector that is an ancestor of your link that will not be destroyed, rebuilt, or otherwise manipulated after the DOM is built. It doesn't have to be the direct ancestor.
[updated below after seeing code sample and comments]
There are a few things going on. First, .on() will only work if you're using jQuery 1.7+. Next, .on() can be invoked a few different ways (I wrote about it here: http://gregpettit.ca/2011/jquery-events-its-on/) and you need to be invoking it while delegating an ancestor listener, not simply as a substitute for click. Next, you haven't provided code for your attempted update, only for the original code; it's hard to tell what "didn't work" about trying to use .on(). Moving along, I'm not actually sure what this line is meant to do:
$(this).text($(this).text() == $("#hideopt")...etc...
I can't think of why you would want to try to treat a jQuery object as a variable. I'm not saying the code is wrong, I'm just saying I don't get it. Also, I hate ternary operators... which is part of the reason I don't get it. I much prefer readable conditionals. ;-)
Next, you're calling preventDefault() on "e" but you're not passing "e" into your functions. You might just be getting a JavaScript error, period. (e is undefined)
Then there's attr("value") which I believe should actually work. But why not use .val() if it is indeed a node that HAS a value attribute?
Finally, there is tonnes of room for caching your objects. Every time you see that an object is being used more than once, you can benefit (to varying degrees of performance and legibility) from caching it. I have not updated the code with any caching, though-- that's really something for a whole other "How can I best cache my objects?" question.
Anyhow... to solve the problem, you first have to choose a valid ancestor. This can be any ancestor that isn't destroyed during the process of loading in your new data. This could be anything, but the closest ancestor is the best. It might be a section wrapper, but if you're truly desperate it could be a page wrapper or even the body tag. If you bind to document, you're reproducing the deprecated .live() function, which I definitely recommend against. I have used a placeholder selector, ".section" but you need to figure out what an appropriate ancestor is on your page.
$(document).ready(function()
{
$(".section").on("click", ".SideBar-blockheader1", function(e)
{
e.preventDefault(); // probably not necessary if there's no default click behaviour
$(".SideBar-blockcontent1").slideToggle("fast");
});
$(".section").on("click", ".SideBar-optionheader", function(e)
{
e.preventDefault(); // probably not necessary if there's no default click behaviour
$(".SideBar-optioncontent").slideToggle("fast");
$(this).text($(this).text() == $("#hideopt").val() ?$("#showopt").val() : $("#hideopt").val());
});
$(".SideBar-optionheader").text($("#showopt").val());
$(".SideBar-optioncontent").hide();
});

Performance difference between jQuery's .live('click', fn) and .click(fn)

I love the new live event in jQuery 1.3. The question I have is the performance of this event. I know the advantages of using live over click/bind('click') but is there a performance hit for using it over click/bind('click')?
If not, why would you ever use click or bind('click')?
If not, why would you ever use click
or bind('click')?
Because $.live() has some significant disadvantages
Live events do not bubble in the traditional manner and cannot be
stopped using stopPropagation (This changed in jquery 1.4.4) or
stopImmediatePropagation. For example,
take the case of two click events -
one bound to "li" and another "li a".
Should a click occur on the inner
anchor BOTH events will be triggered.
This is because when a
$("li").bind("click", fn); is bound
you're actually saying "Whenever a
click event occurs on an LI element -
or inside an LI element - trigger this
click event." To stop further
processing for a live event, fn must
return false.
Live events currently only work when used against a selector. For
example, this would work: $("li
a").live(...) but this would not:
$("a", someElement).live(...) and
neither would this:
$("a").parent().live(...).
See this.
As for why you would ever use click or bind instead of live, the answer is because you don't need the extra functionality.

Categories