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.

Techniquement, setStart/setEnd peuvent faire n’importe quoi, mais plus de mĂ©thodes fournissent plus de commoditĂ©.

Dans toutes ces mĂ©thodes, node peut ĂȘtre un noeud de texte ou d’élĂ©ment : pour les noeuds de texte, offset saute autant de caractĂšres, tandis que pour les noeuds d’élĂ©ments, autant de noeuds enfants.

Encore plus de méthodes pour créer des plages :

  • selectNode(node) dĂ©finit la plage pour sĂ©lectionner le node entier.
  • selectNodeContents(node) sĂ©lectionne le contenu du node dans son intĂ©gralitĂ©.
  • collapse(toStart) si toStart=true, dĂ©finissez end=start, sinon dĂ©finissez start=end, ce qui rĂ©duit la plage de sĂ©lection.
  • cloneRange() crĂ©e une nouvelle plage avec le mĂȘme dĂ©but et la mĂȘme fin.

MĂ©thodes d’édition des plages

Une fois que la plage est créée, nous pouvons manipuler son contenu en utilisant ces méthodes :

  • deleteContents() – supprime le contenu de la plage du document.
  • extractContents() – supprime le contenu de la plage du document et le retourne en tant que DocumentFragment.
  • cloneContents() – clone le contenu d’une plage et le renvoie en tant que DocumentFragment.
  • insertNode(node) – InsĂšre node dans le document au dĂ©but de la plage.
  • surroundContents(node) – entoure node du contenu de la plage. Pour que cela fonctionne, la plage doit contenir Ă  la fois des balises d’ouverture et de fermeture pour tous les Ă©lĂ©ments qu’elle contient : pas de plages partielles comme <i>abc.

Avec ces mĂ©thodes, nous pouvons faire pratiquement n’importe quoi avec les noeuds sĂ©lectionnĂ©s.

Voici le banc d’essai pour les voir en action :

Cliquez sur les boutons pour exécuter des méthodes sur la sélection, "resetExample" pour la réinitialiser.

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

<p id="result"></p>
<script>
  let range = new Range();

  // Chaque méthode démontrée est représentée ici :
  let methods = {
    deleteContents() {
      range.deleteContents()
    },
    extractContents() {
      let content = range.extractContents();
      result.innerHTML = "";
      result.append("extracted: ", content);
    },
    cloneContents() {
      let content = range.cloneContents();
      result.innerHTML = "";
      result.append("cloned: ", content);
    },
    insertNode() {
      let newNode = document.createElement('u');
      newNode.innerHTML = "NEW NODE";
      range.insertNode(newNode);
    },
    surroundContents() {
      let newNode = document.createElement('u');
      try {
        range.surroundContents(newNode);
      } catch(e) { console.log(e) }
    },
    resetExample() {
      p.innerHTML = `Example: <i>italic</i> and <b>bold</b>`;
      result.innerHTML = "";

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

      window.getSelection().removeAllRanges();
      window.getSelection().addRange(range);
    }
  };

  for(let method in methods) {
    document.write(`<div><button onclick="methods.${method}()">${method}</button></div>`);
  }

  methods.resetExample();
</script>

Il existe également des méthodes permettant de comparer des plages, mais elles sont rarement utilisées. Si vous en avez besoin, veuillez vous reporter à la spec ou au manuel MDN.

Sélection

Range est un objet gĂ©nĂ©rique pour gĂ©rer les plages de sĂ©lection. Bien que la crĂ©ation d’une Range ne signifie pas que nous voyons une sĂ©lection Ă  l’écran.

Nous pouvons crĂ©er des objets Range, les faire circuler – ils ne sĂ©lectionnent rien visuellement par eux-mĂȘmes.

La sĂ©lection du document est reprĂ©sentĂ©e par un objet Selection, qui peut ĂȘtre obtenu par window.getSelection() ou document.getSelection(). Une sĂ©lection peut inclure zĂ©ro ou plusieurs plages. C’est du moins ce que dit la spĂ©cification de Selection API. En pratique cependant, seul Firefox permet de sĂ©lectionner plusieurs plages dans le document en utilisant Ctrl+click (Cmd+click pour Mac).

Voici une capture d’écran d’une sĂ©lection avec 3 plages, rĂ©alisĂ©e dans Firefox :

Image "/article/selection-range/s%C3%A9lection-firefox.svg" non trouvée

Les autres navigateurs prennent en charge au maximum 1 plage. Comme nous allons le voir, certaines mĂ©thodes de Selection impliquent qu’il peut y avoir plusieurs plages, mais lĂ  encore, dans tous les navigateurs sauf Firefox, il y a au maximum 1 plage.

Voici une petite démo qui montre la sélection actuelle (sélectionner quelque chose et cliquer) sous forme de texte :

Propriétés de Selection

Comme nous l’avons dit, une sĂ©lection peut en thĂ©orie contenir plusieurs plages. Nous pouvons obtenir ces objets range en utilisant la mĂ©thode :

  • getRangeAt(i) – obtient la i-iĂšme plage, en commençant par 0. Dans tous les navigateurs sauf Firefox, seul 0 est utilisĂ©.

Il existe également des propriétés qui sont souvent plus pratiques.

Comme pour une plage, un objet selection a un dĂ©but, appelĂ© “anchor”, et une fin, appelĂ©e “focus”.

Les principales propriétés de selection sont :

  • anchorNode – le noeud oĂč la sĂ©lection commence.
  • anchorOffset – le dĂ©calage (offset) dans anchorNode oĂč la sĂ©lection commence.
  • focusNode – le noeud oĂč la sĂ©lection se termine.
  • focusOffset – le dĂ©calage dans focusNode oĂč la sĂ©lection se termine.
  • isCollapsed – true si la sĂ©lection ne sĂ©lectionne rien (plage vide), ou si elle n’existe pas.
  • rangeCount – nombre de plages dans la sĂ©lection, maximum 1 dans tous les navigateurs sauf Firefox.
end/start de selection vs Range

Il y a une diffĂ©rence importante entre anchor/focus d’une sĂ©lection et start/end d’un objet Range.

Comme nous le savons, les objets Range ont toujours leur début avant leur fin.

Pour les sĂ©lections, ce n’est pas toujours le cas.

La sĂ©lection d’un objet avec une souris peut se faire dans les deux sens : de gauche Ă  droite ou de droite Ă  gauche.

En d’autres termes, lorsque le bouton de la souris est enfoncĂ©, puis qu’il avance dans le document, sa fin (focus) se trouvera aprĂšs son dĂ©but (anchor).

E.g. si l’utilisateur commence Ă  sĂ©lectionner avec la souris et passe de “Example” Ă  “italic” :


Mais la mĂȘme sĂ©lection pourrait ĂȘtre faite en sens inverse : en partant de “italic” vers “Example” (sens inverse), alors sa fin (focus) sera avant le dĂ©but (anchor) :

ÉvĂ©nements de sĂ©lection

Il y a des événements pour suivre la trace de la sélection :

  • elem.onselectstart – quand une sĂ©lection dĂ©bute spĂ©cifiquement sur l’élĂ©ment elem (ou Ă  l’intĂ©rieur de celui-ci). Par exemple, lorsque l’utilisateur appuie sur le bouton de la souris sur cet Ă©lĂ©ment et commence Ă  dĂ©placer le pointeur.
    • En empĂȘchant l’action par dĂ©faut, on annule le dĂ©marrage de la sĂ©lection. Le dĂ©marrage d’une sĂ©lection Ă  partir de cet Ă©lĂ©ment devient donc impossible, mais l’élĂ©ment reste tout de mĂȘme sĂ©lectionnable. Le visiteur doit simplement dĂ©marrer la sĂ©lection Ă  partir d’un autre endroit.
  • document.onselectionchange – quand une sĂ©lection change ou commence.
    • Attention : ce gestionnaire ne peut ĂȘtre dĂ©fini que sur document, il suit toutes les sĂ©lections qu’il contient.

Démonstration du suivi des sélections

Voici une petite démo. Elle suit la sélection courante sur le document et montre ses limites :

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

From <input id="from" disabled> – To <input id="to" disabled>
<script>
  document.onselectionchange = function() {
    let selection = document.getSelection();

    let {anchorNode, anchorOffset, focusNode, focusOffset} = selection;

    // anchorNode et focusNode sont des nƓuds de texte habituellement
    from.value = `${anchorNode?.data}, offset ${anchorOffset}`;
    to.value = `${focusNode?.data}, offset ${focusOffset}`;
  };
</script>

DĂ©monstration de la copie d’une sĂ©lection

Il existe deux approches pour copier le contenu sélectionné :

  1. Nous pouvons utiliser document.getSelection().toString() pour le récupérer sous forme de texte.
  2. Sinon, pour copier le DOM complet, par exemple si nous devons garder le formatage, nous pouvons obtenir les plages sous-jacentes avec getRangesAt(...). Un objet Range, à son tour, a la méthode cloneContents() qui clone son contenu et retourne un objet DocumentFragment, que nous pouvons insérer ailleurs.

Voici la démonstration de la copie du contenu sélectionné à la fois comme texte et comme noeuds DOM :

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

Cloned: <span id="cloned"></span>
<br>
As text: <span id="astext"></span>

<script>
  document.onselectionchange = function() {
    let selection = document.getSelection();

    cloned.innerHTML = astext.innerHTML = "";

    // Cloner les noeuds du DOM Ă  partir de plages (nous supportons le multiselect ici)
    for (let i = 0; i < selection.rangeCount; i++) {
      cloned.append(selection.getRangeAt(i).cloneContents());
    }

    // Obtenir sous forme de texte
    astext.innerHTML += selection;
  };
</script>

Méthodes de sélection

Nous pouvons travailler avec la sélection en ajoutant/supprimant (add/remove) des plages :

  • getRangeAt(i) – obtient la i-iĂšme plage, en commençant par 0. Dans tous les navigateurs sauf Firefox, seul 0 est utilisĂ©.
  • addRange(range) – ajoute range Ă  la sĂ©lection. Tous les navigateurs, Ă  l’exception de Firefox, ignorent l’appel, si la sĂ©lection a dĂ©jĂ  une plage associĂ©e.
  • removeRange(range) – supprime range de la sĂ©lection.
  • removeAllRanges() – supprime toutes les plages.
  • empty() – alias de removeAllRanges.

Il y a aussi des méthodes pratiques pour manipuler directement la plage de sélection, sans appels intermédiaires à Range :

  • collapse(node, offset) – remplace la plage sĂ©lectionnĂ©e par une nouvelle qui commence et se termine au node donnĂ©, Ă  la position offset.
  • setPosition(node, offset) – alias de collapse.
  • collapseToStart() – rĂ©duit (remplace par une plage vide) au dĂ©but de la sĂ©lection.
  • collapseToEnd() – rĂ©duit Ă  la fin de la sĂ©lection.
  • extend(node, offset) – dĂ©place le focus de la sĂ©lection vers le node et la position offset donnĂ©s.
  • setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset) – remplace la plage de sĂ©lection avec les valeurs de dĂ©but anchorNode/anchorOffset et de fin focusNode/focusOffset. Tout le contenu situĂ© entre les deux est sĂ©lectionnĂ©.
  • selectAllChildren(node) – sĂ©lectionne tous les enfants du node.
  • deleteFromDocument() – supprime le contenu sĂ©lectionnĂ© du document.
  • containsNode(node, allowPartialContainment = false) – vĂ©rifie si la sĂ©lection contient node (partiellement si le second argument est true).

Pour la plupart des tĂąches, ces mĂ©thodes sont suffisantes, il n’y a pas besoin d’accĂ©der Ă  l’objet Range sous-jacent.

Par exemple, sélectionner le contenu entier du paragraphe <p> :

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

<script>
  // sélectionnez du 0-Úme enfant de <p> au dernier element fils
  document.getSelection().setBaseAndExtent(p, 0, p, p.childNodes.length);
</script>

La mĂȘme chose en utilisant Range :

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

<script>
  let range = new Range();
  range.selectNodeContents(p); // ou selectNode(p) pour sélectionner également la balise <p>

  document.getSelection().removeAllRanges(); // effacer la sélection existante s'il y en a une
  document.getSelection().addRange(range);
</script>
Pour sĂ©lectionner quelque chose, il faut d’abord supprimer la sĂ©lection existante

Si une sĂ©lection du document existe dĂ©jĂ , videz-la d’abord avec removeAllRanges(). Puis ajoutez des plages. Sinon, tous les navigateurs, sauf Firefox, ignorent les nouvelles plages.

L’exception est certaines mĂ©thodes de sĂ©lection, qui remplacent la sĂ©lection existante, comme setBaseAndExtent.

Sélection dans les contrÎles de formulaires

Les Ă©lĂ©ments de formulaire, tels que input et textarea fournissent une API spĂ©ciale pour la sĂ©lection, sans objets Selection ou Range. Comme une valeur d’entrĂ©e est un texte pur, pas du HTML, il n’y a pas besoin de tels objets, tout est beaucoup plus simple.

Propriétés :

  • input.selectionStart – position du dĂ©but de la sĂ©lection (accessible en Ă©criture).
  • input.selectionEnd – position de la fin de la sĂ©lection (accessible en Ă©criture).
  • input.selectionDirection – direction de la sĂ©lection, une parmi : “forward”, “backward” ou “none” (si, par exemple, la sĂ©lection est effectuĂ©e par un double clic de souris).

les événements :

  • input.onselect – se dĂ©clenche lorsque quelque chose est sĂ©lectionnĂ©.

Méthodes :

  • input.select() – sĂ©lectionne tout dans le contrĂŽle de texte (peut ĂȘtre textarea au lieu de input).

  • input.setSelectionRange(start, end, [direction]) – modifie la sĂ©lection pour qu’elle s’étende de la position start Ă  end, dans la direction donnĂ©e (facultatif).

  • input.setRangeText(replacement, [start], [end], [selectionMode]) – remplace une plage de texte par le nouveau texte.

    Les arguments facultatifs start et end, s’ils sont fournis, dĂ©finissent le dĂ©but et la fin de la plage, sinon la sĂ©lection de l’utilisateur est utilisĂ©e.

    Le dernier argument, selectionMode, détermine comment la sélection sera définie aprÚs que le texte ait été remplacé. Les valeurs possibles sont :

    • "select" – le texte nouvellement insĂ©rĂ© sera sĂ©lectionnĂ©.
    • "start" – la plage de sĂ©lection se rĂ©duit juste avant le texte insĂ©rĂ© (le curseur sera immĂ©diatement devant).
    • "end" – la plage de sĂ©lection se rĂ©duit juste aprĂšs le texte insĂ©rĂ© (le curseur sera juste aprĂšs).
    • "preserve" – tente de prĂ©server la sĂ©lection. C’est la valeur par dĂ©faut.

Voyons maintenant ces méthodes en action.

Exemple : suivi de sélection

Par exemple, ce code utilise l’évĂ©nement onselect pour suivre la sĂ©lection :

<textarea id="area" style="width:80%;height:60px">
Selecting in this text updates values below.
</textarea>
<br>
From <input id="from" disabled> – To <input id="to" disabled>

<script>
  area.onselect = function() {
    from.value = area.selectionStart;
    to.value = area.selectionEnd;
  };
</script>

Veuillez noter :

  • onselect se dĂ©clenche lorsque quelque chose est sĂ©lectionnĂ©, mais pas lorsque la sĂ©lection est supprimĂ©e.
  • L’évĂ©nement document.onselectionchange ne devrait pas se dĂ©clencher pour les sĂ©lections Ă  l’intĂ©rieur d’un contrĂŽle de formulaire, selon la spec, car il n’est pas liĂ© Ă  la sĂ©lection et aux plages du document. Certains navigateurs le gĂ©nĂšrent, mais il ne faut pas s’y fier.

Exemple : déplacement du curseur

Nous pouvons changer selectionStart et selectionEnd, cela définit la sélection.

Un cas particulier important est celui oĂč selectionStart et selectionEnd sont Ă©gaux. Dans ce cas, il s’agit exactement de la position du curseur. Ou, pour le dire autrement, lorsque rien n’est sĂ©lectionnĂ©, la sĂ©lection est rĂ©duite Ă  la position du curseur.

Donc, en mettant selectionStart et selectionEnd Ă  la mĂȘme valeur, on dĂ©place le curseur.

Par exemple :

<textarea id="area" style="width:80%;height:60px">
Focus on me, the cursor will be at position 10.
</textarea>

<script>
  area.onfocus = () => {
    // setTimeout de délai zéro à exécuter aprÚs la fin de l'action "focus" du navigateur
    setTimeout(() => {
      // nous pouvons définir n'importe quelle sélection
      // si start=end, le curseur se trouve exactement Ă  cet endroit
      area.selectionStart = area.selectionEnd = 10;
    });
  };
</script>

Exemple : modifier la sélection

Pour modifier le contenu de la sélection, on peut utiliser la méthode input.setRangeText(). Bien sûr, nous pouvons lire selectionStart/End et, avec la connaissance de la sélection, modifier la chaßne partielle (substring) correspondante de value, mais setRangeText est plus puissant et souvent plus pratique.

C’est une mĂ©thode plus ou moins complexe. Dans sa forme la plus simple Ă  un argument, elle remplace la plage sĂ©lectionnĂ©e par l’utilisateur et supprime la sĂ©lection.

Par exemple, ici la sĂ©lection de l’utilisateur sera entourĂ©e de *...* :

<input id="input" style="width:200px" value="Select here and click the button">
<button id="button">Wrap selection in stars *...*</button>

<script>
button.onclick = () => {
  if (input.selectionStart == input.selectionEnd) {
    return; // rien n'est sélectionné
  }

  let selected = input.value.slice(input.selectionStart, input.selectionEnd);
  input.setRangeText(`*${selected}*`);
};
</script>

Avec plus d’arguments, nous pouvons dĂ©finir start et end de range.

Dans cet exemple, nous trouvons "THIS" dans le texte de saisie, le remplaçons et gardons le remplacement sélectionné :

<input id="input" style="width:200px" value="Replace THIS in text">
<button id="button">Replace THIS</button>

<script>
button.onclick = () => {
  let pos = input.value.indexOf("THIS");
  if (pos >= 0) {
    input.setRangeText("*THIS*", pos, pos + 4, "select");
    input.focus(); // focus pour rendre la sélection visible
  }
};
</script>

Exemple : insérer au curseur

Si rien n’est sĂ©lectionnĂ©, ou si nous utilisons des start et end Ă©gaux dans setRangeText, alors le nouveau texte est juste insĂ©rĂ©, rien n’est supprimĂ©.

On peut aussi insĂ©rer quelque chose “au curseur” en utilisant setRangeText.

Voici un bouton qui insĂšre "HELLO" Ă  la position du curseur et place le curseur immĂ©diatement aprĂšs. Si la sĂ©lection n’est pas vide, elle est remplacĂ©e (nous pouvons le dĂ©tecter en comparant selectionStart!=selectionEnd et faire autre chose Ă  la place) :

<input id="input" style="width:200px" value="Text Text Text Text Text">
<button id="button">Insert "HELLO" at cursor</button>

<script>
  button.onclick = () => {
    input.setRangeText("HELLO", input.selectionStart, input.selectionEnd, "end");
    input.focus();
  };
</script>

Rendre un élément non sélectionnable

Pour rendre quelque chose non sélectionnable, il y a trois approches :

  1. Utiliser la propriété CSS user-select: none.

    <style>
    #elem {
      user-select: none;
    }
    </style>
    <div>Selectable <div id="elem">Unselectable</div> Selectable</div>

    Cela ne permet pas Ă  la sĂ©lection de commencer Ă  elem. Mais l’utilisateur peut commencer la sĂ©lection ailleurs et inclure elem dans celle-ci.

    Dans ce cas, elem deviendra une partie de document.getSelection(), et la sélection aura bien lieu, mais son contenu sera généralement ignoré dans le copier-coller.

  2. EmpĂȘcher l’action par dĂ©faut dans les Ă©vĂ©nements onselectstart ou mousedown.

    <div>Selectable <div id="elem">Unselectable</div> Selectable</div>
    
    <script>
      elem.onselectstart = () => false;
    </script>

    Cela empĂȘche de commencer la sĂ©lection sur elem, mais le visiteur peut la commencer sur un autre Ă©lĂ©ment, puis l’étendre Ă  elem.

    C’est pratique quand il y a un autre gestionnaire d’évĂ©nement sur la mĂȘme action qui dĂ©clenche la sĂ©lection (par exemple mousedown). Nous dĂ©sactivons donc la sĂ©lection pour Ă©viter tout conflit, tout en permettant au contenu de elem d’ĂȘtre copiĂ©.

  3. On peut aussi effacer la sĂ©lection aprĂšs le fait avec document.getSelection().empty(). C’est rarement utilisĂ©, car cela provoque un clignotement indĂ©sirable lorsque la sĂ©lection apparaĂźt-disparaĂźt.

Quelques références

Résumé

Nous avons couvert deux API différentes pour les sélections :

  1. Pour le document : objets Selection et Range.
  2. Pour input, textarea : méthodes et propriétés supplémentaires.

La deuxiùme API est trùs simple, puisqu’elle fonctionne avec du texte.

Les recettes les plus utilisées sont probablement :

  1. Obtenir la sélection :
    let selection = document.getSelection();
    
    let cloned = /* Ă©lĂ©ment pour cloner les nƓuds sĂ©lectionnĂ©s vers */;
    
    // puis appliquer les méthodes Range à selection.getRangeAt(0)
    // ou, comme ici, à toutes les plages (range) pour supporter la sélection multiple.
    for (let i = 0; i < selection.rangeCount; i++) {
      cloned.append(selection.getRangeAt(i).cloneContents());
    }
  2. Mise en place de la sélection :
    let selection = document.getSelection();
    
    // directement:
    selection.setBaseAndExtent(...from...to...);
    
    // ou nous pouvons créer une plage et :
    selection.removeAllRanges();
    selection.addRange(range);

Et enfin, Ă  propos du curseur. La position du curseur dans les Ă©lĂ©ments modifiables, comme <textarea> est toujours au dĂ©but ou Ă  la fin de la sĂ©lection. Nous pouvons l’utiliser pour obtenir la position du curseur ou pour le dĂ©placer en dĂ©finissant elem.selectionStart et elem.selectionEnd.

Carte du tutoriel

Commentaires

lire ceci avant de commenter

  • Si vous avez des amĂ©liorations Ă  suggĂ©rer, merci de soumettre une issue GitHub ou une pull request au lieu de commenter.
  • Si vous ne comprenez pas quelque chose dans l'article, merci de prĂ©ciser.
  • Pour insĂ©rer quelques bouts de code, utilisez la balise <code>, pour plusieurs lignes – enveloppez-les avec la balise <pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepen
)