web-dev-qa-db-fra.com

Obtenir la position de décalage du curseur dans une zone de texte en pixels

Dans mon projet, j'essaie d'obtenir la position de décalage du curseur dans un textarea en pixels. Cela peut-il être fait?

Avant de demander ici, j'ai parcouru de nombreux liens , en particulier ceux de Tim Down, mais je n'ai pas trouvé de solution qui fonctionne dans IE8 +, Chrome et Firefox. Il semble Tim Down y travaille .

Certains autresliens que j'ai trouvés ont de nombreux problèmes comme ne pas trouver le décalage supérieur de la position du curseur.

J'essaie d'obtenir la position de décalage du curseur car je veux afficher une boîte de suggestion de saisie semi-automatique à l'intérieur du textarea en la positionnant en fonction de la position de décalage du curseur.

PS: Je ne peux pas utiliser un contenteditable div parce que j'ai écrit beaucoup de code lié à un textarea.

29
Mr_Green

Voici une approche utilisant rangyinputs , rangy et jQuery .

Il copie essentiellement le texte entier de l'intérieur du textarea dans un div de la même taille. J'ai défini du CSS pour m'assurer que dans chaque navigateur, le textarea et le div encapsulent leur contenu exactement de la même manière.

Lorsque l'on clique sur textarea, je lis à quel index de caractères le curseur est positionné, puis j'insère un curseur span au même index à l'intérieur du div. Ce faisant, j'ai fini par avoir un problème avec le signe d'insertion span pour revenir à la ligne précédente si l'utilisateur cliquait au début d'une ligne. Pour corriger cela, je vérifie si le caractère précédent est un space (ce qui permettrait un habillage), si c'est true, je l'enveloppe dans un span, et J'encapsule le prochain mot (celui directement après la position du curseur) dans un span. Maintenant, je compare les valeurs les plus élevées entre ces deux span, si elles diffèrent, il y avait un habillage en cours, donc je suppose que la valeur top et la valeur left de les #nextwordspan sont équivalents à la position du curseur.

Cette approche peut encore être améliorée, je suis sûr que je n'ai pas pensé à tout ce qui pourrait mal tourner, et même si je l'ai fait, alors je n'ai pas pris la peine de mettre en œuvre un correctif pour tous, car je n'ai pas le temps de le faire pour le moment, un certain nombre de choses que vous devriez examiner:

  • il ne gère pas encore les retours durs insérés avec Enter (fixé)
  • le positionnement se casse lors de la saisie de plusieurs espaces d'affilée (fixé)
  • Je pense que les traits d'union permettraient également un bouclage de contenu ..

Actuellement, il fonctionne exactement de la même manière sur tous les navigateurs ici sur Windows 8 avec les dernières versions de Chrome, Firefox, IE et Safari. Mes tests n'ont cependant pas été très rigoureux.

Voici un jsFiddle.

J'espère que cela vous aidera, à tout le moins, cela pourrait vous donner quelques idées sur lesquelles s'appuyer.

Certaines fonctionnalités:

  • J'ai inclus un ul pour vous qui est positionné au bon endroit, et corrigé un problème Firefox où la sélection textarea n'était pas réinitialisée à son emplacement d'origine après les manipulations DOM.

  • J'ai ajouté le support IE7 - IE9 et corrigé le problème de sélection de plusieurs mots signalé dans les commentaires.

  • J'ai ajouté la prise en charge des retours durs insérés avec Enter et plusieurs espaces d'affilée.

  • J'ai résolu un problème avec le comportement par défaut du ctrl+shift+left arrow méthode de sélection de texte.

JavaScript

function getTextAreaXandY() {

    // Don't do anything if key pressed is left arrow
    if (e.which == 37) return;     

    // Save selection start
    var selection = $(this).getSelection();
    var index = selection.start;

    // Copy text to div
    $(this).blur();
    $("div").text($(this).val());

    // Get current character
    $(this).setSelection(index, index + 1);
    currentcharacter = $(this).getSelection().text;

    // Get previous character
    $(this).setSelection(index - 1, index)
    previouscharacter = $(this).getSelection().text;

    var start, endchar;
    var end = 0;
    var range = rangy.createRange();

    // If current or previous character is a space or a line break, find the next Word and wrap it in a span
    var linebreak = previouscharacter.match(/(\r\n|\n|\r)/gm) == undefined ? false : true;

    if (previouscharacter == ' ' || currentcharacter == ' ' || linebreak) {
        i = index + 1; // Start at the end of the current space        
        while (endchar != ' ' && end < $(this).val().length) {
            i++;
            $(this).setSelection(i, i + 1)
            var sel = $(this).getSelection();
            endchar = sel.text;
            end = sel.start;
        }

        range.setStart($("div")[0].childNodes[0], index);
        range.setEnd($("div")[0].childNodes[0], end);
        var nextword = range.toHtml();
        range.deleteContents();
        var position = $("<span id='nextword'>" + nextword + "</span>")[0];
        range.insertNode(position);
        var nextwordtop = $("#nextword").position().top;
    }

    // Insert `#caret` at the position of the caret
    range.setStart($("div")[0].childNodes[0], index);
    var caret = $("<span id='caret'></span>")[0];
    range.insertNode(caret);
    var carettop = $("#caret").position().top;

    // If preceding character is a space, wrap it in a span
    if (previouscharacter == ' ') {
        range.setStart($("div")[0].childNodes[0], index - 1);
        range.setEnd($("div")[0].childNodes[0], index);
        var prevchar = $("<span id='prevchar'></span>")[0];
        range.insertNode(prevchar);
        var prevchartop = $("#prevchar").position().top;
    }

    // Set textarea selection back to selection start
    $(this).focus();
    $(this).setSelection(index, selection.end);

    // If the top value of the previous character span is not equal to the top value of the next Word,
    // there must have been some wrapping going on, the previous character was a space, so the wrapping
    // would have occured after this space, its safe to assume that the left and top value of `#nextword`
    // indicate the caret position
    if (prevchartop != undefined && prevchartop != nextwordtop) {
        $("label").text('X: ' + $("#nextword").position().left + 'px, Y: ' + $("#nextword").position().top);
        $('ul').css('left', ($("#nextword").position().left) + 'px');
        $('ul').css('top', ($("#nextword").position().top + 13) + 'px');
    }
    // if not, then there was no wrapping, we can take the left and the top value from `#caret`    
    else {
        $("label").text('X: ' + $("#caret").position().left + 'px, Y: ' + $("#caret").position().top);
        $('ul').css('left', ($("#caret").position().left) + 'px');
        $('ul').css('top', ($("#caret").position().top + 14) + 'px');
    }

    $('ul').css('display', 'block');
}

$("textarea").click(getTextAreaXandY);
$("textarea").keyup(getTextAreaXandY);

[~ # ~] html [~ # ~]

<div></div>
<textarea>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</textarea>
<label></label>
<ul>
    <li>Why don't you type this..</li>
</ul>

[~ # ~] css [~ # ~]

body {
    font-family: Verdana;
    font-size: 12px;
    line-height: 14px;
}
textarea, div {
    font-family: Verdana;
    font-size: 12px;
    line-height: 14px;
    width: 300px;
    display: block;
    overflow: hidden;
    border: 1px solid black;
    padding: 0;
    margin: 0;
    resize: none;
    min-height: 300px;
    position: absolute;
    -moz-box-sizing: border-box;
    white-space: pre-wrap;
}
span {
    display: inline-block;
    height: 14px;
    position: relative;
}
span#caret {
    display: inline;
}
label {
    display: block;
    margin-left: 320px;
}
ul {
    padding: 0px;
    margin: 9px;
    position: absolute;
    z-index: 999;
    border: 1px solid #000;
    background-color: #FFF;
    list-style-type:none;
    display: none;
}
@media screen and (-webkit-min-device-pixel-ratio:0) {
    span {
        white-space: pre-wrap;
    }
}
div {
    /* Firefox wrapping fix */
    -moz-padding-end: 1.5px;
    -moz-padding-start: 1.5px;
    /* IE8/IE9 wrapping fix */
    padding-right: 5px\0/;
    width: 295px\0/;
}
span#caret
{
    display: inline-block\0/;
}
15

Vous pouvez créer un élément séparé (invisible) et le remplir de contenu de zone de texte du début à la position du curseur. La zone de texte et le "clone" doivent avoir un CSS correspondant (propriétés de police, remplissage/marge/bordure et largeur). Empilez ensuite ces éléments les uns sur les autres.

Permettez-moi de commencer par un exemple de travail, puis parcourez le code: http://jsfiddle.net/g7rBk/

Fiddle mis à jour ( avec correctif IE8 )

HTML:

<textarea id="input"></textarea>
<div id="output"><span></span></div>
<div id="xy"></div>

Textarea est explicite. La sortie est un élément caché auquel nous transmettrons le contenu texte et effectuerons des mesures. Ce qui est important, c'est que nous utiliserons un élément en ligne. le div "xy" n'est qu'un indicateur à des fins de test.

CSS:

/* identical styling to match the dimensions and position of textarea and its "clone"
*/
#input, #output {
    position:absolute;
    top:0;
    left:0;
    font:14px/1 monospace;
    padding:5px;
    border:1px solid #999;
    white-space:pre;
    margin:0;
    background:transparent;
    width:300px;
    max-width:300px;
}
/* make sure the textarea isn't obscured by clone */
#input { 
    z-index:2;
    min-height:200px;
}

#output { 
    border-color:transparent; 
}

/* hide the span visually using opacity (not display:none), so it's still measurable; make it break long words inside like textarea does. */
#output span {
    opacity:0;
    Word-wrap: break-Word;
    overflow-wrap: break-Word;
}
/* the cursor position indicator */
#xy { 
    position:absolute; 
    width:4px;
    height:4px;
    background:#f00;
}

JavaScript:

/* get references to DOM nodes we'll use */
var input = document.getElementById('input'),
    output = document.getElementById('output').firstChild,
    position = document.getElementById('position'),

/* And finally, here it goes: */
    update = function(){
         /* Fill the clone with textarea content from start to the position of the caret. You may need to expand here to support older IE [1]. The replace /\n$/ is necessary to get position when cursor is at the beginning of empty new line.
          */
         output.innerHTML = input.value.substr( 0, input.selectionStart ).replace(/\n$/,"\n\001");

        /* the fun part! 
           We use an inline element, so getClientRects[2] will return a collection of rectangles wrapping each line of text.
           We only need the position of the last rectangle.
         */
        var rects = output.getClientRects(),
            lastRect = rects[ rects.length - 1 ],
            top = lastRect.top - input.scrollTop,
            left = lastRect.left+lastRect.width;
        /* position the little div and see if it matches caret position :) */
        xy.style.cssText = "top: "+top+"px;left: "+left+"px";
    }

[1] Position du curseur dans la zone de texte, en caractères depuis le début

[2] https://developer.mozilla.org/en/docs/DOM/element.getClientRects

Modifier: cet exemple ne fonctionne que pour les zones de texte à largeur fixe. Pour le faire fonctionner avec une zone de texte redimensionnable par l'utilisateur, vous devez ajouter un écouteur d'événement à l'événement de redimensionnement et définir les dimensions #output pour qu'elles correspondent aux nouvelles dimensions #input.

17
pawel

Il existe une solution beaucoup plus simple pour obtenir la position du curseur en pixels que celle présentée dans les autres réponses.

Notez que cette question est une copie de celle de 2008, et j'y ai répondu ici. Je ne maintiendrai la réponse qu'à ce lien, car cette question aurait dû être fermée en double il y a des années.

Copie de la réponse

J'ai cherché un plugin de coordonnées de zone de texte pour meteor-autocomplete , j'ai donc évalué les 8 plugins sur GitHub. Le gagnant est, de loin, textarea-caret-position from Component .

Caractéristiques

  • précision des pixels
  • aucune dépendance que ce soit
  • compatibilité du navigateur: Chrome, Safari, Firefox (malgré deuxbugs qu'il a), IE9 +; peut fonctionner mais non testé dans Opera, IE8 ou plus ancien
  • prend en charge n'importe quelle famille et taille de police, ainsi que les transformations de texte
  • la zone de texte peut avoir un remplissage ou des bordures arbitraires
  • pas confondu par les barres de défilement horizontales ou verticales dans la zone de texte
  • prend en charge les retours durs, les tabulations (sauf sur IE) et les espaces consécutifs dans le texte
  • position correcte sur les lignes plus longues que les colonnes dans la zone de texte
  • non position "fantôme" dans l'espace vide à la fin d'une ligne lors de l'habillage de mots longs

Voici une démo - http://jsfiddle.net/dandv/aFPA7/

enter image description here

Comment ça fonctionne

Un miroir <div> est créé hors écran et stylisé exactement comme le <textarea>. Ensuite, le texte de la zone de texte jusqu'au curseur est copié dans le div et un <span> est inséré juste après. Ensuite, le contenu texte de la plage est défini sur le reste du texte dans la zone de texte, afin de reproduire fidèlement l'habillage dans le faux div.

Il s'agit de la seule méthode garantie pour gérer tous les cas Edge relatifs à l'habillage de longues lignes. Il est également utilisé par GitHub pour déterminer la position de son @ menu déroulant utilisateur.

7
Dan Dascalescu

JsFiddle de l'exemple de travail: http://jsfiddle.net/42zHC/2/

Fondamentalement, nous déterminons combien de colonnes tiennent dans la largeur (car ce sera monospace). Nous devons forcer les barres de défilement à toujours être présentes sinon le calcul est désactivé. Ensuite, nous divisons le nombre de colonnes qui correspondent à la largeur et nous obtenons le décalage x par caractère. Ensuite, nous définissons la hauteur de ligne sur la zone de texte. Puisque nous savons combien de caractères sont dans une ligne, nous pouvons diviser cela par le nombre de caractères et nous obtenons le numéro de ligne. Avec la hauteur de ligne, nous avons maintenant le décalage y. Ensuite, nous obtenons le scrollTop de la zone de texte et le soustrayons, de sorte qu'une fois qu'il commence à utiliser la barre de défilement, il s'affiche toujours à la bonne position.

Javascript:

$(document).ready(function () {
  var cols = document.getElementById('t').cols;
  var width = document.getElementById('t').clientWidth;
  var height = $('textarea').css('line-height');
  var pos = $('textarea').position();
  $('#t').on('keyup', function () {
    el = document.getElementById("t");
    if (el.selectionStart) { 
        selection = el.selectionStart; 
      } else if (document.selection) { 
        el.focus(); 
        var r = document.selection.createRange(); 
        if (r == null) { 
           selection = 0; 
        } 
        var re = el.createTextRange(), 
        rc = re.duplicate(); 
        re.moveToBookmark(r.getBookmark()); 
        rc.setEndPoint('EndToStart', re); 
        selection = rc.text.length; 
      } else { selection = 0 }
    var row = Math.floor((selection-1) / cols);
    var col = (selection - (row * cols));
    var x = Math.floor((col*(width/cols)));
    var y = (parseInt(height)*row);
    $('span').html("row: " + row + "<br>columns" + col + "<br>width: " + width + "<br>x: " + x +"px<br>y: " + y +"px<br>Scrolltop: "+$(this).scrollTop()).css('top',pos.top+y-$(this).scrollTop()).css('left',pos.left+x+10);
  });
});

HTML:

<textarea id="t"></textarea>
<br>
<span id="tooltip" style="background:yellow"></span>

CSS:

textarea {
  height: 80px;
  line-height: 12px;
  overflow-y:scroll;
}
span {
  position: absolute;
}
2
dave