35

There are two elements in play:

$('#myInput') // an input field for search
$('#myList') // a list to display search results

I want to hide the list when the input no longer has focus, like so:

$('#myInput').blur(function() {
  $('#myList').hide();
});

This works great, except when a list item is clicked, because the blur event fires and hides the list before the click is registered. The goal is for the list to stay visible when any part of the list is clicked, even though this will cause the input to blur.

How can I do this? Thanks!

Justin Stayton
  • 6,031
  • 8
  • 37
  • 43

5 Answers5

18

You can accomplish this by keeping a global variable, and setTimouts, to wait a delay of 200ms and then check if one of the 2 elements have focus.

var keepFocus = false;

function hideList(){
    if(!keepFocus){
        $('#myList').hide();
    }
}

$('#myInput').blur(function() {
    keepFocus = false;
    window.setTimeout(hideList, 200);
}).focus(function(){
    keepFocus = true;
});


$('#myList').blur(function() {
    keepFocus = false;
    window.setTimeout(hideList, 200);
}).focus(function(){
    keepFocus = true;
});
The Scrum Meister
  • 29,681
  • 8
  • 66
  • 64
  • blur and focus don't seem to work with #myList since it's not an input. – Justin Stayton Feb 04 '11 at 21:58
  • @Justin, What is it? Try removing the `focus` method on the list, and place the `blur` code in your list `click` event – The Scrum Meister Feb 04 '11 at 22:08
  • It's just a div. I've adapted your code to work with my setup though, so thanks! – Justin Stayton Feb 07 '11 at 21:37
  • 2
    You don't want to pollute the global namespace with that keepFocus variable. It's also a bad idea to rely on a setTimeout function in case the click event does not fire within the 200 ms. – Adam Apr 06 '11 at 03:00
  • Since they're both using the same functions, you can use variables and bind them, also utilizing a "namespace" to address concern of @Adam: `var retainFocus = { hide: fn()..., keep: false, onblur: function() { retainFocus.keep = false; setTimeout(retainFocus.hide, 200); }, onfocus: function() { retainFocus.keep = true; } }`, then `$.each([$myList, $myInput], function(i,o) { o.blur(retainFocus.onblur).focus(retainFocus.onfocus); });` $myInput.blur – drzaus Mar 10 '13 at 05:49
16

I've faced with the exact same problem, so this is how I solved it.

I came up with the fact that blur() fires earlier than click().

So I've tried to change click() to mousedown() and found out that mousedown() fires before blur().

And to imitate click() you'll have to fire mousedown() and then mouseup()

So in your case I would do something like this:

var click_in_process = false; // global

$('#myList').mousedown(function() {
    click_in_process = true;
});

$('#myList').mouseup(function() {
    click_in_process = false;
    $('#myInput').focus();

    // a code of $('#myList') clicking event

});

$('#myInput').blur(function() {
    if(!click_in_process) {
        $('#myList').hide();

        // a code of what you want to happen after you really left  $('#myInput')

    }
});

Demo / example: http://jsfiddle.net/bbrh4/

Hope it helps!

Pigalev Pavel
  • 1,155
  • 1
  • 15
  • 29
  • 1
    What if the order of events is different in one specific browser or device? If I recall correctly this order is not defined. – Gherman Jun 20 '17 at 11:40
  • 2
    @German It've worked correctly in all of the devices and browsers that I had at that time (as I recall Chrome/Firefox/Opera/IE/iOS/Android/WP). Maybe something has changed but to be honest I doubt it. But if you have any specific example in mind I'll be very happy to know that. And if it's true I'd recommend writing a service/helper/library, where you could do different logics depending on current browser or device. – Pigalev Pavel Jun 21 '17 at 12:19
  • 1
    I don't have any bad examples. I was just being extra cautious. Theoretically the order of events is not defined in the standards. – Gherman Jun 23 '17 at 12:38
  • 1
    @German You stop interacting with one object, then start interacting with another. So it would be very strange if blur() on one object wouldn't be automatically fired before click() on another. I doubt that it should be documentated. – Pigalev Pavel Jun 29 '17 at 15:30
  • Strange things happen all the time. – Gherman Jul 03 '17 at 11:55
11

You need to be able to say "do this blur() unless the list gains focus at the same time".

This question says how to detect if an element has focus: Using jQuery to test if an input has focus

Then all you need to do is:

$("#myInput").blur(function () {
    if (!$("#myList").is(":focus")) {
        $("#myList").hide();
    }
});
Community
  • 1
  • 1
JK.
  • 21,477
  • 35
  • 135
  • 214
  • 4
    Doesn't seem to work. document.activeElement shows the body tag as the "focused" element. I'm guessing it doesn't work because the idea of "focus" is inherently tied to input fields. – Justin Stayton Feb 04 '11 at 19:32
6

Pigalev Pavel's answer above works great.

However, If you want an even simplier solution, you can just "prevent default" in the "mousedown" of an element to prevent the blur event from taking place. (since preventing default actually means that in the end, the input never looses focus in the first place!)

Of course, this is only if you're alright with preventing default in the div. It does have some side-effects, like the text is no longer selectable. As long as that's not an issue, this will work.

I suppose if you hold the mouse down over the div, move the mouse outside of the div, and then release the mouse, it also doesn't fire the "blur" event. But in my case, I wasn't too worried about that either, since the click started in the target div.

$("input").focus(function(){
 $(this).val("");
});

$("input").blur(function(){
 $(this).val("blur event fired!");
});

$("div").mousedown(function(e){
 e.preventDefault();
})
div{
  height: 200px;
  width: 200px;
  background: blue;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input>
<div>
Click here to prevent blur event!
</div>
Skeets
  • 4,476
  • 2
  • 41
  • 67
1

The best way to do this is to attach an event handler to the body element, then another handler to the list that stops event propagation:

$(body).click(function () {
    $("#myList").hide();
});

$("#myList").click(function (e) {
    e.stopImmediatePropagation();
});

This listens for a click outside of #myInput and hides #myList. At the same time, the second function listens for a click on #myList and if it occurs, it prevents the hide() from firing.

Adam
  • 12,236
  • 9
  • 39
  • 44