101 votes

Obtenir le décalage de début et de fin d'une plage par rapport à son conteneur parent

Supposons que j'ai cet élément HTML :

 Bonjour tout le monde! C'est ma page d'accueil
 Au revoir!

Et l'utilisateur sélectionne "accueil" avec sa souris.

Je veux être en mesure de déterminer à quel caractère à l'intérieur de #parent sa sélection commence (et à quel caractère de la fin de #parent sa sélection se termine). Cela devrait fonctionner même s'il sélectionne une balise HTML. (Et j'ai besoin que cela fonctionne dans tous les navigateurs)

range.startOffset semble prometteur, mais c'est un décalage relatif uniquement au conteneur immédiat de la plage, et c'est un décalage de caractères uniquement si le conteneur est un nœud de texte.

245voto

Tim Down Points 124501

METTRE À JOUR

Comme indiqué dans les commentaires, ma réponse initiale (ci-dessous) ne renvoie que la fin de la sélection ou la position du curseur. Il est assez facile d'adapter le code pour renvoyer un début et une fin de décalage; voici un exemple qui le fait :

function getSelectionCharacterOffsetWithin(element) {
    var start = 0;
    var end = 0;
    var doc = element.ownerDocument || element.document;
    var win = doc.defaultView || doc.parentWindow;
    var sel;
    if (typeof win.getSelection != "undefined") {
        sel = win.getSelection();
        if (sel.rangeCount > 0) {
            var range = win.getSelection().getRangeAt(0);
            var preCaretRange = range.cloneRange();
            preCaretRange.selectNodeContents(element);
            preCaretRange.setEnd(range.startContainer, range.startOffset);
            start = preCaretRange.toString().length;
            preCaretRange.setEnd(range.endContainer, range.endOffset);
            end = preCaretRange.toString().length;
        }
    } else if ( (sel = doc.selection) && sel.type != "Control") {
        var textRange = sel.createRange();
        var preCaretTextRange = doc.body.createTextRange();
        preCaretTextRange.moveToElementText(element);
        preCaretTextRange.setEndPoint("EndToStart", textRange);
        start = preCaretTextRange.text.length;
        preCaretTextRange.setEndPoint("EndToEnd", textRange);
        end = preCaretTextRange.text.length;
    }
    return { start: start, end: end };
}

function reportSelection() {
  var selOffsets = getSelectionCharacterOffsetWithin( document.getElementById("editor") );
  document.getElementById("selectionLog").innerHTML = "Décalages de sélection : " + selOffsets.start + ', ' + selOffsets.end;
}

window.onload = function() {
  document.addEventListener("selectionchange", reportSelection, false);
  document.addEventListener("mouseup", reportSelection, false);
  document.addEventListener("mousedown", reportSelection, false);
  document.addEventListener("keyup", reportSelection, false);
};

#editor {
  padding: 5px;
  border: solid green 1px;
}

Sélectionnez quelque chose dans le contenu ci-dessous:

Un wombat est un marsupial originaire de Australie

Voici une fonction qui obtiendra le décalage de caractère du curseur dans l'élément spécifié; cependant, il s'agit d'une implémentation naïve qui aura presque certainement des incohérences avec les sauts de ligne, et ne tente pas de traiter le texte masqué via CSS (je soupçonne qu'IE ignorera correctement un tel texte tandis que d'autres navigateurs ne le feront pas). Gérer tout cela correctement serait délicat. J'ai maintenant essayé pour ma bibliothèque Rangy.

Exemple en direct : http://jsfiddle.net/TjXEG/900/

function getCaretCharacterOffsetWithin(element) {
    var caretOffset = 0;
    var doc = element.ownerDocument || element.document;
    var win = doc.defaultView || doc.parentWindow;
    var sel;
    if (typeof win.getSelection != "undefined") {
        sel = win.getSelection();
        if (sel.rangeCount > 0) {
            var range = win.getSelection().getRangeAt(0);
            var preCaretRange = range.cloneRange();
            preCaretRange.selectNodeContents(element);
            preCaretRange.setEnd(range.endContainer, range.endOffset);
            caretOffset = preCaretRange.toString().length;
        }
    } else if ( (sel = doc.selection) && sel.type != "Control") {
        var textRange = sel.createRange();
        var preCaretTextRange = doc.body.createTextRange();
        preCaretTextRange.moveToElementText(element);
        preCaretTextRange.setEndPoint("EndToEnd", textRange);
        caretOffset = preCaretTextRange.text.length;
    }
    return caretOffset;
}

29voto

Candor Points 108

Après quelques jours d'expérimentation, j'ai trouvé une approche qui semble prometteuse. Parce que selectNodeContents() ne gère pas correctement les balises, j'ai écrit un algorithme personnalisé pour déterminer la longueur du texte de chaque node à l'intérieur d'un contenteditable. Pour calculer par exemple le début de la sélection, je fais la somme des longueurs de texte de tous les nodes précédents. De cette manière, je peux gérer (plusieurs) sauts de ligne :

var editor = null;
var output = null;

const getTextSelection = function (editor) {
    const selection = window.getSelection();

    if (selection != null && selection.rangeCount > 0) {
        const range = selection.getRangeAt(0);

        return {
            start: getTextLength(editor, range.startContainer, range.startOffset),
            end: getTextLength(editor, range.endContainer, range.endOffset)
        };
    } else
        return null;
}

const getTextLength = function (parent, node, offset) {
    var textLength = 0;

    if (node.nodeName == '#text')
        textLength += offset;
    else for (var i = 0; i < offset; i++)
        textLength += getNodeTextLength(node.childNodes[i]);

    if (node != parent)
        textLength += getTextLength(parent, node.parentNode, getNodeOffset(node));

    return textLength;
}

const getNodeTextLength = function (node) {
    var textLength = 0;

    if (node.nodeName == 'BR')
        textLength = 1;
    else if (node.nodeName == '#text')
        textLength = node.nodeValue.length;
    else if (node.childNodes != null)
        for (var i = 0; i < node.childNodes.length; i++)
            textLength += getNodeTextLength(node.childNodes[i]);

    return textLength;
}

const getNodeOffset = function (node) {
    return node == null ? -1 : 1 + getNodeOffset(node.previousSibling);
}

window.onload = function () {
    editor = document.querySelector('.editor');
    output = document.querySelector('#output');

    document.addEventListener('selectionchange', handleSelectionChange);
}

const handleSelectionChange = function () {
    if (isEditor(document.activeElement)) {
        const textSelection = getTextSelection(document.activeElement);

        if (textSelection != null) {
            const text = document.activeElement.innerText;
            const selection = text.slice(textSelection.start, textSelection.end);
            print(`Sélection : [${selection}] (Début : ${textSelection.start}, Fin : ${textSelection.end})`);
        } else
            print('La sélection est nulle !');
    } else
        print('Sélectionnez du texte ci-dessus');
}

const isEditor = function (element) {
    return element != null && element.classList.contains('editor');
}

const print = function (message) {
    if (output != null)
        output.innerText = message;
    else
        console.log('output est nul !');
}

* {
    font-family: 'Georgia', sans-serif;
    padding: 0;
    margin: 0;
}

body {
    margin: 16px;
}

.p {
    font-size: 16px;
    line-height: 24px;
    padding: 0 2px;
}

.editor {
    border: 1px solid #0000001e;
    border-radius: 2px;
    white-space: pre-wrap;
}

#output {
    margin-top: 16px;
}

    Position du curseur

    Écrivezunsuperbe texte ici...
    Sélectionnez du texte ci-dessus

28voto

Cody Crumrine Points 296

Je sais que cela date d'il y a un an, mais ce message est un résultat de recherche important pour de nombreuses questions sur la recherche de la position du curseur et j'ai trouvé cela utile.

J'essayais d'utiliser le script excellent de Tim ci-dessus pour trouver la nouvelle position du curseur après avoir fait glisser un élément d'une position à une autre dans une div modifiable. Cela a parfaitement fonctionné dans FF et IE, mais dans Chrome, l'action de glisser a mis en surbrillance tout le contenu entre le début et la fin du glissement, ce qui a entraîné le retour de caretOffset étant trop grand ou trop petit (de la longueur de la zone sélectionnée).

J'ai ajouté quelques lignes à la première déclaration if pour vérifier si du texte a été sélectionné et ajuster le résultat en conséquence. La nouvelle déclaration est ci-dessous. Pardonnez-moi si c'est inapproprié d'ajouter cela ici, car ce n'est pas ce que l'OP essayait de faire, mais comme je l'ai dit, plusieurs recherches sur des informations liées à la position du curseur m'ont amené à ce message, il est donc (espérons-le) susceptible d'aider quelqu'un d'autre.

Première déclaration if de Tim avec les lignes ajoutées (*):

if (typeof window.getSelection != "undefined") {
  var range = window.getSelection().getRangeAt(0);
  var selected = range.toString().length; // *
  var preCaretRange = range.cloneRange();
  preCaretRange.selectNodeContents(element);
  preCaretRange.setEnd(range.endContainer, range.endOffset);

  caretOffset = preCaretRange.toString().length - selected; // *
}

3voto

tfwright Points 1829

Cette solution fonctionne en comptant la longueur du contenu texte des frères précédents en remontant jusqu'au conteneur parent. Elle ne couvre probablement pas tous les cas limites, même si elle gère les balises imbriquées de n'importe quelle profondeur, mais c'est un bon point de départ simple si vous avez un besoin similaire.

  calculateTotalOffset(node, offset) {
    let total = offset
    let curNode = node

    while (curNode.id != 'parent') {
      if(curNode.previousSibling) {
        total += curNode.previousSibling.textContent.length

        curNode = curNode.previousSibling
      } else {
        curNode = curNode.parentElement
      }
    }

   return total
 }

 // après la sélection

let start = calculateTotalOffset(range.startContainer, range.startOffset)
let end = calculateTotalOffset(range.endContainer, range.endOffset)

0voto

Escape75 Points 21

Est-ce que cela a été "résolu" de manière à obtenir le caretOffset à l'intérieur de innerText et non pas textContent ? (pour inclure les sauts de ligne, etc.)

J'ai une solution partielle qui fonctionne, après une sélection par double-clic est faite...

Une meilleure façon d'extraire innerText autour de getSelection()

Faites défiler vers le bas jusqu'à --- MISE À JOUR --- pour un code plus récent. Mais je me demande s'il y a une meilleure façon ?

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