Setzen Sie die Cursorposition auf contentEditable <div>

142

Ich bin auf der Suche nach einer endgültigen, browserübergreifenden Lösung, um die Cursor- / Caret-Position auf die letzte bekannte Position zu setzen, wenn ein contentEditable = 'on' <div> den Fokus wiedererlangt. Es scheint, dass die Standardfunktionalität eines inhaltsbearbeitbaren Divs darin besteht, das Caret / den Cursor jedes Mal, wenn Sie darauf klicken, an den Anfang des Textes im Div zu bewegen, was unerwünscht ist.

Ich glaube, ich müsste die aktuelle Cursorposition in einer Variablen speichern, wenn sie den Fokus des Div verlassen, und diese dann zurücksetzen, wenn sie den Fokus wieder im Inneren haben, aber ich konnte sie nicht zusammenstellen oder eine Arbeit finden Codebeispiel noch.

Wenn jemand irgendwelche Gedanken, Arbeitscode-Schnipsel oder Beispiele hat, würde ich mich freuen, sie zu sehen.

Ich habe noch keinen Code, aber hier ist was ich habe:

<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. Ich habe diese Ressource ausprobiert, aber es scheint, dass sie für ein <div> nicht funktioniert. Möglicherweise nur für den Textbereich ( So bewegen Sie den Cursor zum Ende einer inhaltsbearbeitbaren Entität )

GONeale
quelle
Ich wusste nicht, dass es contentEditablein Nicht-IE-Browsern funktioniert. O_o
aditya
10
Ja, es macht Aditya.
GONeale
5
Aditya, Safari 2+, Firefox 3+ denke ich.
Augenlidlosigkeit
Versuchen Sie, tabindex = "0" auf dem div zu setzen. Das sollte es in den meisten Browsern fokussierbar machen.
Tokimon

Antworten:

58

Dies ist mit den standardbasierten Browsern kompatibel, wird jedoch im IE wahrscheinlich fehlschlagen. Ich biete es als Ausgangspunkt. IE unterstützt DOM Range nicht.

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);
};
Augenlidlosigkeit
quelle
Danke Auge, ich habe Ihre Lösung ausprobiert, ich hatte es etwas eilig, aber nach der Verkabelung wird nur die Position "-" am letzten Fokuspunkt platziert (was scheint ein Debug-Marker zu sein?), Und dann verlieren wir Fokus, es scheint nicht den Cursor / Caret wiederherzustellen, wenn ich zurückklicke (zumindest nicht in Chrome, ich werde FF versuchen), es geht nur bis zum Ende des Div. Daher werde ich die Lösung von Nico akzeptieren, da ich weiß, dass sie in allen Browsern kompatibel ist und in der Regel gut funktioniert. Vielen Dank für Ihre Mühe.
GONeale
3
Weißt du was, vergiss meine letzte Antwort, nachdem ich sowohl deine als auch die von Nico weiter untersucht habe, ist deine nicht das, wonach ich in meiner Beschreibung gefragt habe, aber es ist das, was ich bevorzuge und hätte erkannt, dass ich es brauche. Mit freundlichen richtig setzt die Position des Cursors, wo Sie klicken , wenn der Fokus wieder auf die Aktivierung <div>, wie eine normale Textbox. Die Wiederherstellung des Fokus auf den letzten Punkt reicht nicht aus, um ein benutzerfreundliches Eingabefeld zu erstellen. Ich werde Ihnen die Punkte vergeben.
GONeale
9
Funktioniert super! Hier ist eine jsfiddle der obigen Lösung: jsfiddle.net/s5xAr/3
vaughan
4
Vielen Dank, dass Sie echtes JavaScript veröffentlicht haben, obwohl das OP lahmgelegt hat und ein Framework verwenden wollte.
John
cursorStart.appendChild(document.createTextNode('\u0002'));ist ein vernünftiger Ersatz, denken wir. für die - char. Vielen Dank für den Code
Twobob
97

Diese Lösung funktioniert in allen gängigen Browsern:

saveSelection()wird an die onmouseupund onkeyup-Ereignisse des div angehängt und speichert die Auswahl in der Variablen savedRange.

restoreSelection()wird an das onfocusEreignis des div angehängt und wählt die in gespeicherte Auswahl erneut aus savedRange.

Dies funktioniert einwandfrei, es sei denn, Sie möchten, dass die Auswahl wiederhergestellt wird, wenn der Benutzer auch auf das Div klickt (was etwas unintuitiv ist, da Sie normalerweise erwarten, dass der Cursor dorthin geht, wo Sie klicken, der Vollständigkeit halber jedoch Code enthalten).

Um dies zu erreichen, werden die onclickund onmousedownEreignisse von der Funktion abgebrochen, cancelEvent()die eine browserübergreifende Funktion zum Abbrechen des Ereignisses ist. Die cancelEvent()Funktion führt die restoreSelection()Funktion auch aus, da das Div beim Abbrechen des Klickereignisses keinen Fokus erhält und daher überhaupt nichts ausgewählt wird, es sei denn, diese Funktionen werden ausgeführt.

Die Variable isInFocusspeichert, ob sie fokussiert ist und wird in "false" onblurund "true" geändert onfocus. Auf diese Weise können Klickereignisse nur abgebrochen werden, wenn das Div nicht im Fokus steht (andernfalls können Sie die Auswahl überhaupt nicht ändern).

Wenn Sie möchten, dass die Auswahl geändert wird, wenn das Div durch einen Klick fokussiert wird, und die Auswahl nicht wiederhergestellt wird onclick(und nur, wenn das Element programmgesteuert mit document.getElementById("area").focus();oder ähnlich fokussiert wird, entfernen Sie einfach die Ereignisse onclickund onmousedown. Das onblurEreignis und die Funktionen onDivBlur()und cancelEvent()kann unter diesen Umständen auch sicher entfernt werden.

Dieser Code sollte funktionieren, wenn er direkt in den Text einer HTML-Seite eingefügt wird, wenn Sie ihn schnell testen möchten:

<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
quelle
1
Danke, das funktioniert tatsächlich! Getestet in IE, Chrome und FF spätestens. Entschuldigung für die super verspätete Antwort =)
GONeale
Wird nicht if (window.getSelection)...nur getestet, ob der Browser unterstützt getSelection, nicht ob eine Auswahl vorhanden ist oder nicht?
Sandy Gifford
@ Sandy Ja genau. In diesem Teil des Codes wird entschieden, ob die Standard- getSelectionAPI oder die Legacy- document.selectionAPI verwendet werden soll, die von älteren IE-Versionen verwendet wird. Der spätere getRangeAt (0)Aufruf wird zurückgegeben, nullwenn keine Auswahl vorhanden ist, die in der Wiederherstellungsfunktion überprüft wird.
Nico Burns
@NicoBurns richtig, aber der Code im zweiten bedingten Block ( else if (document.createRange)) ist das, was ich sehe. Es wird nur aufgerufen, wenn window.getSelectiones nicht existiert, aber verwendetwindow.getSelection
Sandy Gifford
@NicoBurns außerdem glaube ich nicht, dass Sie einen Browser finden würden, window.getSelectionaber nicht document.createRange- was bedeutet, dass der zweite Block niemals verwendet werden würde ...
Sandy Gifford
19

Aktualisieren

Ich habe eine browserübergreifende Bereichs- und Auswahlbibliothek namens Rangy geschrieben , die eine verbesserte Version des unten veröffentlichten Codes enthält. Sie können das Modul zum Speichern und Wiederherstellen der Auswahl für diese spezielle Frage verwenden, obwohl ich versucht wäre, die Antwort von @Nico Burns zu verwenden, wenn Sie mit der Auswahl in Ihrem Projekt nichts anderes tun und nicht den Großteil von a benötigen Bibliothek.

Vorherige Antwort

Sie können IERange ( http://code.google.com/p/ierange/ ) verwenden, um den TextRange des IE in einen DOM-Bereich zu konvertieren und ihn in Verbindung mit dem Ausgangspunkt der Augenlidlosigkeit zu verwenden. Persönlich würde ich nur die Algorithmen von IERange verwenden, die die Range <-> TextRange-Konvertierungen durchführen, anstatt das Ganze zu verwenden. Das Auswahlobjekt des IE verfügt nicht über die Eigenschaften focusNode und anchorNode, sondern Sie sollten stattdessen nur den aus der Auswahl erhaltenen Bereich / Textbereich verwenden können.

Ich könnte etwas zusammenstellen, um dies zu tun, werde hier zurück posten, wenn und wann ich es tue.

BEARBEITEN:

Ich habe eine Demo eines Skripts erstellt, das dies tut. Es funktioniert in allem, in dem ich es bisher ausprobiert habe, mit Ausnahme eines Fehlers in Opera 9, für den ich noch keine Zeit hatte. Browser, in denen es funktioniert, sind IE 5.5, 6 und 7, Chrome 2, Firefox 2, 3 und 3.5 und Safari 4 unter Windows.

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

Beachten Sie, dass die Auswahl in Browsern rückwärts getroffen werden kann, sodass sich der Fokusknoten am Anfang der Auswahl befindet. Wenn Sie die rechte oder linke Cursortaste drücken, wird das Caret an eine Position relativ zum Beginn der Auswahl verschoben. Ich denke nicht, dass es möglich ist, dies beim Wiederherstellen einer Auswahl zu replizieren, daher befindet sich der Fokusknoten immer am Ende der Auswahl.

Ich werde dies bald vollständig aufschreiben.

Tim Down
quelle
15

Ich hatte eine verwandte Situation, in der ich speziell die Cursorposition auf das ENDE eines inhaltsbearbeitbaren Div setzen musste. Ich wollte keine vollwertige Bibliothek wie Rangy verwenden, und viele Lösungen waren viel zu schwer.

Am Ende habe ich mir diese einfache jQuery-Funktion ausgedacht, um die Karatposition auf das Ende eines inhaltsbearbeitbaren Divs zu setzen:

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

Die Theorie ist einfach: Hängen Sie eine Spanne an das Ende der Bearbeitbarkeit an, wählen Sie sie aus und entfernen Sie die Spanne. Am Ende der Div befindet sich ein Cursor. Sie können diese Lösung anpassen, um die Spanne an einer beliebigen Stelle einzufügen und so den Cursor an einer bestimmten Stelle zu platzieren.

Die Verwendung ist einfach:

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

Das ist es!

Zane Claes
quelle
3
Sie müssen das nicht einfügen <span>, da dies den integrierten Rückgängig-Stapel des Browsers unterbricht. Siehe stackoverflow.com/a/4238971/96100
Tim Down
6

Ich nahm die Antwort von Nico Burns und machte sie mit jQuery:

  • Generisch: Für jeden div contentEditable="true"
  • Kürzer

Sie benötigen jQuery 1.6 oder höher:

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
quelle
@salivan Ich weiß, dass es spät ist, es zu aktualisieren, aber ich denke, es funktioniert jetzt. Grundsätzlich habe ich eine neue Bedingung hinzugefügt und von der Verwendung der Element-ID zum Element-Index geändert, der immer existieren sollte :)
Gatsbimantico
4

Nachdem ich herumgespielt habe, habe ich die Antwort von Eyelidlessness oben geändert und daraus ein jQuery-Plugin gemacht, sodass Sie einfach eine der folgenden Aktionen ausführen können:

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

Entschuldigen Sie den langen Code-Beitrag, aber er kann jemandem helfen:

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

Sie können selectNodeContents nutzen, das von modernen Browsern unterstützt wird.

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
quelle
Ist es möglich, diesen Code so zu ändern, dass der Endbenutzer das Caret weiterhin an eine beliebige Position bewegen kann?
Zabs
Ja. Sie sollten die Methoden setStart & setEnd für das Bereichsobjekt verwenden. developer.mozilla.org/en-US/docs/Web/API/Range/setStart
zoonman
0

In Firefox befindet sich möglicherweise der Text des div in einem untergeordneten Knoten ( 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
quelle