web-dev-qa-db-fra.com

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

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

Je crois que je devrais enregistrer dans une variable la position actuelle du curseur quand ils quittent le focus de la div, puis le réinitialiser quand ils ont encore le focus à l'intérieur, mais je n'ai pas été en mesure de rassembler ou de trouver échantillon de code pour le moment.

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

Je n'ai pas encore 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 que cela ne fonctionne pas pour un <div>. Peut-être uniquement pour textarea ( Comment déplacer le curseur à la fin de l'entité contenteditable )

139
GONeale

Ceci est compatible avec les navigateurs standards, mais échouera probablement dans IE. Je le fournis 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);
};
58
eyelidlessness

Cette solution fonctionne dans tous les principaux navigateurs:

saveSelection() est associé aux événements onmouseup et onkeyup du div et enregistre la sélection dans la variable savedRange.

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

Cela fonctionne parfaitement sauf si vous souhaitez que la sélection soit restaurée lorsque l'utilisateur clique sur la division (ce qui n'est pas très intuitif, car vous vous attendez à ce que le curseur se place à l'endroit où vous cliquez, mais que le code est complet)

Pour ce faire, les événements onclick et onmousedown sont annulés par la fonction cancelEvent(), une fonction de navigation transversale permettant d'annuler l'événement. La fonction cancelEvent() exécute également la fonction restoreSelection() car l'événement de clic est annulé, la div ne reçoit pas le focus et par conséquent rien n'est sélectionné, à moins que cette fonction ne soit exécutée.

La variable isInFocus enregistre si elle est active et est remplacée par "false" onblur et "true" onfocus. Cela permet aux événements de clic d'être annulés uniquement si la div n'est pas active (sinon, vous ne pourriez pas modifier la sélection du tout).

Si vous souhaitez que la sélection soit modifiée lorsque la div est focalisée par un clic, et ne restaurez pas la sélection onclick (et uniquement lorsque l'accent est mis sur l'élément par programmation en utilisant document.getElementById("area").focus(); ou similaire puis supprimez simplement les événements onclick et onmousedown. L'événement onblur et les fonctions onDivBlur() et cancelEvent() peuvent également être supprimés 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>
95
Nico Burns

Mettre à jour

J'ai écrit une bibliothèque inter-navigateurs et de sélections appelée Rangy qui intègre une version améliorée du code que j'ai posté ci-dessous. Vous pouvez utiliser le module de sauvegarde et de restauration de la sélection pour cette question particulière, bien que je serais tenté d'utiliser quelque chose comme réponse de @ Nico Burns si vous ne faites rien d'autre. avec des sélections dans votre projet et n'avez pas besoin de la majeure partie 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 une plage DOM et l'utiliser conjointement avec quelque chose comme le point de départ de eyelidlessness. 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 ne possède pas les propriétés focusNode et anchorNode, mais vous devriez pouvoir simplement utiliser les valeurs Range/TextRange obtenues à partir de la sélection.

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

MODIFIER:

J'ai créé une démo d'un script qui fait cela. Cela fonctionne dans tout ce que j'ai essayé jusqu'ici, à 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 navigateurs peuvent effectuer des sélections à l'envers de manière à ce que le nœud de focus se trouve au début de la sélection et que vous appuyiez sur la touche du curseur droit ou gauche pour déplacer le curseur à une position relative 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. Le nœud de focus est donc toujours à la fin de la sélection.

Je vais écrire cela complètement à un moment donné.

19
Tim Down

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

En fin de compte, j’ai eu 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 un intervalle à la fin du texte modifiable, sélectionnez-le, puis supprimez-le - nous laissant un curseur à la fin du div. Vous pouvez adapter cette solution pour insérer la plage où vous le souhaitez, plaçant ainsi le curseur à un endroit spécifique.

L'utilisation est simple:

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

C'est ça!

15
Zane Claes

J'ai pris la réponse de Nico Burns à l'aide de 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();
    }
});
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>
7
Gatsbimantico

Après avoir joué, j'ai modifié la réponse de eyelidlessness ci-dessus et en ai fait un plugin jQuery pour que vous puissiez simplement faire l'une de celles-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 code long, 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);
    });
};
4
mkaj

Vous pouvez utiliser 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();
1
zoonman

Dans Firefox, vous pourriez avoir le texte de la 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);
0
yoav