How to achieve mouseleave effect with absolutely-positioned non-descendants?
Asked Answered
S

2

6

One problem with the standard mouseout event is that it fires not only when the cursor leaves the region of the screen bounded by the element's external perimeter, but also when the cursor hovers over some other element contained within this perimeter.

The rationale for jQuery's mouseleave event is to signal only the moment when the cursor leaves the the area bounded by an element's external perimeter.

Unfortunately, this seems to work only if the "obstructing" element is a descendant of the "obstructed" element. If the "obstructing" element is where it is through absolute positioning, then when the mouse hovers over it, the mouseleave event on the "obstructed" element gets fired.

For example, with the following HTML:

<div id="b-div">
    <div id="d-div"><span>d</span></div>
</div>
<div id="c-div"><span>c</span></div>

...#d-div is a bona-fide descendant of #b-div, whereas #c-div isn't, but, but we can style it so that it "obstructs" #b-div all the same. This is illustrated in this jsFiddle.

If now one defines the following events on #b-div:

$( '#b-div' ).bind( {
    mouseenter: function () {
        $( this ).addClass( 'outlined' );
    },
    mouseleave: function () {
        $( this ).removeClass( 'outlined' );
    }
} );

...then hovering the mouse within #b-div's outer perimeter causes a blue outline to appear over this perimeter, unless the mouse is over #c-div.

Is there a way to get the same effect with #b-div and #c-div as mouseleave achieves with #b-div and #d-div?

EDIT: I've fixed the example shown in the jsFiddle. The original version of this example showed the unrepresentative special case in which all of the obstructing element overlaps with the obstructed element. In this special case, the desired effect can be simulated by defining the same events on both the obstructing and the obstructed elements, thus, in effect, turning the obstructing element into a patch of the obstructed element. This won't work when the obstructing element is not fully contained within the obstructed element's outer perimeter (as shown in the amended jsFiddle). More generally, any solution that is based on using a mouseover event on the obstructing element is bound to fail, since the real problem is to prevent (or render ineffective) the spurious mouseleave on the obstructed element.

State answered 1/6, 2015 at 18:27 Comment(5)
You mean like this -> jsfiddle.net/adeneo/q1Lgzr4c/1Jit
@adeneo: it turns out that my original example illustrated a somewhat unrepresentative special case; I've updated the example to correct this. Sorry about that.State
What almost works is checking the e.relatedTarget jsfiddle.net/p5yf0dcs/2 The problem is that you now won't lose the highlight when mousing out of the b-divPunner
Not sure I get it, wouldn't that still be trivial -> jsfiddle.net/adeneo/p5yf0dcs/4Jit
@adeneo, I think kjo doesn't want #b-div to get the class unless it's under the mouse. In your example, it gets the class when you mouse over any part of #c-div.Kenyakenyatta
K
7

This does it, based on your initial post in which #c-div was completely contained within #b-div:

$('#b-div, #c-div').on( {
  mouseenter: function (ev) {
    $('#b-div').addClass('outlined');
  },
  mouseleave: function (ev) {
    $('#b-div').removeClass('outlined');
  }
});

Fiddle 1


Since #c-div may not always be contained completely within #b-div, you can use your existing code if you add this style:
#c-div {
  pointer-events: none;
}

But this will make it impossible to interact with #c-div using the mouse.

Fiddle 2


If you do need to interact with #c-div, and it's not completely within #b-div, you can use Element.getBoundingClientRect like this:

$('#b-div, #c-div').on('mousemove mouseleave',
  function(ev) {
    var br= $('#b-div')[0].getBoundingClientRect();
    $('#b-div').toggleClass(
      'outlined',
      ev.pageX > br.left && ev.pageX < br.left+br.width &&
      ev.pageY > br.top  && ev.pageY < br.top +br.height
    )
  }
);

Fiddle 3

Kenyakenyatta answered 1/6, 2015 at 18:47 Comment(7)
Upon seeing your solution I realized that my original example was seriously flawed. I have fixed it now. I trust that with the new version it will be clearer why solutions like the one you proposed won't work in general (hovering over the parts of the obstructing element that do not overlap with the obstructing element should not trigger the mouseenter event). I apologize for the lack of clarity in my original post.State
You could add pointer-events: none to the #c-div style – unless it has content that you need to interact with using the mouse.Kenyakenyatta
@State do you still need an answer or you have found the issue resolutionEfface
@RickHitchcock: that idea works great. I'd be happy to accept if you post it.State
@MaxZoom: the solution in Rick Hitchcock's latest comment works very well, but if you have another solution, by all means, please post it.State
Updated. If you do need to interact with #c-div, you could use getBoundingClientRect to make sure mouse coordinates are within #b-div's boundaries.Kenyakenyatta
@RitchHItchcock There's an easier wayPunner
P
0

If you are not able to use Rich's suggestions of pointer-events: none (maybe you need to support IE 10 or you do need to interact with the absolutely positioned div), you can manually check that the event is not going to #c-div using relatedTarget.

However, you also have to check that the mouseleave from #c-div is not going to #b-div.

$( '#b-div' ).bind( {
    mouseenter: function () {
        $( this ).addClass( 'outlined' );
    },
    mouseleave: function (e) {
        if (e.relatedTarget.id == 'c-div' ||  $.contains(document.getElementById('c-div'), e.relatedTarget)) {
            return;
        }
        $( this ).removeClass( 'outlined' );
    }
} );

$( '#c-div' ).bind( {
    mouseleave: function (e) {
        if (e.relatedTarget.id == 'b-div' ||  $.contains(document.getElementById('b-div'), e.relatedTarget)) {
            return;
        }
        $( '#b-div' ).removeClass( 'outlined' );
    }
});
#a-div {
    position: relative;
    margin: 20px;
}
#b-div {
    height: 100px;
    width: 100px;
    background-color: #555;
    padding: 50px;
}
#c-div {
    position: absolute;
    height: 50px;
    width: 200px;
    top: 100px;
    left: 100px;
    background-color: #999;
}
#d-div {
    height: 50px;
    width: 50px;
    background-color: #ddd;
    text-align: center;
}
#c-div span {
    margin: 21.5px;
    line-height: 50px;
}
#d-div span {
    margin: auto;
    line-height: 50px;
}
.outlined {
    outline: 10px solid steelblue;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="a-div">
    <div id="b-div"><div id="d-div"><span>d</span></div></div>
    <div id="c-div"><span>c</span></div>
</div>
Punner answered 1/6, 2015 at 20:58 Comment(1)
This doesn't highlight #b-div if you initially hover #c-div, so I don't think it completely meets the OP's requirements.Kenyakenyatta

© 2022 - 2024 — McMap. All rights reserved.