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

142

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

Je crois que je devrais stocker dans une variable la position actuelle du curseur lorsqu'ils quittent le focus du div, puis le redéfinir lorsqu'ils se sont à nouveau concentrés à l'intérieur, mais je n'ai pas été en mesure de rassembler ou de trouver un travail échantillon de code encore.

Si quelqu'un a des idées, des extraits de code de travail ou des échantillons, je serais heureux de les voir.

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 uniquement pour textarea ( Comment déplacer le curseur à la fin de l'entité contenteditable )

GONeale
la source
Je ne savais pas que je contentEditabletravaillais dans des navigateurs non IE o_o
aditya
10
Oui, c'est aditya.
GONeale
5
aditya, Safari 2+, Firefox 3+ je pense.
paupière
Essayez de définir tabindex = "0" sur le div. Cela devrait le rendre focalisable dans la plupart des navigateurs.
Tokimon

Réponses:

58

Ceci est compatible avec les navigateurs basés sur des standards, mais échouera probablement dans IE. Je le donne comme point de départ. IE ne prend pas en charge la plage DOM.

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);
};
paupière
la source
Merci oeil, j'ai essayé votre solution, j'étais un peu pressé mais après l'avoir câblé, il ne place que la position "-" au dernier point de mise au point (qui semble être un marqueur de débogage?) Et c'est là que nous perdons focus, il ne semble pas restaurer le curseur / curseur lorsque je clique en arrière (du moins pas dans Chrome, je vais essayer FF), cela va juste à la fin du div. J'accepterai donc la solution de Nico car je sais qu'elle est compatible dans tous les navigateurs et qu'elle a tendance à bien fonctionner. Merci beaucoup pour vos efforts.
GONeale
3
Savez-vous quoi, oubliez ma dernière réponse, après avoir examiné plus en détail la vôtre et celle de Nico, la vôtre n'est pas ce que j'ai demandé dans ma description, mais c'est ce que je préfère et j'aurais réalisé que j'avais besoin. Bien à vous correctement définit la position du curseur de l' endroit où vous cliquez lors de l' activation focus sur la balise <div>, comme une zone de texte régulier. Rétablir le focus sur le dernier point ne suffit pas pour créer un champ de saisie convivial. Je vais vous attribuer les points.
GONeale du
9
Fonctionne très bien! Voici un jsfiddle de la solution ci-dessus: jsfiddle.net/s5xAr/3
vaughan
4
Merci d'avoir posté du vrai JavaScript même si l'OP lâchait et voulait utiliser un framework.
John
cursorStart.appendChild(document.createTextNode('\u0002'));est un remplacement raisonnable que nous pensons. pour le - char. Merci pour le code
twobob
97

Cette solution fonctionne dans tous les principaux navigateurs:

saveSelection()est attaché aux événements onmouseupet onkeyupdu div et enregistre la sélection dans la variable savedRange.

restoreSelection()est attaché à l' onfocusévénement du div et resélectionne la sélection enregistrée dans savedRange.

Cela fonctionne parfaitement à moins que vous ne souhaitiez que la sélection soit restaurée lorsque l'utilisateur clique sur le div également (ce qui est un peu peu intuitif car normalement vous vous attendez à ce que le curseur aille là où vous cliquez mais le code inclus pour l'exhaustivité)

Pour ce faire, les événements onclicket onmousedownsont annulés par la fonction cancelEvent()qui est une fonction de navigateur croisé pour annuler l'événement. La cancelEvent()fonction exécute également la restoreSelection()fonction car lorsque l'événement de clic est annulé, le div ne reçoit pas le focus et par conséquent rien n'est sélectionné du tout à moins que cette fonction ne soit exécutée.

La variable isInFocusstocke si elle est dans le focus et est remplacée par "false" onbluret "true" onfocus. Cela permet d'annuler les événements de clic uniquement si le div n'est pas sélectionné (sinon vous ne pourrez pas du tout modifier la sélection).

Si vous souhaitez que la sélection soit modifiée lorsque le div est focalisé par un clic, et ne pas restaurer la sélection onclick(et uniquement lorsque le focus est donné à l'élément par programme en utilisant document.getElementById("area").focus();ou similaire, supprimez simplement les événements onclicket onmousedown. L' onblurévénement et les fonctions onDivBlur()et cancelEvent()peut également être retiré en toute sécurité dans ces circonstances.

Ce code devrait fonctionner s'il est déposé directement dans le corps d'une page html si vous souhaitez 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>
Nico Burns
la source
1
Merci cela fonctionne réellement! Testé dans IE, Chrome et FF les plus récents. Désolé pour la réponse super retardée =)
GONeale
Ne if (window.getSelection)...testera pas seulement si le navigateur prend en charge getSelection, pas s'il y a ou non une sélection?
Sandy Gifford
@Sandy Oui exactement. Cette partie du code détermine s'il faut utiliser l' getSelectionAPI standard ou l'ancienne document.selectionAPI utilisée par les anciennes versions d'IE. L' getRangeAt (0)appel ultérieur sera renvoyé nulls'il n'y a pas de sélection, ce qui est vérifié dans la fonction de restauration.
Nico Burns
@NicoBurns à droite, mais le code dans le deuxième bloc conditionnel ( else if (document.createRange)) est ce que je regarde. Il ne sera appelé que s'il window.getSelectionn'existe pas, mais utilise encorewindow.getSelection
Sandy Gifford
@NicoBurns en outre, je ne pense pas que vous trouverez un navigateur avec window.getSelectionmais pas document.createRange- ce qui signifie que le deuxième bloc ne serait jamais utilisé ...
Sandy Gifford
19

Mettre à jour

J'ai écrit une gamme de navigateurs et une bibliothèque de sélection appelée Rangy qui intègre une version améliorée du code que j'ai publié ci-dessous. Vous pouvez utiliser le module de sauvegarde et de restauration de sélection 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 des sélections dans votre projet et que vous n'avez pas besoin de la majeure partie d'un bibliothèque.

Réponse précédente

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

Je pourrais mettre quelque chose ensemble pour faire ceci, posterai de nouveau ici si et quand je le fais.

ÉDITER:

J'ai créé une démo d'un script qui fait cela. Cela 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 avec 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 de focus soit au début de la sélection et appuyer sur la touche curseur droite ou gauche déplacera le curseur vers une position par rapport au début de la sélection. Je ne pense pas qu'il soit possible de reproduire cela lors de la restauration d'une sélection, donc le nœud de focus est toujours à la fin de la sélection.

J'écrirai ceci complètement à un moment donné bientôt.

Tim Down
la source
15

J'ai eu une situation connexe, où j'avais spécifiquement besoin de définir la position du curseur sur la fin d'un div contenteditable. Je ne voulais pas utiliser une bibliothèque à part entière comme Rangy, et de nombreuses solutions étaient beaucoup trop lourdes.

En fin de compte, j'ai créé cette simple fonction jQuery pour définir la position carat à la fin d'un div contenteditable:

$.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: ajoutez une plage à la fin de la variable modifiable, sélectionnez-la, puis supprimez la plage - nous laissant avec un curseur à la fin de la division. Vous pouvez adapter cette solution pour insérer la travée où vous le souhaitez, plaçant ainsi le curseur à un endroit précis.

L'utilisation est simple:

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

C'est tout!

Zane Claes
la source
3
Vous n'avez pas besoin d'insérer le <span>, ce qui cassera accidentellement la pile d'annulation intégrée du navigateur. Voir stackoverflow.com/a/4238971/96100
Tim Down
6

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 supérieur:

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();
    }
});

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

Après avoir joué, j'ai modifié la réponse de eyelidlessness ci-dessus et en ai fait un plugin jQuery afin que vous puissiez simplement faire l'un de ceux-ci:

var html = "The quick brown fox";
$div.html(html);

// Select at the text "quick":
$div.setContentEditableSelection(4, 5);

// Select at the beginning of the contenteditable div:
$div.setContentEditableSelection(0);

// Select at the end of the contenteditable div:
$div.setContentEditableSelection(html.length);

Excusez le long code postal, mais cela peut aider quelqu'un:

$.fn.setContentEditableSelection = function(position, length) {
    if (typeof(length) == "undefined") {
        length = 0;
    }

    return this.each(function() {
        var $this = $(this);
        var editable = this;
        var selection;
        var range;

        var html = $this.html();
        html = html.substring(0, position) +
            '<a id="cursorStart"></a>' +
            html.substring(position, position + length) +
            '<a id="cursorEnd"></a>' +
            html.substring(position + length, html.length);
        console.log(html);
        $this.html(html);

        // 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
            }
        };

        // Slight delay will avoid the initial selection
        // (at start or of contents depending on browser) being mistaken
        setTimeout(function() {
            var cursorStart = document.getElementById('cursorStart');
            var 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();
                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);
                }
            }

            // Register selection again
            captureSelection();
        }, 10);
    });
};
mkaj
la source
3

Vous pouvez tirer parti de selectNodeContents qui est pris en charge par les navigateurs modernes.

var el = document.getElementById('idOfYoursContentEditable');
var selection = window.getSelection();
var range = document.createRange();
selection.removeAllRanges();
range.selectNodeContents(el);
range.collapse(false);
selection.addRange(range);
el.focus();
zoonman
la source
est-il possible de modifier ce code pour permettre à l'utilisateur final de pouvoir toujours déplacer le curseur vers n'importe quelle position qu'il souhaite?
Zabs
Oui. Vous devez utiliser les méthodes setStart & setEnd sur l'objet range. developer.mozilla.org/en-US/docs/Web/API/Range/setStart
zoonman
0

Dans Firefox, vous pouvez avoir le texte du div dans un nœud enfant ( o_div.childNodes[0])

var range = document.createRange();

range.setStart(o_div.childNodes[0],last_caret_pos);
range.setEnd(o_div.childNodes[0],last_caret_pos);
range.collapse(false);

var sel = window.getSelection(); 
sel.removeAllRanges();
sel.addRange(range);
yoav
la source