19 octobre 2023

Selection et Range

Dans ce chapitre, nous aborderons la sélection dans le document, ainsi que la sélection dans les champs de formulaire, tels que <input>.

JavaScript peut accéder à une sélection existante, sélectionner/désélectionner des nœuds du DOM dans leur ensemble ou partiellement, supprimer le contenu sélectionné du document, l’envelopper dans une balise, etc.

Vous pouvez trouver quelques recettes de tâches courantes à la fin du chapitre, dans la section “Résumé”. Peut-être que cela couvre vos besoins actuels, mais vous obtiendrez beaucoup plus si vous lisez le texte en entier.

Les objets Range et Selection sont faciles à comprendre, et vous n’aurez besoin d’aucune recette pour les faire faire ce que vous voulez.

Range

Le concept de base de la sélection est la plage (Range), qui est essentiellement une paire de “points limites”: le début et la fin de la plage.

Un objet Range est créé sans paramètres :

let range = new Range();

Ensuite, nous pouvons définir les limites de la sélection en utilisant range.setStart(node, offset) et range.setEnd(node, offset).

Comme vous pouvez le deviner, nous allons utiliser les objets Range pour la sélection, mais créons d’abord quelques-uns de ces objets.

Sélection partielle du texte

La chose intéressante est que le premier argument node dans les deux méthodes peut être soit un noeud de texte ou un noeud d’élément, et la signification du deuxième argument dépend de cela.

Si node est un noeud de texte, alors offset doit être la position dans son texte.

Par exemple, étant donné l’élément <p>Hello</p>, nous pouvons créer la plage contenant les lettres “ll” comme suit :

<p id="p">Hello</p>
<script>
  let range = new Range();
  range.setStart(p.firstChild, 2);
  range.setEnd(p.firstChild, 4);

  // toString d'une plage renvoie son contenu sous forme de texte
  console.log(range); // ll
</script>

Ici, nous prenons le premier enfant de <p> (c’est le noeud texte) et nous spécifions les positions du texte à l’intérieur de celui-ci :

Sélection des noeuds d’éléments

Alternativement, si node est un noeud d’élément, alors offset doit être le numéro de l’enfant.

C’est pratique pour faire des plages qui contiennent les noeuds dans leur ensemble, et non pas s’arrêter quelque part dans leur texte.

Par exemple, nous avons un fragment de document plus complexe :

<p id="p">Example: <i>italic</i> and <b>bold</b></p>

Voici sa structure DOM avec les nœuds d’élément et de texte :

Créons une plage pour "Example: <i>italic</i>".

Comme nous pouvons le voir, cette phrase est composée d’exactement deux enfants de <p>, avec les indices 0 et 1 :

  • Le point de départ a <p> comme node parent, et 0 comme décalage (offset).

    On peut donc le définir comme range.setStart(p, 0).

  • Le point de fin a aussi <p> comme node parent, mais 2 comme décalage (il spécifie la plage jusqu’à, mais sans inclure offset).

    On peut donc le définir comme range.setEnd(p, 2).

Voici la démo. Si vous l’exécutez, vous pouvez voir que le texte est sélectionné :

<p id="p">Example: <i>italic</i> and <b>bold</b></p>

<script>
  let range = new Range();

  range.setStart(p, 0);
  range.setEnd(p, 2);

  // toString d'une plage renvoie son contenu sous forme de texte, sans balises
  console.log(range); // Example: italic

  // appliquer cette plage pour la sélection du document (expliqué plus loin)
  document.getSelection().addRange(range);
</script>

Voici un banc d’essai plus flexible dans lequel vous pouvez définir des valeurs de début et de fin de plage et explorer d’autres variantes :

<p id="p">Example: <i>italic</i> and <b>bold</b></p>

From <input id="start" type="number" value=1> – To <input id="end" type="number" value=4>
<button id="button">Click to select</button>
<script>
  button.onclick = () => {
    let range = new Range();

    range.setStart(p, start.value);
    range.setEnd(p, end.value);

    // appliquer la sélection, expliquée plus loin
    document.getSelection().removeAllRanges();
    document.getSelection().addRange(range);
  };
</script>

E.g. en sélectionnant dans le même <p> de l’offset 1 à 4 on obtient <i>italic</i> and <b>bold</b>:

Les nœuds de début et de fin peuvent être différents

Nous ne sommes pas obligés d’utiliser le même noeud dans setStart et setEnd. Une plage peut s’étendre sur de nombreux noeuds non liés. Il est seulement important que la fin soit après le début dans le document.

Sélection d’un plus grand fragment

Faisons une sélection plus grande dans notre exemple, comme ceci :

Nous savons déjà comment faire. Nous devons juste définir le début et la fin comme un offset relatif dans les nœuds de texte.

Nous devons créer une plage, qui :

  • commence à la position 2 dans <p> firstChild (en prenant toutes les lettres sauf les deux premières de "Example: ").
  • se termine à la position 3 dans <b> firstChild (en prenant les trois premières lettres de “bold”, mais pas plus) :
<p id="p">Example: <i>italic</i> and <b>bold</b></p>

<script>
  let range = new Range();

  range.setStart(p.firstChild, 2);
  range.setEnd(p.querySelector('b').firstChild, 3);

  console.log(range); // ample: italic and bol

  // utiliser cette plage pour la sélection (expliqué plus loin)
  window.getSelection().addRange(range);
</script>

Comme vous pouvez le voir, il est assez facile de créer une plage de ce que l’on veut.

Si nous voulons prendre les noeuds dans leur ensemble, nous pouvons passer des éléments dans setStart/setEnd. Sinon, nous pouvons travailler au niveau du texte.

Propriétés de la plage

L’objet range que nous avons créé dans l’exemple ci-dessus a les propriétés suivantes :

  • startContainer, startOffset – nœud et offset du début,
    • dans l’exemple ci-dessus : premier noeud de texte à l’intérieur de <p> et 2.
  • endContainer, endOffset – noeud et offset de la fin,
    • dans l’exemple ci-dessus : premier noeud de texte dans <b> et 3.
  • collapsed – booléen, true si la plage commence et se termine sur le même point (donc il n’y a pas de contenu à l’intérieur de la plage),
    • dans l’exemple ci-dessus : false.
  • commonAncestorContainer – l’ancêtre commun le plus proche de tous les noeuds de la plage,
    • dans l’exemple ci-dessus : <p>.

Méthodes de sélection de plages

Il existe de nombreuses méthodes pratiques pour manipuler les plages.

Nous avons déjà vu setStart et setEnd, voici d’autres méthodes similaires.

Définir le début de la plage :

  • setStart(node, offset) définit le début à : position offset dans node.
  • setStartBefore(node) définit le début à : juste avant node.
  • setStartAfter(node) définit le début à : juste après node.

Définir la fin de la plage (méthodes similaires) :

  • setEnd(node, offset) définit la fin à : position offset dans node.
  • setEndBefore(node) définit la fin à : juste avant node.
  • setEndAfter(node) définit la fin à : juste après node.