2723 votes

Comment détecter un clic à l'extérieur d'un élément ?

J'ai quelques menus HTML, que j'affiche complètement lorsqu'un utilisateur clique sur l'en-tête de ces menus. Je voudrais cacher ces éléments lorsque l'utilisateur clique en dehors de la zone des menus.

Est-ce que quelque chose comme cela est possible avec jQuery ?

$("#menuscontainer").clickOutsideThisElement(function() {
    // Hide the menus
});

47 votes

Voici un exemple de cette stratégie : jsfiddle.net/tedp/aL7Xe/1

21 votes

Comme Tom l'a mentionné, vous voudrez lire css-tricks.com/dangers-stopping-event-propagation avant d'utiliser cette approche. L'outil jsfiddle est cependant assez cool.

3 votes

Obtenir une référence à l'élément puis à event.target, et enfin != ou == les deux, puis exécuter le code en conséquence

1938voto

Eran Galperin Points 49594

Remarque : L'utilisation de stopPropagation est une chose à éviter car elle rompt le flux normal des événements dans le DOM. Voir cet article sur les astuces CSS pour plus d'informations. Pensez à utiliser cette méthode à la place.

Attachez un événement de clic au corps du document qui ferme la fenêtre. Attachez un événement de clic séparé au conteneur qui arrête la propagation vers le corps du document.

$(window).click(function() {
  //Hide the menus if visible
});

$('#menucontainer').click(function(event){
  event.stopPropagation();
});

27 votes

Je préfère lier le document à l'événement de clic, puis délier l'événement lorsque cela est nécessaire. C'est plus efficace.

773 votes

Cela rompt le comportement standard de nombreux éléments, y compris les boutons et les liens, contenus dans le #menucontainer. Je suis surpris que cette réponse soit si populaire.

6 votes

J'ai posté une solution alternative, qui ne casse pas son comportement. stackoverflow.com/questions/152975/

1521voto

Art Points 5151

Vous pouvez écouter un cliquez sur événement sur document et ensuite s'assurer #menucontainer n'est pas un ancêtre ou la cible de l'élément cliqué en utilisant la fonction .closest() .

Si ce n'est pas le cas, l'élément cliqué se trouve en dehors de l'espace de travail de l'utilisateur. #menucontainer et vous pouvez le cacher en toute sécurité.

$(document).click(function(event) { 
  var $target = $(event.target);
  if(!$target.closest('#menucontainer').length && 
  $('#menucontainer').is(":visible")) {
    $('#menucontainer').hide();
  }        
});

Modifier - 2017-06-23

Vous pouvez également nettoyer l'écouteur d'événements si vous prévoyez de rejeter le menu et si vous voulez arrêter d'écouter les événements. Cette fonction ne nettoiera que l'écouteur nouvellement créé, en préservant tous les autres écouteurs de clics sur le menu. document . Avec la syntaxe ES2015 :

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)
}

Modifier - 2018-03-11

Pour ceux qui ne veulent pas utiliser jQuery. Voici le code ci-dessus en 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 : Ceci est basé sur le commentaire d'Alex qui propose d'utiliser simplement !element.contains(event.target) au lieu de la partie jQuery.

Mais element.closest() est désormais disponible dans tous les principaux navigateurs (la version du W3C diffère légèrement de celle de jQuery). Les polyfills peuvent être trouvés ici : Element.closest()

Editer - 2020-05-21

Dans le cas où vous voulez que l'utilisateur puisse cliquer et glisser à l'intérieur de l'élément, puis relâcher la souris à l'extérieur de l'élément, sans fermer l'élément :

      ...
      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);

Et dans 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()
        }
    }

1 votes

Bien que votre méthode fonctionne aussi bien, votre déclaration est complètement erronée. #menucontainer est le niveau inférieur de la chaîne de propagation pour tous les éléments qu'il contient, il ne change donc rien à son comportement. Vous devriez l'essayer et voir par vous-même.

32 votes

J'ai essayé plusieurs des autres réponses, mais seule celle-ci a fonctionné. Merci. Le code que j'ai fini par utiliser est le suivant : $(document).click( function(event) { if( $(event.target).closest('.window').length == 0 ) { $('.window').fadeOut('fast') ; } } ) ;

42 votes

En fait, j'ai fini par opter pour cette solution parce qu'elle supporte mieux les menus multiples sur la même page, où le fait de cliquer sur un second menu alors que le premier est ouvert laissera le premier ouvert dans la solution stopPropagation.

367voto

zzzzBov Points 62084

Comment détecter un clic à l'extérieur d'un élément ?

La raison pour laquelle cette question est si populaire et a tant de réponses est qu'elle est faussement complexe. Après presque huit ans et des dizaines de réponses, je suis sincèrement surpris de voir le peu de soin apporté à l'accessibilité.

Je voudrais masquer ces éléments lorsque l'utilisateur clique en dehors de la zone des menus.

C'est une noble cause et c'est le réel question. Le titre de la question - qui est ce à quoi la plupart des réponses semblent tenter de répondre - contient un malheureux faux-fuyant.

Indice : c'est le mot "cliquer" !

Vous ne voulez pas vraiment lier les gestionnaires de clics.

Si vous liez les gestionnaires de clics pour fermer la boîte de dialogue, vous avez déjà échoué. La raison pour laquelle vous avez échoué est que tout le monde ne déclenche pas la fonction click événements. Les utilisateurs n'utilisant pas de souris pourront échapper à votre dialogue (et votre menu contextuel est sans doute un type de dialogue) en appuyant sur Tab et ils ne seront pas en mesure de lire le contenu de la boîte de dialogue sans déclencher un message d'erreur. click événement.

Alors reformulons la question.

Comment fermer une boîte de dialogue lorsque l'utilisateur en a fini avec elle ?

C'est l'objectif. Malheureusement, nous devons maintenant lier le userisfinishedwiththedialog et cette liaison n'est pas si simple.

Alors comment détecter qu'un utilisateur a fini d'utiliser un dialogue ?

focusout événement

Un bon début est de déterminer si le focus a quitté le dialogue.

Conseil : soyez prudent avec les blur événement, blur ne se propage pas si l'événement était lié à la phase de bouillonnement !

La méthode de jQuery focusout fera parfaitement l'affaire. Si vous ne pouvez pas utiliser jQuery, alors vous pouvez utiliser blur pendant la phase de capture :

element.addEventListener('blur', ..., true);
//                       use capture: ^^^^

En outre, pour de nombreuses boîtes de dialogue, vous devrez permettre au conteneur d'obtenir le focus. Ajouter tabindex="-1" pour permettre à la boîte de dialogue de recevoir le focus de manière dynamique sans interrompre le flux de tabulation.

$('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>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
  Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>

Si vous jouez avec cette démo pendant plus d'une minute, vous devriez rapidement commencer à voir des problèmes.

La première est que le lien dans la boîte de dialogue n'est pas cliquable. Si vous tentez de cliquer dessus ou d'utiliser la tabulation, la boîte de dialogue se ferme avant que l'interaction n'ait lieu. Cela est dû au fait que la mise au point de l'élément intérieur déclenche une action focusout avant de déclencher un focusin l'événement à nouveau.

La solution consiste à mettre en file d'attente le changement d'état dans la boucle d'événement. Cela peut être fait en utilisant setImmediate(...) ou setTimeout(..., 0) pour les navigateurs qui ne prennent pas en charge setImmediate . Une fois mis en file d'attente, il peut être annulé par une nouvelle demande d'accès. 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>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
  Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>

Le deuxième problème est que la boîte de dialogue ne se ferme pas lorsque l'on appuie à nouveau sur le lien. Cela est dû au fait que la boîte de dialogue perd le focus, ce qui déclenche le comportement de fermeture, après quoi le clic sur le lien déclenche la réouverture de la boîte de dialogue.

Comme pour le problème précédent, il faut gérer l'état du focus. Étant donné que le changement d'état a déjà été mis en file d'attente, il s'agit simplement de gérer les événements de focus sur les déclencheurs de dialogue :

Cela devrait vous sembler familier

$('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>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
  Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>

Esc clé

Si vous pensiez en avoir fini avec la gestion des états du focus, vous pouvez faire plus pour simplifier l'expérience de l'utilisateur.

Il s'agit souvent d'une fonctionnalité "agréable à avoir", mais il est courant, lorsque vous avez une modale ou une fenêtre contextuelle de quelque sorte que ce soit, que l'icône de l'outil de gestion des données soit utilisée. Esc La clef de voûte va le fermer.

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>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
  Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>

Si vous savez que vous avez des éléments focalisables dans la boîte de dialogue, vous n'aurez pas besoin de focaliser la boîte de dialogue directement. Si vous créez un menu, vous pouvez plutôt cibler le premier élément du menu.

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 <a href="http://example.com/">dolor</a> sit amet.

Rôles WAI-ARIA et autres supports d'accessibilité

Cette réponse couvre, nous l'espérons, les bases de la prise en charge de l'accessibilité du clavier et de la souris pour cette fonctionnalité, mais comme elle est déjà assez importante, je vais éviter toute discussion sur les éléments suivants Rôles et attributs WAI-ARIA Cependant, je hautement Nous recommandons aux responsables de la mise en œuvre de se référer à la spécification pour obtenir des détails sur les rôles qu'ils doivent utiliser et tout autre attribut approprié.

159voto

Dennis Points 1073

Les autres solutions proposées ici n'ont pas fonctionné pour moi, j'ai donc dû les utiliser :

if(!$(event.target).is('#foo'))
{
    // hide menu
}

Editer : variante du Javascript simple (2021-03-31)

J'ai utilisé cette méthode pour gérer la fermeture d'un menu déroulant lorsque l'on clique en dehors de celui-ci.

Tout d'abord, j'ai créé un nom de classe personnalisé pour tous les éléments du composant. Ce nom de classe sera ajouté à tous les éléments qui composent le widget de menu.

const className = `dropdown-${Date.now()}-${Math.random() * 100}`;

Je crée une fonction pour vérifier les clics et le nom de la classe de l'élément cliqué. Si l'élément cliqué ne contient pas le nom de classe personnalisé que j'ai généré ci-dessus, il doit définir l'attribut show pour false et le menu se ferme.

const onClickOutside = (e) => {
  if (!e.target.className.includes(className)) {
    show = false;
  }
};

Puis j'ai attaché le gestionnaire de clic à l'objet fenêtre.

// add when widget loads
window.addEventListener("click", onClickOutside);

... et enfin un peu de ménage

// remove listener when destroying the widget
window.removeEventListener("click", onClickOutside);

0 votes

J'ai publié un autre exemple pratique de l'utilisation de event.target pour éviter de déclencher d'autres gestionnaires html de clics extérieurs de widgets Jquery UI lorsque vous les intégrez dans votre propre boîte pop-over : La meilleure façon d'obtenir l'Original Target

45 votes

Cela a fonctionné pour moi, sauf que j'ai ajouté && !$(event.target).parents("#foo").is("#foo") à l'intérieur de la IF afin que les éléments enfants ne ferment pas le menu lorsqu'ils sont cliqués.

0 votes

@Frug Votre réponse peut être valable donc vous avez obtenu 5, mais j'ai été confus par votre réponse. et qui est MBJ. Je cherchais à créer une meilleure solution basée sur votre réponse.

132voto

Joe Lencioni Points 4642

J'ai une application qui fonctionne de manière similaire à l'exemple d'Eran, sauf que j'attache l'événement de clic au corps lorsque j'ouvre le menu... Un peu comme ceci :

$('#menucontainer').click(function(event) {
  $('html').one('click',function() {
    // Hide the menus
  });

  event.stopPropagation();
});

Plus d'informations sur La méthode de jQuery one() fonction

10 votes

Mais si vous cliquez sur le menu lui-même, puis en dehors, cela ne fonctionnera pas :)

3 votes

Il est utile de placer event.stopProgagantion() avant de lier l'écouteur de clic au corps.

4 votes

Le problème est que "un" s'applique à la méthode jQuery qui consiste à ajouter plusieurs fois des événements à un tableau. Ainsi, si vous cliquez sur le menu pour l'ouvrir plusieurs fois, l'événement est à nouveau lié au corps et tente de masquer le menu plusieurs fois. Il convient d'appliquer un dispositif de sécurité pour résoudre ce problème.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X