Establecer la posición del cursor en contentEditable <div>

142

Busco una solución definitiva de navegador cruzado para establecer la posición del cursor / cursor en la última posición conocida cuando un contentEditable = 'on' <div> recupera el foco. Parece que la funcionalidad predeterminada de un div editable de contenido es mover el cursor / cursor al comienzo del texto en el div cada vez que hace clic en él, lo que no es deseable.

Creo que tendría que almacenar en una variable la posición actual del cursor cuando dejan el foco del div, y luego volver a configurarlo cuando vuelvan a enfocar, pero no he podido juntar o encontrar un trabajo código de muestra todavía.

Si alguien tiene alguna idea, fragmentos de código de trabajo o muestras, me encantaría verlos.

Realmente todavía no tengo ningún código, 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 una entidad contentable )

GONeale
fuente
Yo no sabía que contentEditabletrabajaba en los navegadores no-IE O_O
Aditya
10
Sí lo hace aditya.
GONeale
55
Aditya, Safari 2+, Firefox 3+ creo.
párpado
Intente configurar tabindex = "0" en el div. Eso debería hacer que sea enfocable en la mayoría de los navegadores.
Tokimon

Respuestas:

58

Esto es compatible con los navegadores basados ​​en estándares, pero probablemente fallará en IE. Lo estoy proporcionando 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);
};
párpado
fuente
Gracias ojo, probé su solución, tenía un poco de prisa pero después de conectarlo, solo coloca la posición "-" en el último punto de enfoque (¿qué parece ser un marcador de depuración?) Y ahí es cuando perdemos enfoque, no parece restaurar el cursor / cursor cuando hago clic de nuevo (al menos no en Chrome, intentaré FF), solo va al final del div. Así que aceptaré la solución de Nico porque sé que es compatible en todos los navegadores y tiende a funcionar bien. Sin embargo, muchas gracias por tu esfuerzo.
GONeale
3
¿Sabes qué? Olvida mi última respuesta, después de examinar más a fondo la tuya y 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. La suya establece correctamente la posición del cursor donde hace clic cuando activa el foco de nuevo a <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 otorgaré los puntos.
GONeale
9
¡Funciona genial! Aquí hay un jsfiddle de la solución anterior: jsfiddle.net/s5xAr/3
vaughan
44
Gracias por publicar JavaScript real a pesar de que el OP desapareció y quería usar un marco.
John
cursorStart.appendChild(document.createTextNode('\u0002'));Es un reemplazo razonable, creemos. para el - char. Gracias por el código
twobob
97

Esta solución funciona en todos los principales navegadores:

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 se restaure la selección cuando el usuario hace clic en el div también (lo cual es un poco poco intuitivo, ya que normalmente espera que el cursor vaya donde hace clic, pero el código está incluido 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 a medida que se cancela el evento de clic, el div no recibe el foco y, por lo tanto, no se selecciona nada a menos que se ejecute esta función.

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

Si desea que la selección se cambie cuando el div se enfoca con un clic y no se restaura la selección onclick(y solo cuando se le da 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 eliminar de forma segura en estas circunstancias.

Este código debería funcionar si se deja caer 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>
Nico Burns
fuente
1
Gracias, esto realmente funciona! Probado en IE, Chrome y FF más recientes. Perdón por la respuesta súper retrasada =)
GONeale
¿No if (window.getSelection)...solo probará si el navegador admite getSelection, no si hay o no una selección?
Sandy Gifford
@ Sandy Sí, exactamente. Esta parte del código decide si usar la getSelectionAPI estándar o la document.selectionAPI heredada utilizada por versiones anteriores de IE. La getRangeAt (0)llamada posterior volverá nullsi no hay una selección, que se verifica en la función de restauración.
Nico Burns,
@NicoBurns a la derecha, 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
Sandy Gifford
@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 ...
Sandy Gifford
19

Actualizar

He escrito 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 la selección para esta pregunta en particular, aunque estaría tentado de 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 TextRange de IE en algo así como un rango DOM y usarlo junto con algo como el punto de partida de la falta de párpados. Personalmente, solo usaría los algoritmos de IERange que realizan 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, volveré a publicar aquí si lo hago.

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 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.

Tim Down
fuente
15

Tuve una situación relacionada, donde específicamente necesitaba 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 simple función jQuery para establecer la posición en quilates al final de 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 teoría es simple: agregue un intervalo al final de lo 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 el cursor en un lugar específico.

El uso es simple:

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

¡Eso es!

Zane Claes
fuente
3
No es necesario insertar el <span>, que incidentalmente romperá la pila de deshacer incorporada en el navegador. Ver stackoverflow.com/a/4238971/96100
Tim Down
6

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

  • Genérico: por cada div contentEditable="true"
  • Corta

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

Gatsbimantico
fuente
@salivan Sé que es tarde para actualizarlo, pero creo que funciona ahora. 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 :)
Gatsbimantico
4

Después de jugar, modifiqué la respuesta anterior de párpado y lo convertí en un complemento jQuery para que pueda hacer uno de estos:

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 larga de código, 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);
    });
};
mkaj
fuente
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();
zoonman
fuente
¿Es posible modificar este código para permitir que el usuario final pueda mover el cursor a la posición que desee?
Zabs
Si. Debe usar los métodos setStart & setEnd en el objeto de rango. developer.mozilla.org/en-US/docs/Web/API/Range/setStart
zoonman
0

En Firefox, puede tener 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);
yoav
fuente