Düzenlenebilir düzeltme dizini konumu


119

Bir öğede imleç veya düzeltme dizini konumunun nasıl ayarlanacağına dair tonlarca iyi, çapraz tarayıcı yanıtı buluyorum contentEditable, ancak GET veya dizinini nasıl bulacağımı bilmiyorum ...

Yapmak istediğim şey, bu div içindeki düzeltme işaretinin dizinini bilmek keyup.

Bu nedenle, kullanıcı metin yazarken, imleci contentEditableöğenin içindeki indeksini her an bilebilirim .

DÜZENLE: İmleç koordinatlarında değil div içeriği (metin) içinde INDEX'i arıyorum .

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

$('#contentbox').keyup(function() { 
    // ... ? 
});

Metindeki konumuna bakın. Ardından, bu konumdan önceki son '@' geçtiğine bakın. Yani sadece biraz metin mantığı.
Bertvan

Ayrıca, <diV> içinde başka etiketlere izin vermeyi planlamıyorum, sadece metin
Bertvan

Tamam, evet am <div> içindeki diğer etiketler ihtiyacımız olacak. <a> etiketleri olacak, ancak yuvalama olmayacak ...
Bertvan

@Bertvan: İmleç, içindeki bir <a>elemanın içindeyse, <div>o zaman hangi ofseti istiyorsunuz? <a>? İçindeki metin içindeki ofset.
Tim Down

Asla bir <a> öğesinin içinde olmamalıdır. <a> öğesi html olarak gösterilmelidir, böylece kullanıcı aslında imleci oraya yerleştiremez.
Bertvan

Yanıtlar:


121

Aşağıdaki kod şunları varsayar:

  • Düzenlenebilir içinde her zaman tek bir metin düğümü vardır <div>ve başka düğüm yoktur
  • Düzenlenebilir div, CSS white-spaceözelliğine sahip değilpre

İçeriği iç içe yerleştirilmiş öğelerle çalıştıracak daha genel bir yaklaşıma ihtiyacınız varsa, şu cevabı deneyin:

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

Kod:

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>


9
İçinde başka etiket varsa bu işe yaramaz. Soru: İmleç, <a>öğenin içindeki bir öğenin içindeyse, <div>o zaman hangi ofseti istiyorsunuz? <a>? İçindeki metin içindeki ofset.
Tim Down

3
@Richard: Bunun keyupiçin yanlış bir olay olması muhtemel ama asıl soruda kullanılan şey buydu. getCaretPosition()kendisi kendi sınırları içinde iyidir.
Tim Down

3
Enter tuşuna basıp yeni bir satıra gidersem JSFIDDLE demosu başarısız oluyor. Pozisyon 0'ı gösterecek.
giorgio79

5
@ giorgio79: Evet, çünkü satır sonu , yanıtta bahsedilen ilk varsayımı ihlal eden bir <br>veya <div>öğesi oluşturur . Biraz daha genel bir çözüme ihtiyacınız varsa, stackoverflow.com/a/4812022/96100
Tim Down

2
Bunu satır numarasını içerecek şekilde yapmanın bir yolu var mı?
2016

28

Diğer cevaplarda ele alınmadığını düşündüğüm birkaç kırışıklık:

  1. eleman birden fazla alt düğüm seviyesi içerebilir (örneğin, alt düğümlere sahip alt düğümlere sahip alt düğümler ...)
  2. bir seçim, farklı başlangıç ​​ve bitiş konumlarından oluşabilir (örneğin, birden çok karakter seçilir)
  3. Caret başlangıcı / sonu içeren düğüm, öğe veya onun doğrudan alt öğesi olamaz

Öğenin textContent değerinin göreli konumları olarak başlangıç ​​ve bitiş konumlarını almanın bir yolu:

// 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]];
}

3
Bu doğru cevap olarak seçilmelidir. Metnin içindeki etiketlerle çalışır (kabul edilen yanıt değil)
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>


3
Bu maalesef enter tuşuna basıp başka bir satırda başlar başlamaz çalışmayı durdurur (tekrar 0'dan başlar - muhtemelen CR / LF'den sayılır).
Ian

Bazı Kalın ve / veya İtalik sözcükleriniz varsa düzgün çalışmaz.
user2824371

14

Bunu dene:

Caret.js Metin alanından şapka konumunu ve ofseti alın

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

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


Bu tatlı. İçeriğini contenteditable liyeniden adlandırmak için bir düğme tıklandığında imleci a'nın sonuna ayarlamak için bu davranışa ihtiyacım vardı li.
akinuri

@AndroidDev Caret.js'nin yazarı değilim, ancak tüm büyük tarayıcılar için düzeltme işareti konumunu almanın birkaç satırdan daha karmaşık olduğunu düşündünüz mü? Bizimle paylaşabileceğiniz şişirilmemiş bir alternatifi biliyor musunuz veya yarattınız mı?
adelriosantiago

8

Partiye biraz geç, ama başka birinin mücadele etmesi durumunda. Son iki gündür bulduğum Google aramalarının hiçbiri işe yarayan bir şey bulamadı, ancak kaç tane iç içe etiketiniz olursa olsun her zaman işe yarayacak kısa ve zarif bir çözüm buldum:

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>

Paragrafın başına kadar tüm yolu seçer ve ardından geçerli konumu almak için dizenin uzunluğunu sayar ve ardından imleci geçerli konuma döndürmek için seçimi geri alır. Bunu tüm bir belge için (birden fazla paragraf) yapmak istiyorsanız, o zaman vakanız paragraphboundaryiçin documentboundaryveya herhangi bir ayrıntı düzeyine geçin . Daha fazla ayrıntı için API'ye bakın . Şerefe! :)


1
Ben varsa <div contenteditable> some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text </div> önce Everytime yer imleci ietiketi veya herhangi bir çocuk html elemanı içeriden div, imleç pozisyonu 0'dan başlıyor bu yeniden başlatma sayısını kaçmak için bir yol var mı?
vam

Garip. Chrome'da bu davranışı anlamıyorum. Hangi tarayıcıyı kullanıyorsunuz?
Soubriquet

2
Görünüşe göre selection.modify tüm tarayıcılarda desteklenebilir veya desteklenmeyebilir. 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
    };
}

bu aslında benim için çalıştı, yukarıdakilerin hepsini denedim, yapmadılar.
iStudLion

teşekkür ederim ama aynı zamanda yeni satırda {x: 0, y: 0} döndürüyor.
hichamkazan

bu, karakter ofsetini değil piksel konumunu döndürür
4esn0k

teşekkürler, imleçten piksel konumunu almak arıyordum ve iyi çalışıyor.
Sameesh

6

window.getSelection - vs - document.selection

Bu benim için çalışıyor:

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>

Çağıran hat olay türüne bağlıdır, anahtar olay için şunu kullanın:

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

fare olayı için şunu kullanın:

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

bu iki durumda hedef dizini ekleyerek kesme satırlarına dikkat ederim


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

Not: aralık nesnesi, kendisi bir değişkende saklanabilir ve tatmin edilebilir div'in içeriği değişmedikçe herhangi bir zamanda yeniden seçilebilir.

IE 8 ve altı için referans: http://msdn.microsoft.com/en-us/library/ms535872(VS.85).aspx

Standartlar (diğer tüm) tarayıcıları için referans: https://developer.mozilla.org/en/DOM/range (mozilla belgeleri, ancak kod chrome, safari, opera ve ie9'da da çalışıyor)


1
Teşekkürler, ama div içeriğindeki imleç konumunun "dizinini" tam olarak nasıl alırım?
Bertvan

Tamam, .getSelection () üzerinde .baseOffset'i çağırmak işe yarıyor gibi görünüyor. Yani bu, cevabınızla birlikte sorumu cevaplıyor. Teşekkürler!
Bertvan

2
Ne yazık ki .baseOffset yalnızca webkit'te çalışır (sanırım). Ayrıca, size yalnızca imleç öğesinin imediate ebeveyninden uzaklığı verir (<div> içinde bir <b> etiketiniz varsa, <div> başından değil, <b> başından itibaren ofseti verecektir. . Standartlara dayalı aralıklar, seçimin üst düğümünden ve düğümün kendisinden uzaklığı almak için range.endOffset range.startOffset range.endContainer ve range.startContainer'ı kullanabilir (bu, metin düğümlerini içerir). IE, soldan piksel olarak ofset ve çok kullanışsız.
Nico Burns

En iyisi, aralık nesnesini kendi başına saklamak ve window.getSelection (). Addrange (aralık); <- standartlar ve aralık.select (); <- İmleci aynı yere yeniden konumlandırmak için IE. range.insertNode (nodetoinsert); <- standartlar ve range.pasteHTML (htmlcode); <- IE, imlece metin veya html eklemek için.
Nico Burns

RangeÇoğu tarayıcı ve döndürdüğü nesnenin TextRangeben çok emin bu cevap çözer değilim bu yüzden IE tarafından döndürülen nesne, son derece farklı şeylerdir.
Tim Down

3

Bu, yeni window.getSelection API'sini kullanarak anlamam sonsuza kadar sürdüğünden , gelecek nesilleri için paylaşacağım. MDN'nin window.getSelection için daha geniş bir destek önerdiğini ancak kilometrenizin değişiklik gösterebileceğini unutmayın.

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,
    }
} 

İşte keyup üzerine ateşleyen bir jsfiddle . Ancak, hızlı yön tuşlarına basmanın yanı sıra hızlı silme işleminin atlama olayları gibi göründüğünü unutmayın.


Benim için çalışıyor! Çok teşekkür ederim.
dmodo

Bu metin ile seçim artık daraltılmış olduğu için mümkün değildir. Olası senaryo: her keyUp olayında değerlendirme yapılması gerekiyor
hschmieder

0

EndContainer'a ulaşana kadar mutlu div'in tüm çocukları boyunca yinelenen basit bir yol. Sonra uç konteyner ofsetini ekliyorum ve karakter indeksimiz var. Herhangi bir sayıda yuva ile çalışmalıdır. özyineleme kullanır.

Not: ie'nin desteklenmesi için poli dolgu gerektirirElement.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;
    }
  }
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.