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

Resuelto GONeale asked hace 15 años • 9 respuestas

Estoy buscando una solución definitiva para varios navegadores para establecer la posición del cursor/carácter en la última posición conocida cuando contentEditable='on' <div> recupera el foco. Parece que la funcionalidad predeterminada de un div de contenido editable es mover el cursor/carácter 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 salen del foco del div, y luego restablecer esto cuando vuelvan a tener el foco dentro, pero no he podido armarlo ni encontrar un método que funcione. muestra de código todavía.

Si alguien tiene alguna idea, fragmentos de código de trabajo o muestras, estaré encantado de 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. Probé este recurso pero parece que no funciona para un <div>. Quizás solo para el área de texto ( Cómo mover el cursor al final de una entidad contenta )

GONeale avatar Jul 25 '09 16:07 GONeale
Aceptado

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 restablezca cuando el usuario también haga clic en el div (lo cual es un poco poco intuitivo ya que normalmente espera que el cursor vaya donde hace clic, pero se incluye código para que esté completo)

Para lograr esto, los eventos onclicky onmousedownse cancelan mediante la función cancelEvent(), que es una función de varios navegadores 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 ejecute esta función.

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

Si desea que la selección cambie cuando el div está enfocado con un clic, y no restaurar la selección onclick(y solo cuando el foco se le da 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 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>
Nico Burns avatar Jul 24 '2010 04:07 Nico Burns

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 el rango 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);
};
eyelidlessness avatar Jul 28 '2009 08:07 eyelidlessness

Actualizar

Escribí una biblioteca de selección y rango para varios 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 estarí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 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 Rango <-> TextRange en lugar de usar todo. Y el objeto de selección de IE no tiene las propiedades focusNode y AnchorNode, pero en su lugar debería poder usar el Rango/TextRange obtenido de la selección.

Podría preparar algo para hacer esto, lo publicaré aquí cuando lo haga.

EDITAR:

Creé 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 investigar. 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 realizar hacia atrás en los navegadores, de modo que el nodo de enfoque esté al inicio de la selección y al presionar la tecla de cursor derecha o izquierda se moverá el cursor a una posición relativa al inicio 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 avatar Aug 14 '2009 17:08 Tim Down

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

  • Genérico: Para cadadiv contentEditable="true"
  • 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();
    }
});

Mostrar fragmento de código

Gatsbimantico avatar Apr 22 '2013 19:04 Gatsbimantico

Después de jugar, modifiqué la respuesta anterior sobre la falta de párpados y la convertí en un complemento de jQuery para que puedas 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 del 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);
    });
};
mkaj avatar Aug 17 '2013 08:08 mkaj