Definir a posição do cursor em contentEditable <div>

142

Estou atrás de uma solução definitiva entre navegadores para definir a posição do cursor / cursor para a última posição conhecida quando um contentEditable = 'on' <div> recupera o foco. Parece que a funcionalidade padrão de uma div editável de conteúdo é mover o cursor / cursor para o início do texto na div toda vez que você clicar nele, o que é indesejável.

Eu acredito que teria que armazenar em uma variável a posição atual do cursor quando eles estiverem deixando o foco da div e reconfigurá-la quando eles tiverem o foco interno novamente, mas não consegui montar ou encontrar um trabalho amostra de código ainda.

Se alguém tiver alguma ideia, trechos de código de trabalho ou amostras, ficaria feliz em vê-los.

Eu realmente não tenho nenhum código ainda, mas aqui está o que eu tenho:

<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. Eu tentei esse recurso, mas parece que ele não funciona para um <div>. Talvez apenas para a área de texto ( Como mover o cursor para o final da entidade editável por conteúdo )

GONeale
fonte
Eu não sabia que contentEditabletrabalhava em navegadores não-IE o_o
aditya
10
Sim, aditya.
31510 GONeale
5
aditya, Safari 2+, Firefox 3+ eu acho.
#
Tente definir tabindex = "0" na div. Isso deve torná-lo focável na maioria dos navegadores.
Tokimon

Respostas:

58

Isso é compatível com os navegadores baseados em padrões, mas provavelmente falhará no IE. Estou fornecendo isso como ponto de partida. O IE não suporta a faixa 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);
};
ausência de pálpebra
fonte
Obrigado, tentei a sua solução, estava com pressa, mas depois de ligá-la, ela coloca apenas a posição "-" no último ponto de foco (que parece ser um marcador de depuração?) E é aí que perdemos foco, ele não parece restaurar o cursor / sinal de intercalação quando clico em voltar (pelo menos não no Chrome, tentarei o FF), apenas vai para o final da div. Por isso, aceitarei a solução do Nico porque sei que é compatível em todos os navegadores e tende a funcionar bem. Muito obrigado pelo seu esforço.
GONeale
3
Você sabe o que, esqueça a minha última resposta, depois de examinar mais a sua e a de Nico, a sua não é o que eu pedi na minha descrição, mas é o que eu prefiro e teria percebido que preciso. O seu define corretamente a posição do cursor de onde você clica ao ativar o foco de volta ao <div>, como uma caixa de texto comum. Restaurar o foco até o último ponto não é suficiente para criar um campo de entrada fácil de usar. Vou lhe dar os pontos.
GONeale
9
Funciona bem! Aqui está um jsfiddle da solução acima: jsfiddle.net/s5xAr/3
vaughan
4
Obrigado por postar JavaScript real, embora o OP tenha divulgado e quisesse usar uma estrutura.
John John
cursorStart.appendChild(document.createTextNode('\u0002'));é um substituto razoável que pensamos. para o - char. Obrigado pelo código
twobob
97

Esta solução funciona em todos os principais navegadores:

saveSelection()é anexado ao onmouseupe onkeyupeventos da div e salva a seleção para a variável savedRange.

restoreSelection()é anexado ao onfocusevento da div e seleciona novamente a seleção salva em savedRange.

Isso funciona perfeitamente, a menos que você queira que a seleção seja restaurada quando o usuário clicar na div também (o que é um pouco pouco intuitivo, pois normalmente você espera que o cursor vá para onde você clica, mas o código está incluso)

Para conseguir isso, os eventos onclicke onmousedownsão cancelados pela função cancelEvent()que é uma função de navegador cruzado para cancelar o evento. A cancelEvent()função também executa a restoreSelection()função porque, como o evento click é cancelado, a div não recebe foco e, portanto, nada é selecionado, a menos que essas funções sejam executadas.

A variável isInFocusarmazena se está em foco e é alterada para "false" onblure "true" onfocus. Isso permite que os eventos de clique sejam cancelados apenas se a div não estiver em foco (caso contrário, você não poderá alterar a seleção).

Se você deseja a seleção para ser a mudança quando o div é focado através de um clique, e não restaurar a seleção onclick(e somente quando o foco é dado ao elemento programtically usando document.getElementById("area").focus();ou similar, então simplesmente remover os onclicke onmousedowneventos. O onblurevento eo onDivBlur()e cancelEvent()funções também pode ser removido com segurança nessas circunstâncias.

Esse código deve funcionar se cair diretamente no corpo de uma página html, se você quiser testá-lo rapidamente:

<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
fonte
1
Obrigado, isso realmente funciona! Testado no IE, Chrome e FF mais recente. Desculpe a resposta super-atrasada =)
GONeale
Não if (window.getSelection)...testará apenas se o navegador suporta getSelection, não se há ou não uma seleção?
Sandy Gifford
@ Sandy Sim exatamente. Esta parte do código está decidindo se deve usar a getSelectionAPI padrão ou a document.selectionAPI herdada usada pelas versões mais antigas do IE. A getRangeAt (0)chamada posterior retornará nullse não houver nenhuma seleção, que é verificada na função de restauração.
Nico Burns
@NicoBurns à direita, mas o código no segundo bloco condicional ( else if (document.createRange)) é o que estou vendo. Só será chamado se window.getSelectionnão existir, mas usa #window.getSelection
Sandy Gifford
@NicoBurns além disso, eu não acho que você gostaria de encontrar um navegador com window.getSelection, mas não document.createRange- o que significa o segundo bloco nunca ser usado ...
Sandy Gifford
19

Atualizar

Eu escrevi uma biblioteca de seleção e intervalo entre navegadores chamada Rangy que incorpora uma versão aprimorada do código que publiquei abaixo. Você pode usar o módulo de seleção salvar e restaurar para esta pergunta em particular, embora eu fique tentado a usar algo como a resposta de @Nico Burns se você não estiver fazendo mais nada com seleções em seu projeto e não precisar da maior parte de um biblioteca.

Resposta anterior

Você pode usar o IERange ( http://code.google.com/p/ierange/ ) para converter o TextRange do IE em algo como um intervalo DOM e usá-lo em conjunto com algo como o ponto de partida da ausência de pálpebra. Pessoalmente, eu usaria apenas os algoritmos de IERange que fazem as conversões Range <-> TextRange em vez de usar a coisa toda. E o objeto de seleção do IE não possui as propriedades focusNode e anchorNode, mas você deve usar apenas o Range / TextRange obtido a partir da seleção.

Eu posso montar algo para fazer isso, postarei aqui se e quando eu fizer.

EDITAR:

Eu criei uma demonstração de um script que faz isso. Funciona em tudo o que tentei até agora, exceto por um bug no Opera 9, que ainda não tive tempo de analisar. Os navegadores em que trabalha são IE 5.5, 6 e 7, Chrome 2, Firefox 2, 3 e 3.5 e Safari 4, todos no Windows.

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

Observe que as seleções podem ser feitas ao contrário nos navegadores, para que o nó de foco esteja no início da seleção e pressionar a tecla do cursor para a direita ou para a esquerda moverá o cursor para uma posição relativa ao início da seleção. Eu não acho que é possível replicar isso ao restaurar uma seleção, portanto o nó de foco está sempre no final da seleção.

Escreverei isso completamente em algum momento em breve.

Tim Down
fonte
15

Eu tive uma situação relacionada, onde especificamente precisei definir a posição do cursor para END de uma div editável por conteúdo. Eu não queria usar uma biblioteca completa como Rangy, e muitas soluções eram muito pesadas.

No final, criei essa função jQuery simples para definir a posição do quilate no final de uma div editável por conteúdo:

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

A teoria é simples: acrescente uma extensão ao final do editável, selecione-a e remova a extensão - deixando-nos com um cursor no final da div. Você pode adaptar esta solução para inserir a extensão onde quiser, colocando o cursor em um ponto específico.

O uso é simples:

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

É isso aí!

Zane Claes
fonte
3
Você não precisa inserir o <span>, o que acidentalmente quebrará a pilha de desfazer interna do navegador. Veja stackoverflow.com/a/4238971/96100
Tim Down
6

Peguei a resposta de Nico Burns e usei o jQuery:

  • Genérico: para cada div contentEditable="true"
  • Mais curta

Você precisará do jQuery 1.6 ou 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
fonte
@ Salivan Eu sei que é tarde para atualizá-lo, mas acho que funciona agora. Basicamente eu adicionei uma nova condição e mudou de usar id do elemento para o índice do elemento, wich deve existir sempre :)
Gatsbimantico
4

Depois de brincar, modifiquei a resposta da ausência de pálpebras acima e criei um plugin jQuery para que você possa fazer um destes:

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

Desculpe a publicação longa do código, mas isso pode ajudar alguém:

$.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
fonte
3

Você pode aproveitar o selectNodeContents que é suportado pelos 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
fonte
é possível modificar esse código para permitir que o usuário final ainda possa mover o cursor para qualquer posição que desejar?
Zabs
Sim. Você deve usar os métodos setStart e setEnd no objeto range. developer.mozilla.org/pt-BR/docs/Web/API/Range/setStart #
21418 zoonman
0

No Firefox, você pode ter o texto da div em um nó filho ( 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
fonte