Dapatkan posisi indeks tanda sisipan contentEditable

119

Saya menemukan banyak sekali jawaban crossbrowser yang bagus tentang cara MENGATUR posisi indeks kursor atau tanda sisipan dalam sebuah contentEditableelemen, tetapi tidak ada tentang cara MENDAPATKAN atau menemukan indeksnya ...

Yang ingin saya lakukan adalah mengetahui indeks tanda sisipan dalam div ini, di keyup.

Jadi, saat pengguna mengetik teks, saya bisa mengetahui indeks kursornya di dalam contentEditableelemen kapan saja .

EDIT: Saya mencari INDEX dalam konten div (teks), bukan koordinat kursor.

<div id="contentBox" contentEditable="true"></div>

$('#contentbox').keyup(function() { 
    // ... ? 
});
Bertvan
sumber
Lihat posisinya dalam teks. Kemudian, lihat kemunculan terakhir '@' sebelum posisi itu. Jadi hanya beberapa logika teks.
Bertvan
Juga, saya tidak berencana untuk mengizinkan tag lain di dalam <diV>, hanya teks
Bertvan
ok, ya saya saya akan membutuhkan tag lain dalam <div>. Akan ada tag <a>, tapi tidak akan ada bersarang ...
Bertvan
@Bertvan: jika tanda sisipan berada di dalam <a>elemen di dalam <div>, offset apa yang Anda inginkan? Offset dalam teks di dalam <a>?
Tim Down
Ini tidak boleh berada di dalam elemen <a>. Elemen <a> harus dirender menjadi html, sehingga pengguna tidak dapat benar-benar menempatkan tanda sisipan di sana.
Bertvan

Jawaban:

121

Kode berikut mengasumsikan:

  • Selalu ada satu node teks dalam yang dapat diedit <div>dan tidak ada node lain
  • Div yang dapat diedit tidak memiliki white-spaceproperti CSS yang disetel kepre

Jika Anda memerlukan pendekatan yang lebih umum yang akan mengerjakan konten dengan elemen bersarang, coba jawaban ini:

https://stackoverflow.com/a/4812022/96100

Kode:

function getCaretPosition(editableDiv) {
  var caretPos = 0,
    sel, range;
  if (window.getSelection) {
    sel = window.getSelection();
    if (sel.rangeCount) {
      range = sel.getRangeAt(0);
      if (range.commonAncestorContainer.parentNode == editableDiv) {
        caretPos = range.endOffset;
      }
    }
  } else if (document.selection && document.selection.createRange) {
    range = document.selection.createRange();
    if (range.parentElement() == editableDiv) {
      var tempEl = document.createElement("span");
      editableDiv.insertBefore(tempEl, editableDiv.firstChild);
      var tempRange = range.duplicate();
      tempRange.moveToElementText(tempEl);
      tempRange.setEndPoint("EndToEnd", range);
      caretPos = tempRange.text.length;
    }
  }
  return caretPos;
}
#caretposition {
  font-weight: bold;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div id="contentbox" contenteditable="true">Click me and move cursor with keys or mouse</div>
<div id="caretposition">0</div>
<script>
  var update = function() {
    $('#caretposition').html(getCaretPosition(this));
  };
  $('#contentbox').on("mousedown mouseup keydown keyup", update);
</script>

Tim Down
sumber
9
Ini tidak akan berfungsi jika ada tag lain di sana. Pertanyaan: jika tanda sisipan berada di dalam <a>elemen di dalam <div>, offset apa yang Anda inginkan? Offset dalam teks di dalam <a>?
Tim Down
3
@ Richard: Yah, keyupsepertinya acara yang salah untuk ini tetapi itulah yang digunakan dalam pertanyaan awal. getCaretPosition()itu sendiri baik-baik saja dalam batasannya sendiri.
Tim Down
3
Demo JSFIDDLE itu gagal jika saya menekan enter dan masuk ke baris baru. Posisi akan menunjukkan 0.
giorgio79
5
@ giorgio79: Ya, karena jeda baris menghasilkan elemen <br>atau <div>, yang melanggar asumsi pertama yang disebutkan dalam jawaban. Jika Anda memerlukan solusi yang sedikit lebih umum, Anda dapat mencoba stackoverflow.com/a/4812022/96100
Tim Down
2
Apakah ada cara untuk melakukan ini sehingga termasuk nomor baris?
Adjit
28

Beberapa kerutan yang menurut saya tidak diatasi dalam jawaban lain:

  1. elemen dapat berisi beberapa level node anak (misalnya node anak yang memiliki node anak yang memiliki node anak ...)
  2. pilihan dapat terdiri dari posisi awal dan akhir yang berbeda (mis. beberapa karakter dipilih)
  3. node yang berisi awal / akhir Caret mungkin bukan elemen atau turunan langsungnya

Berikut cara untuk mendapatkan posisi awal dan akhir sebagai offset ke nilai textContent elemen:

// node_walk: walk the element tree, stop when func(node) returns false
function node_walk(node, func) {
  var result = func(node);
  for(node = node.firstChild; result !== false && node; node = node.nextSibling)
    result = node_walk(node, func);
  return result;
};

// getCaretPosition: return [start, end] as offsets to elem.textContent that
//   correspond to the selected portion of text
//   (if start == end, caret is at given position and no text is selected)
function getCaretPosition(elem) {
  var sel = window.getSelection();
  var cum_length = [0, 0];

  if(sel.anchorNode == elem)
    cum_length = [sel.anchorOffset, sel.extentOffset];
  else {
    var nodes_to_find = [sel.anchorNode, sel.extentNode];
    if(!elem.contains(sel.anchorNode) || !elem.contains(sel.extentNode))
      return undefined;
    else {
      var found = [0,0];
      var i;
      node_walk(elem, function(node) {
        for(i = 0; i < 2; i++) {
          if(node == nodes_to_find[i]) {
            found[i] = true;
            if(found[i == 0 ? 1 : 0])
              return false; // all done
          }
        }

        if(node.textContent && !node.firstChild) {
          for(i = 0; i < 2; i++) {
            if(!found[i])
              cum_length[i] += node.textContent.length;
          }
        }
      });
      cum_length[0] += sel.anchorOffset;
      cum_length[1] += sel.extentOffset;
    }
  }
  if(cum_length[0] <= cum_length[1])
    return cum_length;
  return [cum_length[1], cum_length[0]];
}
mwag
sumber
3
Ini harus dipilih sebagai jawaban yang benar. Ia bekerja dengan tag di dalam teks (tanggapan yang diterima tidak)
hamboy75
17

$("#editable").on('keydown keyup mousedown mouseup',function(e){
		   
       if($(window.getSelection().anchorNode).is($(this))){
    	  $('#position').html('0')
       }else{
         $('#position').html(window.getSelection().anchorOffset);
       }
 });
body{
  padding:40px;
}
#editable{
  height:50px;
  width:400px;
  border:1px solid #000;
}
#editable p{
  margin:0;
  padding:0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.1/jquery.min.js"></script>
<div contenteditable="true" id="editable">move the cursor to see position</div>
<div>
position : <span id="position"></span>
</div>

Eisa Qasemi
sumber
3
Sayangnya ini berhenti bekerja segera setelah Anda menekan enter dan mulai di baris lain (dimulai dari 0 lagi - mungkin dihitung dari CR / LF).
Ian
Ini tidak berfungsi dengan baik jika Anda memiliki beberapa kata Tebal dan / atau Miring.
pengguna2824371
14

Coba ini:

Caret.js Dapatkan posisi tanda sisipan dan offset dari bidang teks

https://github.com/ichord/Caret.js

demo: http://ichord.github.com/Caret.js

JY Han
sumber
Ini manis. Saya memerlukan perilaku ini untuk menyetel tanda sisipan ke akhir contenteditable liketika mengklik tombol untuk mengganti nama likonten.
akinuri
@AndroidDev Saya bukan penulis Caret.js tetapi apakah Anda menganggap bahwa mendapatkan posisi tanda sisipan untuk semua browser utama lebih rumit daripada beberapa baris? Tahukah Anda atau telah membuat alternatif non-kembung yang dapat Anda bagikan dengan kami?
adelriosantiago
8

Agak terlambat ke pesta, tetapi siapa pun yang kesulitan. Tak satu pun dari penelusuran Google yang saya temukan selama dua hari terakhir ini menghasilkan apa pun yang berhasil, tetapi saya menemukan solusi ringkas dan elegan yang akan selalu berfungsi, tidak peduli berapa banyak tag bersarang yang Anda miliki:

function cursor_position() {
    var sel = document.getSelection();
    sel.modify("extend", "backward", "paragraphboundary");
    var pos = sel.toString().length;
    if(sel.anchorNode != undefined) sel.collapseToEnd();

    return pos;
}

// Demo:
var elm = document.querySelector('[contenteditable]');
elm.addEventListener('click', printCaretPosition)
elm.addEventListener('keydown', printCaretPosition)

function printCaretPosition(){
  console.log( cursor_position(), 'length:', this.textContent.trim().length )
}
<div contenteditable>some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text</div>

Ini memilih semua jalan kembali ke awal paragraf dan kemudian menghitung panjang string untuk mendapatkan posisi saat ini dan kemudian membatalkan pemilihan untuk mengembalikan kursor ke posisi saat ini. Jika Anda ingin melakukan ini untuk seluruh dokumen (lebih dari satu paragraf), ubah paragraphboundaryke documentboundaryatau perincian apa pun untuk kasus Anda. Lihat API untuk lebih jelasnya . Bersulang! :)

Soubriquet
sumber
1
Jika saya memiliki <div contenteditable> some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text </div> Everytime saya menempatkan kursor sebelum itag atau elemen html anak apa pun di dalam div, posisi kursor dimulai dari 0. Apakah ada cara untuk menghindari penghitungan memulai ulang ini?
vam
Aneh. Saya tidak mendapatkan perilaku itu di Chrome. Browser apa yang Anda gunakan?
Soubriquet
2
Sepertinya selection.modify mungkin atau mungkin tidak didukung di semua browser. developer.mozilla.org/en-US/docs/Web/API/Selection
Chris Sullivan
7
function getCaretPosition() {
    var x = 0;
    var y = 0;
    var sel = window.getSelection();
    if(sel.rangeCount) {
        var range = sel.getRangeAt(0).cloneRange();
        if(range.getClientRects()) {
        range.collapse(true);
        var rect = range.getClientRects()[0];
        if(rect) {
            y = rect.top;
            x = rect.left;
        }
        }
    }
    return {
        x: x,
        y: y
    };
}
Nishad Up
sumber
yang ini benar-benar berhasil untuk saya, saya sudah mencoba semua yang di atas, mereka tidak.
iStudLion
terima kasih tetapi juga mengembalikan {x: 0, y: 0} di baris baru.
hichamkazan
ini mengembalikan posisi piksel, bukan offset karakter
4esn0k
terima kasih, saya sedang mencari posisi piksel dari tanda sisipan dan berfungsi dengan baik.
Sameesh
6

window.getSelection - vs - document.selection

Yang ini berhasil untuk saya:

function getCaretCharOffset(element) {
  var caretOffset = 0;

  if (window.getSelection) {
    var range = window.getSelection().getRangeAt(0);
    var preCaretRange = range.cloneRange();
    preCaretRange.selectNodeContents(element);
    preCaretRange.setEnd(range.endContainer, range.endOffset);
    caretOffset = preCaretRange.toString().length;
  } 

  else if (document.selection && document.selection.type != "Control") {
    var textRange = document.selection.createRange();
    var preCaretTextRange = document.body.createTextRange();
    preCaretTextRange.moveToElementText(element);
    preCaretTextRange.setEndPoint("EndToEnd", textRange);
    caretOffset = preCaretTextRange.text.length;
  }

  return caretOffset;
}


// Demo:
var elm = document.querySelector('[contenteditable]');
elm.addEventListener('click', printCaretPosition)
elm.addEventListener('keydown', printCaretPosition)

function printCaretPosition(){
  console.log( getCaretCharOffset(elm), 'length:', this.textContent.trim().length )
}
<div contenteditable>some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text</div>

Saluran panggilan bergantung pada jenis peristiwa, untuk peristiwa utama gunakan ini:

getCaretCharOffsetInDiv(e.target) + ($(window.getSelection().getRangeAt(0).startContainer.parentNode).index());

untuk acara mouse gunakan ini:

getCaretCharOffsetInDiv(e.target.parentElement) + ($(e.target).index())

pada dua kasus ini saya menangani garis putus-putus dengan menambahkan indeks target

Jonathan R.
sumber
4
//global savedrange variable to store text range in
var savedrange = null;

function getSelection()
{
    var savedRange;
    if(window.getSelection && window.getSelection().rangeCount > 0) //FF,Chrome,Opera,Safari,IE9+
    {
        savedRange = window.getSelection().getRangeAt(0).cloneRange();
    }
    else if(document.selection)//IE 8 and lower
    { 
        savedRange = document.selection.createRange();
    }
    return savedRange;
}

$('#contentbox').keyup(function() { 
    var currentRange = getSelection();
    if(window.getSelection)
    {
        //do stuff with standards based object
    }
    else if(document.selection)
    { 
        //do stuff with microsoft object (ie8 and lower)
    }
});

Catatan: objek rentang itu sendiri dapat disimpan dalam variabel, dan dapat dipilih kembali kapan saja kecuali konten dari div konten yang dapat diedit berubah.

Referensi untuk IE 8 dan yang lebih rendah: http://msdn.microsoft.com/en-us/library/ms535872(VS.85).aspx

Referensi untuk browser standar (semua lainnya): https://developer.mozilla.org/en/DOM/range (ini adalah dokumen mozilla, tetapi kode juga berfungsi di chrome, safari, opera, dan ie9)

Nico Burns
sumber
1
Terima kasih, tapi bagaimana tepatnya cara mendapatkan 'indeks' dari posisi tanda sisipan di konten div?
Bertvan
Oke, sepertinya memanggil .baseOffset pada .getSelection () sudah cukup. Jadi ini, bersama dengan jawaban Anda, menjawab pertanyaan saya. Terima kasih!
Bertvan
2
Sayangnya .baseOffset hanya berfungsi di webkit (menurut saya). Ini juga hanya memberi Anda offset dari induk segera dari tanda sisipan (jika Anda memiliki tag <b> di dalam <div> itu akan memberikan offset dari awal <b>, bukan awal <div> Rentang berbasis standar dapat menggunakan range.endOffset range.startOffset range.endContainer dan range.startContainer untuk mendapatkan offset dari node induk pilihan, dan node itu sendiri (ini termasuk node teks). IE menyediakan range.offsetLeft yang merupakan diimbangi dari kiri dalam piksel , dan jadi tidak berguna.
Nico Burns
Yang terbaik adalah menyimpan objek range itu sendiri dan menggunakan window.getSelection (). Addrange (range); <- standar dan range.select (); <- IE untuk memposisikan ulang kursor di tempat yang sama. range.insertNode (nodetoinsert); <- standar dan range.pasteHTML (htmlcode); <- IE untuk menyisipkan teks atau html di kursor.
Nico Burns
The Rangeobjek dikembalikan oleh kebanyakan browser dan TextRangeobjek dikembalikan oleh IE adalah hal-hal yang sangat berbeda, jadi saya tidak yakin jawaban ini memecahkan banyak.
Tim Down
3

Karena ini membutuhkan waktu lama bagi saya untuk mencari tahu menggunakan jendela baru.getSelection API yang akan saya bagikan untuk anak cucu. Perhatikan bahwa MDN menyarankan ada dukungan yang lebih luas untuk window.getSelection, namun, jarak tempuh Anda mungkin berbeda.

const getSelectionCaretAndLine = () => {
    // our editable div
    const editable = document.getElementById('editable');

    // collapse selection to end
    window.getSelection().collapseToEnd();

    const sel = window.getSelection();
    const range = sel.getRangeAt(0);

    // get anchor node if startContainer parent is editable
    let selectedNode = editable === range.startContainer.parentNode
      ? sel.anchorNode 
      : range.startContainer.parentNode;

    if (!selectedNode) {
        return {
            caret: -1,
            line: -1,
        };
    }

    // select to top of editable
    range.setStart(editable.firstChild, 0);

    // do not use 'this' sel anymore since the selection has changed
    const content = window.getSelection().toString();
    const text = JSON.stringify(content);
    const lines = (text.match(/\\n/g) || []).length + 1;

    // clear selection
    window.getSelection().collapseToEnd();

    // minus 2 because of strange text formatting
    return {
        caret: text.length - 2, 
        line: lines,
    }
} 

Berikut adalah jsfiddle yang aktif pada keyup. Namun perlu dicatat, bahwa penekanan tombol arah cepat, serta penghapusan cepat tampaknya merupakan peristiwa lewati.

Chris Sullivan
sumber
Bekerja untuk saya! Terima kasih banyak.
dmodo
Dengan pilihan teks ini tidak mungkin lagi karena diciutkan. Skenario yang mungkin: perlu mengevaluasi pada setiap acara
keyUp
0

Cara lurus ke depan, yang melakukan iterasi melalui semua anak div konten yang dapat diedit hingga mencapai endContainer. Kemudian saya menambahkan offset penampung akhir dan kami memiliki indeks karakter. Harus bekerja dengan sejumlah sarang. menggunakan rekursi.

Catatan: membutuhkan poly fill untuk dukungan yaituElement.closest('div[contenteditable]')

https://codepen.io/alockwood05/pen/vMpdmZ

function caretPositionIndex() {
    const range = window.getSelection().getRangeAt(0);
    const { endContainer, endOffset } = range;

    // get contenteditableDiv from our endContainer node
    let contenteditableDiv;
    const contenteditableSelector = "div[contenteditable]";
    switch (endContainer.nodeType) {
      case Node.TEXT_NODE:
        contenteditableDiv = endContainer.parentElement.closest(contenteditableSelector);
        break;
      case Node.ELEMENT_NODE:
        contenteditableDiv = endContainer.closest(contenteditableSelector);
        break;
    }
    if (!contenteditableDiv) return '';


    const countBeforeEnd = countUntilEndContainer(contenteditableDiv, endContainer);
    if (countBeforeEnd.error ) return null;
    return countBeforeEnd.count + endOffset;

    function countUntilEndContainer(parent, endNode, countingState = {count: 0}) {
      for (let node of parent.childNodes) {
        if (countingState.done) break;
        if (node === endNode) {
          countingState.done = true;
          return countingState;
        }
        if (node.nodeType === Node.TEXT_NODE) {
          countingState.count += node.length;
        } else if (node.nodeType === Node.ELEMENT_NODE) {
          countUntilEndContainer(node, endNode, countingState);
        } else {
          countingState.error = true;
        }
      }
      return countingState;
    }
  }
alockwood05
sumber