Establecer la posición del cursor en contentEditable

148

Estoy buscando una solución definitiva para todos los navegadores para establecer la posición del cursor / cursor en la última posición conocida cuando un contentEditable = 'on' <div> recupera el enfoque. Parece que la funcionalidad predeterminada de un div editable de contenido es mover el cursor / cursor al principio del texto en el div cada vez que hace clic en él, lo cual no es deseable.

Creo que tendría que almacenar en una variable la posición actual del cursor cuando están dejando el foco del div, y luego volver a establecer esto cuando tengan el foco dentro de nuevo, pero no he podido juntar o encontrar un trabajo muestra de código todavía.

Si alguien tiene alguna idea, fragmentos de código que funcionan o ejemplos, me encantaría verlos.

Realmente no tengo ningún código todavía, pero esto es lo que tengo:

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

PD. He probado este recurso pero parece que no funciona para un <div>. Quizás solo para textarea ( Cómo mover el cursor al final de la entidad contenteditable )

4
  • No sabía que contentEditablefuncionaba en navegadores que no son IE o_o
    aditya
    25 de julio de 2009 a las 15:14
  • 10
    Sí lo hace aditya.
    GONeale
    27 de julio de 2009 a las 1:39
  • 5
    aditya, Safari 2+, Firefox 3+, creo. 28 de julio de 2009 a las 6:46
  • Intente establecer tabindex = "0" en el div. Eso debería hacer que se pueda enfocar en la mayoría de los navegadores.
    Tokimon
    14/06/10 a las 13:24
101

Esta solución funciona en todos los navegadores principales:

saveSelection()se adjunta a los eventos onmouseupy onkeyupdel div y guarda la selección en la variable savedRange.

restoreSelection()se adjunta al onfocusevento del div y vuelve a seleccionar la selección guardada en savedRange.

Esto funciona perfectamente a menos que desee que la selección se restaure cuando el usuario haga clic en el div también (lo cual no es un poco intuitivo, ya que normalmente espera que el cursor vaya donde hace clic, pero el código se incluye para completar)

Para lograr esto, los eventos onclicky onmousedownson cancelados por la función cancelEvent()que es una función de navegador cruzado para cancelar el evento. La cancelEvent()función también ejecuta la restoreSelection()función porque cuando se cancela el evento de clic, el div no recibe el foco y, por lo tanto, no se selecciona nada a menos que se ejecuten estas funciones.

La variable isInFocusalmacena si está enfocada y se cambia a "falso" onblury "verdadero" onfocus. Esto permite que los eventos de clic se cancelen solo si el div no está enfocado (de lo contrario, no podría cambiar la selección en absoluto).

Si desea que se cambie la selección cuando el div se enfoca con un clic, y no restaurar la selección onclick(y solo cuando se le da el foco al elemento mediante programación document.getElementById("area").focus();o similar, simplemente elimine los eventos onclicky onmousedown. El onblurevento y las funciones onDivBlur()y cancelEvent()también se puede quitar de forma segura en estas circunstancias.

Este código debería funcionar si se coloca directamente en el cuerpo de una página html si desea probarlo rápidamente:

<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>
6
  • 1
    ¡Gracias, esto realmente funciona! Probado en IE, Chrome y FF más reciente. Perdón por la respuesta súper retrasada =)
    GONeale
    6 de septiembre de 2010 a las 1:02
  • ¿No if (window.getSelection)...solo probará si el navegador es compatible getSelection, no si hay una selección o no? 25/04/18 a las 15:04
  • @Sandy Sí exactamente. Esta parte del código es decidir si usar la getSelectionapi estándar o la document.selectionapi heredada utilizada por versiones anteriores de IE. La última getRangeAt (0)llamada volverá nullsi no hay ninguna selección, que se comprueba en la función de restauración. 5 de mayo de 2018 a las 19:45
  • 1
    @NicoBurns correcto, pero el código en el segundo bloque condicional ( else if (document.createRange)) es lo que estoy viendo. Solo se llamará si window.getSelectionno existe, pero usawindow.getSelection 5 de mayo de 2018 a las 20:10
  • 1
    @NicoBurns además, no creo que encuentres un navegador con window.getSelectionpero no document.createRange, lo que significa que el segundo bloque nunca se usaría ... 5 de mayo de 2018 a las 20:11
59

Esto es compatible con los navegadores basados ​​en estándares, pero probablemente fallará en IE. Lo proporciono como punto de partida. IE no es compatible con 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);
};
6
  • Gracias ojo, probé tu solución, tenía un poco de prisa, pero después de conectarlo, solo coloca la posición "-" en el último punto de enfoque (¿que parece ser un marcador de depuración?) Y ahí es cuando perdemos focus, no parece restaurar el cursor / cursor cuando hago clic en atrás (al menos no en Chrome, intentaré FF), simplemente va al final del div. Así que aceptaré la solución de Nico porque sé que es compatible con todos los navegadores y suele funcionar bien. Sin embargo, muchas gracias por tu esfuerzo.
    GONeale
    6 de septiembre de 2010 a las 1:16
  • 3
    ¿Sabes qué ?, olvídate de mi última respuesta, después de examinar más a fondo tanto la tuya como la de Nico, la tuya no es lo que pedí en mi descripción, pero es lo que prefiero y me habría dado cuenta de que necesito. El suyo establece correctamente la posición del cursor de donde hace clic al activar el enfoque de nuevo al <div>, como un cuadro de texto normal. Restaurar el enfoque al último punto no es suficiente para crear un campo de entrada fácil de usar. Te concederé los puntos.
    GONeale
    6 de septiembre de 2010 a las 1:24
  • 10
    ¡Funciona genial! Aquí hay un jsfiddle de la solución anterior: jsfiddle.net/s5xAr/3
    vaughan
    3 de septiembre de 2011 a las 2:45
  • 5
    Gracias por publicar JavaScript real a pesar de que el OP falló y quería usar un marco.
    John
    19/10/2014 a las 15:46
  • cursorStart.appendChild(document.createTextNode('\u0002'));Creemos que es un reemplazo razonable. para el - char. Gracias por el codigo
    twobob
    29 de febrero de 2016 a las 17:07
20

Actualizar

Escribí una biblioteca de selección y rango entre navegadores llamada Rangy que incorpora una versión mejorada del código que publiqué a continuación. Puede usar el módulo de guardar y restaurar selección para esta pregunta en particular, aunque me sentiría tentado a usar algo como la respuesta de @Nico Burns si no está haciendo nada más con las selecciones en su proyecto y no necesita la mayor parte de un Biblioteca.

Respuesta anterior

Puede usar IERange ( http://code.google.com/p/ierange/ ) para convertir el TextRange de IE en algo así como un DOM Range y usarlo junto con algo como el punto de partida de eyelidlessness. Personalmente, solo usaría los algoritmos de IERange que hacen las conversiones Range <-> TextRange en lugar de usar todo. Y el objeto de selección de IE no tiene las propiedades focusNode y anchorNode, pero debería poder usar el Range / TextRange obtenido de la selección en su lugar.

Podría armar algo para hacer esto, lo publicaré aquí si lo hago y cuando lo haga.

EDITAR:

He creado una demostración de un script que hace esto. Funciona en todo lo que he probado hasta ahora, excepto por un error en Opera 9, que aún no he tenido tiempo de analizar. Los navegadores en los que funciona son IE 5.5, 6 y 7, Chrome 2, Firefox 2, 3 y 3.5 y Safari 4, todos en Windows.

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

Tenga en cuenta que las selecciones se pueden hacer hacia atrás en los navegadores para que el nodo de enfoque esté al comienzo de la selección y al presionar la tecla de cursor derecha o izquierda se moverá el cursor a una posición relativa al comienzo de la selección. No creo que sea posible replicar esto al restaurar una selección, por lo que el nodo de enfoque siempre está al final de la selección.

Escribiré esto completamente en algún momento pronto.

16

Tuve una situación relacionada, en la que necesitaba específicamente establecer la posición del cursor al FINAL de un div contenteditable. No quería usar una biblioteca completa como Rangy, y muchas soluciones eran demasiado pesadas.

Al final, se me ocurrió esta función simple de jQuery para establecer la posición del quilate al final de un div contento:

$.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 teoría es simple: agregue un intervalo al final del editable, selecciónelo y luego elimine el intervalo, dejándonos con un cursor al final del div. Puede adaptar esta solución para insertar el tramo donde desee, colocando así el cursor en un lugar específico.

El uso es simple:

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

¡Eso es todo!

1
7

Tomé la respuesta de Nico Burns y la hice usando jQuery:

  • Genérico: para todos div contentEditable="true"
  • Más corta

Necesitará jQuery 1.6 o superior:

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>
1
  • @salivan Sé que es tarde para actualizarlo, pero creo que ahora funciona. Básicamente, agregué una nueva condición y cambié de usar la identificación del elemento al índice del elemento, que debería existir siempre :) 28 de enero de 2015 a las 23:14
5

Después de jugar, modifiqué la respuesta anterior de eyelidlessness y la convertí en un complemento de jQuery para que pueda hacer una de estas:

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

Disculpe la publicación de código largo, pero puede ayudar a alguien:

$.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);
    });
};
3

Puede aprovechar selectNodeContents, que es compatible con los navegadores modernos.

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();
2
0

En Firefox, es posible que tenga el texto del div en un nodo secundario ( 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);