148 votes

Définir la position du curseur sur le <div> contentEditable

Je suis à la recherche d'une solution définitive et multi-navigateur pour définir la position du curseur/caret à la dernière position connue lorsqu'une <div> contentEditable='on' regagne le focus. Il semble que la fonctionnalité par défaut d'une div éditable par le contenu soit de déplacer le curseur au début du texte de la div chaque fois que vous cliquez dessus, ce qui n'est pas souhaitable.

Je pense que je devrais stocker dans une variable la position actuelle du curseur lorsqu'ils quittent le focus de la div, et la redéfinir lorsqu'ils ont à nouveau le focus à l'intérieur, mais je n'ai pas encore été capable de mettre en place ou de trouver un exemple de code fonctionnel.

Si vous avez des idées, des extraits de code ou des échantillons, je serais heureux de les recevoir.

Je n'ai pas encore vraiment de code mais voici ce que j'ai :

<script type="text/javascript">
// jQuery
$(document).ready(function() {
   $('#area').focus(function() { .. }  // focus I would imagine I need.
}
</script>
<div id="area" contentEditable="true"></div>

PS. J'ai essayé cette ressource mais il semble qu'elle ne fonctionne pas pour une <div>. Peut-être seulement pour les textarea ( Comment déplacer le curseur à la fin d'une entité modifiable par le contenu ? )

0 votes

Je ne savais pas. contentEditable fonctionne dans les navigateurs non-IE o_o

10 votes

Oui, c'est vrai, Aditya.

5 votes

Aditya, Safari 2+, Firefox 3+ je crois.

101voto

Nico Burns Points 6012

Cette solution fonctionne dans tous les principaux navigateurs :

saveSelection() est attaché à la onmouseup y onkeyup de la div et enregistre la sélection dans la variable savedRange .

restoreSelection() est attaché à la onfocus de la div et resélectionne la sélection sauvegardée dans le fichier savedRange .

Cela fonctionne parfaitement, à moins que vous ne souhaitiez que la sélection soit restaurée lorsque l'utilisateur clique sur le div (ce qui n'est pas très intuitif car normalement le curseur doit aller là où vous cliquez, mais le code est inclus pour être complet).

Pour y parvenir, la onclick y onmousedown sont annulés par la fonction cancelEvent() qui est une fonction de navigateur croisé pour annuler l'événement. Le site cancelEvent() exécute également la fonction restoreSelection() car comme l'événement de clic est annulé, la division ne reçoit pas le focus et donc rien n'est sélectionné du tout à moins que cette fonction ne soit exécutée.

La variable isInFocus enregistre s'il est au point et est changé en "faux". onblur et "vrai" onfocus . Cela permet d'annuler les événements de clics uniquement si la div n'est pas en focus (sinon vous ne pourriez pas du tout modifier la sélection).

Si vous souhaitez que la sélection soit modifiée lorsqu'un clic est effectué sur la division, et que la sélection ne soit pas rétablie. onclick (et seulement lorsque le focus est donné à l'élément de façon programmatique en utilisant le bouton document.getElementById("area").focus(); ou similaire, il suffit de supprimer le onclick y onmousedown événements. Le site onblur et l'événement onDivBlur() y cancelEvent() Les fonctions peuvent également être supprimées en toute sécurité dans ces circonstances.

Ce code devrait fonctionner si vous le déposez directement dans le corps d'une page html si vous voulez le tester rapidement :

<div id="area" style="width:300px;height:300px;" onblur="onDivBlur();" onmousedown="return cancelEvent(event);" onclick="return cancelEvent(event);" contentEditable="true" onmouseup="saveSelection();" onkeyup="saveSelection();" onfocus="restoreSelection();"></div>
<script type="text/javascript">
var savedRange,isInFocus;
function saveSelection()
{
    if(window.getSelection)//non IE Browsers
    {
        savedRange = window.getSelection().getRangeAt(0);
    }
    else if(document.selection)//IE
    { 
        savedRange = document.selection.createRange();  
    } 
}

function restoreSelection()
{
    isInFocus = true;
    document.getElementById("area").focus();
    if (savedRange != null) {
        if (window.getSelection)//non IE and there is already a selection
        {
            var s = window.getSelection();
            if (s.rangeCount > 0) 
                s.removeAllRanges();
            s.addRange(savedRange);
        }
        else if (document.createRange)//non IE and no selection
        {
            window.getSelection().addRange(savedRange);
        }
        else if (document.selection)//IE
        {
            savedRange.select();
        }
    }
}
//this part onwards is only needed if you want to restore selection onclick
var isInFocus = false;
function onDivBlur()
{
    isInFocus = false;
}

function cancelEvent(e)
{
    if (isInFocus == false && savedRange != null) {
        if (e && e.preventDefault) {
            //alert("FF");
            e.stopPropagation(); // DOM style (return false doesn't always work in FF)
            e.preventDefault();
        }
        else {
            window.event.cancelBubble = true;//IE stopPropagation
        }
        restoreSelection();
        return false; // false = IE style
    }
}
</script>

1 votes

Merci, ça marche vraiment ! Testé dans IE, Chrome et FF les plus récents. Désolé pour la réponse super retardée =)

0 votes

Je ne le ferai pas. if (window.getSelection)... tester seulement si le navigateur supporte getSelection et non pas s'il y a ou non une sélection ?

0 votes

@Sandy Oui, exactement. Cette partie du code décide s'il faut utiliser le standard getSelection ou l'ancienne document.selection api utilisée par les anciennes versions d'IE. Les dernières getRangeAt (0) l'appel retournera null s'il n'y a pas de sélection, ce qui est vérifié dans la fonction de restauration.

59voto

eyelidlessness Points 28034

Il est compatible avec les navigateurs standard, mais échouera probablement dans IE. Je le fournis comme point de départ. IE ne supporte pas DOM Range.

var editable = document.getElementById('editable'),
    selection, range;

// Populates selection and range variables
var captureSelection = function(e) {
    // Don't capture selection outside editable region
    var isOrContainsAnchor = false,
        isOrContainsFocus = false,
        sel = window.getSelection(),
        parentAnchor = sel.anchorNode,
        parentFocus = sel.focusNode;

    while(parentAnchor && parentAnchor != document.documentElement) {
        if(parentAnchor == editable) {
            isOrContainsAnchor = true;
        }
        parentAnchor = parentAnchor.parentNode;
    }

    while(parentFocus && parentFocus != document.documentElement) {
        if(parentFocus == editable) {
            isOrContainsFocus = true;
        }
        parentFocus = parentFocus.parentNode;
    }

    if(!isOrContainsAnchor || !isOrContainsFocus) {
        return;
    }

    selection = window.getSelection();

    // Get range (standards)
    if(selection.getRangeAt !== undefined) {
        range = selection.getRangeAt(0);

    // Get range (Safari 2)
    } else if(
        document.createRange &&
        selection.anchorNode &&
        selection.anchorOffset &&
        selection.focusNode &&
        selection.focusOffset
    ) {
        range = document.createRange();
        range.setStart(selection.anchorNode, selection.anchorOffset);
        range.setEnd(selection.focusNode, selection.focusOffset);
    } else {
        // Failure here, not handled by the rest of the script.
        // Probably IE or some older browser
    }
};

// Recalculate selection while typing
editable.onkeyup = captureSelection;

// Recalculate selection after clicking/drag-selecting
editable.onmousedown = function(e) {
    editable.className = editable.className + ' selecting';
};
document.onmouseup = function(e) {
    if(editable.className.match(/\sselecting(\s|$)/)) {
        editable.className = editable.className.replace(/ selecting(\s|$)/, '');
        captureSelection();
    }
};

editable.onblur = function(e) {
    var cursorStart = document.createElement('span'),
        collapsed = !!range.collapsed;

    cursorStart.id = 'cursorStart';
    cursorStart.appendChild(document.createTextNode('—'));

    // Insert beginning cursor marker
    range.insertNode(cursorStart);

    // Insert end cursor marker if any text is selected
    if(!collapsed) {
        var cursorEnd = document.createElement('span');
        cursorEnd.id = 'cursorEnd';
        range.collapse();
        range.insertNode(cursorEnd);
    }
};

// Add callbacks to afterFocus to be called after cursor is replaced
// if you like, this would be useful for styling buttons and so on
var afterFocus = [];
editable.onfocus = function(e) {
    // Slight delay will avoid the initial selection
    // (at start or of contents depending on browser) being mistaken
    setTimeout(function() {
        var cursorStart = document.getElementById('cursorStart'),
            cursorEnd = document.getElementById('cursorEnd');

        // Don't do anything if user is creating a new selection
        if(editable.className.match(/\sselecting(\s|$)/)) {
            if(cursorStart) {
                cursorStart.parentNode.removeChild(cursorStart);
            }
            if(cursorEnd) {
                cursorEnd.parentNode.removeChild(cursorEnd);
            }
        } else if(cursorStart) {
            captureSelection();
            var range = document.createRange();

            if(cursorEnd) {
                range.setStartAfter(cursorStart);
                range.setEndBefore(cursorEnd);

                // Delete cursor markers
                cursorStart.parentNode.removeChild(cursorStart);
                cursorEnd.parentNode.removeChild(cursorEnd);

                // Select range
                selection.removeAllRanges();
                selection.addRange(range);
            } else {
                range.selectNode(cursorStart);

                // Select range
                selection.removeAllRanges();
                selection.addRange(range);

                // Delete cursor marker
                document.execCommand('delete', false, null);
            }
        }

        // Call callbacks here
        for(var i = 0; i < afterFocus.length; i++) {
            afterFocus[i]();
        }
        afterFocus = [];

        // Register selection again
        captureSelection();
    }, 10);
};

0 votes

Merci eye, j'ai essayé votre solution, j'étais un peu pressé mais après l'avoir câblée, elle ne place la position "-" qu'au dernier point de focus (qui semble être un marqueur de débogage ?) et c'est là que nous perdons le focus, elle ne semble pas rétablir le curseur/caret lorsque je clique en arrière (du moins pas dans Chrome, je vais essayer FF), elle va juste à la fin de la div. Je vais donc accepter la solution de Nico car je sais qu'elle est compatible avec tous les navigateurs et qu'elle a tendance à bien fonctionner. Merci quand même pour votre effort.

3 votes

Vous savez quoi, oubliez ma dernière réponse, après avoir examiné de plus près le vôtre et celui de Nico, le vôtre n'est pas ce que j'ai demandé dans ma description, mais c'est ce que je préfère et ce dont j'aurais réalisé avoir besoin. Bien à vous fixe la position du curseur de l'endroit où l'on clique lorsqu'on active le focus de nouveau sur la <div>, comme une boîte de texte ordinaire. Rétablir le focus sur le dernier point n'est pas suffisant pour faire un champ de saisie convivial. Je vous accorde les points.

10 votes

Cela fonctionne très bien ! Voici une jsfiddle de la solution ci-dessus : jsfiddle.net/s5xAr/3

20voto

Tim Down Points 124501

Mise à jour

J'ai écrit une bibliothèque de sélection et d'étendue multi-navigateurs appelée Rangy qui incorpore une version améliorée du code que j'ai posté ci-dessous. Vous pouvez utiliser le module de sauvegarde et de restauration des sélections pour cette question particulière, même si je serais tenté d'utiliser quelque chose comme La réponse de @Nico Burns si vous ne faites rien d'autre avec les sélections dans votre projet et que vous n'avez pas besoin de l'ensemble d'une bibliothèque.

Réponse précédente

Vous pouvez utiliser IERange ( http://code.google.com/p/ierange/ ) pour convertir le TextRange d'IE en quelque chose comme un DOM Range et l'utiliser en conjonction avec quelque chose comme le point de départ de eyelidlessness. Personnellement, je n'utiliserais que les algorithmes de IERange qui font les conversions Range <-> TextRange plutôt que d'utiliser l'ensemble. Et l'objet de sélection d'IE n'a pas les propriétés focusNode et anchorNode mais vous devriez être en mesure d'utiliser simplement le Range/TextRange obtenu à partir de la sélection à la place.

~~

Je pourrais mettre en place quelque chose pour faire cela, je posterai ici si et quand je le ferai.

EDIT :

J'ai créé une démo d'un script qui fait cela. Il fonctionne dans tout ce que j'ai essayé jusqu'à présent, à l'exception d'un bogue dans Opera 9, que je n'ai pas encore eu le temps d'examiner. Les navigateurs dans lesquels il fonctionne sont IE 5.5, 6 et 7, Chrome 2, Firefox 2, 3 et 3.5, et Safari 4, tous sous Windows.

http://www.timdown.co.uk/code/selections/

Notez que les sélections peuvent être effectuées à l'envers dans les navigateurs, de sorte que le nœud central se trouve au début de la sélection et que le fait d'appuyer sur la touche droite ou gauche du curseur déplace le signe d'insertion à une position relative au début de la sélection. Je ne pense pas qu'il soit possible de reproduire ce phénomène lors du rétablissement d'une sélection, de sorte que le nœud central se trouve toujours à la fin de la sélection.

~~

J'écrirai bientôt un article complet à ce sujet.

16voto

Zane Claes Points 4001

J'ai eu une situation similaire, où j'avais spécifiquement besoin de placer la position du curseur à la FIN d'une division modifiable. Je ne voulais pas utiliser une bibliothèque à part entière comme Rangy, et de nombreuses solutions étaient bien trop lourdes.

En fin de compte, j'ai trouvé cette simple fonction jQuery pour définir la position du carat à la fin d'une div modifiable :

$.fn.focusEnd = function() {
    $(this).focus();
    var tmp = $('<span />').appendTo($(this)),
        node = tmp.get(0),
        range = null,
        sel = null;

    if (document.selection) {
        range = document.body.createTextRange();
        range.moveToElementText(node);
        range.select();
    } else if (window.getSelection) {
        range = document.createRange();
        range.selectNode(node);
        sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    }
    tmp.remove();
    return this;
}

La théorie est simple : ajouter un span à la fin de l'élément modifiable, le sélectionner, puis supprimer le span - ce qui nous laisse avec un curseur à la fin de la div. Vous pouvez adapter cette solution pour insérer le span où vous le souhaitez et placer ainsi le curseur à un endroit précis.

L'utilisation est simple :

$('#editable').focusEnd();

C'est ça !

3 votes

Vous n'avez pas besoin d'insérer le <span> ce qui aura pour effet de casser la pile d'annulation intégrée du navigateur. Voir stackoverflow.com/a/4238971/96100

7voto

Gatsbimantico Points 39

J'ai pris la réponse de Nico Burns et l'ai faite en utilisant jQuery :

  • Générique : Pour chaque div contentEditable="true"
  • Plus court

Vous aurez besoin de jQuery 1.6 ou plus :

savedRanges = new Object();
$('div[contenteditable="true"]').focus(function(){
    var s = window.getSelection();
    var t = $('div[contenteditable="true"]').index(this);
    if (typeof(savedRanges[t]) === "undefined"){
        savedRanges[t]= new Range();
    } else if(s.rangeCount > 0) {
        s.removeAllRanges();
        s.addRange(savedRanges[t]);
    }
}).bind("mouseup keyup",function(){
    var t = $('div[contenteditable="true"]').index(this);
    savedRanges[t] = window.getSelection().getRangeAt(0);
}).on("mousedown click",function(e){
    if(!$(this).is(":focus")){
        e.stopPropagation();
        e.preventDefault();
        $(this).focus();
    }
});

savedRanges = new Object();
$('div[contenteditable="true"]').focus(function(){
    var s = window.getSelection();
    var t = $('div[contenteditable="true"]').index(this);
    if (typeof(savedRanges[t]) === "undefined"){
        savedRanges[t]= new Range();
    } else if(s.rangeCount > 0) {
        s.removeAllRanges();
        s.addRange(savedRanges[t]);
    }
}).bind("mouseup keyup",function(){
    var t = $('div[contenteditable="true"]').index(this);
    savedRanges[t] = window.getSelection().getRangeAt(0);
}).on("mousedown click",function(e){
    if(!$(this).is(":focus")){
        e.stopPropagation();
        e.preventDefault();
        $(this).focus();
    }
});

div[contenteditable] {
    padding: 1em;
    font-family: Arial;
    outline: 1px solid rgba(0,0,0,0.5);
}

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div contentEditable="true"></div>
<div contentEditable="true"></div>
<div contentEditable="true"></div>

0 votes

@salivan Je sais qu'il est tard pour le mettre à jour, mais je pense que cela fonctionne maintenant. En fait, j'ai ajouté une nouvelle condition et j'ai changé l'utilisation de l'id de l'élément pour l'index de l'élément, qui devrait toujours exister :)

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