I have some HTML menus, which I show completely when a user clicks on the head of these menus. I would like to hide these elements when the user clicks outside the menus' area.
Is something like this possible with jQuery?
$("#menuscontainer").clickOutsideThisElement(function() {
// Hide the menus
});
Note: Using stopPropagation is something that should be avoided as it breaks normal event flow in the DOM. See this CSS Tricks article for more information. Consider using this method instead.
Attach a click event to the document body which closes the window. Attach a separate click event to the container which stops propagation to the document body.
$(window).click(function() {
//Hide the menus if visible
});
$('#menucontainer').click(function(event){
event.stopPropagation();
});
You can listen for a click event on document and then make sure #menucontainer is not an ancestor or the target of the clicked element by using .closest().
If it is not, then the clicked element is outside of the #menucontainer and you can safely hide it.
$(document).click(function(event) {
var $target = $(event.target);
if(!$target.closest('#menucontainer').length &&
$('#menucontainer').is(":visible")) {
$('#menucontainer').hide();
}
});
Edit – 2017-06-23
You can also clean up after the event listener if you plan to dismiss the menu and want to stop listening for events. This function will clean up only the newly created listener, preserving any other click listeners on document. With ES2015 syntax:
export function hideOnClickOutside(selector) {
const outsideClickListener = (event) => {
const $target = $(event.target);
if (!$target.closest(selector).length && $(selector).is(':visible')) {
$(selector).hide();
removeClickListener();
}
}
const removeClickListener = () => {
document.removeEventListener('click', outsideClickListener);
}
document.addEventListener('click', outsideClickListener);
}
Edit – 2018-03-11
For those who don't want to use jQuery. Here's the above code in plain vanillaJS (ECMAScript6).
function hideOnClickOutside(element) {
const outsideClickListener = event => {
if (!element.contains(event.target) && isVisible(element)) { // or use: event.target.closest(selector) === null
element.style.display = 'none';
removeClickListener();
}
}
const removeClickListener = () => {
document.removeEventListener('click', outsideClickListener);
}
document.addEventListener('click', outsideClickListener);
}
const isVisible = elem => !!elem && !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js
NOTE:
This is based on Alex comment to just use !element.contains(event.target) instead of the jQuery part.
But element.closest() is now also available in all major browsers (the W3C version differs a bit from the jQuery one).
Polyfills can be found here: Element.closest()
Edit – 2020-05-21
In the case where you want the user to be able to click-and-drag inside the element, then release the mouse outside the element, without closing the element:
...
let lastMouseDownX = 0;
let lastMouseDownY = 0;
let lastMouseDownWasOutside = false;
const mouseDownListener = (event: MouseEvent) => {
lastMouseDownX = event.offsetX;
lastMouseDownY = event.offsetY;
lastMouseDownWasOutside = !$(event.target).closest(element).length;
}
document.addEventListener('mousedown', mouseDownListener);
And in outsideClickListener:
const outsideClickListener = event => {
const deltaX = event.offsetX - lastMouseDownX;
const deltaY = event.offsetY - lastMouseDownY;
const distSq = (deltaX * deltaX) + (deltaY * deltaY);
const isDrag = distSq > 3;
const isDragException = isDrag && !lastMouseDownWasOutside;
if (!element.contains(event.target) && isVisible(element) && !isDragException) { // or use: event.target.closest(selector) === null
element.style.display = 'none';
removeClickListener();
document.removeEventListener('mousedown', mouseDownListener); // Or add this line to removeClickListener()
}
}
How to detect a click outside an element?
The reason that this question is so popular and has so many answers is that it is deceptively complex. After almost eight years and dozens of answers, I am genuinely surprised to see how little care has been given to accessibility.
I would like to hide these elements when the user clicks outside the menus' area.
This is a noble cause and is the actual issue. The title of the question—which is what most answers appear to attempt to address—contains an unfortunate red herring.
Hint: it's the word "click"!
You don't actually want to bind click handlers.
If you're binding click handlers to close the dialog, you've already failed. The reason you've failed is that not everyone triggers click events. Users not using a mouse will be able to escape your dialog (and your pop-up menu is arguably a type of dialog) by pressing Tab, and they then won't be able to read the content behind the dialog without subsequently triggering a click event.
So let's rephrase the question.
How does one close a dialog when a user is finished with it?
This is the goal. Unfortunately, now we need to bind the userisfinishedwiththedialog event, and that binding isn't so straightforward.
So how can we detect that a user has finished using a dialog?
focusout event
A good start is to determine if focus has left the dialog.
Hint: be careful with the blur event, blur doesn't propagate if the event was bound to the bubbling phase!
jQuery's focusout will do just fine. If you can't use jQuery, then you can use blur during the capturing phase:
element.addEventListener('blur', ..., true);
// use capture: ^^^^
Also, for many dialogs you'll need to allow the container to gain focus. Add tabindex="-1" to allow the dialog to receive focus dynamically without otherwise interrupting the tabbing flow.
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on('focusout', function () {
$(this).removeClass('active');
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Example
<div id="example" tabindex="-1">
Lorem ipsum dolor sit amet.
</div>
If you play with that demo for more than a minute you should quickly start seeing issues.
The first is that the link in the dialog isn't clickable. Attempting to click on it or tab to it will lead to the dialog closing before the interaction takes place. This is because focusing the inner element triggers a focusout event before triggering a focusin event again.
The fix is to queue the state change on the event loop. This can be done by using setImmediate(...), or setTimeout(..., 0) for browsers that don't support setImmediate. Once queued it can be cancelled by a subsequent focusin:
$('.submenu').on({
focusout: function (e) {
$(this).data('submenuTimer', setTimeout(function () {
$(this).removeClass('submenu--active');
}.bind(this), 0));
},
focusin: function (e) {
clearTimeout($(this).data('submenuTimer'));
}
});
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on({
focusout: function () {
$(this).data('timer', setTimeout(function () {
$(this).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('timer'));
}
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Example
<div id="example" tabindex="-1">
Lorem ipsum dolor sit amet.
</div>
The second issue is that the dialog won't close when the link is pressed again. This is because the dialog loses focus, triggering the close behavior, after which the link click triggers the dialog to reopen.
Similar to the previous issue, the focus state needs to be managed. Given that the state change has already been queued, it's just a matter of handling focus events on the dialog triggers:
This should look familiar
$('a').on({
focusout: function () {
$(this.hash).data('timer', setTimeout(function () {
$(this.hash).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('timer'));
}
});
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on({
focusout: function () {
$(this).data('timer', setTimeout(function () {
$(this).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('timer'));
}
});
$('a').on({
focusout: function () {
$(this.hash).data('timer', setTimeout(function () {
$(this.hash).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('timer'));
}
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Example
<div id="example" tabindex="-1">
Lorem ipsum dolor sit amet.
</div>
Esc key
If you thought you were done by handling the focus states, there's more you can do to simplify the user experience.
This is often a "nice to have" feature, but it's common that when you have a modal or popup of any sort that the Esc key will close it out.
keydown: function (e) {
if (e.which === 27) {
$(this).removeClass('active');
e.preventDefault();
}
}
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on({
focusout: function () {
$(this).data('timer', setTimeout(function () {
$(this).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('timer'));
},
keydown: function (e) {
if (e.which === 27) {
$(this).removeClass('active');
e.preventDefault();
}
}
});
$('a').on({
focusout: function () {
$(this.hash).data('timer', setTimeout(function () {
$(this.hash).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('timer'));
}
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Example
<div id="example" tabindex="-1">
Lorem ipsum dolor sit amet.
</div>
If you know you have focusable elements within the dialog, you won't need to focus the dialog directly. If you're building a menu, you could focus the first menu item instead.
click: function (e) {
$(this.hash)
.toggleClass('submenu--active')
.find('a:first')
.focus();
e.preventDefault();
}
$('.menu__link').on({
click: function (e) {
$(this.hash)
.toggleClass('submenu--active')
.find('a:first')
.focus();
e.preventDefault();
},
focusout: function () {
$(this.hash).data('submenuTimer', setTimeout(function () {
$(this.hash).removeClass('submenu--active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('submenuTimer'));
}
});
$('.submenu').on({
focusout: function () {
$(this).data('submenuTimer', setTimeout(function () {
$(this).removeClass('submenu--active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('submenuTimer'));
},
keydown: function (e) {
if (e.which === 27) {
$(this).removeClass('submenu--active');
e.preventDefault();
}
}
});
.menu {
list-style: none;
margin: 0;
padding: 0;
}
.menu:after {
clear: both;
content: '';
display: table;
}
.menu__item {
float: left;
position: relative;
}
.menu__link {
background-color: lightblue;
color: black;
display: block;
padding: 0.5em 1em;
text-decoration: none;
}
.menu__link:hover,
.menu__link:focus {
background-color: black;
color: lightblue;
}
.submenu {
border: 1px solid black;
display: none;
left: 0;
list-style: none;
margin: 0;
padding: 0;
position: absolute;
top: 100%;
}
.submenu--active {
display: block;
}
.submenu__item {
width: 150px;
}
.submenu__link {
background-color: lightblue;
color: black;
display: block;
padding: 0.5em 1em;
text-decoration: none;
}
.submenu__link:hover,
.submenu__link:focus {
background-color: black;
color: lightblue;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul class="menu">
<li class="menu__item">
<a class="menu__link" href="#menu-1">Menu 1</a>
<ul class="submenu" id="menu-1" tabindex="-1">
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#1">Example 1</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#2">Example 2</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#3">Example 3</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#4">Example 4</a></li>
</ul>
</li>
<li class="menu__item">
<a class="menu__link" href="#menu-2">Menu 2</a>
<ul class="submenu" id="menu-2" tabindex="-1">
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#1">Example 1</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#2">Example 2</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#3">Example 3</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#4">Example 4</a></li>
</ul>
</li>
</ul>
lorem ipsum dolor sit amet.
WAI-ARIA Roles and Other Accessibility Support
This answer hopefully covers the basics of accessible keyboard and mouse support for this feature, but as it's already quite sizable I'm going to avoid any discussion of WAI-ARIA roles and attributes, however I highly recommend that implementers refer to the spec for details on what roles they should use and any other appropriate attributes.
The other solutions here didn't work for me so I had to use:
if(!$(event.target).is('#foo'))
{
// hide menu
}
Edit: Plain Javascript variant (2021-03-31)
I used this method to handle closing a drop down menu when clicking outside of it.
First, I created a custom class name for all the elements of the component. This class name will be added to all elements that make up the menu widget.
const className = `dropdown-${Date.now()}-${Math.random() * 100}`;
I create a function to check for clicks and the class name of the clicked element. If clicked element does not contain the custom class name I generated above, it should set the show flag to false and the menu will close.
const onClickOutside = (e) => {
if (!e.target.className.includes(className)) {
show = false;
}
};
Then I attached the click handler to the window object.
// add when widget loads
window.addEventListener("click", onClickOutside);
... and finally some housekeeping
// remove listener when destroying the widget
window.removeEventListener("click", onClickOutside);
I have an application that works similarly to Eran's example, except I attach the click event to the body when I open the menu... Kinda like this:
$('#menucontainer').click(function(event) {
$('html').one('click',function() {
// Hide the menus
});
event.stopPropagation();
});
More information on jQuery's one() function
It's 2020 and you can use event.composedPath()
From: Event.composedPath()
The composedPath() method of the Event interface returns the event’s path, which is an array of the objects on which listeners will be invoked.
const target = document.querySelector('#myTarget')
document.addEventListener('click', (event) => {
const withinBoundaries = event.composedPath().includes(target)
if (withinBoundaries) {
target.innerText = 'Click happened inside element'
} else {
target.innerText = 'Click happened **OUTSIDE** element'
}
})
/* Just to make it good looking. You don't need this */
#myTarget {
margin: 50px auto;
width: 500px;
height: 500px;
background: gray;
border: 10px solid black;
}
<div id="myTarget">
Click me (or not!)
</div>
After research, I have found three working solutions
First solution
<script>
//The good thing about this solution is it doesn't stop event propagation.
var clickFlag = 0;
$('body').on('click', function () {
if(clickFlag == 0) {
console.log('hide element here');
/* Hide element here */
}
else {
clickFlag=0;
}
});
$('body').on('click','#testDiv', function (event) {
clickFlag = 1;
console.log('showed the element');
/* Show the element */
});
</script>
Second solution
<script>
$('body').on('click', function(e) {
if($(e.target).closest('#testDiv').length == 0) {
/* Hide dropdown here */
}
});
</script>
Third solution
<script>
var specifiedElement = document.getElementById('testDiv');
document.addEventListener('click', function(event) {
var isClickInside = specifiedElement.contains(event.target);
if (isClickInside) {
console.log('You clicked inside')
}
else {
console.log('You clicked outside')
}
});
</script>
$("#menuscontainer").click(function() {
$(this).focus();
});
$("#menuscontainer").blur(function(){
$(this).hide();
});
Works for me just fine.
Now there is a plugin for that: outside events (blog post)
The following happens when a clickoutside handler (WLOG) is bound to an element:
the element is added to an array which holds all elements with clickoutside handlers
a (namespaced) click handler is bound to the document (if not already there)
on any click in the document, the clickoutside event is triggered for those elements in that array that are not equal to or a parent of the click-events target
additionally, the event.target for the clickoutside event is set to the element the user clicked on (so you even know what the user clicked, not just that he clicked outside)
So no events are stopped from propagation and additional click handlers may be used "above" the element with the outside-handler.
This worked for me perfectly!!
$('html').click(function (e) {
if (e.target.id == 'YOUR-DIV-ID') {
//do something
} else {
//do something
}
});
I don't think what you really need is to close the menu when the user clicks outside; what you need is for the menu to close when the user clicks anywhere at all on the page. If you click on the menu, or off the menu it should close right?
Finding no satisfactory answers above prompted me to write this blog post the other day. For the more pedantic, there are a number of gotchas to take note of:
If you attach a click event handler to the body element at click time be sure to wait for the 2nd click before closing the menu, and unbinding the event. Otherwise the click event that opened the menu will bubble up to the listener that has to close the menu.
If you use event.stopPropogation() on a click event, no other elements in your page can have a click-anywhere-to-close feature.
Attaching a click event handler to the body element indefinitely is not a performant solution
Comparing the target of the event, and its parents to the handler's creator assumes that what you want is to close the menu when you click off it, when what you really want is to close it when you click anywhere on the page.
Listening for events on the body element will make your code more brittle. Styling as innocent as this would break it: body { margin-left:auto; margin-right: auto; width:960px;}
As another poster said there are a lot of gotchas, especially if the element you are displaying (in this case a menu) has interactive elements.
I've found the following method to be fairly robust:
$('#menuscontainer').click(function(event) {
//your code that shows the menus fully
//now set up an event listener so that clicking anywhere outside will close the menu
$('html').click(function(event) {
//check up the tree of the click target to check whether user has clicked outside of menu
if ($(event.target).parents('#menuscontainer').length==0) {
// your code to hide menu
//this event listener has done its job so we can unbind it.
$(this).unbind(event);
}
})
});
A simple solution for the situation is:
$(document).mouseup(function (e)
{
var container = $("YOUR SELECTOR"); // Give you class or ID
if (!container.is(e.target) && // If the target of the click is not the desired div or section
container.has(e.target).length === 0) // ... nor a descendant-child of the container
{
container.hide();
}
});
The above script will hide the div if outside of the div click event is triggered.
Check the window click event target (it should propagate to the window, as long as it's not captured anywhere else), and ensure that it's not any of the menu elements. If it's not, then you're outside your menu.
Or check the position of the click, and see if it's contained within the menu area.
I am surprised nobody actually acknowledged focusout event:
var button = document.getElementById('button');
button.addEventListener('click', function(e){
e.target.style.backgroundColor = 'green';
});
button.addEventListener('focusout', function(e){
e.target.style.backgroundColor = '';
});
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<button id="button">Click</button>
</body>
</html>
Solution1
Instead of using event.stopPropagation() which can have some side affects, just define a simple flag variable and add one if condition. I tested this and worked properly without any side affects of stopPropagation:
var flag = "1";
$('#menucontainer').click(function(event){
flag = "0"; // flag 0 means click happened in the area where we should not do any action
});
$('html').click(function() {
if(flag != "0"){
// Hide the menus if visible
}
else {
flag = "1";
}
});
Solution2
With just a simple if condition:
$(document).on('click', function(event){
var container = $("#menucontainer");
if (!container.is(event.target) && // If the target of the click isn't the container...
container.has(event.target).length === 0) // ... nor a descendant of the container
{
// Do whatever you want to do when click is outside the element
}
});
2020 solution using native JS API closest method.
document.addEventListener('click', ({ target }) => {
if (!target.closest('#menupop')) {
document.querySelector('#menupop').style.display = 'none'
}
})
#menupop {
width: 300px;
height: 300px;
background-color: red;
}
<div id="menupop">
clicking outside will close this
</div>
I've had success with something like this:
var $menuscontainer = ...;
$('#trigger').click(function() {
$menuscontainer.show();
$('body').click(function(event) {
var $target = $(event.target);
if ($target.parents('#menuscontainer').length == 0) {
$menuscontainer.hide();
}
});
});
The logic is: when #menuscontainer is shown, bind a click handler to the body that hides #menuscontainer only if the target (of the click) isn't a child of it.
The event has a property called event.path of the element which is a "static ordered list of all its ancestors in tree order". To check if an event originated from a specific DOM element or one of its children, just check the path for that specific DOM element. It can also be used to check multiple elements by logically ORing the element check in the some function.
$("body").click(function() {
target = document.getElementById("main");
flag = event.path.some(function(el, i, arr) {
return (el == target)
})
if (flag) {
console.log("Inside")
} else {
console.log("Outside")
}
});
#main {
display: inline-block;
background:yellow;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="main">
<ul>
<li>Test-Main</li>
<li>Test-Main</li>
<li>Test-Main</li>
<li>Test-Main</li>
<li>Test-Main</li>
</ul>
</div>
<div id="main2">
Outside Main
</div>
So for your case It should be
$("body").click(function() {
target = $("#menuscontainer")[0];
flag = event.path.some(function(el, i, arr) {
return (el == target)
});
if (!flag) {
// Hide the menus
}
});
As a variant:
var $menu = $('#menucontainer');
$(document).on('click', function (e) {
// If element is opened and click target is outside it, hide it
if ($menu.is(':visible') && !$menu.is(e.target) && !$menu.has(e.target).length) {
$menu.hide();
}
});
It has no problem with stopping event propagation and better supports multiple menus on the same page where clicking on a second menu while a first is open will leave the first open in the stopPropagation solution.
Use focusout for accessibility
There is one answer here that says (quite correctly) that focusing on click events is an accessibility problem since we want to cater for keyboard users. The focusout event is the correct thing to use here, but it can be done much more simply than in the other answer (and in pure JavaScript too):
A simpler way of doing it:
The 'problem' with using focusout is that if an element inside your dialog/modal/menu loses focus, to something also 'inside', the event will still get fired. We can check that this isn't the case by looking at event.relatedTarget (which tells us what element will have gained focus).
dialog = document.getElementById("dialogElement")
dialog.addEventListener("focusout", function (event) {
if (
// We are still inside the dialog so don't close
dialog.contains(event.relatedTarget) ||
// We have switched to another tab so probably don't want to close
!document.hasFocus()
) {
return;
}
dialog.close(); // Or whatever logic you want to use to close
});
There is one slight gotcha to the above, which is that relatedTarget may be null. This is fine if the user is clicking outside the dialog, but will be a problem if unless the user clicks inside the dialog and the dialog happens to not be focusable. To fix this you have to make sure to set tabIndex=0 so your dialog is focusable.
I found this method in some jQuery calendar plugin.
function ClickOutsideCheck(e)
{
var el = e.target;
var popup = $('.popup:visible')[0];
if (popup==undefined)
return true;
while (true){
if (el == popup ) {
return true;
} else if (el == document) {
$(".popup").hide();
return false;
} else {
el = $(el).parent()[0];
}
}
};
$(document).bind('mousedown.popup', ClickOutsideCheck);
Here is the vanilla JavaScript solution for future viewers.
Upon clicking any element within the document, if the clicked element's id is toggled, or the hidden element is not hidden and the hidden element does not contain the clicked element, toggle the element.
(function () {
"use strict";
var hidden = document.getElementById('hidden');
document.addEventListener('click', function (e) {
if (e.target.id == 'toggle' || (hidden.style.display != 'none' && !hidden.contains(e.target))) hidden.style.display = hidden.style.display == 'none' ? 'block' : 'none';
}, false);
})();
(function () {
"use strict";
var hidden = document.getElementById('hidden');
document.addEventListener('click', function (e) {
if (e.target.id == 'toggle' || (hidden.style.display != 'none' && !hidden.contains(e.target))) hidden.style.display = hidden.style.display == 'none' ? 'block' : 'none';
}, false);
})();
Toggle Hidden Div
<div id="hidden" style="display: none;">This content is normally hidden. click anywhere other than this content to make me disappear</div>
If you are going to have multiple toggles on the same page you can use something like this:
Add the class name hidden to the collapsible item.
Upon document click, close all hidden elements which do not contain the clicked element and are not hidden
If the clicked element is a toggle, toggle the specified element.
(function () {
"use strict";
var hiddenItems = document.getElementsByClassName('hidden'), hidden;
document.addEventListener('click', function (e) {
for (var i = 0; hidden = hiddenItems[i]; i++) {
if (!hidden.contains(e.target) && hidden.style.display != 'none')
hidden.style.display = 'none';
}
if (e.target.getAttribute('data-toggle')) {
var toggle = document.querySelector(e.target.getAttribute('data-toggle'));
toggle.style.display = toggle.style.display == 'none' ? 'block' : 'none';
}
}, false);
})();
Toggle Hidden Div
<div class="hidden" id="hidden1" style="display: none;" data-hidden="true">This content is normally hidden</div>
Toggle Hidden Div
<div class="hidden" id="hidden2" style="display: none;" data-hidden="true">This content is normally hidden</div>
Toggle Hidden Div
<div class="hidden" id="hidden3" style="display: none;" data-hidden="true">This content is normally hidden</div>
Instead using flow interruption, blur/focus event or any other tricky technics, simply match event flow with element's kinship:
$(document).on("click.menu-outside", function(event){
// Test if target and it's parent aren't #menuscontainer
// That means the click event occur on other branch of document tree
if(!$(event.target).parents().andSelf().is("#menuscontainer")){
// Click outisde #menuscontainer
// Hide the menus (but test if menus aren't already hidden)
}
});
To remove click outside event listener, simply:
$(document).off("click.menu-outside");
If someone curious here is javascript solution(es6):
window.addEventListener('mouseup', e => {
if (e.target != yourDiv && e.target.parentNode != yourDiv) {
yourDiv.classList.remove('show-menu');
//or yourDiv.style.display = 'none';
}
})
and es5, just in case:
window.addEventListener('mouseup', function (e) {
if (e.target != yourDiv && e.target.parentNode != yourDiv) {
yourDiv.classList.remove('show-menu');
//or yourDiv.style.display = 'none';
}
});
If you are scripting for IE and FF 3.* and you just want to know if the click occured within a certain box area, you could also use something like:
this.outsideElementClick = function(objEvent, objElement) {
var objCurrentElement = objEvent.target || objEvent.srcElement;
var blnInsideX = false;
var blnInsideY = false;
if (objCurrentElement.getBoundingClientRect().left >= objElement.getBoundingClientRect().left && objCurrentElement.getBoundingClientRect().right <= objElement.getBoundingClientRect().right)
blnInsideX = true;
if (objCurrentElement.getBoundingClientRect().top >= objElement.getBoundingClientRect().top && objCurrentElement.getBoundingClientRect().bottom <= objElement.getBoundingClientRect().bottom)
blnInsideY = true;
if (blnInsideX && blnInsideY)
return false;
else
return true;
}
Use:
var go = false;
$(document).click(function(){
if(go){
$('#divID').hide();
go = false;
}
})
$("#divID").mouseover(function(){
go = false;
});
$("#divID").mouseout(function (){
go = true;
});
$("btnID").click( function(){
if($("#divID:visible").length==1)
$("#divID").hide(); // Toggle
$("#divID").show();
});
Hook a click event listener on the document. Inside the event listener, you can look at the event object, in particular, the event.target to see what element was clicked:
$(document).click(function(e){
if ($(e.target).closest("#menuscontainer").length == 0) {
// .closest can help you determine if the element
// or one of its ancestors is #menuscontainer
console.log("hide");
}
});
Here is a simple solution by pure javascript. It is up-to-date with ES6:
var isMenuClick = false;
var menu = document.getElementById('menuscontainer');
document.addEventListener('click',()=>{
if(!isMenuClick){
//Hide the menu here
}
//Reset isMenuClick
isMenuClick = false;
})
menu.addEventListener('click',()=>{
isMenuClick = true;
})
I have used below script and done with jQuery.
jQuery(document).click(function(e) {
var target = e.target; //target div recorded
if (!jQuery(target).is('#tobehide') ) {
jQuery(this).fadeOut(); //if the click element is not the above id will hide
}
})
Below find the HTML code
<div class="main-container">
<div> Hello I am the title</div>
<div class="tobehide">I will hide when you click outside of me</div>
</div>
You can read the tutorial here
Related
I have some HTML menus, which I show completely when a user clicks on the head of these menus. I would like to hide these elements when the user clicks outside the menus' area.
Is something like this possible with jQuery?
$("#menuscontainer").clickOutsideThisElement(function() {
// Hide the menus
});
Note: Using stopPropagation is something that should be avoided as it breaks normal event flow in the DOM. See this CSS Tricks article for more information. Consider using this method instead.
Attach a click event to the document body which closes the window. Attach a separate click event to the container which stops propagation to the document body.
$(window).click(function() {
//Hide the menus if visible
});
$('#menucontainer').click(function(event){
event.stopPropagation();
});
You can listen for a click event on document and then make sure #menucontainer is not an ancestor or the target of the clicked element by using .closest().
If it is not, then the clicked element is outside of the #menucontainer and you can safely hide it.
$(document).click(function(event) {
var $target = $(event.target);
if(!$target.closest('#menucontainer').length &&
$('#menucontainer').is(":visible")) {
$('#menucontainer').hide();
}
});
Edit – 2017-06-23
You can also clean up after the event listener if you plan to dismiss the menu and want to stop listening for events. This function will clean up only the newly created listener, preserving any other click listeners on document. With ES2015 syntax:
export function hideOnClickOutside(selector) {
const outsideClickListener = (event) => {
const $target = $(event.target);
if (!$target.closest(selector).length && $(selector).is(':visible')) {
$(selector).hide();
removeClickListener();
}
}
const removeClickListener = () => {
document.removeEventListener('click', outsideClickListener);
}
document.addEventListener('click', outsideClickListener);
}
Edit – 2018-03-11
For those who don't want to use jQuery. Here's the above code in plain vanillaJS (ECMAScript6).
function hideOnClickOutside(element) {
const outsideClickListener = event => {
if (!element.contains(event.target) && isVisible(element)) { // or use: event.target.closest(selector) === null
element.style.display = 'none';
removeClickListener();
}
}
const removeClickListener = () => {
document.removeEventListener('click', outsideClickListener);
}
document.addEventListener('click', outsideClickListener);
}
const isVisible = elem => !!elem && !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js
NOTE:
This is based on Alex comment to just use !element.contains(event.target) instead of the jQuery part.
But element.closest() is now also available in all major browsers (the W3C version differs a bit from the jQuery one).
Polyfills can be found here: Element.closest()
Edit – 2020-05-21
In the case where you want the user to be able to click-and-drag inside the element, then release the mouse outside the element, without closing the element:
...
let lastMouseDownX = 0;
let lastMouseDownY = 0;
let lastMouseDownWasOutside = false;
const mouseDownListener = (event: MouseEvent) => {
lastMouseDownX = event.offsetX;
lastMouseDownY = event.offsetY;
lastMouseDownWasOutside = !$(event.target).closest(element).length;
}
document.addEventListener('mousedown', mouseDownListener);
And in outsideClickListener:
const outsideClickListener = event => {
const deltaX = event.offsetX - lastMouseDownX;
const deltaY = event.offsetY - lastMouseDownY;
const distSq = (deltaX * deltaX) + (deltaY * deltaY);
const isDrag = distSq > 3;
const isDragException = isDrag && !lastMouseDownWasOutside;
if (!element.contains(event.target) && isVisible(element) && !isDragException) { // or use: event.target.closest(selector) === null
element.style.display = 'none';
removeClickListener();
document.removeEventListener('mousedown', mouseDownListener); // Or add this line to removeClickListener()
}
}
How to detect a click outside an element?
The reason that this question is so popular and has so many answers is that it is deceptively complex. After almost eight years and dozens of answers, I am genuinely surprised to see how little care has been given to accessibility.
I would like to hide these elements when the user clicks outside the menus' area.
This is a noble cause and is the actual issue. The title of the question—which is what most answers appear to attempt to address—contains an unfortunate red herring.
Hint: it's the word "click"!
You don't actually want to bind click handlers.
If you're binding click handlers to close the dialog, you've already failed. The reason you've failed is that not everyone triggers click events. Users not using a mouse will be able to escape your dialog (and your pop-up menu is arguably a type of dialog) by pressing Tab, and they then won't be able to read the content behind the dialog without subsequently triggering a click event.
So let's rephrase the question.
How does one close a dialog when a user is finished with it?
This is the goal. Unfortunately, now we need to bind the userisfinishedwiththedialog event, and that binding isn't so straightforward.
So how can we detect that a user has finished using a dialog?
focusout event
A good start is to determine if focus has left the dialog.
Hint: be careful with the blur event, blur doesn't propagate if the event was bound to the bubbling phase!
jQuery's focusout will do just fine. If you can't use jQuery, then you can use blur during the capturing phase:
element.addEventListener('blur', ..., true);
// use capture: ^^^^
Also, for many dialogs you'll need to allow the container to gain focus. Add tabindex="-1" to allow the dialog to receive focus dynamically without otherwise interrupting the tabbing flow.
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on('focusout', function () {
$(this).removeClass('active');
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Example
<div id="example" tabindex="-1">
Lorem ipsum dolor sit amet.
</div>
If you play with that demo for more than a minute you should quickly start seeing issues.
The first is that the link in the dialog isn't clickable. Attempting to click on it or tab to it will lead to the dialog closing before the interaction takes place. This is because focusing the inner element triggers a focusout event before triggering a focusin event again.
The fix is to queue the state change on the event loop. This can be done by using setImmediate(...), or setTimeout(..., 0) for browsers that don't support setImmediate. Once queued it can be cancelled by a subsequent focusin:
$('.submenu').on({
focusout: function (e) {
$(this).data('submenuTimer', setTimeout(function () {
$(this).removeClass('submenu--active');
}.bind(this), 0));
},
focusin: function (e) {
clearTimeout($(this).data('submenuTimer'));
}
});
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on({
focusout: function () {
$(this).data('timer', setTimeout(function () {
$(this).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('timer'));
}
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Example
<div id="example" tabindex="-1">
Lorem ipsum dolor sit amet.
</div>
The second issue is that the dialog won't close when the link is pressed again. This is because the dialog loses focus, triggering the close behavior, after which the link click triggers the dialog to reopen.
Similar to the previous issue, the focus state needs to be managed. Given that the state change has already been queued, it's just a matter of handling focus events on the dialog triggers:
This should look familiar
$('a').on({
focusout: function () {
$(this.hash).data('timer', setTimeout(function () {
$(this.hash).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('timer'));
}
});
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on({
focusout: function () {
$(this).data('timer', setTimeout(function () {
$(this).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('timer'));
}
});
$('a').on({
focusout: function () {
$(this.hash).data('timer', setTimeout(function () {
$(this.hash).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('timer'));
}
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Example
<div id="example" tabindex="-1">
Lorem ipsum dolor sit amet.
</div>
Esc key
If you thought you were done by handling the focus states, there's more you can do to simplify the user experience.
This is often a "nice to have" feature, but it's common that when you have a modal or popup of any sort that the Esc key will close it out.
keydown: function (e) {
if (e.which === 27) {
$(this).removeClass('active');
e.preventDefault();
}
}
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on({
focusout: function () {
$(this).data('timer', setTimeout(function () {
$(this).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('timer'));
},
keydown: function (e) {
if (e.which === 27) {
$(this).removeClass('active');
e.preventDefault();
}
}
});
$('a').on({
focusout: function () {
$(this.hash).data('timer', setTimeout(function () {
$(this.hash).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('timer'));
}
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Example
<div id="example" tabindex="-1">
Lorem ipsum dolor sit amet.
</div>
If you know you have focusable elements within the dialog, you won't need to focus the dialog directly. If you're building a menu, you could focus the first menu item instead.
click: function (e) {
$(this.hash)
.toggleClass('submenu--active')
.find('a:first')
.focus();
e.preventDefault();
}
$('.menu__link').on({
click: function (e) {
$(this.hash)
.toggleClass('submenu--active')
.find('a:first')
.focus();
e.preventDefault();
},
focusout: function () {
$(this.hash).data('submenuTimer', setTimeout(function () {
$(this.hash).removeClass('submenu--active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('submenuTimer'));
}
});
$('.submenu').on({
focusout: function () {
$(this).data('submenuTimer', setTimeout(function () {
$(this).removeClass('submenu--active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('submenuTimer'));
},
keydown: function (e) {
if (e.which === 27) {
$(this).removeClass('submenu--active');
e.preventDefault();
}
}
});
.menu {
list-style: none;
margin: 0;
padding: 0;
}
.menu:after {
clear: both;
content: '';
display: table;
}
.menu__item {
float: left;
position: relative;
}
.menu__link {
background-color: lightblue;
color: black;
display: block;
padding: 0.5em 1em;
text-decoration: none;
}
.menu__link:hover,
.menu__link:focus {
background-color: black;
color: lightblue;
}
.submenu {
border: 1px solid black;
display: none;
left: 0;
list-style: none;
margin: 0;
padding: 0;
position: absolute;
top: 100%;
}
.submenu--active {
display: block;
}
.submenu__item {
width: 150px;
}
.submenu__link {
background-color: lightblue;
color: black;
display: block;
padding: 0.5em 1em;
text-decoration: none;
}
.submenu__link:hover,
.submenu__link:focus {
background-color: black;
color: lightblue;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul class="menu">
<li class="menu__item">
<a class="menu__link" href="#menu-1">Menu 1</a>
<ul class="submenu" id="menu-1" tabindex="-1">
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#1">Example 1</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#2">Example 2</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#3">Example 3</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#4">Example 4</a></li>
</ul>
</li>
<li class="menu__item">
<a class="menu__link" href="#menu-2">Menu 2</a>
<ul class="submenu" id="menu-2" tabindex="-1">
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#1">Example 1</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#2">Example 2</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#3">Example 3</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#4">Example 4</a></li>
</ul>
</li>
</ul>
lorem ipsum dolor sit amet.
WAI-ARIA Roles and Other Accessibility Support
This answer hopefully covers the basics of accessible keyboard and mouse support for this feature, but as it's already quite sizable I'm going to avoid any discussion of WAI-ARIA roles and attributes, however I highly recommend that implementers refer to the spec for details on what roles they should use and any other appropriate attributes.
The other solutions here didn't work for me so I had to use:
if(!$(event.target).is('#foo'))
{
// hide menu
}
Edit: Plain Javascript variant (2021-03-31)
I used this method to handle closing a drop down menu when clicking outside of it.
First, I created a custom class name for all the elements of the component. This class name will be added to all elements that make up the menu widget.
const className = `dropdown-${Date.now()}-${Math.random() * 100}`;
I create a function to check for clicks and the class name of the clicked element. If clicked element does not contain the custom class name I generated above, it should set the show flag to false and the menu will close.
const onClickOutside = (e) => {
if (!e.target.className.includes(className)) {
show = false;
}
};
Then I attached the click handler to the window object.
// add when widget loads
window.addEventListener("click", onClickOutside);
... and finally some housekeeping
// remove listener when destroying the widget
window.removeEventListener("click", onClickOutside);
I have an application that works similarly to Eran's example, except I attach the click event to the body when I open the menu... Kinda like this:
$('#menucontainer').click(function(event) {
$('html').one('click',function() {
// Hide the menus
});
event.stopPropagation();
});
More information on jQuery's one() function
It's 2020 and you can use event.composedPath()
From: Event.composedPath()
The composedPath() method of the Event interface returns the event’s path, which is an array of the objects on which listeners will be invoked.
const target = document.querySelector('#myTarget')
document.addEventListener('click', (event) => {
const withinBoundaries = event.composedPath().includes(target)
if (withinBoundaries) {
target.innerText = 'Click happened inside element'
} else {
target.innerText = 'Click happened **OUTSIDE** element'
}
})
/* Just to make it good looking. You don't need this */
#myTarget {
margin: 50px auto;
width: 500px;
height: 500px;
background: gray;
border: 10px solid black;
}
<div id="myTarget">
Click me (or not!)
</div>
After research, I have found three working solutions
First solution
<script>
//The good thing about this solution is it doesn't stop event propagation.
var clickFlag = 0;
$('body').on('click', function () {
if(clickFlag == 0) {
console.log('hide element here');
/* Hide element here */
}
else {
clickFlag=0;
}
});
$('body').on('click','#testDiv', function (event) {
clickFlag = 1;
console.log('showed the element');
/* Show the element */
});
</script>
Second solution
<script>
$('body').on('click', function(e) {
if($(e.target).closest('#testDiv').length == 0) {
/* Hide dropdown here */
}
});
</script>
Third solution
<script>
var specifiedElement = document.getElementById('testDiv');
document.addEventListener('click', function(event) {
var isClickInside = specifiedElement.contains(event.target);
if (isClickInside) {
console.log('You clicked inside')
}
else {
console.log('You clicked outside')
}
});
</script>
$("#menuscontainer").click(function() {
$(this).focus();
});
$("#menuscontainer").blur(function(){
$(this).hide();
});
Works for me just fine.
Now there is a plugin for that: outside events (blog post)
The following happens when a clickoutside handler (WLOG) is bound to an element:
the element is added to an array which holds all elements with clickoutside handlers
a (namespaced) click handler is bound to the document (if not already there)
on any click in the document, the clickoutside event is triggered for those elements in that array that are not equal to or a parent of the click-events target
additionally, the event.target for the clickoutside event is set to the element the user clicked on (so you even know what the user clicked, not just that he clicked outside)
So no events are stopped from propagation and additional click handlers may be used "above" the element with the outside-handler.
This worked for me perfectly!!
$('html').click(function (e) {
if (e.target.id == 'YOUR-DIV-ID') {
//do something
} else {
//do something
}
});
I don't think what you really need is to close the menu when the user clicks outside; what you need is for the menu to close when the user clicks anywhere at all on the page. If you click on the menu, or off the menu it should close right?
Finding no satisfactory answers above prompted me to write this blog post the other day. For the more pedantic, there are a number of gotchas to take note of:
If you attach a click event handler to the body element at click time be sure to wait for the 2nd click before closing the menu, and unbinding the event. Otherwise the click event that opened the menu will bubble up to the listener that has to close the menu.
If you use event.stopPropogation() on a click event, no other elements in your page can have a click-anywhere-to-close feature.
Attaching a click event handler to the body element indefinitely is not a performant solution
Comparing the target of the event, and its parents to the handler's creator assumes that what you want is to close the menu when you click off it, when what you really want is to close it when you click anywhere on the page.
Listening for events on the body element will make your code more brittle. Styling as innocent as this would break it: body { margin-left:auto; margin-right: auto; width:960px;}
As another poster said there are a lot of gotchas, especially if the element you are displaying (in this case a menu) has interactive elements.
I've found the following method to be fairly robust:
$('#menuscontainer').click(function(event) {
//your code that shows the menus fully
//now set up an event listener so that clicking anywhere outside will close the menu
$('html').click(function(event) {
//check up the tree of the click target to check whether user has clicked outside of menu
if ($(event.target).parents('#menuscontainer').length==0) {
// your code to hide menu
//this event listener has done its job so we can unbind it.
$(this).unbind(event);
}
})
});
A simple solution for the situation is:
$(document).mouseup(function (e)
{
var container = $("YOUR SELECTOR"); // Give you class or ID
if (!container.is(e.target) && // If the target of the click is not the desired div or section
container.has(e.target).length === 0) // ... nor a descendant-child of the container
{
container.hide();
}
});
The above script will hide the div if outside of the div click event is triggered.
Check the window click event target (it should propagate to the window, as long as it's not captured anywhere else), and ensure that it's not any of the menu elements. If it's not, then you're outside your menu.
Or check the position of the click, and see if it's contained within the menu area.
Solution1
Instead of using event.stopPropagation() which can have some side affects, just define a simple flag variable and add one if condition. I tested this and worked properly without any side affects of stopPropagation:
var flag = "1";
$('#menucontainer').click(function(event){
flag = "0"; // flag 0 means click happened in the area where we should not do any action
});
$('html').click(function() {
if(flag != "0"){
// Hide the menus if visible
}
else {
flag = "1";
}
});
Solution2
With just a simple if condition:
$(document).on('click', function(event){
var container = $("#menucontainer");
if (!container.is(event.target) && // If the target of the click isn't the container...
container.has(event.target).length === 0) // ... nor a descendant of the container
{
// Do whatever you want to do when click is outside the element
}
});
I am surprised nobody actually acknowledged focusout event:
var button = document.getElementById('button');
button.addEventListener('click', function(e){
e.target.style.backgroundColor = 'green';
});
button.addEventListener('focusout', function(e){
e.target.style.backgroundColor = '';
});
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<button id="button">Click</button>
</body>
</html>
2020 solution using native JS API closest method.
document.addEventListener('click', ({ target }) => {
if (!target.closest('#menupop')) {
document.querySelector('#menupop').style.display = 'none'
}
})
#menupop {
width: 300px;
height: 300px;
background-color: red;
}
<div id="menupop">
clicking outside will close this
</div>
I've had success with something like this:
var $menuscontainer = ...;
$('#trigger').click(function() {
$menuscontainer.show();
$('body').click(function(event) {
var $target = $(event.target);
if ($target.parents('#menuscontainer').length == 0) {
$menuscontainer.hide();
}
});
});
The logic is: when #menuscontainer is shown, bind a click handler to the body that hides #menuscontainer only if the target (of the click) isn't a child of it.
The event has a property called event.path of the element which is a "static ordered list of all its ancestors in tree order". To check if an event originated from a specific DOM element or one of its children, just check the path for that specific DOM element. It can also be used to check multiple elements by logically ORing the element check in the some function.
$("body").click(function() {
target = document.getElementById("main");
flag = event.path.some(function(el, i, arr) {
return (el == target)
})
if (flag) {
console.log("Inside")
} else {
console.log("Outside")
}
});
#main {
display: inline-block;
background:yellow;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="main">
<ul>
<li>Test-Main</li>
<li>Test-Main</li>
<li>Test-Main</li>
<li>Test-Main</li>
<li>Test-Main</li>
</ul>
</div>
<div id="main2">
Outside Main
</div>
So for your case It should be
$("body").click(function() {
target = $("#menuscontainer")[0];
flag = event.path.some(function(el, i, arr) {
return (el == target)
});
if (!flag) {
// Hide the menus
}
});
As a variant:
var $menu = $('#menucontainer');
$(document).on('click', function (e) {
// If element is opened and click target is outside it, hide it
if ($menu.is(':visible') && !$menu.is(e.target) && !$menu.has(e.target).length) {
$menu.hide();
}
});
It has no problem with stopping event propagation and better supports multiple menus on the same page where clicking on a second menu while a first is open will leave the first open in the stopPropagation solution.
Use focusout for accessibility
There is one answer here that says (quite correctly) that focusing on click events is an accessibility problem since we want to cater for keyboard users. The focusout event is the correct thing to use here, but it can be done much more simply than in the other answer (and in pure JavaScript too):
A simpler way of doing it:
The 'problem' with using focusout is that if an element inside your dialog/modal/menu loses focus, to something also 'inside', the event will still get fired. We can check that this isn't the case by looking at event.relatedTarget (which tells us what element will have gained focus).
dialog = document.getElementById("dialogElement")
dialog.addEventListener("focusout", function (event) {
if (
// We are still inside the dialog so don't close
dialog.contains(event.relatedTarget) ||
// We have switched to another tab so probably don't want to close
!document.hasFocus()
) {
return;
}
dialog.close(); // Or whatever logic you want to use to close
});
There is one slight gotcha to the above, which is that relatedTarget may be null. This is fine if the user is clicking outside the dialog, but will be a problem if unless the user clicks inside the dialog and the dialog happens to not be focusable. To fix this you have to make sure to set tabIndex=0 so your dialog is focusable.
I found this method in some jQuery calendar plugin.
function ClickOutsideCheck(e)
{
var el = e.target;
var popup = $('.popup:visible')[0];
if (popup==undefined)
return true;
while (true){
if (el == popup ) {
return true;
} else if (el == document) {
$(".popup").hide();
return false;
} else {
el = $(el).parent()[0];
}
}
};
$(document).bind('mousedown.popup', ClickOutsideCheck);
Here is the vanilla JavaScript solution for future viewers.
Upon clicking any element within the document, if the clicked element's id is toggled, or the hidden element is not hidden and the hidden element does not contain the clicked element, toggle the element.
(function () {
"use strict";
var hidden = document.getElementById('hidden');
document.addEventListener('click', function (e) {
if (e.target.id == 'toggle' || (hidden.style.display != 'none' && !hidden.contains(e.target))) hidden.style.display = hidden.style.display == 'none' ? 'block' : 'none';
}, false);
})();
(function () {
"use strict";
var hidden = document.getElementById('hidden');
document.addEventListener('click', function (e) {
if (e.target.id == 'toggle' || (hidden.style.display != 'none' && !hidden.contains(e.target))) hidden.style.display = hidden.style.display == 'none' ? 'block' : 'none';
}, false);
})();
Toggle Hidden Div
<div id="hidden" style="display: none;">This content is normally hidden. click anywhere other than this content to make me disappear</div>
If you are going to have multiple toggles on the same page you can use something like this:
Add the class name hidden to the collapsible item.
Upon document click, close all hidden elements which do not contain the clicked element and are not hidden
If the clicked element is a toggle, toggle the specified element.
(function () {
"use strict";
var hiddenItems = document.getElementsByClassName('hidden'), hidden;
document.addEventListener('click', function (e) {
for (var i = 0; hidden = hiddenItems[i]; i++) {
if (!hidden.contains(e.target) && hidden.style.display != 'none')
hidden.style.display = 'none';
}
if (e.target.getAttribute('data-toggle')) {
var toggle = document.querySelector(e.target.getAttribute('data-toggle'));
toggle.style.display = toggle.style.display == 'none' ? 'block' : 'none';
}
}, false);
})();
Toggle Hidden Div
<div class="hidden" id="hidden1" style="display: none;" data-hidden="true">This content is normally hidden</div>
Toggle Hidden Div
<div class="hidden" id="hidden2" style="display: none;" data-hidden="true">This content is normally hidden</div>
Toggle Hidden Div
<div class="hidden" id="hidden3" style="display: none;" data-hidden="true">This content is normally hidden</div>
Instead using flow interruption, blur/focus event or any other tricky technics, simply match event flow with element's kinship:
$(document).on("click.menu-outside", function(event){
// Test if target and it's parent aren't #menuscontainer
// That means the click event occur on other branch of document tree
if(!$(event.target).parents().andSelf().is("#menuscontainer")){
// Click outisde #menuscontainer
// Hide the menus (but test if menus aren't already hidden)
}
});
To remove click outside event listener, simply:
$(document).off("click.menu-outside");
If someone curious here is javascript solution(es6):
window.addEventListener('mouseup', e => {
if (e.target != yourDiv && e.target.parentNode != yourDiv) {
yourDiv.classList.remove('show-menu');
//or yourDiv.style.display = 'none';
}
})
and es5, just in case:
window.addEventListener('mouseup', function (e) {
if (e.target != yourDiv && e.target.parentNode != yourDiv) {
yourDiv.classList.remove('show-menu');
//or yourDiv.style.display = 'none';
}
});
If you are scripting for IE and FF 3.* and you just want to know if the click occured within a certain box area, you could also use something like:
this.outsideElementClick = function(objEvent, objElement) {
var objCurrentElement = objEvent.target || objEvent.srcElement;
var blnInsideX = false;
var blnInsideY = false;
if (objCurrentElement.getBoundingClientRect().left >= objElement.getBoundingClientRect().left && objCurrentElement.getBoundingClientRect().right <= objElement.getBoundingClientRect().right)
blnInsideX = true;
if (objCurrentElement.getBoundingClientRect().top >= objElement.getBoundingClientRect().top && objCurrentElement.getBoundingClientRect().bottom <= objElement.getBoundingClientRect().bottom)
blnInsideY = true;
if (blnInsideX && blnInsideY)
return false;
else
return true;
}
Use:
var go = false;
$(document).click(function(){
if(go){
$('#divID').hide();
go = false;
}
})
$("#divID").mouseover(function(){
go = false;
});
$("#divID").mouseout(function (){
go = true;
});
$("btnID").click( function(){
if($("#divID:visible").length==1)
$("#divID").hide(); // Toggle
$("#divID").show();
});
Hook a click event listener on the document. Inside the event listener, you can look at the event object, in particular, the event.target to see what element was clicked:
$(document).click(function(e){
if ($(e.target).closest("#menuscontainer").length == 0) {
// .closest can help you determine if the element
// or one of its ancestors is #menuscontainer
console.log("hide");
}
});
Here is a simple solution by pure javascript. It is up-to-date with ES6:
var isMenuClick = false;
var menu = document.getElementById('menuscontainer');
document.addEventListener('click',()=>{
if(!isMenuClick){
//Hide the menu here
}
//Reset isMenuClick
isMenuClick = false;
})
menu.addEventListener('click',()=>{
isMenuClick = true;
})
I have used below script and done with jQuery.
jQuery(document).click(function(e) {
var target = e.target; //target div recorded
if (!jQuery(target).is('#tobehide') ) {
jQuery(this).fadeOut(); //if the click element is not the above id will hide
}
})
Below find the HTML code
<div class="main-container">
<div> Hello I am the title</div>
<div class="tobehide">I will hide when you click outside of me</div>
</div>
You can read the tutorial here
I have some HTML menus, which I show completely when a user clicks on the head of these menus. I would like to hide these elements when the user clicks outside the menus' area.
Is something like this possible with jQuery?
$("#menuscontainer").clickOutsideThisElement(function() {
// Hide the menus
});
Note: Using stopPropagation is something that should be avoided as it breaks normal event flow in the DOM. See this CSS Tricks article for more information. Consider using this method instead.
Attach a click event to the document body which closes the window. Attach a separate click event to the container which stops propagation to the document body.
$(window).click(function() {
//Hide the menus if visible
});
$('#menucontainer').click(function(event){
event.stopPropagation();
});
You can listen for a click event on document and then make sure #menucontainer is not an ancestor or the target of the clicked element by using .closest().
If it is not, then the clicked element is outside of the #menucontainer and you can safely hide it.
$(document).click(function(event) {
var $target = $(event.target);
if(!$target.closest('#menucontainer').length &&
$('#menucontainer').is(":visible")) {
$('#menucontainer').hide();
}
});
Edit – 2017-06-23
You can also clean up after the event listener if you plan to dismiss the menu and want to stop listening for events. This function will clean up only the newly created listener, preserving any other click listeners on document. With ES2015 syntax:
export function hideOnClickOutside(selector) {
const outsideClickListener = (event) => {
const $target = $(event.target);
if (!$target.closest(selector).length && $(selector).is(':visible')) {
$(selector).hide();
removeClickListener();
}
}
const removeClickListener = () => {
document.removeEventListener('click', outsideClickListener);
}
document.addEventListener('click', outsideClickListener);
}
Edit – 2018-03-11
For those who don't want to use jQuery. Here's the above code in plain vanillaJS (ECMAScript6).
function hideOnClickOutside(element) {
const outsideClickListener = event => {
if (!element.contains(event.target) && isVisible(element)) { // or use: event.target.closest(selector) === null
element.style.display = 'none';
removeClickListener();
}
}
const removeClickListener = () => {
document.removeEventListener('click', outsideClickListener);
}
document.addEventListener('click', outsideClickListener);
}
const isVisible = elem => !!elem && !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js
NOTE:
This is based on Alex comment to just use !element.contains(event.target) instead of the jQuery part.
But element.closest() is now also available in all major browsers (the W3C version differs a bit from the jQuery one).
Polyfills can be found here: Element.closest()
Edit – 2020-05-21
In the case where you want the user to be able to click-and-drag inside the element, then release the mouse outside the element, without closing the element:
...
let lastMouseDownX = 0;
let lastMouseDownY = 0;
let lastMouseDownWasOutside = false;
const mouseDownListener = (event: MouseEvent) => {
lastMouseDownX = event.offsetX;
lastMouseDownY = event.offsetY;
lastMouseDownWasOutside = !$(event.target).closest(element).length;
}
document.addEventListener('mousedown', mouseDownListener);
And in outsideClickListener:
const outsideClickListener = event => {
const deltaX = event.offsetX - lastMouseDownX;
const deltaY = event.offsetY - lastMouseDownY;
const distSq = (deltaX * deltaX) + (deltaY * deltaY);
const isDrag = distSq > 3;
const isDragException = isDrag && !lastMouseDownWasOutside;
if (!element.contains(event.target) && isVisible(element) && !isDragException) { // or use: event.target.closest(selector) === null
element.style.display = 'none';
removeClickListener();
document.removeEventListener('mousedown', mouseDownListener); // Or add this line to removeClickListener()
}
}
How to detect a click outside an element?
The reason that this question is so popular and has so many answers is that it is deceptively complex. After almost eight years and dozens of answers, I am genuinely surprised to see how little care has been given to accessibility.
I would like to hide these elements when the user clicks outside the menus' area.
This is a noble cause and is the actual issue. The title of the question—which is what most answers appear to attempt to address—contains an unfortunate red herring.
Hint: it's the word "click"!
You don't actually want to bind click handlers.
If you're binding click handlers to close the dialog, you've already failed. The reason you've failed is that not everyone triggers click events. Users not using a mouse will be able to escape your dialog (and your pop-up menu is arguably a type of dialog) by pressing Tab, and they then won't be able to read the content behind the dialog without subsequently triggering a click event.
So let's rephrase the question.
How does one close a dialog when a user is finished with it?
This is the goal. Unfortunately, now we need to bind the userisfinishedwiththedialog event, and that binding isn't so straightforward.
So how can we detect that a user has finished using a dialog?
focusout event
A good start is to determine if focus has left the dialog.
Hint: be careful with the blur event, blur doesn't propagate if the event was bound to the bubbling phase!
jQuery's focusout will do just fine. If you can't use jQuery, then you can use blur during the capturing phase:
element.addEventListener('blur', ..., true);
// use capture: ^^^^
Also, for many dialogs you'll need to allow the container to gain focus. Add tabindex="-1" to allow the dialog to receive focus dynamically without otherwise interrupting the tabbing flow.
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on('focusout', function () {
$(this).removeClass('active');
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Example
<div id="example" tabindex="-1">
Lorem ipsum dolor sit amet.
</div>
If you play with that demo for more than a minute you should quickly start seeing issues.
The first is that the link in the dialog isn't clickable. Attempting to click on it or tab to it will lead to the dialog closing before the interaction takes place. This is because focusing the inner element triggers a focusout event before triggering a focusin event again.
The fix is to queue the state change on the event loop. This can be done by using setImmediate(...), or setTimeout(..., 0) for browsers that don't support setImmediate. Once queued it can be cancelled by a subsequent focusin:
$('.submenu').on({
focusout: function (e) {
$(this).data('submenuTimer', setTimeout(function () {
$(this).removeClass('submenu--active');
}.bind(this), 0));
},
focusin: function (e) {
clearTimeout($(this).data('submenuTimer'));
}
});
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on({
focusout: function () {
$(this).data('timer', setTimeout(function () {
$(this).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('timer'));
}
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Example
<div id="example" tabindex="-1">
Lorem ipsum dolor sit amet.
</div>
The second issue is that the dialog won't close when the link is pressed again. This is because the dialog loses focus, triggering the close behavior, after which the link click triggers the dialog to reopen.
Similar to the previous issue, the focus state needs to be managed. Given that the state change has already been queued, it's just a matter of handling focus events on the dialog triggers:
This should look familiar
$('a').on({
focusout: function () {
$(this.hash).data('timer', setTimeout(function () {
$(this.hash).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('timer'));
}
});
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on({
focusout: function () {
$(this).data('timer', setTimeout(function () {
$(this).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('timer'));
}
});
$('a').on({
focusout: function () {
$(this.hash).data('timer', setTimeout(function () {
$(this.hash).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('timer'));
}
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Example
<div id="example" tabindex="-1">
Lorem ipsum dolor sit amet.
</div>
Esc key
If you thought you were done by handling the focus states, there's more you can do to simplify the user experience.
This is often a "nice to have" feature, but it's common that when you have a modal or popup of any sort that the Esc key will close it out.
keydown: function (e) {
if (e.which === 27) {
$(this).removeClass('active');
e.preventDefault();
}
}
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on({
focusout: function () {
$(this).data('timer', setTimeout(function () {
$(this).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('timer'));
},
keydown: function (e) {
if (e.which === 27) {
$(this).removeClass('active');
e.preventDefault();
}
}
});
$('a').on({
focusout: function () {
$(this.hash).data('timer', setTimeout(function () {
$(this.hash).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('timer'));
}
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Example
<div id="example" tabindex="-1">
Lorem ipsum dolor sit amet.
</div>
If you know you have focusable elements within the dialog, you won't need to focus the dialog directly. If you're building a menu, you could focus the first menu item instead.
click: function (e) {
$(this.hash)
.toggleClass('submenu--active')
.find('a:first')
.focus();
e.preventDefault();
}
$('.menu__link').on({
click: function (e) {
$(this.hash)
.toggleClass('submenu--active')
.find('a:first')
.focus();
e.preventDefault();
},
focusout: function () {
$(this.hash).data('submenuTimer', setTimeout(function () {
$(this.hash).removeClass('submenu--active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('submenuTimer'));
}
});
$('.submenu').on({
focusout: function () {
$(this).data('submenuTimer', setTimeout(function () {
$(this).removeClass('submenu--active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('submenuTimer'));
},
keydown: function (e) {
if (e.which === 27) {
$(this).removeClass('submenu--active');
e.preventDefault();
}
}
});
.menu {
list-style: none;
margin: 0;
padding: 0;
}
.menu:after {
clear: both;
content: '';
display: table;
}
.menu__item {
float: left;
position: relative;
}
.menu__link {
background-color: lightblue;
color: black;
display: block;
padding: 0.5em 1em;
text-decoration: none;
}
.menu__link:hover,
.menu__link:focus {
background-color: black;
color: lightblue;
}
.submenu {
border: 1px solid black;
display: none;
left: 0;
list-style: none;
margin: 0;
padding: 0;
position: absolute;
top: 100%;
}
.submenu--active {
display: block;
}
.submenu__item {
width: 150px;
}
.submenu__link {
background-color: lightblue;
color: black;
display: block;
padding: 0.5em 1em;
text-decoration: none;
}
.submenu__link:hover,
.submenu__link:focus {
background-color: black;
color: lightblue;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul class="menu">
<li class="menu__item">
<a class="menu__link" href="#menu-1">Menu 1</a>
<ul class="submenu" id="menu-1" tabindex="-1">
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#1">Example 1</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#2">Example 2</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#3">Example 3</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#4">Example 4</a></li>
</ul>
</li>
<li class="menu__item">
<a class="menu__link" href="#menu-2">Menu 2</a>
<ul class="submenu" id="menu-2" tabindex="-1">
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#1">Example 1</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#2">Example 2</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#3">Example 3</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#4">Example 4</a></li>
</ul>
</li>
</ul>
lorem ipsum dolor sit amet.
WAI-ARIA Roles and Other Accessibility Support
This answer hopefully covers the basics of accessible keyboard and mouse support for this feature, but as it's already quite sizable I'm going to avoid any discussion of WAI-ARIA roles and attributes, however I highly recommend that implementers refer to the spec for details on what roles they should use and any other appropriate attributes.
The other solutions here didn't work for me so I had to use:
if(!$(event.target).is('#foo'))
{
// hide menu
}
Edit: Plain Javascript variant (2021-03-31)
I used this method to handle closing a drop down menu when clicking outside of it.
First, I created a custom class name for all the elements of the component. This class name will be added to all elements that make up the menu widget.
const className = `dropdown-${Date.now()}-${Math.random() * 100}`;
I create a function to check for clicks and the class name of the clicked element. If clicked element does not contain the custom class name I generated above, it should set the show flag to false and the menu will close.
const onClickOutside = (e) => {
if (!e.target.className.includes(className)) {
show = false;
}
};
Then I attached the click handler to the window object.
// add when widget loads
window.addEventListener("click", onClickOutside);
... and finally some housekeeping
// remove listener when destroying the widget
window.removeEventListener("click", onClickOutside);
I have an application that works similarly to Eran's example, except I attach the click event to the body when I open the menu... Kinda like this:
$('#menucontainer').click(function(event) {
$('html').one('click',function() {
// Hide the menus
});
event.stopPropagation();
});
More information on jQuery's one() function
It's 2020 and you can use event.composedPath()
From: Event.composedPath()
The composedPath() method of the Event interface returns the event’s path, which is an array of the objects on which listeners will be invoked.
const target = document.querySelector('#myTarget')
document.addEventListener('click', (event) => {
const withinBoundaries = event.composedPath().includes(target)
if (withinBoundaries) {
target.innerText = 'Click happened inside element'
} else {
target.innerText = 'Click happened **OUTSIDE** element'
}
})
/* Just to make it good looking. You don't need this */
#myTarget {
margin: 50px auto;
width: 500px;
height: 500px;
background: gray;
border: 10px solid black;
}
<div id="myTarget">
Click me (or not!)
</div>
After research, I have found three working solutions
First solution
<script>
//The good thing about this solution is it doesn't stop event propagation.
var clickFlag = 0;
$('body').on('click', function () {
if(clickFlag == 0) {
console.log('hide element here');
/* Hide element here */
}
else {
clickFlag=0;
}
});
$('body').on('click','#testDiv', function (event) {
clickFlag = 1;
console.log('showed the element');
/* Show the element */
});
</script>
Second solution
<script>
$('body').on('click', function(e) {
if($(e.target).closest('#testDiv').length == 0) {
/* Hide dropdown here */
}
});
</script>
Third solution
<script>
var specifiedElement = document.getElementById('testDiv');
document.addEventListener('click', function(event) {
var isClickInside = specifiedElement.contains(event.target);
if (isClickInside) {
console.log('You clicked inside')
}
else {
console.log('You clicked outside')
}
});
</script>
$("#menuscontainer").click(function() {
$(this).focus();
});
$("#menuscontainer").blur(function(){
$(this).hide();
});
Works for me just fine.
Now there is a plugin for that: outside events (blog post)
The following happens when a clickoutside handler (WLOG) is bound to an element:
the element is added to an array which holds all elements with clickoutside handlers
a (namespaced) click handler is bound to the document (if not already there)
on any click in the document, the clickoutside event is triggered for those elements in that array that are not equal to or a parent of the click-events target
additionally, the event.target for the clickoutside event is set to the element the user clicked on (so you even know what the user clicked, not just that he clicked outside)
So no events are stopped from propagation and additional click handlers may be used "above" the element with the outside-handler.
This worked for me perfectly!!
$('html').click(function (e) {
if (e.target.id == 'YOUR-DIV-ID') {
//do something
} else {
//do something
}
});
I don't think what you really need is to close the menu when the user clicks outside; what you need is for the menu to close when the user clicks anywhere at all on the page. If you click on the menu, or off the menu it should close right?
Finding no satisfactory answers above prompted me to write this blog post the other day. For the more pedantic, there are a number of gotchas to take note of:
If you attach a click event handler to the body element at click time be sure to wait for the 2nd click before closing the menu, and unbinding the event. Otherwise the click event that opened the menu will bubble up to the listener that has to close the menu.
If you use event.stopPropogation() on a click event, no other elements in your page can have a click-anywhere-to-close feature.
Attaching a click event handler to the body element indefinitely is not a performant solution
Comparing the target of the event, and its parents to the handler's creator assumes that what you want is to close the menu when you click off it, when what you really want is to close it when you click anywhere on the page.
Listening for events on the body element will make your code more brittle. Styling as innocent as this would break it: body { margin-left:auto; margin-right: auto; width:960px;}
As another poster said there are a lot of gotchas, especially if the element you are displaying (in this case a menu) has interactive elements.
I've found the following method to be fairly robust:
$('#menuscontainer').click(function(event) {
//your code that shows the menus fully
//now set up an event listener so that clicking anywhere outside will close the menu
$('html').click(function(event) {
//check up the tree of the click target to check whether user has clicked outside of menu
if ($(event.target).parents('#menuscontainer').length==0) {
// your code to hide menu
//this event listener has done its job so we can unbind it.
$(this).unbind(event);
}
})
});
A simple solution for the situation is:
$(document).mouseup(function (e)
{
var container = $("YOUR SELECTOR"); // Give you class or ID
if (!container.is(e.target) && // If the target of the click is not the desired div or section
container.has(e.target).length === 0) // ... nor a descendant-child of the container
{
container.hide();
}
});
The above script will hide the div if outside of the div click event is triggered.
Check the window click event target (it should propagate to the window, as long as it's not captured anywhere else), and ensure that it's not any of the menu elements. If it's not, then you're outside your menu.
Or check the position of the click, and see if it's contained within the menu area.
I am surprised nobody actually acknowledged focusout event:
var button = document.getElementById('button');
button.addEventListener('click', function(e){
e.target.style.backgroundColor = 'green';
});
button.addEventListener('focusout', function(e){
e.target.style.backgroundColor = '';
});
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<button id="button">Click</button>
</body>
</html>
Solution1
Instead of using event.stopPropagation() which can have some side affects, just define a simple flag variable and add one if condition. I tested this and worked properly without any side affects of stopPropagation:
var flag = "1";
$('#menucontainer').click(function(event){
flag = "0"; // flag 0 means click happened in the area where we should not do any action
});
$('html').click(function() {
if(flag != "0"){
// Hide the menus if visible
}
else {
flag = "1";
}
});
Solution2
With just a simple if condition:
$(document).on('click', function(event){
var container = $("#menucontainer");
if (!container.is(event.target) && // If the target of the click isn't the container...
container.has(event.target).length === 0) // ... nor a descendant of the container
{
// Do whatever you want to do when click is outside the element
}
});
2020 solution using native JS API closest method.
document.addEventListener('click', ({ target }) => {
if (!target.closest('#menupop')) {
document.querySelector('#menupop').style.display = 'none'
}
})
#menupop {
width: 300px;
height: 300px;
background-color: red;
}
<div id="menupop">
clicking outside will close this
</div>
I've had success with something like this:
var $menuscontainer = ...;
$('#trigger').click(function() {
$menuscontainer.show();
$('body').click(function(event) {
var $target = $(event.target);
if ($target.parents('#menuscontainer').length == 0) {
$menuscontainer.hide();
}
});
});
The logic is: when #menuscontainer is shown, bind a click handler to the body that hides #menuscontainer only if the target (of the click) isn't a child of it.
The event has a property called event.path of the element which is a "static ordered list of all its ancestors in tree order". To check if an event originated from a specific DOM element or one of its children, just check the path for that specific DOM element. It can also be used to check multiple elements by logically ORing the element check in the some function.
$("body").click(function() {
target = document.getElementById("main");
flag = event.path.some(function(el, i, arr) {
return (el == target)
})
if (flag) {
console.log("Inside")
} else {
console.log("Outside")
}
});
#main {
display: inline-block;
background:yellow;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="main">
<ul>
<li>Test-Main</li>
<li>Test-Main</li>
<li>Test-Main</li>
<li>Test-Main</li>
<li>Test-Main</li>
</ul>
</div>
<div id="main2">
Outside Main
</div>
So for your case It should be
$("body").click(function() {
target = $("#menuscontainer")[0];
flag = event.path.some(function(el, i, arr) {
return (el == target)
});
if (!flag) {
// Hide the menus
}
});
As a variant:
var $menu = $('#menucontainer');
$(document).on('click', function (e) {
// If element is opened and click target is outside it, hide it
if ($menu.is(':visible') && !$menu.is(e.target) && !$menu.has(e.target).length) {
$menu.hide();
}
});
It has no problem with stopping event propagation and better supports multiple menus on the same page where clicking on a second menu while a first is open will leave the first open in the stopPropagation solution.
Use focusout for accessibility
There is one answer here that says (quite correctly) that focusing on click events is an accessibility problem since we want to cater for keyboard users. The focusout event is the correct thing to use here, but it can be done much more simply than in the other answer (and in pure JavaScript too):
A simpler way of doing it:
The 'problem' with using focusout is that if an element inside your dialog/modal/menu loses focus, to something also 'inside', the event will still get fired. We can check that this isn't the case by looking at event.relatedTarget (which tells us what element will have gained focus).
dialog = document.getElementById("dialogElement")
dialog.addEventListener("focusout", function (event) {
if (
// We are still inside the dialog so don't close
dialog.contains(event.relatedTarget) ||
// We have switched to another tab so probably don't want to close
!document.hasFocus()
) {
return;
}
dialog.close(); // Or whatever logic you want to use to close
});
There is one slight gotcha to the above, which is that relatedTarget may be null. This is fine if the user is clicking outside the dialog, but will be a problem if unless the user clicks inside the dialog and the dialog happens to not be focusable. To fix this you have to make sure to set tabIndex=0 so your dialog is focusable.
I found this method in some jQuery calendar plugin.
function ClickOutsideCheck(e)
{
var el = e.target;
var popup = $('.popup:visible')[0];
if (popup==undefined)
return true;
while (true){
if (el == popup ) {
return true;
} else if (el == document) {
$(".popup").hide();
return false;
} else {
el = $(el).parent()[0];
}
}
};
$(document).bind('mousedown.popup', ClickOutsideCheck);
Here is the vanilla JavaScript solution for future viewers.
Upon clicking any element within the document, if the clicked element's id is toggled, or the hidden element is not hidden and the hidden element does not contain the clicked element, toggle the element.
(function () {
"use strict";
var hidden = document.getElementById('hidden');
document.addEventListener('click', function (e) {
if (e.target.id == 'toggle' || (hidden.style.display != 'none' && !hidden.contains(e.target))) hidden.style.display = hidden.style.display == 'none' ? 'block' : 'none';
}, false);
})();
(function () {
"use strict";
var hidden = document.getElementById('hidden');
document.addEventListener('click', function (e) {
if (e.target.id == 'toggle' || (hidden.style.display != 'none' && !hidden.contains(e.target))) hidden.style.display = hidden.style.display == 'none' ? 'block' : 'none';
}, false);
})();
Toggle Hidden Div
<div id="hidden" style="display: none;">This content is normally hidden. click anywhere other than this content to make me disappear</div>
If you are going to have multiple toggles on the same page you can use something like this:
Add the class name hidden to the collapsible item.
Upon document click, close all hidden elements which do not contain the clicked element and are not hidden
If the clicked element is a toggle, toggle the specified element.
(function () {
"use strict";
var hiddenItems = document.getElementsByClassName('hidden'), hidden;
document.addEventListener('click', function (e) {
for (var i = 0; hidden = hiddenItems[i]; i++) {
if (!hidden.contains(e.target) && hidden.style.display != 'none')
hidden.style.display = 'none';
}
if (e.target.getAttribute('data-toggle')) {
var toggle = document.querySelector(e.target.getAttribute('data-toggle'));
toggle.style.display = toggle.style.display == 'none' ? 'block' : 'none';
}
}, false);
})();
Toggle Hidden Div
<div class="hidden" id="hidden1" style="display: none;" data-hidden="true">This content is normally hidden</div>
Toggle Hidden Div
<div class="hidden" id="hidden2" style="display: none;" data-hidden="true">This content is normally hidden</div>
Toggle Hidden Div
<div class="hidden" id="hidden3" style="display: none;" data-hidden="true">This content is normally hidden</div>
Instead using flow interruption, blur/focus event or any other tricky technics, simply match event flow with element's kinship:
$(document).on("click.menu-outside", function(event){
// Test if target and it's parent aren't #menuscontainer
// That means the click event occur on other branch of document tree
if(!$(event.target).parents().andSelf().is("#menuscontainer")){
// Click outisde #menuscontainer
// Hide the menus (but test if menus aren't already hidden)
}
});
To remove click outside event listener, simply:
$(document).off("click.menu-outside");
If someone curious here is javascript solution(es6):
window.addEventListener('mouseup', e => {
if (e.target != yourDiv && e.target.parentNode != yourDiv) {
yourDiv.classList.remove('show-menu');
//or yourDiv.style.display = 'none';
}
})
and es5, just in case:
window.addEventListener('mouseup', function (e) {
if (e.target != yourDiv && e.target.parentNode != yourDiv) {
yourDiv.classList.remove('show-menu');
//or yourDiv.style.display = 'none';
}
});
If you are scripting for IE and FF 3.* and you just want to know if the click occured within a certain box area, you could also use something like:
this.outsideElementClick = function(objEvent, objElement) {
var objCurrentElement = objEvent.target || objEvent.srcElement;
var blnInsideX = false;
var blnInsideY = false;
if (objCurrentElement.getBoundingClientRect().left >= objElement.getBoundingClientRect().left && objCurrentElement.getBoundingClientRect().right <= objElement.getBoundingClientRect().right)
blnInsideX = true;
if (objCurrentElement.getBoundingClientRect().top >= objElement.getBoundingClientRect().top && objCurrentElement.getBoundingClientRect().bottom <= objElement.getBoundingClientRect().bottom)
blnInsideY = true;
if (blnInsideX && blnInsideY)
return false;
else
return true;
}
Use:
var go = false;
$(document).click(function(){
if(go){
$('#divID').hide();
go = false;
}
})
$("#divID").mouseover(function(){
go = false;
});
$("#divID").mouseout(function (){
go = true;
});
$("btnID").click( function(){
if($("#divID:visible").length==1)
$("#divID").hide(); // Toggle
$("#divID").show();
});
Hook a click event listener on the document. Inside the event listener, you can look at the event object, in particular, the event.target to see what element was clicked:
$(document).click(function(e){
if ($(e.target).closest("#menuscontainer").length == 0) {
// .closest can help you determine if the element
// or one of its ancestors is #menuscontainer
console.log("hide");
}
});
Here is a simple solution by pure javascript. It is up-to-date with ES6:
var isMenuClick = false;
var menu = document.getElementById('menuscontainer');
document.addEventListener('click',()=>{
if(!isMenuClick){
//Hide the menu here
}
//Reset isMenuClick
isMenuClick = false;
})
menu.addEventListener('click',()=>{
isMenuClick = true;
})
I have used below script and done with jQuery.
jQuery(document).click(function(e) {
var target = e.target; //target div recorded
if (!jQuery(target).is('#tobehide') ) {
jQuery(this).fadeOut(); //if the click element is not the above id will hide
}
})
Below find the HTML code
<div class="main-container">
<div> Hello I am the title</div>
<div class="tobehide">I will hide when you click outside of me</div>
</div>
You can read the tutorial here
I have some HTML menus, which I show completely when a user clicks on the head of these menus. I would like to hide these elements when the user clicks outside the menus' area.
Is something like this possible with jQuery?
$("#menuscontainer").clickOutsideThisElement(function() {
// Hide the menus
});
Note: Using stopPropagation is something that should be avoided as it breaks normal event flow in the DOM. See this CSS Tricks article for more information. Consider using this method instead.
Attach a click event to the document body which closes the window. Attach a separate click event to the container which stops propagation to the document body.
$(window).click(function() {
//Hide the menus if visible
});
$('#menucontainer').click(function(event){
event.stopPropagation();
});
You can listen for a click event on document and then make sure #menucontainer is not an ancestor or the target of the clicked element by using .closest().
If it is not, then the clicked element is outside of the #menucontainer and you can safely hide it.
$(document).click(function(event) {
var $target = $(event.target);
if(!$target.closest('#menucontainer').length &&
$('#menucontainer').is(":visible")) {
$('#menucontainer').hide();
}
});
Edit – 2017-06-23
You can also clean up after the event listener if you plan to dismiss the menu and want to stop listening for events. This function will clean up only the newly created listener, preserving any other click listeners on document. With ES2015 syntax:
export function hideOnClickOutside(selector) {
const outsideClickListener = (event) => {
const $target = $(event.target);
if (!$target.closest(selector).length && $(selector).is(':visible')) {
$(selector).hide();
removeClickListener();
}
}
const removeClickListener = () => {
document.removeEventListener('click', outsideClickListener);
}
document.addEventListener('click', outsideClickListener);
}
Edit – 2018-03-11
For those who don't want to use jQuery. Here's the above code in plain vanillaJS (ECMAScript6).
function hideOnClickOutside(element) {
const outsideClickListener = event => {
if (!element.contains(event.target) && isVisible(element)) { // or use: event.target.closest(selector) === null
element.style.display = 'none';
removeClickListener();
}
}
const removeClickListener = () => {
document.removeEventListener('click', outsideClickListener);
}
document.addEventListener('click', outsideClickListener);
}
const isVisible = elem => !!elem && !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js
NOTE:
This is based on Alex comment to just use !element.contains(event.target) instead of the jQuery part.
But element.closest() is now also available in all major browsers (the W3C version differs a bit from the jQuery one).
Polyfills can be found here: Element.closest()
Edit – 2020-05-21
In the case where you want the user to be able to click-and-drag inside the element, then release the mouse outside the element, without closing the element:
...
let lastMouseDownX = 0;
let lastMouseDownY = 0;
let lastMouseDownWasOutside = false;
const mouseDownListener = (event: MouseEvent) => {
lastMouseDownX = event.offsetX;
lastMouseDownY = event.offsetY;
lastMouseDownWasOutside = !$(event.target).closest(element).length;
}
document.addEventListener('mousedown', mouseDownListener);
And in outsideClickListener:
const outsideClickListener = event => {
const deltaX = event.offsetX - lastMouseDownX;
const deltaY = event.offsetY - lastMouseDownY;
const distSq = (deltaX * deltaX) + (deltaY * deltaY);
const isDrag = distSq > 3;
const isDragException = isDrag && !lastMouseDownWasOutside;
if (!element.contains(event.target) && isVisible(element) && !isDragException) { // or use: event.target.closest(selector) === null
element.style.display = 'none';
removeClickListener();
document.removeEventListener('mousedown', mouseDownListener); // Or add this line to removeClickListener()
}
}
How to detect a click outside an element?
The reason that this question is so popular and has so many answers is that it is deceptively complex. After almost eight years and dozens of answers, I am genuinely surprised to see how little care has been given to accessibility.
I would like to hide these elements when the user clicks outside the menus' area.
This is a noble cause and is the actual issue. The title of the question—which is what most answers appear to attempt to address—contains an unfortunate red herring.
Hint: it's the word "click"!
You don't actually want to bind click handlers.
If you're binding click handlers to close the dialog, you've already failed. The reason you've failed is that not everyone triggers click events. Users not using a mouse will be able to escape your dialog (and your pop-up menu is arguably a type of dialog) by pressing Tab, and they then won't be able to read the content behind the dialog without subsequently triggering a click event.
So let's rephrase the question.
How does one close a dialog when a user is finished with it?
This is the goal. Unfortunately, now we need to bind the userisfinishedwiththedialog event, and that binding isn't so straightforward.
So how can we detect that a user has finished using a dialog?
focusout event
A good start is to determine if focus has left the dialog.
Hint: be careful with the blur event, blur doesn't propagate if the event was bound to the bubbling phase!
jQuery's focusout will do just fine. If you can't use jQuery, then you can use blur during the capturing phase:
element.addEventListener('blur', ..., true);
// use capture: ^^^^
Also, for many dialogs you'll need to allow the container to gain focus. Add tabindex="-1" to allow the dialog to receive focus dynamically without otherwise interrupting the tabbing flow.
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on('focusout', function () {
$(this).removeClass('active');
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Example
<div id="example" tabindex="-1">
Lorem ipsum dolor sit amet.
</div>
If you play with that demo for more than a minute you should quickly start seeing issues.
The first is that the link in the dialog isn't clickable. Attempting to click on it or tab to it will lead to the dialog closing before the interaction takes place. This is because focusing the inner element triggers a focusout event before triggering a focusin event again.
The fix is to queue the state change on the event loop. This can be done by using setImmediate(...), or setTimeout(..., 0) for browsers that don't support setImmediate. Once queued it can be cancelled by a subsequent focusin:
$('.submenu').on({
focusout: function (e) {
$(this).data('submenuTimer', setTimeout(function () {
$(this).removeClass('submenu--active');
}.bind(this), 0));
},
focusin: function (e) {
clearTimeout($(this).data('submenuTimer'));
}
});
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on({
focusout: function () {
$(this).data('timer', setTimeout(function () {
$(this).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('timer'));
}
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Example
<div id="example" tabindex="-1">
Lorem ipsum dolor sit amet.
</div>
The second issue is that the dialog won't close when the link is pressed again. This is because the dialog loses focus, triggering the close behavior, after which the link click triggers the dialog to reopen.
Similar to the previous issue, the focus state needs to be managed. Given that the state change has already been queued, it's just a matter of handling focus events on the dialog triggers:
This should look familiar
$('a').on({
focusout: function () {
$(this.hash).data('timer', setTimeout(function () {
$(this.hash).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('timer'));
}
});
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on({
focusout: function () {
$(this).data('timer', setTimeout(function () {
$(this).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('timer'));
}
});
$('a').on({
focusout: function () {
$(this.hash).data('timer', setTimeout(function () {
$(this.hash).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('timer'));
}
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Example
<div id="example" tabindex="-1">
Lorem ipsum dolor sit amet.
</div>
Esc key
If you thought you were done by handling the focus states, there's more you can do to simplify the user experience.
This is often a "nice to have" feature, but it's common that when you have a modal or popup of any sort that the Esc key will close it out.
keydown: function (e) {
if (e.which === 27) {
$(this).removeClass('active');
e.preventDefault();
}
}
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on({
focusout: function () {
$(this).data('timer', setTimeout(function () {
$(this).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('timer'));
},
keydown: function (e) {
if (e.which === 27) {
$(this).removeClass('active');
e.preventDefault();
}
}
});
$('a').on({
focusout: function () {
$(this.hash).data('timer', setTimeout(function () {
$(this.hash).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('timer'));
}
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Example
<div id="example" tabindex="-1">
Lorem ipsum dolor sit amet.
</div>
If you know you have focusable elements within the dialog, you won't need to focus the dialog directly. If you're building a menu, you could focus the first menu item instead.
click: function (e) {
$(this.hash)
.toggleClass('submenu--active')
.find('a:first')
.focus();
e.preventDefault();
}
$('.menu__link').on({
click: function (e) {
$(this.hash)
.toggleClass('submenu--active')
.find('a:first')
.focus();
e.preventDefault();
},
focusout: function () {
$(this.hash).data('submenuTimer', setTimeout(function () {
$(this.hash).removeClass('submenu--active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('submenuTimer'));
}
});
$('.submenu').on({
focusout: function () {
$(this).data('submenuTimer', setTimeout(function () {
$(this).removeClass('submenu--active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('submenuTimer'));
},
keydown: function (e) {
if (e.which === 27) {
$(this).removeClass('submenu--active');
e.preventDefault();
}
}
});
.menu {
list-style: none;
margin: 0;
padding: 0;
}
.menu:after {
clear: both;
content: '';
display: table;
}
.menu__item {
float: left;
position: relative;
}
.menu__link {
background-color: lightblue;
color: black;
display: block;
padding: 0.5em 1em;
text-decoration: none;
}
.menu__link:hover,
.menu__link:focus {
background-color: black;
color: lightblue;
}
.submenu {
border: 1px solid black;
display: none;
left: 0;
list-style: none;
margin: 0;
padding: 0;
position: absolute;
top: 100%;
}
.submenu--active {
display: block;
}
.submenu__item {
width: 150px;
}
.submenu__link {
background-color: lightblue;
color: black;
display: block;
padding: 0.5em 1em;
text-decoration: none;
}
.submenu__link:hover,
.submenu__link:focus {
background-color: black;
color: lightblue;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul class="menu">
<li class="menu__item">
<a class="menu__link" href="#menu-1">Menu 1</a>
<ul class="submenu" id="menu-1" tabindex="-1">
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#1">Example 1</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#2">Example 2</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#3">Example 3</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#4">Example 4</a></li>
</ul>
</li>
<li class="menu__item">
<a class="menu__link" href="#menu-2">Menu 2</a>
<ul class="submenu" id="menu-2" tabindex="-1">
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#1">Example 1</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#2">Example 2</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#3">Example 3</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#4">Example 4</a></li>
</ul>
</li>
</ul>
lorem ipsum dolor sit amet.
WAI-ARIA Roles and Other Accessibility Support
This answer hopefully covers the basics of accessible keyboard and mouse support for this feature, but as it's already quite sizable I'm going to avoid any discussion of WAI-ARIA roles and attributes, however I highly recommend that implementers refer to the spec for details on what roles they should use and any other appropriate attributes.
The other solutions here didn't work for me so I had to use:
if(!$(event.target).is('#foo'))
{
// hide menu
}
Edit: Plain Javascript variant (2021-03-31)
I used this method to handle closing a drop down menu when clicking outside of it.
First, I created a custom class name for all the elements of the component. This class name will be added to all elements that make up the menu widget.
const className = `dropdown-${Date.now()}-${Math.random() * 100}`;
I create a function to check for clicks and the class name of the clicked element. If clicked element does not contain the custom class name I generated above, it should set the show flag to false and the menu will close.
const onClickOutside = (e) => {
if (!e.target.className.includes(className)) {
show = false;
}
};
Then I attached the click handler to the window object.
// add when widget loads
window.addEventListener("click", onClickOutside);
... and finally some housekeeping
// remove listener when destroying the widget
window.removeEventListener("click", onClickOutside);
I have an application that works similarly to Eran's example, except I attach the click event to the body when I open the menu... Kinda like this:
$('#menucontainer').click(function(event) {
$('html').one('click',function() {
// Hide the menus
});
event.stopPropagation();
});
More information on jQuery's one() function
It's 2020 and you can use event.composedPath()
From: Event.composedPath()
The composedPath() method of the Event interface returns the event’s path, which is an array of the objects on which listeners will be invoked.
const target = document.querySelector('#myTarget')
document.addEventListener('click', (event) => {
const withinBoundaries = event.composedPath().includes(target)
if (withinBoundaries) {
target.innerText = 'Click happened inside element'
} else {
target.innerText = 'Click happened **OUTSIDE** element'
}
})
/* Just to make it good looking. You don't need this */
#myTarget {
margin: 50px auto;
width: 500px;
height: 500px;
background: gray;
border: 10px solid black;
}
<div id="myTarget">
Click me (or not!)
</div>
After research, I have found three working solutions
First solution
<script>
//The good thing about this solution is it doesn't stop event propagation.
var clickFlag = 0;
$('body').on('click', function () {
if(clickFlag == 0) {
console.log('hide element here');
/* Hide element here */
}
else {
clickFlag=0;
}
});
$('body').on('click','#testDiv', function (event) {
clickFlag = 1;
console.log('showed the element');
/* Show the element */
});
</script>
Second solution
<script>
$('body').on('click', function(e) {
if($(e.target).closest('#testDiv').length == 0) {
/* Hide dropdown here */
}
});
</script>
Third solution
<script>
var specifiedElement = document.getElementById('testDiv');
document.addEventListener('click', function(event) {
var isClickInside = specifiedElement.contains(event.target);
if (isClickInside) {
console.log('You clicked inside')
}
else {
console.log('You clicked outside')
}
});
</script>
$("#menuscontainer").click(function() {
$(this).focus();
});
$("#menuscontainer").blur(function(){
$(this).hide();
});
Works for me just fine.
Now there is a plugin for that: outside events (blog post)
The following happens when a clickoutside handler (WLOG) is bound to an element:
the element is added to an array which holds all elements with clickoutside handlers
a (namespaced) click handler is bound to the document (if not already there)
on any click in the document, the clickoutside event is triggered for those elements in that array that are not equal to or a parent of the click-events target
additionally, the event.target for the clickoutside event is set to the element the user clicked on (so you even know what the user clicked, not just that he clicked outside)
So no events are stopped from propagation and additional click handlers may be used "above" the element with the outside-handler.
This worked for me perfectly!!
$('html').click(function (e) {
if (e.target.id == 'YOUR-DIV-ID') {
//do something
} else {
//do something
}
});
I don't think what you really need is to close the menu when the user clicks outside; what you need is for the menu to close when the user clicks anywhere at all on the page. If you click on the menu, or off the menu it should close right?
Finding no satisfactory answers above prompted me to write this blog post the other day. For the more pedantic, there are a number of gotchas to take note of:
If you attach a click event handler to the body element at click time be sure to wait for the 2nd click before closing the menu, and unbinding the event. Otherwise the click event that opened the menu will bubble up to the listener that has to close the menu.
If you use event.stopPropogation() on a click event, no other elements in your page can have a click-anywhere-to-close feature.
Attaching a click event handler to the body element indefinitely is not a performant solution
Comparing the target of the event, and its parents to the handler's creator assumes that what you want is to close the menu when you click off it, when what you really want is to close it when you click anywhere on the page.
Listening for events on the body element will make your code more brittle. Styling as innocent as this would break it: body { margin-left:auto; margin-right: auto; width:960px;}
As another poster said there are a lot of gotchas, especially if the element you are displaying (in this case a menu) has interactive elements.
I've found the following method to be fairly robust:
$('#menuscontainer').click(function(event) {
//your code that shows the menus fully
//now set up an event listener so that clicking anywhere outside will close the menu
$('html').click(function(event) {
//check up the tree of the click target to check whether user has clicked outside of menu
if ($(event.target).parents('#menuscontainer').length==0) {
// your code to hide menu
//this event listener has done its job so we can unbind it.
$(this).unbind(event);
}
})
});
A simple solution for the situation is:
$(document).mouseup(function (e)
{
var container = $("YOUR SELECTOR"); // Give you class or ID
if (!container.is(e.target) && // If the target of the click is not the desired div or section
container.has(e.target).length === 0) // ... nor a descendant-child of the container
{
container.hide();
}
});
The above script will hide the div if outside of the div click event is triggered.
Check the window click event target (it should propagate to the window, as long as it's not captured anywhere else), and ensure that it's not any of the menu elements. If it's not, then you're outside your menu.
Or check the position of the click, and see if it's contained within the menu area.
I am surprised nobody actually acknowledged focusout event:
var button = document.getElementById('button');
button.addEventListener('click', function(e){
e.target.style.backgroundColor = 'green';
});
button.addEventListener('focusout', function(e){
e.target.style.backgroundColor = '';
});
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<button id="button">Click</button>
</body>
</html>
Solution1
Instead of using event.stopPropagation() which can have some side affects, just define a simple flag variable and add one if condition. I tested this and worked properly without any side affects of stopPropagation:
var flag = "1";
$('#menucontainer').click(function(event){
flag = "0"; // flag 0 means click happened in the area where we should not do any action
});
$('html').click(function() {
if(flag != "0"){
// Hide the menus if visible
}
else {
flag = "1";
}
});
Solution2
With just a simple if condition:
$(document).on('click', function(event){
var container = $("#menucontainer");
if (!container.is(event.target) && // If the target of the click isn't the container...
container.has(event.target).length === 0) // ... nor a descendant of the container
{
// Do whatever you want to do when click is outside the element
}
});
2020 solution using native JS API closest method.
document.addEventListener('click', ({ target }) => {
if (!target.closest('#menupop')) {
document.querySelector('#menupop').style.display = 'none'
}
})
#menupop {
width: 300px;
height: 300px;
background-color: red;
}
<div id="menupop">
clicking outside will close this
</div>
I've had success with something like this:
var $menuscontainer = ...;
$('#trigger').click(function() {
$menuscontainer.show();
$('body').click(function(event) {
var $target = $(event.target);
if ($target.parents('#menuscontainer').length == 0) {
$menuscontainer.hide();
}
});
});
The logic is: when #menuscontainer is shown, bind a click handler to the body that hides #menuscontainer only if the target (of the click) isn't a child of it.
The event has a property called event.path of the element which is a "static ordered list of all its ancestors in tree order". To check if an event originated from a specific DOM element or one of its children, just check the path for that specific DOM element. It can also be used to check multiple elements by logically ORing the element check in the some function.
$("body").click(function() {
target = document.getElementById("main");
flag = event.path.some(function(el, i, arr) {
return (el == target)
})
if (flag) {
console.log("Inside")
} else {
console.log("Outside")
}
});
#main {
display: inline-block;
background:yellow;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="main">
<ul>
<li>Test-Main</li>
<li>Test-Main</li>
<li>Test-Main</li>
<li>Test-Main</li>
<li>Test-Main</li>
</ul>
</div>
<div id="main2">
Outside Main
</div>
So for your case It should be
$("body").click(function() {
target = $("#menuscontainer")[0];
flag = event.path.some(function(el, i, arr) {
return (el == target)
});
if (!flag) {
// Hide the menus
}
});
As a variant:
var $menu = $('#menucontainer');
$(document).on('click', function (e) {
// If element is opened and click target is outside it, hide it
if ($menu.is(':visible') && !$menu.is(e.target) && !$menu.has(e.target).length) {
$menu.hide();
}
});
It has no problem with stopping event propagation and better supports multiple menus on the same page where clicking on a second menu while a first is open will leave the first open in the stopPropagation solution.
Use focusout for accessibility
There is one answer here that says (quite correctly) that focusing on click events is an accessibility problem since we want to cater for keyboard users. The focusout event is the correct thing to use here, but it can be done much more simply than in the other answer (and in pure JavaScript too):
A simpler way of doing it:
The 'problem' with using focusout is that if an element inside your dialog/modal/menu loses focus, to something also 'inside', the event will still get fired. We can check that this isn't the case by looking at event.relatedTarget (which tells us what element will have gained focus).
dialog = document.getElementById("dialogElement")
dialog.addEventListener("focusout", function (event) {
if (
// We are still inside the dialog so don't close
dialog.contains(event.relatedTarget) ||
// We have switched to another tab so probably don't want to close
!document.hasFocus()
) {
return;
}
dialog.close(); // Or whatever logic you want to use to close
});
There is one slight gotcha to the above, which is that relatedTarget may be null. This is fine if the user is clicking outside the dialog, but will be a problem if unless the user clicks inside the dialog and the dialog happens to not be focusable. To fix this you have to make sure to set tabIndex=0 so your dialog is focusable.
I found this method in some jQuery calendar plugin.
function ClickOutsideCheck(e)
{
var el = e.target;
var popup = $('.popup:visible')[0];
if (popup==undefined)
return true;
while (true){
if (el == popup ) {
return true;
} else if (el == document) {
$(".popup").hide();
return false;
} else {
el = $(el).parent()[0];
}
}
};
$(document).bind('mousedown.popup', ClickOutsideCheck);
Here is the vanilla JavaScript solution for future viewers.
Upon clicking any element within the document, if the clicked element's id is toggled, or the hidden element is not hidden and the hidden element does not contain the clicked element, toggle the element.
(function () {
"use strict";
var hidden = document.getElementById('hidden');
document.addEventListener('click', function (e) {
if (e.target.id == 'toggle' || (hidden.style.display != 'none' && !hidden.contains(e.target))) hidden.style.display = hidden.style.display == 'none' ? 'block' : 'none';
}, false);
})();
(function () {
"use strict";
var hidden = document.getElementById('hidden');
document.addEventListener('click', function (e) {
if (e.target.id == 'toggle' || (hidden.style.display != 'none' && !hidden.contains(e.target))) hidden.style.display = hidden.style.display == 'none' ? 'block' : 'none';
}, false);
})();
Toggle Hidden Div
<div id="hidden" style="display: none;">This content is normally hidden. click anywhere other than this content to make me disappear</div>
If you are going to have multiple toggles on the same page you can use something like this:
Add the class name hidden to the collapsible item.
Upon document click, close all hidden elements which do not contain the clicked element and are not hidden
If the clicked element is a toggle, toggle the specified element.
(function () {
"use strict";
var hiddenItems = document.getElementsByClassName('hidden'), hidden;
document.addEventListener('click', function (e) {
for (var i = 0; hidden = hiddenItems[i]; i++) {
if (!hidden.contains(e.target) && hidden.style.display != 'none')
hidden.style.display = 'none';
}
if (e.target.getAttribute('data-toggle')) {
var toggle = document.querySelector(e.target.getAttribute('data-toggle'));
toggle.style.display = toggle.style.display == 'none' ? 'block' : 'none';
}
}, false);
})();
Toggle Hidden Div
<div class="hidden" id="hidden1" style="display: none;" data-hidden="true">This content is normally hidden</div>
Toggle Hidden Div
<div class="hidden" id="hidden2" style="display: none;" data-hidden="true">This content is normally hidden</div>
Toggle Hidden Div
<div class="hidden" id="hidden3" style="display: none;" data-hidden="true">This content is normally hidden</div>
Instead using flow interruption, blur/focus event or any other tricky technics, simply match event flow with element's kinship:
$(document).on("click.menu-outside", function(event){
// Test if target and it's parent aren't #menuscontainer
// That means the click event occur on other branch of document tree
if(!$(event.target).parents().andSelf().is("#menuscontainer")){
// Click outisde #menuscontainer
// Hide the menus (but test if menus aren't already hidden)
}
});
To remove click outside event listener, simply:
$(document).off("click.menu-outside");
If someone curious here is javascript solution(es6):
window.addEventListener('mouseup', e => {
if (e.target != yourDiv && e.target.parentNode != yourDiv) {
yourDiv.classList.remove('show-menu');
//or yourDiv.style.display = 'none';
}
})
and es5, just in case:
window.addEventListener('mouseup', function (e) {
if (e.target != yourDiv && e.target.parentNode != yourDiv) {
yourDiv.classList.remove('show-menu');
//or yourDiv.style.display = 'none';
}
});
If you are scripting for IE and FF 3.* and you just want to know if the click occured within a certain box area, you could also use something like:
this.outsideElementClick = function(objEvent, objElement) {
var objCurrentElement = objEvent.target || objEvent.srcElement;
var blnInsideX = false;
var blnInsideY = false;
if (objCurrentElement.getBoundingClientRect().left >= objElement.getBoundingClientRect().left && objCurrentElement.getBoundingClientRect().right <= objElement.getBoundingClientRect().right)
blnInsideX = true;
if (objCurrentElement.getBoundingClientRect().top >= objElement.getBoundingClientRect().top && objCurrentElement.getBoundingClientRect().bottom <= objElement.getBoundingClientRect().bottom)
blnInsideY = true;
if (blnInsideX && blnInsideY)
return false;
else
return true;
}
Use:
var go = false;
$(document).click(function(){
if(go){
$('#divID').hide();
go = false;
}
})
$("#divID").mouseover(function(){
go = false;
});
$("#divID").mouseout(function (){
go = true;
});
$("btnID").click( function(){
if($("#divID:visible").length==1)
$("#divID").hide(); // Toggle
$("#divID").show();
});
Hook a click event listener on the document. Inside the event listener, you can look at the event object, in particular, the event.target to see what element was clicked:
$(document).click(function(e){
if ($(e.target).closest("#menuscontainer").length == 0) {
// .closest can help you determine if the element
// or one of its ancestors is #menuscontainer
console.log("hide");
}
});
Here is a simple solution by pure javascript. It is up-to-date with ES6:
var isMenuClick = false;
var menu = document.getElementById('menuscontainer');
document.addEventListener('click',()=>{
if(!isMenuClick){
//Hide the menu here
}
//Reset isMenuClick
isMenuClick = false;
})
menu.addEventListener('click',()=>{
isMenuClick = true;
})
I have used below script and done with jQuery.
jQuery(document).click(function(e) {
var target = e.target; //target div recorded
if (!jQuery(target).is('#tobehide') ) {
jQuery(this).fadeOut(); //if the click element is not the above id will hide
}
})
Below find the HTML code
<div class="main-container">
<div> Hello I am the title</div>
<div class="tobehide">I will hide when you click outside of me</div>
</div>
You can read the tutorial here
How can I close an open collapsed navbar on clicking outside of the navbar element? Currently, the only way to open or close it is by clicking on the navbar-toggle button.
See here for an example and code:
So far, I have tried the following which doesn't seem to work:
jQuery(document).click(function() {
});
jQuery('.navbar').click(function(event) {
jQuery(".navbar-collapse").collapse('hide');
event.stopPropagation();
});
Have a look that:
$(document).ready(function () {
$(document).click(function (event) {
var clickover = $(event.target);
var _opened = $(".navbar-collapse").hasClass("navbar-collapse in");
if (_opened === true && !clickover.hasClass("navbar-toggle")) {
$("button.navbar-toggle").click();
}
});
});
Your fiddle works with that: http://jsfiddle.net/52VtD/5718/
Its a modified version of this answer, which lacks the animation and is also a tiny bit more complicated.
I know, invoking the click() isn't very elegant, but collapse('hide') did not work for me either, and i think the animation is a bit nicer than adding and removing the classes hardly.
The accepted answer doesn't appear to work correctly. It only needs to check if "navbar-collapse" has the "in" class. We can then fire the collapse method as expected by using our reference to the navbar.
$(document).click(function (event) {
var clickover = $(event.target);
var $navbar = $(".navbar-collapse");
var _opened = $navbar.hasClass("in");
if (_opened === true && !clickover.hasClass("navbar-toggle")) {
$navbar.collapse('hide');
}
});
Using this works for me.
$(function() {
$(document).click(function (event) {
$('.navbar-collapse').collapse('hide');
});
});
The solution I decided to use was taken from the accepted answer here and from this answer
jQuery('body').bind('click', function(e) {
if(jQuery(e.target).closest('.navbar').length == 0) {
// click happened outside of .navbar, so hide
var opened = jQuery('.navbar-collapse').hasClass('collapse in');
if ( opened === true ) {
jQuery('.navbar-collapse').collapse('hide');
}
}
});
This hides an opened collapsed nav menu if the user clicks anywhere outside of the .navbar element. Of course clicking on .navbar-toggle still works to close the menu too.
Converted nozzleman's answer for Bootstrap 4(.3.1):
$(document).ready(function () {
$(document).click(
function (event) {
var target = $(event.target);
var _mobileMenuOpen = $(".navbar-collapse").hasClass("show");
if (_mobileMenuOpen === true && !target.hasClass("navbar-toggler")) {
$("button.navbar-toggler").click();
}
}
);
});
Placed in the ngOnInit().
When the document is loaded, this code waits for click events. If the mobile menu dropdown is open (i.e. the collapsible part of the navbar has the "show" class) and the clicked object (target) is not the mobile menu button (i.e. does not have the "navbar-toggler" class), then we tell the mobile menu button it has been clicked, and the menu closes.
stopPropagation() is not always the best solution. Rather use something like:
jQuery(document.body).on('click', function(ev){
if(jQuery(ev.target).closest('.navbar-collapse').length) return; // Not return false
// Hide navbar
});
I think it's dangerous to assume that you never want to listen to any other event from the .navbar. Which is impossible if you use stopPropagation().
I had a scenario where I had plain text and I didn't want the panel to close if a user clicks on the plain text on accident. The other answers here will close the panel even if you click on the text of an item that isn't a link.
To fix this I added on to Paul Tarr's answer by wrapping the solution in a check to see whether or not the click occurred anywhere inside:
if ($(event.target).parents(".navbar-collapse").length < 1) { }
The full code would become:
$(document).click(function (event) {
if ($(event.target).parents(".navbar-collapse").length < 1) {
var clickover = $(event.target);
var $navbar = $(".navbar-collapse");
var _opened = $navbar.hasClass("in");
if (_opened === true && !clickover.hasClass("navbar-toggle")) {
$navbar.collapse('hide');
}
}
});
In this demo fiddle you can see that if you click on a non-link inside of the panel it won't collapse it.
For latest Bootstrap, this is the correct answer.
$(document).click(function (event) {
var clickover = $(event.target);
var $navbar = $(".navbar-collapse");
var _opened = $navbar.hasClass("show");
if (_opened === true && !clickover.hasClass("navbar-toggler")) {
$navbar.collapse('hide');
}
});
It reads if .navbar-collapse has the word show in classes (which means menu is opened) and hides the navbar when you click/tap anywhere.
I've added a condition to #nozzleman's answer to check if the tap or click has been made on any element within the menu, and if that's the case, not to collapse it.
$(document).ready(function () {
$(document).click(function (event) {
var clickover = $(event.target);
var _opened = $(".navbar-collapse").hasClass("navbar-collapse in");
if (_opened === true && !clickover.hasClass("navbar-toggle") && clickover.parents('.navbar-collapse').length == 0) {
$("button.navbar-toggle").click();
}
});
});
The following code works for me and the advantage is that on small screens, it does not hide the .collapse when you click on its nav parent with .navbar .navbar-expand classes:
$(document).click(function (e) {
if($('.collapse').hasClass('show') && !$('nav').is(e.target) && $('nav').has(e.target).length === 0){
$('.navbar-toggler').click()
}
})
Vanilla Javascript.
Working on Bootstrap 5.2.
window.onload = function () {
document.addEventListener("click", function (event) {
// if the clicked element isn't child of the navbar, you must close it if is open
if (!event.target.closest("#navbar_id") && document.getElementById("navbarSupportedContent").classList.contains("show")) {
document.getElementById("hamburger_menu_button").click();
}
});
}
https://jsfiddle.net/j4tgpbxz/
You just need to add an id to the navbar element, and then check if the clicked element is a child of that same navbar and the content you want to hide (dropdown) is being shown.
For Bootstrap 4
Bootstrap 4 doesn't have an in class. This is Coffeescript.
$(document).click (e)->
#console.log e.target
unless $('#toggle-button').has(e.target).length || $('#toggle-menu').has(e.target).length
$('#toggle-menu').collapse('hide')
So basically, unless you click the button or the menu, close the menu.
Note: Strange, on iOS clicking on text doesn't register a click event, nor a mouseup event. Clicking on an image does fire events though.
For Bootstrap 4:
$(document).click(function(event) {
$(event.target).closest(".navbar").length || $(".navbar-collapse.show").length && $(".navbar-collapse.show").collapse("hide")
});
$(document).click(function (event) {
if ($('.navbar-collapse').attr('aria-expanded') == "true") {
$('.navbar-collapse:visible').click();
}
});
$(window).click(function (e) {
if ($(e.target).closest('.codehim-dropdown').length) {
return;
}
if ($(e.target).closest(offCanvas).length) {
return;
}
//check if menu really opened
if ($(hamburger).hasClass("active")) {
closeMenu();
}
$(dimOverlay).fadeOut();
$(".menu-items").slideUp();
$(".dropdown-heading").removeClass("active");
});
I had some problems with some answers here, and I would like to also be able to close the expanded menu on demand. So I did it with a simple function, and simulating the click.
function closeMenu(){
element = document.getElementById('nav_top');
if(element){
if(element.classList.contains('show')){
document.getElementById('navbar_toggler').dispatchEvent(new CustomEvent('click'));
}
}
}
$(document).ready(function () {
$(document).click(function (event) {
closeMenu();
});
});
Using this method you can close it when clicked outside, but also you can call the closeMenu() at any time from any other function.
I know its quite awhile for the answer. But I think the answer here could helps.
Lets say the condition: if user want to close the navbar when click outside but not when user click any element inside of the navbar
use the event.target and target the element's closest classname whether its has the navbar class or not. If yes which means user is clicking element inside of the navbar and not to close the navbar.
$(function() {
$(document).click(function (event) {
var clickover = $(event.target);
var _opened = $(".navbar-collapse").hasClass("navbar-collapse collapse show");
if (_opened === true && clickover.closest('.navbar').length === 0) {
$(".navbar-collapse").collapse('hide');
}
});
});
I'm currently using popovers with Twitter Bootstrap, initiated like this:
$('.popup-marker').popover({
html: true,
trigger: 'manual'
}).click(function(e) {
$(this).popover('toggle');
e.preventDefault();
});
As you can see, they're triggered manually, and clicking on .popup-marker (which is a div with a background image) toggles a popover. This works great, but I'd like to also be able to close the popover with a click anywhere else on the page (but not on the popover itself!).
I've tried a few different things, including the following, but with no results to show for it:
$('body').click(function(e) {
$('.popup-marker').popover('hide');
});
How can I close the popover with a click anywhere else on the page, but not with a click onthe popover itself?
Presuming that only one popover can be visible at any time, you can use a set of flags to mark when there's a popover visible, and only then hide them.
If you set the event listener on the document body, it will trigger when you click the element marked with 'popup-marker'. So you'll have to call stopPropagation() on the event object. And apply the same trick when clicking on the popover itself.
Below is a working JavaScript code that does this. It uses jQuery >= 1.7
jQuery(function() {
var isVisible = false;
var hideAllPopovers = function() {
$('.popup-marker').each(function() {
$(this).popover('hide');
});
};
$('.popup-marker').popover({
html: true,
trigger: 'manual'
}).on('click', function(e) {
// if any other popovers are visible, hide them
if(isVisible) {
hideAllPopovers();
}
$(this).popover('show');
// handle clicking on the popover itself
$('.popover').off('click').on('click', function(e) {
e.stopPropagation(); // prevent event for bubbling up => will not get caught with document.onclick
});
isVisible = true;
e.stopPropagation();
});
$(document).on('click', function(e) {
hideAllPopovers();
isVisible = false;
});
});
http://jsfiddle.net/AFffL/539/
The only caveat is that you won't be able to open 2 popovers at the same time. But I think that would be confusing for the user, anyway :-)
This is even easier :
$('html').click(function(e) {
$('.popup-marker').popover('hide');
});
$('.popup-marker').popover({
html: true,
trigger: 'manual'
}).click(function(e) {
$(this).popover('toggle');
e.stopPropagation();
});
I had a similar need, and found this great little extension of the Twitter Bootstrap Popover by Lee Carmichael, called BootstrapX - clickover. He also has some usage examples here. Basically it will change the popover into an interactive component which will close when you click elsewhere on the page, or on a close button within the popover. This will also allow multiple popovers open at once and a bunch of other nice features.
Plugin can be found here.
Usage example
<button rel="clickover" data-content="Show something here.
<button data-dismiss='clickover'
>Close Clickover</button>"
>Show clickover</button>
javascript:
// load click overs using 'rel' attribute
$('[rel="clickover"]').clickover();
The accepted solution gave me some issues (clicking on the '.popup-marker' element of the opened popover made the popovers not work afterwards). I came up with this other solution that works perfectly for me and it's quite simple (I'm using Bootstrap 2.3.1):
$('.popup-marker').popover({
html: true,
trigger: 'manual'
}).click(function(e) {
$('.popup-marker').not(this).popover('hide');
$(this).popover('toggle');
});
$(document).click(function(e) {
if (!$(e.target).is('.popup-marker, .popover-title, .popover-content')) {
$('.popup-marker').popover('hide');
}
});
UPDATE: This code works with Bootstrap 3 as well!
read "Dismiss on next click"
here http://getbootstrap.com/javascript/#popovers
You can use the focus trigger to dismiss popovers on the next click, but you have to use use the <a> tag, not the <button> tag, and you also must include a tabindex attribute...
Example:
<a href="#" tabindex="0" class="btn btn-lg btn-danger"
data-toggle="popover" data-trigger="focus" title="Dismissible popover"
data-content="And here's some amazing content. It's very engaging. Right?">
Dismissible popover
</a>
All of the existing answers are fairly weak, as they rely on capturing all document events then finding active popovers, or modifying the call to .popover().
A much better approach is to listen for show.bs.popover events on the document's body then react accordingly. Below is code which will close popovers when the document is clicked or esc is pressed, only binding event listeners when popovers are shown:
function closePopoversOnDocumentEvents() {
var visiblePopovers = [];
var $body = $("body");
function hideVisiblePopovers() {
$.each(visiblePopovers, function() {
$(this).popover("hide");
});
}
function onBodyClick(event) {
if (event.isDefaultPrevented())
return;
var $target = $(event.target);
if ($target.data("bs.popover"))
return;
if ($target.parents(".popover").length)
return;
hideVisiblePopovers();
}
function onBodyKeyup(event) {
if (event.isDefaultPrevented())
return;
if (event.keyCode != 27) // esc
return;
hideVisiblePopovers();
event.preventDefault();
}
function onPopoverShow(event) {
if (!visiblePopovers.length) {
$body.on("click", onBodyClick);
$body.on("keyup", onBodyKeyup);
}
visiblePopovers.push(event.target);
}
function onPopoverHide(event) {
var target = event.target;
var index = visiblePopovers.indexOf(target);
if (index > -1) {
visiblePopovers.splice(index, 1);
}
if (visiblePopovers.length == 0) {
$body.off("click", onBodyClick);
$body.off("keyup", onBodyKeyup);
}
}
$body.on("show.bs.popover", onPopoverShow);
$body.on("hide.bs.popover", onPopoverHide);
}
https://github.com/lecar-red/bootstrapx-clickover
It's an extension of twitter bootstrap popover and will solve the problem very simply.
For some reason none of the other solutions here worked for me. However, after a lot of troubleshooting, I finally arrived at this method which works perfectly (for me at least).
$('html').click(function(e) {
if( !$(e.target).parents().hasClass('popover') ) {
$('#popover_parent').popover('destroy');
}
});
In my case I was adding a popover to a table and absolutely positioning it above/below the td that was clicked. The table selection was handled by jQuery-UI Selectable so I'm not sure if that was interfering. However whenever I clicked inside the popover my click handler which targeted $('.popover') never worked and the event handling was always delegated to the $(html) click handler. I'm fairly new to JS so perhaps I'm just missing something?
Anyways I hope this helps someone!
I give all my popovers anchors the class activate_popover. I activate them all at once onload
$('body').popover({selector: '.activate-popover', html : true, container: 'body'})
to get the click away functionality working I use (in coffee script):
$(document).on('click', (e) ->
clickedOnActivate = ($(e.target).parents().hasClass("activate-popover") || $(e.target).hasClass("activate-popover"))
clickedAway = !($(e.target).parents().hasClass("popover") || $(e.target).hasClass("popover"))
if clickedAway && !clickedOnActivate
$(".popover.in").prev().popover('hide')
if clickedOnActivate
$(".popover.in").prev().each () ->
if !$(this).is($(e.target).closest('.activate-popover'))
$(this).popover('hide')
)
Which works perfectly fine with bootstrap 2.3.1
Even though there are a lot of solutions here, i'd like to propose mine as well, i don't know if there is some solution up there that solves it all, but i tried 3 of them and they had issues, like clicking on the popover it self makes it hide, others that if i had another popover buttons clicked both/multiple popovers would still appear (like in the selected solution), How ever, This one fixed it all
var curr_popover_btn = null;
// Hide popovers function
function hide_popovers(e)
{
var container = $(".popover.in");
if (!container.is(e.target) // if the target of the click isn't the container...
&& container.has(e.target).length === 0) // ... nor a descendant of the container
{
if( curr_popover_btn != null )
{
$(curr_popover_btn).popover('hide');
curr_popover_btn = null;
}
container.hide();
}
}
// Hide popovers when out of focus
$('html').click(function(e) {
hide_popovers(e);
});
$('.popover-marker').popover({
trigger: 'manual'
}).click(function(e) {
hide_popovers(e);
var $popover_btns = $('.popover-marker');
curr_popover_btn = this;
var $other_popover_btns = jQuery.grep($($popover_btns), function(popover_btn){
return ( popover_btn !== curr_popover_btn );
});
$($other_popover_btns).popover('hide');
$(this).popover('toggle');
e.stopPropagation();
});
I would set the focus to the newly created pop-over and remove it on blur. That way it's not needed to check which element of the DOM has been clicked and the pop-over can be clicked, and selected too: it will not lose its focus and will not disappear.
The code:
$('.popup-marker').popover({
html: true,
trigger: 'manual'
}).click(function(e) {
$(this).popover('toggle');
// set the focus on the popover itself
jQuery(".popover").attr("tabindex",-1).focus();
e.preventDefault();
});
// live event, will delete the popover by clicking any part of the page
$('body').on('blur','.popover',function(){
$('.popup-marker').popover('hide');
});
Here is the solution which worked very fine for me, if it can help :
/**
* Add the equals method to the jquery objects
*/
$.fn.equals = function(compareTo) {
if (!compareTo || this.length !== compareTo.length) {
return false;
}
for (var i = 0; i < this.length; ++i) {
if (this[i] !== compareTo[i]) {
return false;
}
}
return true;
};
/**
* Activate popover message for all concerned fields
*/
var popoverOpened = null;
$(function() {
$('span.btn').popover();
$('span.btn').unbind("click");
$('span.btn').bind("click", function(e) {
e.stopPropagation();
if($(this).equals(popoverOpened)) return;
if(popoverOpened !== null) {
popoverOpened.popover("hide");
}
$(this).popover('show');
popoverOpened = $(this);
e.preventDefault();
});
$(document).click(function(e) {
if(popoverOpened !== null) {
popoverOpened.popover("hide");
popoverOpened = null;
}
});
});
Here's my solution, for what it's worth:
// Listen for clicks or touches on the page
$("html").on("click.popover.data-api touchend.popover.data-api", function(e) {
// Loop through each popover on the page
$("[data-toggle=popover]").each(function() {
// Hide this popover if it's visible and if the user clicked outside of it
if ($(this).next('div.popover:visible').length && $(".popover").has(e.target).length === 0) {
$(this).popover("hide");
}
});
});
I had some problem to get it working on bootstrap 2.3.2.
But i sloved it this way:
$(function () {
$(document).mouseup(function (e) {
if(($('.popover').length > 0) && !$(e.target).hasClass('popInfo')) {
$('.popover').each(function(){
$(this).prev('.popInfo').popover('hide');
});
}
});
$('.popInfo').popover({
trigger: 'click',
html: true
});
});
tweaked #David Wolever solution slightly:
function closePopoversOnDocumentEvents() {
var visiblePopovers = [];
var $body = $("body");
function hideVisiblePopovers() {
/* this was giving problems and had a bit of overhead
$.each(visiblePopovers, function() {
$(this).popover("hide");
});
*/
while (visiblePopovers.length !== 0) {
$(visiblePopovers.pop()).popover("hide");
}
}
function onBodyClick(event) {
if (event.isDefaultPrevented())
return;
var $target = $(event.target);
if ($target.data("bs.popover"))
return;
if ($target.parents(".popover").length)
return;
hideVisiblePopovers();
}
function onBodyKeyup(event) {
if (event.isDefaultPrevented())
return;
if (event.keyCode != 27) // esc
return;
hideVisiblePopovers();
event.preventDefault();
}
function onPopoverShow(event) {
if (!visiblePopovers.length) {
$body.on("click", onBodyClick);
$body.on("keyup", onBodyKeyup);
}
visiblePopovers.push(event.target);
}
function onPopoverHide(event) {
var target = event.target;
var index = visiblePopovers.indexOf(target);
if (index > -1) {
visiblePopovers.splice(index, 1);
}
if (visiblePopovers.length == 0) {
$body.off("click", onBodyClick);
$body.off("keyup", onBodyKeyup);
}
}
$body.on("show.bs.popover", onPopoverShow);
$body.on("hide.bs.popover", onPopoverHide);
}
This question was also asked here and my answer provides not only a way to understand jQuery DOM traversal methods but 2 options for handling the closing of popovers by clicking outside.
Open multiple popovers at once or one popover at a time.
Plus these small code snippets can handle the closing of buttons containing icons!
https://stackoverflow.com/a/14857326/1060487
This one works like a charm and I use it.
It will open the popover when you click and if you click again it will close, also if you click outside of the popover the popover will be closed.
This also works with more than 1 popover.
function hideAllPopovers(){
$('[data-toggle="popover"]').each(function() {
if ($(this).data("showing") == "true"){
$(this).data("showing", "false");
$(this).popover('hide');
}
});
}
$('[data-toggle="popover"]').each(function() {
$(this).popover({
html: true,
trigger: 'manual'
}).click(function(e) {
if ($(this).data("showing") != "true"){
hideAllPopovers();
$(this).data("showing", "true");
$(this).popover('show');
}else{
hideAllPopovers();
}
e.stopPropagation();
});
});
$(document).click(function(e) {
hideAllPopovers();
});
Another solution, it covered the problem I had with clicking descendants of the popover:
$(document).mouseup(function (e) {
// The target is not popover or popover descendants
if (!$(".popover").is(e.target) && 0 === $(".popover").has(e.target).length) {
$("[data-toggle=popover]").popover('hide');
}
});
I do it as below
$("a[rel=popover]").click(function(event){
if(event.which == 1)
{
$thisPopOver = $(this);
$thisPopOver.popover('toggle');
$thisPopOver.parent("li").click(function(event){
event.stopPropagation();
$("html").click(function(){
$thisPopOver.popover('hide');
});
});
}
});
Hope this helps!
If you're trying to use twitter bootstrap popover with pjax, this worked for me:
App.Utils.Popover = {
enableAll: function() {
$('.pk-popover').popover(
{
trigger: 'click',
html : true,
container: 'body',
placement: 'right',
}
);
},
bindDocumentClickEvent: function(documentObj) {
$(documentObj).click(function(event) {
if( !$(event.target).hasClass('pk-popover') ) {
$('.pk-popover').popover('hide');
}
});
}
};
$(document).on('ready pjax:end', function() {
App.Utils.Popover.enableAll();
App.Utils.Popover.bindDocumentClickEvent(this);
});
#RayOnAir, I have same issue with previous solutions. I come close to #RayOnAir solution too. One thing that improved is close already opened popover when click on other popover marker. So my code is:
var clicked_popover_marker = null;
var popover_marker = '#pricing i';
$(popover_marker).popover({
html: true,
trigger: 'manual'
}).click(function (e) {
clicked_popover_marker = this;
$(popover_marker).not(clicked_popover_marker).popover('hide');
$(clicked_popover_marker).popover('toggle');
});
$(document).click(function (e) {
if (e.target != clicked_popover_marker) {
$(popover_marker).popover('hide');
clicked_popover_marker = null;
}
});
I found this to be a modified solution of pbaron's suggestion above, because his solution activated the popover('hide') on all elements with class 'popup-marker'. However, when you're using popover() for html content instead of the data-content, as I'm doing below, any clicks inside that html popup actually activate the popover('hide'), which promptly closes the window. This method below iterates through each .popup-marker element and discovers first if the parent is related to the .popup-marker's id that was clicked, and if so then does not hide it. All other divs are hidden...
$(function(){
$('html').click(function(e) {
// this is my departure from pbaron's code above
// $('.popup-marker').popover('hide');
$('.popup-marker').each(function() {
if ($(e.target).parents().children('.popup-marker').attr('id')!=($(this).attr('id'))) {
$(this).popover('hide');
}
});
});
$('.popup-marker').popover({
html: true,
// this is where I'm setting the html for content from a nearby hidden div with id="html-"+clicked_div_id
content: function() { return $('#html-'+$(this).attr('id')).html(); },
trigger: 'manual'
}).click(function(e) {
$(this).popover('toggle');
e.stopPropagation();
});
});
I came up with this:
My scenario included more popovers on the same page, and hiding them just made them invisible and because of that, clicking on items behind the popover was not possible.
The idea is to mark the specific popover-link as 'active' and then you can simply 'toggle' the active popover. Doing so will close the popover completely.
$('.popover-link').popover({ html : true, container: 'body' })
$('.popover-link').popover().on 'shown.bs.popover', ->
$(this).addClass('toggled')
$('.popover-link').popover().on 'hidden.bs.popover', ->
$(this).removeClass('toggled')
$("body").on "click", (e) ->
$openedPopoverLink = $(".popover-link.toggled")
if $openedPopoverLink.has(e.target).length == 0
$openedPopoverLink.popover "toggle"
$openedPopoverLink.removeClass "toggled"
I was trying to make a simple solution for a simple issue. Above posts are good but so complicated for a simple issue. So i made a simple thing. Just added a close button. Its perfect for me.
$(".popover-link").click(function(){
$(".mypopover").hide();
$(this).parent().find(".mypopover").show();
})
$('.close').click(function(){
$(this).parents('.mypopover').css('display','none');
});
<div class="popover-content">
<i class="fa fa-times close"></i>
<h3 class="popover-title">Title here</h3>
your other content here
</div>
.popover-content {
position:relative;
}
.close {
position:absolute;
color:#CCC;
right:5px;
top:5px;
cursor:pointer;
}
I like this, simple yet effective..
var openPopup;
$('[data-toggle="popover"]').on('click',function(){
if(openPopup){
$(openPopup).popover('hide');
}
openPopup=this;
});
Add btn-popover class to your popover button/link that opens the popover. This code will close the popovers when clicking outside of it.
$('body').on('click', function(event) {
if (!$(event.target).closest('.btn-popover, .popover').length) {
$('.popover').popover('hide');
}
});
An even easier solution, just iterate through all popovers and hide if not this.
$(document).on('click', '.popup-marker', function() {
$(this).popover('toggle')
})
$(document).bind('click touchstart', function(e) {
var target = $(e.target)[0];
$('.popup-marker').each(function () {
// hide any open popovers except for the one we've clicked
if (!$(this).is(target)) {
$(this).popover('hide');
}
});
});
$('.popForm').popover();
$('.conteneurPopForm').on("click",".fermePopover",function(){
$(".popForm").trigger("click");
});
To be clear, just trigger the popover
This should work in Bootstrap 4:
$("#my-popover-trigger").popover({
template: '<div class="popover my-popover-content" role="tooltip"><div class="arrow"></div><div class="popover-body"></div></div>',
trigger: "manual"
})
$(document).click(function(e) {
if ($(e.target).closest($("#my-popover-trigger")).length > 0) {
$("#my-popover-trigger").popover("toggle")
} else if (!$(e.target).closest($(".my-popover-content")).length > 0) {
$("#my-popover-trigger").popover("hide")
}
})
Explanation:
The first section inits the popover as per the docs: https://getbootstrap.com/docs/4.0/components/popovers/
The first "if" in the second section checks whether the clicked element is a descendant of #my-popover-trigger. If that is true, it toggles the popover (it handles the click on the trigger).
The second "if" in the second section checks whether the clicked element is a descendant of the popover content class which was defined in the init template. If it is not this means that the click was not inside the popover content, and the popover can be hidden.
Try data-trigger="focus" instead of "click".
This solved the problem for me.