Tetapkan posisi kursor pada contentEditable <div>

142

Saya mencari solusi lintas browser yang definitif untuk mengatur posisi kursor / tanda sisipan ke posisi terakhir yang diketahui ketika contentEditable = 'on' <div> mendapatkan kembali fokus. Tampaknya fungsi default div konten yang dapat diedit adalah untuk memindahkan tanda sisipan / kursor ke awal teks dalam div setiap kali Anda mengkliknya, yang tidak diinginkan.

Saya percaya saya harus menyimpan dalam variabel posisi kursor saat ini ketika mereka meninggalkan fokus div, dan kemudian mengatur ulang ini ketika mereka memiliki fokus di dalam lagi, tetapi saya belum dapat mengumpulkan, atau menemukan pekerjaan contoh kode belum.

Jika ada yang punya pemikiran, cuplikan kode kerja atau sampel, saya akan senang melihatnya.

Saya belum memiliki kode apa pun, tetapi inilah yang saya miliki:

<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. Saya telah mencoba sumber ini tetapi tampaknya tidak berfungsi untuk <div>. Mungkin hanya untuk textarea ( Cara memindahkan kursor ke akhir entitas yang dapat diedit )

GONeale
sumber
Saya tidak tahu contentEditablebekerja di browser non-IE o_o
aditya
10
Ya itu aditya.
GONeale
5
aditya, Safari 2+, Firefox 3+ saya pikir.
kelopak mata
Coba Pengaturan tabindex = "0" pada div. Itu harus membuatnya fokus di sebagian besar browser.
Tokimon

Jawaban:

58

Ini kompatibel dengan browser berbasis standar, tetapi mungkin akan gagal di IE. Saya menyediakannya sebagai titik awal. IE tidak mendukung Rentang 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);
};
kelopak mata
sumber
Terima kasih mata, saya mencoba solusi Anda, saya agak terburu-buru tetapi setelah kabel itu, itu hanya menempatkan posisi "-" pada titik fokus terakhir (yang tampaknya menjadi penanda debug?) Dan saat itulah kita kehilangan fokus, sepertinya tidak mengembalikan kursor / tanda sisipan ketika saya mengklik kembali (setidaknya tidak di Chrome, saya akan mencoba FF), itu hanya pergi ke akhir div. Jadi saya akan menerima solusi Nico karena saya tahu itu kompatibel di semua browser, dan cenderung berfungsi dengan baik. Terima kasih banyak atas usaha Anda.
GONeale
3
Tahukah Anda, lupakan tanggapan terakhir saya, setelah memeriksa Anda dan Nico lebih jauh, tanggapan Anda bukanlah yang saya minta dalam uraian saya, tetapi adalah apa yang saya sukai dan sadari bahwa saya membutuhkannya. Anda dengan benar mengatur posisi kursor tempat Anda mengklik saat mengaktifkan fokus kembali ke <div>, seperti kotak teks biasa. Mengembalikan fokus ke titik terakhir tidak cukup untuk membuat bidang entri yang mudah digunakan. Saya akan memberi Anda poin.
GONeale
9
Bagus sekali! Inilah jsfiddle dari solusi di atas: jsfiddle.net/s5xAr/3
vaughan
4
Terima kasih telah memposting JavaScript yang sebenarnya meskipun OP menjengkelkan dan ingin menggunakan kerangka kerja.
John
cursorStart.appendChild(document.createTextNode('\u0002'));adalah pengganti yang masuk akal kami pikir. untuk - char. Terima kasih untuk kodenya
twobob
97

Solusi ini berfungsi di semua browser utama:

saveSelection()dilampirkan ke onmouseupdan onkeyupacara div dan menyimpan pilihan ke variabel savedRange.

restoreSelection()terlampir pada onfocusacara div dan memilih kembali seleksi disimpan savedRange.

Ini berfungsi dengan baik kecuali jika Anda ingin pilihan dikembalikan ketika pengguna mengklik div juga (yang agak tidak intuitif seperti biasanya Anda mengharapkan kursor pergi ke mana Anda mengklik tetapi kode termasuk untuk kelengkapan)

Untuk mencapai ini onclickdan onmousedownacara dibatalkan oleh fungsi cancelEvent()yang merupakan fungsi lintas browser untuk membatalkan acara. The cancelEvent()Fungsi juga menjalankan restoreSelection()fungsi karena sebagai acara klik dibatalkan div tidak menerima fokus dan oleh karena itu tidak ada yang dipilih sama sekali kecuali fungsi ini dijalankan.

Variabel isInFocusmenyimpan apakah itu dalam fokus dan diubah menjadi "false" onblurdan "true" onfocus. Ini memungkinkan acara klik dibatalkan hanya jika div tidak dalam fokus (jika tidak, Anda tidak akan dapat mengubah pilihan sama sekali).

Jika Anda ingin pemilihan diubah ketika div difokuskan dengan klik, dan tidak mengembalikan pilihan onclick(dan hanya ketika fokus diberikan kepada elemen yang menggunakan document.getElementById("area").focus();atau serupa secara terprogram maka cukup hapus onclickdan onmousedownacara. onblurAcara dan onDivBlur()dan cancelEvent()fungsi juga dapat dengan aman dihilangkan dalam kondisi ini.

Kode ini akan berfungsi jika dijatuhkan langsung ke badan laman html jika Anda ingin mengujinya dengan cepat:

<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
sumber
1
Terima kasih ini berhasil! Diuji dalam IE, Chrome dan FF terbaru. Maaf tentang balasan super tertunda =)
GONeale
Tidak akan if (window.getSelection)...hanya menguji apakah browser mendukung getSelection, bukan apakah ada pilihan?
Sandy Gifford
@Sandy Ya persis. Bagian kode ini memutuskan apakah akan menggunakan getSelectionapi standar atau document.selectionapi warisan yang digunakan oleh versi IE yang lebih lama. getRangeAt (0)Panggilan nanti akan kembali nulljika tidak ada pilihan, yang diperiksa untuk mengembalikan fungsi.
Nico Burns
@NicoBurns benar, tetapi kode di blok bersyarat kedua ( else if (document.createRange)) adalah apa yang saya lihat. Ini hanya akan dipanggil jika window.getSelectiontidak ada, belum menggunakanwindow.getSelection
Sandy Gifford
@NicoBurns selanjutnya, saya tidak berpikir Anda akan menemukan browser dengan window.getSelectiontetapi tidak document.createRange- yang berarti blok kedua tidak akan pernah digunakan ...
Sandy Gifford
19

Memperbarui

Saya telah menulis perpustakaan lintas jangkauan dan pemilihan yang disebut Rangy yang menggabungkan versi yang disempurnakan dari kode yang saya posting di bawah ini. Anda dapat menggunakan modul save and restore pilihan untuk pertanyaan khusus ini, walaupun saya akan tergoda untuk menggunakan sesuatu seperti jawaban @Nico Burns jika Anda tidak melakukan hal lain dengan pilihan dalam proyek Anda dan tidak memerlukan sebagian besar dari Perpustakaan.

Jawaban sebelumnya

Anda dapat menggunakan IERange ( http://code.google.com/p/ierange/ ) untuk mengubah TextRange IE menjadi sesuatu seperti DOM Range dan menggunakannya bersamaan dengan sesuatu seperti titik awal kelopak mata. Secara pribadi saya hanya akan menggunakan algoritma dari IERange yang melakukan konversi Range <-> TextRange daripada menggunakan semuanya. Dan objek seleksi IE tidak memiliki properti focusNode dan anchorNode tetapi Anda harus bisa menggunakan Range / TextRange yang diperoleh dari seleksi.

Saya mungkin mengumpulkan sesuatu untuk melakukan ini, akan dikirim kembali ke sini jika dan ketika saya melakukannya.

EDIT:

Saya telah membuat demo skrip yang melakukan ini. Ini berfungsi dalam semua yang saya coba sejauh ini kecuali untuk bug di Opera 9, yang saya belum punya waktu untuk melihatnya. Browser yang berfungsi adalah IE 5.5, 6 dan 7, Chrome 2, Firefox 2, 3 dan 3.5, dan Safari 4, semuanya di Windows.

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

Perhatikan bahwa pilihan dapat dibuat mundur di browser sehingga simpul fokus berada di awal seleksi dan menekan tombol kursor kanan atau kiri akan menggerakkan tanda sisipan ke posisi relatif ke awal pemilihan. Saya tidak berpikir mungkin untuk mereplikasi ini ketika mengembalikan pilihan, jadi simpul fokus selalu di akhir seleksi.

Saya akan menulis ini sepenuhnya pada suatu saat nanti.

Tim Down
sumber
15

Saya memiliki situasi terkait, di mana saya secara khusus perlu mengatur posisi kursor ke AKHIR div yang dapat diedit. Saya tidak ingin menggunakan perpustakaan lengkap seperti Rangy, dan banyak solusi yang terlalu berat.

Pada akhirnya, saya datang dengan fungsi jQuery sederhana ini untuk mengatur posisi karat ke akhir div yang dapat diedit:

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

Teorinya sederhana: tambahkan rentang ke ujung yang dapat diedit, pilih, dan kemudian hapus rentang - meninggalkan kita dengan kursor di akhir div. Anda bisa menyesuaikan solusi ini untuk memasukkan rentang di mana pun Anda inginkan, sehingga menempatkan kursor di tempat tertentu.

Penggunaannya sederhana:

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

Itu dia!

Zane Claes
sumber
3
Anda tidak perlu memasukkan <span>, yang secara tidak sengaja akan merusak tumpukan undo bawaan browser. Lihat stackoverflow.com/a/4238971/96100
Tim Down
6

Saya mengambil jawaban Nico Burns dan membuatnya menggunakan jQuery:

  • Generik: Untuk setiap div contentEditable="true"
  • Singkat

Anda membutuhkan jQuery 1.6 atau lebih tinggi:

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
sumber
@salivan Saya tahu sudah terlambat untuk memperbaruinya, tapi saya pikir itu berfungsi sekarang. Pada dasarnya saya menambahkan kondisi baru dan berubah dari menggunakan id elemen ke indeks elemen, yang seharusnya selalu ada :)
Gatsbimantico
4

Setelah bermain-main, saya telah memodifikasi jawaban kelopak mata di atas dan menjadikannya plugin jQuery sehingga Anda bisa melakukan salah satunya:

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

Maafkan kiriman kode panjang, tetapi mungkin membantu seseorang:

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

Anda dapat memanfaatkan selectNodeContents yang didukung oleh browser modern.

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
sumber
apakah mungkin untuk memodifikasi kode ini agar pengguna akhir masih dapat memindahkan tanda sisipan ke posisi yang mereka inginkan?
Zabs
Iya. Anda harus menggunakan metode setStart & setEnd pada objek rentang. developer.mozilla.org/en-US/docs/Web/API/Range/setStart
zoonman
0

Di Firefox Anda mungkin memiliki teks div di simpul anak ( 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
sumber