Ustaw pozycję kursora na contentEditable <div>

142

Szukam ostatecznego rozwiązania w różnych przeglądarkach, aby ustawić pozycję kursora / karetki na ostatnią znaną pozycję, gdy contentEditable = 'on' <div> odzyska fokus. Wygląda na to, że domyślną funkcją edytowalnego elementu div jest przesuwanie kursora / kursora na początek tekstu w elemencie div za każdym kliknięciem, co jest niepożądane.

Uważam, że musiałbym zapisać w zmiennej bieżącą pozycję kursora, gdy opuszczają fokus elementu div, a następnie ustawić go ponownie, gdy mają ponownie fokus w środku, ale nie byłem w stanie złożyć razem ani znaleźć działającego przykładowy kod jeszcze.

Jeśli ktoś ma jakieś przemyślenia, działające fragmenty kodu lub próbki, z przyjemnością je zobaczę.

Nie mam jeszcze żadnego kodu, ale oto co mam:

<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. Wypróbowałem ten zasób, ale wygląda na to, że nie działa dla <div>. Być może tylko dla obszaru tekstowego ( Jak przenieść kursor na koniec treści edytowalnej )

GONeale
źródło
Nie wiedziałem, że contentEditablepracował w przeglądarkach innych niż IE-O_o
Aditya
10
Tak, robi aditya.
GONeale
5
Myślę, że aditya, Safari 2+, Firefox 3+.
powiek 28.07.2009
Spróbuj ustawić tabindex = "0" w div. To powinno sprawić, że będzie można go ustawić w większości przeglądarek.
Tokimon,

Odpowiedzi:

58

Jest to zgodne z przeglądarkami opartymi na standardach, ale prawdopodobnie zawiedzie w IE. Podaję to jako punkt wyjścia. IE nie obsługuje zakresu 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);
};
bez powiek
źródło
Dzięki oko, wypróbowałem twoje rozwiązanie, trochę mi się spieszyło, ale po podłączeniu go ustawia tylko pozycję „-” w ostatnim punkcie ostrości (który wydaje się być znacznikiem debugowania?) I wtedy przegrywamy focus, nie wydaje się przywracać kursora / daszka po kliknięciu z powrotem (przynajmniej nie w Chrome, spróbuję FF), po prostu przechodzi do końca div. Więc zaakceptuję rozwiązanie Nico, ponieważ wiem, że jest kompatybilne ze wszystkimi przeglądarkami i zwykle działa dobrze. Wielkie dzięki za twój wysiłek.
GONeale,
3
Wiesz co, zapomnij o mojej ostatniej odpowiedzi, po dokładniejszym zbadaniu zarówno twojej, jak i Nico, twoja nie jest tym, o co prosiłem w moim opisie, ale jest tym, co wolę i zdałbym sobie sprawę, że potrzebuję. Twój poprawnie ustawia pozycję kursora w miejscu kliknięcia podczas aktywowania fokusu z powrotem na <div>, jak zwykłe pole tekstowe. Przywrócenie fokusu do ostatniego punktu nie wystarczy, aby utworzyć przyjazne dla użytkownika pole wprowadzania. Przyznam ci punkty.
GONeale,
9
Działa świetnie! Oto jsfiddle powyższego rozwiązania: jsfiddle.net/s5xAr/3
vaughan
4
Dziękuję za opublikowanie prawdziwego JavaScript, mimo że OP był oszołomiony i chciał użyć frameworka.
Jan
cursorStart.appendChild(document.createTextNode('\u0002'));uważamy za rozsądny zamiennik. dla - char. Dzięki za kod
dwie osoby
97

To rozwiązanie działa we wszystkich głównych przeglądarkach:

saveSelection()jest dołączony do zdarzeń onmouseupi onkeyupelementu div i zapisuje wybór w zmiennej savedRange.

restoreSelection()jest dołączony do onfocuszdarzenia div i ponownie wybiera zaznaczenie zapisane w savedRange.

Działa to doskonale, chyba że chcesz, aby zaznaczenie zostało przywrócone, gdy użytkownik kliknie również element div (co jest trochę nieintuicyjne, ponieważ zwykle oczekujesz, że kursor znajdzie się w miejscu kliknięcia, ale kod zawiera kompletność)

Aby to osiągnąć, zdarzenia onclicki onmousedownsą anulowane przez funkcję, cancelEvent()która jest funkcją cross browser do anulowania zdarzenia. cancelEvent()Funkcja działa również restoreSelection()funkcję ponieważ jako zdarzenie click jest anulowane div nie odbiera ostrość i dlatego nic nie jest zaznaczone w ogóle, chyba że jest to uruchomić funkcje.

Zmienna isInFocusprzechowuje, czy jest fokus, i jest zmieniana na „false” onbluri „true”onfocus . Dzięki temu zdarzenia kliknięcia mogą być anulowane tylko wtedy, gdy element div nie jest aktywny (w przeciwnym razie nie można by w ogóle zmienić zaznaczenia).

Jeśli chcesz, aby zaznaczenie onclickuległo zmianie, gdy element div jest skupiony przez kliknięcie, i nie chcesz go przywracać (i tylko wtedy, gdy fokus zostanie nadany elementowi programowo za pomocą document.getElementById("area").focus();lub podobnie, po prostu usuń zdarzenia onclicki onmousedown. onblurZdarzenie onDivBlur()i cancelEvent()funkcje i w takich okolicznościach można również bezpiecznie usunąć.

Ten kod powinien działać, jeśli zostanie umieszczony bezpośrednio w treści strony html, jeśli chcesz go szybko przetestować:

<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
źródło
1
Dziękuję, to faktycznie działa! Najnowsze testy w IE, Chrome i FF. Przepraszam za super opóźnioną odpowiedź =)
GONeale
Czy if (window.getSelection)...sprawdzisz nie tylko, czy przeglądarka obsługuje getSelection, a nie, czy istnieje wybór?
Sandy Gifford
@Sandy Tak, dokładnie. Ta część kodu decyduje o tym, czy użyć standardowego getSelectioninterfejsu API, czy starszego document.selectioninterfejsu API używanego przez starsze wersje IE. Późniejsze getRangeAt (0)wywołanie powróci, nulljeśli nie ma wyboru, co jest sprawdzane w funkcji przywracania.
Nico Burns
@NicoBurns right, ale kod w drugim bloku warunkowym ( else if (document.createRange)) jest tym, na co patrzę. Zostanie wywołany tylko wtedy, window.getSelectiongdy nie istnieje, ale używawindow.getSelection
Sandy Gifford
@NicoBurns ponadto, nie sądzę, żebyś znalazł przeglądarkę z, window.getSelectionale nie document.createRange- co oznacza, że ​​drugi blok nigdy nie zostałby użyty ...
Sandy Gifford
19

Aktualizacja

Napisałem zakres i bibliotekę wyboru dla różnych przeglądarek o nazwie Rangy, która zawiera ulepszoną wersję kodu, który zamieściłem poniżej. Możesz użyć modułu zapisywania i przywracania wyboru dla tego konkretnego pytania, chociaż kusiłbym, aby użyć czegoś takiego jak odpowiedź @Nico Burns, jeśli nie robisz nic innego z zaznaczeniami w swoim projekcie i nie potrzebujesz większości biblioteka.

Poprzednia odpowiedź

Możesz użyć IERange ( http://code.google.com/p/ierange/ ), aby przekonwertować TextRange IE na coś takiego jak zakres DOM i użyć go w połączeniu z czymś w rodzaju punktu wyjścia bez powieki. Osobiście użyłbym tylko algorytmów z IERange, które dokonują konwersji Range <-> TextRange, zamiast używać całości. Obiekt selekcji IE nie ma właściwości focusNode i anchorNode, ale zamiast tego powinieneś być w stanie po prostu użyć Range / TextRange uzyskanego z zaznaczenia.

Mogę coś zrobić, aby to zrobić, wrócę tutaj, jeśli i kiedy to zrobię.

EDYTOWAĆ:

Stworzyłem demo skryptu, który to robi. Działa we wszystkim, czego do tej pory próbowałem, z wyjątkiem błędu w Operze 9, którego nie miałem jeszcze czasu sprawdzić. Przeglądarki, w których działa, to IE 5.5, 6 i 7, Chrome 2, Firefox 2, 3 i 3.5 oraz Safari 4, wszystkie w systemie Windows.

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

Zwróć uwagę, że w przeglądarkach zaznaczeń można dokonywać wstecz, tak aby węzeł fokusu znajdował się na początku zaznaczenia, a naciśnięcie prawego lub lewego klawisza kursora spowoduje przesunięcie kursora do pozycji względem początku zaznaczenia. Nie sądzę, aby można to powtórzyć podczas przywracania zaznaczenia, więc węzeł fokusu znajduje się zawsze na końcu zaznaczenia.

Wkrótce opiszę to w pełni.

Tim Down
źródło
15

Miałem podobną sytuację, w której specjalnie potrzebowałem ustawić pozycję kursora na KONIEC elementu div, który można edytować. Nie chciałem używać pełnoprawnej biblioteki, takiej jak Rangy, a wiele rozwiązań było zdecydowanie zbyt ciężkich.

W końcu wymyśliłem tę prostą funkcję jQuery, aby ustawić pozycję w karatach na koniec edytowalnego elementu div:

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

Teoria jest prosta: dodaj rozpiętość do końca edytowalnego elementu, zaznacz ją, a następnie usuń rozpiętość - pozostawiając nam kursor na końcu elementu div. Możesz dostosować to rozwiązanie, aby wstawić rozpiętość w dowolnym miejscu, umieszczając w ten sposób kursor w określonym miejscu.

Użycie jest proste:

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

Otóż ​​to!

Zane Claes
źródło
3
Nie musisz wstawiać polecenia <span>, co przypadkowo złamie wbudowany stos cofania przeglądarki. Zobacz stackoverflow.com/a/4238971/96100
Tim Down
6

Wziąłem odpowiedź Nico Burnsa i zrobiłem to za pomocą jQuery:

  • Ogólny: dla każdego div contentEditable="true"
  • Krótszy

Będziesz potrzebować jQuery 1.6 lub nowszego:

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
źródło
@salivan Wiem, że jest późno na aktualizację, ale myślę, że teraz działa. Zasadniczo dodałem nowy warunek i zmieniłem z używania id elementu na indeks elementu, który powinien istnieć zawsze :)
Gatsbimantico
4

Po zabawie zmodyfikowałem powyższą odpowiedź bez powiek i uczyniłem ją wtyczką jQuery, więc możesz zrobić jedną z tych rzeczy:

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

Przepraszam za długi kod, ale może komuś pomóc:

$.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
źródło
3

Możesz wykorzystać selectNodeContents, który jest obsługiwany przez nowoczesne przeglądarki.

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
źródło
czy można zmodyfikować ten kod, aby użytkownik końcowy mógł nadal przesuwać karetkę w dowolne miejsce?
Zabs
Tak. Powinieneś użyć metod setStart i setEnd na obiekcie range. developer.mozilla.org/en-US/docs/Web/API/Range/setStart
zoonman
0

W przeglądarce Firefox możesz mieć tekst elementu div w węźle potomnym ( 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
źródło