รับตำแหน่งดัชนีคาเร็ต contentEditable


119

ฉันพบว่าครอสเบราว์เซอร์ที่ดีจำนวนมากปฏิเสธวิธีตั้งค่าตำแหน่งเคอร์เซอร์หรือดัชนีคาเร็ตในcontentEditableองค์ประกอบ แต่ไม่มีวิธีรับหรือหาดัชนี ...

สิ่งที่ฉันต้องการจะทำคือการรู้ว่าดัชนีของลูกศรภายใน div keyupนี้บน

ดังนั้นเมื่อผู้ใช้พิมพ์ข้อความฉันสามารถรู้ดัชนีเคอร์เซอร์ภายในcontentEditableองค์ประกอบได้ทุกเมื่อ

แก้ไข: ฉันกำลังมองหาINDEXภายในเนื้อหา div (ข้อความ) ไม่ใช่พิกัดเคอร์เซอร์

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

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

ดูตำแหน่งในข้อความ จากนั้นมองขึ้นไปที่เหตุการณ์สุดท้ายของ "@" ก่อนตำแหน่งนั้น ตรรกะข้อความก็แค่บางส่วน
Bertvan

นอกจากนี้ฉันไม่ได้วางแผนที่จะอนุญาตให้ใช้แท็กอื่น ๆ ใน <diV> แต่เป็นข้อความเท่านั้น
Bertvan

ตกลงใช่ฉันกำลังจะต้องแท็กอื่น ๆ ภายใน <div> จะมี <a> แท็ก แต่จะไม่มีการซ้อน ...
Bertvan

@Bertvan: ถ้าคาเร็ตอยู่ในไฟล์ <a>องค์ประกอบภายใน<div>คุณต้องการชดเชยอะไร ค่าชดเชยภายในข้อความภายใน<a>?
Tim Down

ไม่ควรอยู่ในองค์ประกอบ <a> องค์ประกอบ <a> ควรแสดงผล html ดังนั้นผู้ใช้จึงไม่สามารถวางคาเร็ตไว้ที่นั่นได้
Bertvan

คำตอบ:


121

รหัสต่อไปนี้ถือว่า:

  • มีโหนดข้อความเดียวเสมอภายในแก้ไขได้<div>และไม่มีโหนดอื่น
  • div ที่แก้ไขได้ไม่มีการwhite-spaceตั้งค่าคุณสมบัติCSS เป็นpre

หากคุณต้องการแนวทางทั่วไปที่จะใช้กับเนื้อหาที่มีองค์ประกอบซ้อนกันให้ลองใช้คำตอบนี้

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

รหัส:

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
สิ่งนี้จะใช้ไม่ได้หากมีแท็กอื่นอยู่ในนั้น คำถาม: ถ้าคาเร็ตอยู่ใน<a>องค์ประกอบภายใน<div>คุณต้องการชดเชยอะไร ค่าชดเชยภายในข้อความภายใน<a>?
Tim Down

3
@ ริชาร์ด: keyupน่าจะเป็นเหตุการณ์ที่ไม่ถูกต้องสำหรับสิ่งนี้ แต่เป็นสิ่งที่ใช้ในคำถามเดิม getCaretPosition()ตัวมันเองก็ทำได้ดีภายในข้อ จำกัด ของมันเอง
Tim Down

3
การสาธิต JSFIDDLE นั้นล้มเหลวหากฉันกด Enter และขึ้นบรรทัดใหม่ ตำแหน่งจะแสดง 0.
giorgio79

5
@ giorgio79: ใช่เนื่องจากเส้นแบ่งสร้าง a <br>หรือ<div>องค์ประกอบซึ่งละเมิดสมมติฐานแรกที่กล่าวถึงในคำตอบ หากคุณต้องการวิธีแก้ปัญหาทั่วไปเล็กน้อยคุณสามารถลองstackoverflow.com/a/4812022/96100
Tim Down

2
มีการทำเช่นนี้ต่อไปเพื่อให้มีหมายเลขบรรทัดหรือไม่?
Adjit

28

ริ้วรอยเล็กน้อยที่ฉันไม่เห็นว่ามีอยู่ในคำตอบอื่น ๆ :

  1. องค์ประกอบสามารถมีโหนดลูกหลายระดับ (เช่นโหนดลูกที่มีโหนดลูกที่มีโหนดลูก ... )
  2. การเลือกอาจประกอบด้วยตำแหน่งเริ่มต้นและตำแหน่งสิ้นสุดที่แตกต่างกัน (เช่นมีการเลือกหลายตัวอักษร)
  3. โหนดที่มีจุดเริ่มต้น / จุดสิ้นสุดของ Caret อาจไม่ใช่องค์ประกอบหรือลูกโดยตรง

ต่อไปนี้เป็นวิธีรับตำแหน่งเริ่มต้นและสิ้นสุดโดยชดเชยกับค่า textContent ขององค์ประกอบ:

// 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
สิ่งนี้ต้องเลือกเป็นคำตอบที่ถูกต้อง ใช้งานได้กับแท็กในข้อความ (คำตอบที่ยอมรับไม่ได้)
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
สิ่งนี้หยุดทำงานทันทีที่คุณกด Enter และเริ่มในบรรทัดอื่น (เริ่มที่ 0 อีกครั้ง - อาจนับจาก CR / LF)
เอียน

มันจะทำงานไม่ถูกต้องหากคุณมีคำที่เป็นตัวหนาและ / หรือตัวเอียง
user2824371

14

ลองสิ่งนี้:

Caret.js รับตำแหน่งคาเร็ตและออฟเซ็ตจากช่องข้อความ

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

การสาธิต: http://ichord.github.com/Caret.js


นี่คือความหวาน ฉันต้องการพฤติกรรมนี้เพื่อตั้งค่าคาเร็ตให้สิ้นสุดcontenteditable liเมื่อคลิกที่ปุ่มเพื่อเปลี่ยนชื่อliเนื้อหา
akinuri

@AndroidDev ฉันไม่ใช่คนเขียน Caret.js แต่คุณคิดว่าการได้รับตำแหน่งคาเร็ตสำหรับเบราว์เซอร์หลักทั้งหมดนั้นซับซ้อนกว่าสองสามบรรทัดหรือไม่? คุณรู้จักหรือเคยสร้างทางเลือกอื่น ๆ ที่คุณสามารถแบ่งปันกับเราได้หรือไม่?
adelriosantiago

8

สายไปงานปาร์ตี้ แต่ในกรณีที่คนอื่นกำลังดิ้นรน ไม่มีการค้นหาใด ๆ ของ Google ที่ฉันพบในช่วงสองวันที่ผ่านมาซึ่งพบว่ามีอะไรที่ใช้ได้ผล แต่ฉันได้มาพร้อมกับโซลูชันที่กระชับและสวยงามซึ่งจะใช้งานได้ตลอดเวลาไม่ว่าคุณจะมีแท็กซ้อนกันกี่แท็ก:

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>

มันเลือกทางกลับไปที่จุดเริ่มต้นของย่อหน้าจากนั้นนับความยาวของสตริงเพื่อรับตำแหน่งปัจจุบันจากนั้นยกเลิกการเลือกเพื่อให้เคอร์เซอร์กลับไปที่ตำแหน่งปัจจุบัน หากคุณต้องการทำสิ่งนี้สำหรับทั้งเอกสาร (มากกว่าหนึ่งย่อหน้า) ให้เปลี่ยนparagraphboundaryเป็นdocumentboundaryหรือรายละเอียดใด ๆ สำหรับกรณีของคุณ ตรวจสอบ API สำหรับรายละเอียดเพิ่มเติม ไชโย! :)


1
ถ้าฉันมี <div contenteditable> some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text </div> ทุกครั้งที่วางเคอร์เซอร์ไว้ก่อนiแท็กหรือองค์ประกอบ html ลูกใด ๆ ภายในdivตำแหน่งเคอร์เซอร์จะเริ่มต้นที่ 0 มีวิธีหลีกเลี่ยงการนับการเริ่มต้นใหม่นี้หรือไม่?
vam

แปลก ฉันไม่เข้าใจพฤติกรรมนั้นใน Chrome สิ่งที่เบราว์เซอร์ที่คุณใช้?
Soubriquet

2
ดูเหมือนว่า selection.modify อาจรองรับหรือไม่รองรับในทุกเบราว์เซอร์ 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
    };
}

อันนี้ใช้งานได้จริงสำหรับฉันฉันได้ลองทุกข้อข้างต้นแล้วพวกเขาไม่ได้
iStudLion

ขอบคุณ แต่มันส่งคืน {x: 0, y: 0} ในบรรทัดใหม่ด้วย
hichamkazan

สิ่งนี้ส่งคืนตำแหน่งพิกเซลไม่ใช่การชดเชยอักขระ
4esn0k

ขอบคุณฉันกำลังมองหาการดึงตำแหน่งพิกเซลจากคาเร็ตและมันใช้งานได้ดี
Sameesh

6

window.getSelection - vs - document.selection

อันนี้ใช้ได้กับฉัน:

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>

สายการโทรขึ้นอยู่กับประเภทเหตุการณ์สำหรับเหตุการณ์สำคัญให้ใช้สิ่งนี้:

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

สำหรับเหตุการณ์เมาส์ใช้สิ่งนี้:

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

ในสองกรณีนี้ฉันดูแลเส้นแบ่งโดยการเพิ่มดัชนีเป้าหมาย


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

หมายเหตุ: ออบเจ็กต์ช่วงของตัวเองสามารถเก็บไว้ในตัวแปรและสามารถเลือกใหม่ได้ตลอดเวลาเว้นแต่เนื้อหาของ div ที่แก้ไขได้จะเปลี่ยนไป

ข้อมูลอ้างอิงสำหรับ IE 8 และต่ำกว่า: http://msdn.microsoft.com/en-us/library/ms535872(VS.85).aspx

ข้อมูลอ้างอิงสำหรับเบราว์เซอร์มาตรฐาน (อื่น ๆ ทั้งหมด): https://developer.mozilla.org/en/DOM/range (เป็นเอกสารของ mozilla แต่โค้ดใช้งานได้ใน chrome, safari, opera และ ie9 ด้วย)


1
ขอบคุณ แต่ฉันจะรับ 'ดัชนี' ของตำแหน่งคาเร็ตในเนื้อหา div ได้อย่างไร?
Bertvan

ตกลงดูเหมือนว่าการเรียก .baseOffset บน. getSelection () จะใช้เคล็ดลับ ดังนั้นพร้อมกับคำตอบของคุณจึงตอบคำถามของฉัน ขอบคุณ!
Bertvan

2
น่าเสียดายที่ .baseOffset ใช้งานได้เฉพาะใน webkit (ฉันคิดว่า) นอกจากนี้ยังให้ค่าชดเชยจากพาเรนต์ imediate ของคาเร็ตเท่านั้น (ถ้าคุณมี <b> แท็กใน <div> มันจะให้ค่าชดเชยจากจุดเริ่มต้นของ <b> ไม่ใช่จุดเริ่มต้นของ <div> ช่วงตามมาตรฐานสามารถใช้ range.endOffset range.startOffset range.endContainer และ range.startContainer เพื่อรับออฟเซ็ตจากโหนดแม่ของการเลือกและโหนดเอง (ซึ่งรวมถึงโหนดข้อความ) IE จัดเตรียม range.offsetLeft ซึ่งเป็น ชดเชยจากด้านซ้ายเป็นพิกเซลและไร้ประโยชน์
Nico Burns

ที่ดีที่สุดคือเก็บวัตถุ range ไว้ในตัวเองและใช้ window.getSelection (). addrange (range); <- มาตรฐานและ range.select (); <- IE สำหรับการจัดตำแหน่งเคอร์เซอร์ใหม่ที่เดิม range.insertNode (nodetoinsert); <- มาตรฐานและ range.pasteHTML (htmlcode); <- IE เพื่อแทรกข้อความหรือ html ที่เคอร์เซอร์
Nico Burns

Rangeวัตถุกลับโดยเบราว์เซอร์มากที่สุดและTextRangeวัตถุกลับโดย IE เป็นสิ่งที่แตกต่างกันมากดังนั้นผมไม่แน่ใจว่าคำตอบนี้แก้มาก
Tim Down

3

เนื่องจากสิ่งนี้ทำให้ฉันต้องใช้เวลาตลอดไปในการใช้window.getSelection API ใหม่ฉันจะแบ่งปันเพื่อลูกหลาน โปรดทราบว่า MDN แนะนำว่ามีการรองรับ window.getSelection ที่กว้างขึ้นอย่างไรก็ตามระยะทางของคุณอาจแตกต่างกันไป

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

นี่คือjsfiddleที่เริ่มการทำงานของ keyup อย่างไรก็ตามโปรดทราบว่าการกดแป้นทิศทางอย่างรวดเร็วเช่นเดียวกับการลบอย่างรวดเร็วดูเหมือนจะข้ามเหตุการณ์


ใช้ได้ผลสำหรับฉัน! ขอบคุณมาก.
dmodo

ด้วยการเลือกข้อความนี้ไม่สามารถทำได้อีกต่อไปเนื่องจากถูกยุบ สถานการณ์ที่เป็นไปได้: จำเป็นต้องประเมินทุกเหตุการณ์
keyUp

0

วิธีตรงไปตรงมาที่วนซ้ำผ่าน chidren ทั้งหมดของ div ที่สามารถแก้ไขได้จนกว่าจะถึง endContainer จากนั้นฉันเพิ่มออฟเซ็ต end container และเรามีดัชนีอักขระ ควรใช้กับจำนวนรัง ใช้การเรียกซ้ำ

หมายเหตุ: ต้องมีการเติมโพลีสำหรับ ie เพื่อรองรับElement.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;
    }
  }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.