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

Cette question a déjà une réponse ici:

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 impossible de trouver une solution qui fonctionne dans IE8+, Chrome et Firefox. Il semble que Tim Down travaille sur ce .

Certains autres les liens que j'ai trouvés ont beaucoup de 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 parce que je veux montrer une boîte à suggestions auto-complète à l'intérieur du textarea en le 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.

27
demandé sur Community 2013-04-25 14:54:45

5 réponses

Voici une approche à l'aide de rangyinputs, longiligne et jQuery.

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

Lorsque le textarea est cliqué, je lis à quel index de caractères le caret est positionné, puis j'insère un caret span au même index à l'intérieur du div. En seulement en faisant cela, j'ai fini par avoir un problème avec le caret span en revenant à la ligne précédente si l'Utilisateur a cliqué au début d'une ligne. Pour corriger que je vérifie si le caractère précédent est un space (ce qui permettrait une écharpe pour se produire), si c'est true, je l'envelopper dans un span, et j'enveloppe le mot suivant (l'une directement après la position du curseur) dans un span. Maintenant, je compare les valeurs supérieures entre ces deux span, si elles diffèrent, il y avait un emballage en cours, donc je suppose que le top et le left valeur du #nextword span 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 d'implémenter un correctif pour tous car je n'ai pas le temps de le faire pour le moment, un certain nombre de choses que vous]}

  • Il ne gère pas encore les retours durs insérés avec Enter (fixe)
  • le positionnement se casse lors de la saisie de plusieurs espaces dans une rangée (fixe)
  • je pense que les tirets permettraient également un enveloppement de contenu..

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

Voici un jsFiddle.

J'espère qu'il vous aidera, à tout le moins il peut vous donner quelques idées pour construire sur.

Certaines Caractéristiques:

  • 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 remise à son endroit d'origine après les manipulations DOM.

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

  • J'ai ajouté le support pour les retours durs insérés avec Entrer et plusieurs espaces dans une rangée.

  • J'ai corrigé un problème avec le comportement par défaut pour la ctrl+maj+flèche vers la gauche texte de la méthode de sélection.

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
répondu Mathijs Flietstra 2017-06-15 10:35:19

Vous pouvez créer un élément séparé (invisible) et le remplir avec du contenu textarea du début à la position du curseur. Textarea et le" clone " doivent avoir des CSS correspondants (propriétés de police, padding / margin / border et width). Ensuite, empilez 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/

Violon mis à jour (avec IE8 fix)

HTML:

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

Textarea est auto-explicatif. La sortie est un élément caché auquel nous allons passer le contenu du texte et faire des mesures. Ce qui est important, c'est que nous utilisions un élément en ligne. le div" xy " est juste 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 textarea, en caractères depuis le début

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

Edit: cet exemple ne fonctionne que pour une largeur fixe textarea. Pour le faire fonctionner avec textarea redimensionnable par l'utilisateur, vous devez ajouter un écouteur d'événement à l'événement redimensionner et définir les dimensions # output pour correspondre aux nouvelles dimensions # input .

17
répondu pawel 2017-05-23 12:34:51

Il existe une solution beaucoup plus simple pour obtenir la position du curseur en pixels, que ce qui a été présenté dans les autres réponses.

Notez que cette question est un doublon de 2008, et j'ai répondu ici. Je maintiendrai seulement la réponse à ce lien, puisque 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 textarea caret coordinates pour meteor-autocomplete , donc j'ai évalué tous les 8 plugins sur GitHub. Le gagnant est, de loin, textarea-caret-position à partir de Composant.

Caractéristiques

  • précision des pixels
  • aucune dépendance
  • compatibilité du navigateur: Chrome, Safari, Firefox (malgré deux bugs {[8] } Il a), IE9+; peut fonctionner mais pas testé dans Opera, IE8 ou plus
  • prend en charge toute famille de polices et la taille, ainsi que le texte-transforme
  • la zone de texte peut avoir rembourrage ou bordures
  • pas confondu par des 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 de la zone de texte
  • non "fantôme" de la position dans l'espace vide à la fin d'une ligne lors de l'emballage des mots longs

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

entrez la description de l'image ici

Comment ça travaux

Un miroir {[0] } 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 du texte de la durée est définie à la suite du texte dans le textarea, afin de reproduire fidèlement l'emballage dans le faux-div.

C'est la seule méthode garantie pour gérer tous les cas de bord relatifs à l'encapsulation de longues lignes. Il est également utilisé par GitHub pour déterminer la position de son @ liste déroulante utilisateur.

6
répondu Dan Dascalescu 2017-05-23 10:30:49

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

Fondamentalement, nous déterminons combien de colonnes correspondent à la largeur (car ce sera monospace). Nous devons forcer les barres de défilement à toujours être là sinon le calcul est désactivé. Ensuite, nous divisons le nombre de colonnes qui correspondent à la largeur, et nous obtenons le décalage en x par caractère. Ensuite, nous définissons la hauteur de la ligne sur le textarea. Puisque nous savons combien de caractères sont dans une rangée, nous pouvons diviser cela avec 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 soustrayons cela, de sorte qu'une fois qu'il commence à utiliser la barre de défilement, il apparaît toujours dans 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
répondu dave 2013-05-16 06:48:54

Je n'ai pas pu obtenir quelque chose de similaire au travail, donc ma solution était de localiser la position du caractère du curseur dans la zone de texte, de découper le paragraphe actuel et de l'afficher à côté de la zone de texte.

En utilisant le décalage, j'ai placé un faux curseur (div, display:inline, 1px large, border-left: 1px solid black) dans cette vue du texte modifiable.

De cette façon, vous pouvez créer une zone de rétroaction visuelle où vous pouvez afficher le résultat des effets (tout comme le fait stackoverflow lorsque vous écrivez une réponse).

-1
répondu Aaron Digulla 2013-04-25 12:42:15