Imposta la posizione del cursore su contentEditable <div>


142

Sto cercando una soluzione definitiva e cross-browser per impostare la posizione cursore / cursore sull'ultima posizione nota quando un contentEditable = 'on' <div> riacquista il focus. Sembra che la funzionalità predefinita di un div modificabile del contenuto sia spostare il cursore / cursore all'inizio del testo nel div ogni volta che si fa clic su di esso, il che è indesiderabile.

Credo che dovrei memorizzare in una variabile la posizione corrente del cursore quando stanno lasciando il focus del div, e quindi reimpostarlo quando hanno nuovamente il focus all'interno, ma non sono stato in grado di mettere insieme o trovare un lavoro codice di esempio ancora.

Se qualcuno ha qualche idea, frammenti di codice funzionanti o campioni sarei felice di vederli.

Non ho ancora alcun codice, ma ecco cosa ho:

<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. Ho provato questa risorsa ma sembra che non funzioni per un <div>. Forse solo per textarea ( Come spostare il cursore alla fine dell'entità contenteditable )


Non sapevo che contentEditablefunzionasse con browser non IE o_o
aditya,

10
Sì, fa aditya.
GONeale,

5
aditya, Safari 2+, Firefox 3+ penso.
mancanza di palpebre

Prova a impostare tabindex = "0" sul div. Ciò dovrebbe renderlo focalizzabile nella maggior parte dei browser.
Tokimon,

Risposte:


58

Questo è compatibile con i browser basati su standard, ma probabilmente fallirà in IE. Lo sto fornendo come punto di partenza. IE non supporta 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);
};

Grazie occhio, ho provato la tua soluzione, avevo un po 'di fretta ma dopo averlo collegato, posiziona solo la posizione "-" all'ultimo punto di messa a fuoco (che sembra essere un marcatore di debug?) Ed è allora che perdiamo messa a fuoco, non sembra ripristinare il cursore / cursore quando clicco indietro (almeno non in Chrome, proverò FF), va solo alla fine del div. Accetterò quindi la soluzione di Nico perché so che è compatibile in tutti i browser e tende a funzionare bene. Grazie mille per il tuo impegno.
GONeale,

3
Sai cosa, dimentica la mia ultima risposta, dopo aver esaminato ulteriormente la tua e quella di Nico, la tua non è ciò che ho chiesto nella mia descrizione, ma è ciò che preferisco e avrei realizzato di aver bisogno. Yours imposta correttamente la posizione del cursore del punto in cui si fa clic quando si riattiva lo stato attivo su <div>, come una normale casella di testo. Ripristinare lo stato attivo fino all'ultimo punto non è sufficiente per creare un campo di inserimento intuitivo. Ti assegnerò i punti.
GONeale,

9
Funziona alla grande! Ecco una jsfiddle della soluzione di cui sopra: jsfiddle.net/s5xAr/3
Vaughan

4
Grazie per aver pubblicato JavaScript reale anche se l'OP si è ritirato e voleva utilizzare un framework.
Giovanni,

cursorStart.appendChild(document.createTextNode('\u0002'));è una ragionevole sostituzione che pensiamo. per il - char. Grazie per il codice
twobob

97

Questa soluzione funziona in tutti i principali browser:

saveSelection()è associato agli eventi onmouseupe onkeyupdel div e salva la selezione nella variabile savedRange.

restoreSelection()è associato onfocusall'evento del div e seleziona nuovamente la selezione salvata in savedRange.

Funziona perfettamente a meno che tu non voglia ripristinare la selezione quando l'utente fa clic anche sul div (il che è un po 'poco intuitivo come normalmente ti aspetti che il cursore vada dove fai clic ma il codice è incluso per completezza)

A tale scopo, gli eventi onclicke onmousedownvengono annullati dalla funzione cancelEvent()che è una funzione cross-browser per annullare l'evento. La cancelEvent()funzione esegue anche la restoreSelection()funzione perché quando l'evento click viene annullato il div non riceve lo stato attivo e quindi nulla viene selezionato affatto a meno che questa funzione non venga eseguita.

La variabile isInFocusmemorizza se è attiva e viene modificata in "false" onblure "true" onfocus. Ciò consente di annullare gli eventi di clic solo se il div non è attivo (altrimenti non saresti in grado di modificare affatto la selezione).

Se desideri che la selezione venga modificata quando il div è focalizzato da un clic e non ripristina la selezione onclick(e solo quando il focus è dato all'elemento programmaticamente usando document.getElementById("area").focus();o simili, rimuovi semplicemente gli eventi onclicke onmousedown. L' onblurevento e le funzioni onDivBlur()e cancelEvent()può anche essere rimosso in modo sicuro in queste circostanze.

Questo codice dovrebbe funzionare se rilasciato direttamente nel corpo di una pagina HTML se si desidera testarlo 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>

1
Grazie, funziona davvero! Testato su IE, Chrome e FF più recenti.
Ci

Non if (window.getSelection)...testerà solo se il browser supporta getSelection, non se esiste o meno una selezione?
Sandy Gifford,

@Sandy Sì, esattamente. Questa parte del codice decide se utilizzare l' getSelectionAPI standard o l' document.selectionAPI legacy utilizzato dalle versioni precedenti di IE. La getRangeAt (0)chiamata successiva verrà restituita nullse non è presente alcuna selezione, che viene verificata nella funzione di ripristino.
Nico Burns,

@NicoBurns, ma il codice nel secondo blocco condizionale ( else if (document.createRange)) è quello che sto guardando. Verrà chiamato solo se window.getSelectionnon esiste, ma utilizzawindow.getSelection
Sandy Gifford

@NicoBurns, inoltre, non credo che troverai un browser con, window.getSelectionma non document.createRange- il che significa che il secondo blocco non verrebbe mai usato ...
Sandy Gifford,

19

Aggiornare

Ho scritto un intervallo tra browser e una libreria di selezione chiamata Rangy che incorpora una versione migliorata del codice che ho pubblicato di seguito. Puoi utilizzare il modulo di salvataggio e ripristino della selezione per questa particolare domanda, anche se sarei tentato di usare qualcosa come la risposta di @Nico Burns se non stai facendo altro con le selezioni nel tuo progetto e non hai bisogno della maggior parte di un biblioteca.

Risposta precedente

Puoi utilizzare IERange ( http://code.google.com/p/ierange/ ) per convertire TextRange di IE in qualcosa di simile a un intervallo DOM e utilizzarlo insieme a qualcosa come il punto di partenza della palpebra. Personalmente userei solo gli algoritmi di IERange che eseguono le conversioni Range <-> TextRange invece di usare tutto. E l'oggetto di selezione di IE non ha le proprietà focusNode e anchorNode, ma dovresti essere in grado di usare semplicemente Range / TextRange ottenuto dalla selezione.

Potrei mettere insieme qualcosa per fare questo, riporterò qui se e quando lo farò.

MODIFICARE:

Ho creato una demo di uno script che fa questo. Funziona in tutto ciò che ho provato finora, tranne per un bug in Opera 9, che non ho ancora avuto il tempo di esaminare. I browser in cui funziona sono IE 5.5, 6 e 7, Chrome 2, Firefox 2, 3 e 3.5 e Safari 4, tutti su Windows.

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

Si noti che le selezioni possono essere fatte all'indietro nei browser in modo che il nodo attivo sia all'inizio della selezione e premendo il tasto cursore destro o sinistro si sposta il cursore in una posizione relativa all'inizio della selezione. Non penso che sia possibile replicarlo quando si ripristina una selezione, quindi il nodo attivo è sempre alla fine della selezione.

Presto lo scriverò per intero.


15

Avevo una situazione correlata, in cui avevo specificamente bisogno di impostare la posizione del cursore sulla FINE di un div contenteditable. Non volevo usare una libreria a tutti gli effetti come Rangy, e molte soluzioni erano decisamente troppo pesanti.

Alla fine, mi è venuta in mente questa semplice funzione jQuery per impostare la posizione in carati alla fine di 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 teoria è semplice: aggiungi un intervallo alla fine del modificabile, selezionalo e quindi rimuovi l'intervallo - lasciandoci con un cursore alla fine del div. È possibile adattare questa soluzione per inserire l'intervallo dove desiderato, posizionando il cursore in un punto specifico.

L'uso è semplice:

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

Questo è tutto!


3
Non è necessario inserire l'opzione <span>, che interromperà accidentalmente lo stack di annullamento incorporato nel browser. Vedi stackoverflow.com/a/4238971/96100
Tim Down

6

Ho preso la risposta di Nico Burns e l'ho fatta usando jQuery:

  • Generico: per tutti div contentEditable="true"
  • Più breve

Avrai bisogno di jQuery 1.6 o versioni successive:

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


@salivan So che è tardi per aggiornarlo, ma penso che funzioni ora. Fondamentalmente ho aggiunto una nuova condizione e cambiato
dall'usare

4

Dopo aver giocato, ho modificato la risposta sulla palpebra sopra e ho creato un plug-in jQuery in modo che tu possa fare solo una di queste:

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

Scusa il lungo post in codice, ma può aiutare qualcuno:

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

È possibile sfruttare selectNodeContents che è supportato dai browser moderni.

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

è possibile modificare questo codice per consentire all'utente finale di spostare ancora il cursore in qualsiasi posizione desideri?
Zab

Sì. Dovresti usare i metodi setStart & setEnd sull'oggetto range. developer.mozilla.org/en-US/docs/Web/API/Range/setStart
zoonman

0

In Firefox potresti avere il testo del div in un nodo figlio ( 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);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.