jQuery / Sélecteur / :has() - Sélectionner les éléments contenant un élément correspondant

Avec jQuery, :has(selector) sélectionne les éléments contenant au moins un descendant correspondant au sélecteur. $("#menu > li:has(a)") renvoie les li contenant des liens, et non les a internes. Les descendants comprennent les enfants directs et les éléments plus profondément imbriqués : précisez la relation si seuls les enfants immédiats doivent compter.

Le conteneur et son contenu

La documentation de :has() décrit un filtre qui conserve les candidats possédant un descendant correspondant. Un lien placé dans un span reste un descendant du li et satisfait li:has(a). La seule classe has-link ne suffit pas : il faut un véritable élément a dans le li.

Expression Condition Éléments renvoyés
li:has(a) Un a à une profondeur quelconque Les li extérieurs correspondants
li:has(> a) Un a enfant direct Les li extérieurs correspondants
Sélectionner les li, puis .has("a") Un descendant a dans chaque li Un sous-ensemble des li sélectionnés
Sélectionner les li, puis .find("a") Rechercher les a dans les li Les a intérieurs

> indique la relation d'enfant direct. À l'extérieur, #menu > li définit les candidats. À l'intérieur de :has(), > a précise la relation à tester pour chacun. Distinguer ces deux rôles évite les correspondances inattendues dans une structure imbriquée.

Exemple complet : surligner les éléments contenant des liens

Enregistrez tout le code en UTF-8 dans has-demo.html, puis ouvrez ce fichier dans un navigateur. Aucun paquet n'est à installer ; une connexion internet est nécessaire pour charger la distribution officielle de jQuery depuis son réseau de diffusion de contenu, ou CDN. Le fichier utilise la version 4.0.0 ; les comportements présentés ont été exécutés et vérifiés avec 3.7.1 et 4.0.0.

plain ne contient pas de lien, direct possède un a enfant direct, nested contient un a dans un span, et self porte seulement la classe has-link. Les boutons comparent les relations et modifient le document.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Items that contain links</title>
  <style>
    body { font: 16px/1.6 sans-serif; margin: 24px; }
    #menu { list-style: none; padding: 0; max-width: 600px; }
    #menu > li { border: 2px solid #ddd; margin: 8px 0; padding: 12px; }
    #menu > li.marked { border-color: #c45f00; background: #fff5e7; }
    button { margin: 4px; padding: 8px; }
  </style>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
</head>
<body>
  <h1>Items that contain links</h1>
  <ul id="menu">
    <li id="plain">No link</li>
    <li id="direct"><a id="link-1" href="#example">Direct link</a></li>
    <li id="nested"><span><a id="link-2" href="#example">Nested link</a></span></li>
    <li id="self" class="has-link">Class name only</li>
  </ul>
  <button type="button" id="descendants">Any descendant link</button>
  <button type="button" id="children">Direct child link</button>
  <button type="button" id="add">Add a link to plain</button>
  <button type="button" id="reset">Reset</button>
  <p id="result" aria-live="polite"></p>
  <script>
    $(function () {
      function show($chosen) {
        $("#menu > li").removeClass("marked");
        $chosen.addClass("marked");
        const ids = $chosen.map((_, item) => item.id).get().join(", ");
        $("#result").text(`${ids || "none"} | ${$chosen.length}`);
      }
      function refresh() {
        show($("#menu > li:has(a)"));
      }
      $("#descendants").on("click", refresh);
      $("#children").on("click", () => show($("#menu > li:has(> a)")));
      $("#add").on("click", function () {
        if (!$("#added-link").length) {
          $("#plain").append('<a id="added-link" href="#example"> Added link</a>');
        }
        refresh();
      });
      $("#reset").on("click", function () {
        $("#added-link").remove();
        refresh();
      });
      refresh();
    });
  </script>
</body>
</html>

Au départ, les bordures extérieures de direct et nested sont mises en évidence. Le résultat est :

direct, nested | 2

Comparer les boutons et revenir à l'état initial

  1. Any descendant link accepte les liens à toute profondeur : direct, nested | 2.
  2. Direct child link exige un a immédiatement sous le li : direct | 1. Dans nested, le span est intercalé.
  3. Add a link to plain ajoute un lien et affiche plain, direct, nested | 3. La vérification de l'identifiant empêche un deuxième clic de dupliquer le lien.
  4. Reset supprime uniquement le lien ajouté et réapplique la condition sur tous les descendants. Le texte No link et les éléments d'origine restent présents ; deux entrées correspondent de nouveau.

show efface les anciennes marques avant d'appliquer les nouvelles. refresh interroge la structure actuelle à chaque appel. Cela évite de conserver une sélection périmée après l'ajout ou des marques devenues incorrectes après un changement de condition.

Comparer .has() et .find()

Si les li sont déjà sélectionnés, la méthode .has() peut conserver les candidats voulus. Juste après l'ouverture ou après Reset, exécutez ce fragment dans la console des outils de développement :

const ids = $set => $set.map((_, item) => item.id).get().join(", ");
console.log(ids($("#menu > li:has(a)")));
console.log(ids($("#menu > li").has("a")));
console.log(ids($("#menu > li").find("a")));
direct, nested
direct, nested
link-1, link-2

Les deux premières lignes donnent les identifiants des li extérieurs ; .find() renvoie ceux des a intérieurs. .has() filtre une sélection existante selon ses descendants ; elle ne remonte pas vers des parents inconnus. .filter() teste, elle, les candidats eux-mêmes.

.has() accepte une chaîne de sélecteur ou un élément précis du modèle objet du document, appelé DOM. Avec un élément également, seuls les candidats qui le contiennent comme descendant sont conservés. Le candidat lui-même ne compte pas comme son propre descendant ; sans correspondance interne, le résultat est vide.

Limiter la portée, refaire la sélection et distinguer CSS

Une expression non limitée comme $(":has(a)") peut sélectionner plusieurs ancêtres contenant des liens. Précisez le conteneur et les balises utiles. Une sélection n'est pas actualisée automatiquement : réinterrogez le document après l'ajout ou la suppression de descendants.

Selectors Level 4 définit aussi :has() en CSS. Il serait donc inexact de présenter cette écriture comme toujours exclusive à jQuery. Ici, nous manipulons des ensembles d'éléments par l'API jQuery. Pour comparer avec une règle CSS purement visuelle, vérifiez les navigateurs pris en charge et les conditions de sélection nécessaires.

En cas de problème, vérifiez la requête jQuery dans Network et les erreurs dans Console. Contrôlez parenthèses, guillemets, balises réelles et imbrication. La présence d'un véritable a compte, pas le mot « lien » dans le texte ni un nom de classe.