contentEditable 캐럿 인덱스 위치 가져 오기


119

contentEditable요소 에서 커서 또는 캐럿 인덱스 위치를 설정하는 방법에 대한 좋은 크로스 브라우저 답변을 많이 찾았 지만 인덱스를 가져 오거나 찾는 방법에 대해서는 없습니다.

내가하고 싶은 것은이 div 내에서 캐럿의 인덱스를 아는 것입니다 keyup.

따라서 사용자가 텍스트를 입력 할 때 contentEditable요소 내에서 커서의 인덱스를 언제든지 알 수 있습니다 .

편집 : 커서 좌표가 아닌 div 내용 (텍스트) 내 에서 INDEX를 찾고 있습니다.

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

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

텍스트에서 그 위치를보십시오. 그런 다음 해당 위치 이전의 '@'이 마지막으로 나오는 것을 찾습니다. 그래서 그냥 텍스트 로직입니다.
Bertvan

또한 <diV> 내에 다른 태그를 허용 할 계획이 아닙니다. 텍스트 만 허용됩니다
Bertvan

좋아, 그래 내가 하고 의 <div> 내에서 다른 태그를해야 할 것이다. <a> 태그가 있지만 중첩은 없습니다 ...
Bertvan

@Bertvan : 캐럿이 내부 <a>요소 안에 있으면 <div>어떤 오프셋을 원합니까? 안에있는 텍스트 내의 오프셋 <a>?
팀 다운

<a> 요소 안에 있으면 안됩니다. <a> 요소는 html로 렌더링되어야하므로 사용자가 실제로 거기에 캐럿을 넣을 수 없습니다.
Bertvan

답변:


121

다음 코드는 다음을 가정합니다.

  • 편집 가능한 노드에는 항상 단일 텍스트 노드가 있으며 <div>다른 노드는 없습니다.
  • 편집 가능한 div에 CSS white-space속성이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
@Richard : 음, keyup이것에 대한 잘못된 사건 일 가능성이 있지만 원래 질문에서 사용 된 것입니다. getCaretPosition()그 자체는 한계 내에서 괜찮습니다.
Tim Down

3
해당 JSFIDDLE 데모는 Enter 키를 누르고 새 줄로 이동하면 실패합니다. 위치는 0으로 표시됩니다.
giorgio79

5
@ giorgio79 : 예, 줄 바꿈은 <br>또는 <div>요소를 생성하기 때문에 답변에 언급 된 첫 번째 가정을 위반합니다. 좀 더 일반적인 솔루션이 필요한 경우 stackoverflow.com/a/4812022/96100
Tim Down

2
어쨌든 줄 번호를 포함하도록 할 수 있습니까?
Adjit

28

다른 답변에서 해결되지 않은 몇 가지 주름 :

  1. 요소는 여러 수준의 자식 노드를 포함 할 수 있습니다 (예 : 자식 노드가있는 자식 노드가있는 자식 노드 ...)
  2. 선택은 다른 시작 및 끝 위치로 구성 될 수 있습니다 (예 : 여러 문자가 선택됨).
  3. 캐럿 시작 / 종료를 포함하는 노드는 요소 또는 직계 자식이 아닐 수 있습니다.

다음은 요소의 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에서 계산).
이안

Bold 및 / 또는 Italic 단어가있는 경우 제대로 작동하지 않습니다.
user2824371

14

이 시도:

Caret.js 텍스트 필드에서 캐럿 위치 및 오프셋 가져 오기

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

데모 : http://ichord.github.com/Caret.js


이것은 달콤합니다. 의 콘텐츠 contenteditable li이름을 변경하기 위해 버튼을 클릭 할 때 캐럿을 끝으로 설정하려면이 동작이 필요했습니다 li.
akinuri

@AndroidDev 저는 Caret.js의 작성자는 아니지만 모든 주요 브라우저의 캐럿 위치를 얻는 것이 몇 줄보다 복잡하다고 생각 했습니까? 우리와 공유 할 수있는 부 풀지 않은 대안을 알고 있거나 만들었 습니까?
adelriosantiago 19.11.28

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>

단락의 시작 부분까지 다시 선택하고 문자열의 길이를 계산하여 현재 위치를 가져온 다음 선택을 취소하여 커서를 현재 위치로 되돌립니다. 당신은 전체 문서 (하나 이상의 단락)이 작업을 수행 할 경우, 변경 paragraphboundarydocumentboundary사건 또는 어떤 단위. 자세한 내용 은 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 장소 커서를 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-대-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)
    }
});

참고 : 범위 객체 자체는 변수에 저장 될 수 있으며 contenteditable div의 내용이 변경되지 않는 한 언제든지 다시 선택할 수 있습니다.

IE 8 이하에 대한 참조 : http://msdn.microsoft.com/en-us/library/ms535872(VS.85).aspx

표준 (기타 모든) 브라우저에 대한 참조 : https://developer.mozilla.org/en/DOM/range (모질라 문서이지만 코드는 chrome, safari, opera 및 ie9에서도 작동 함)


1
감사합니다.하지만 div 내용에서 캐럿 위치의 '인덱스'를 정확히 어떻게 얻습니까?
Bertvan

좋아, .getSelection ()에서 .baseOffset을 호출하는 것이 트릭을 수행하는 것처럼 보입니다. 그래서 이것은 당신의 대답과 함께 제 질문에 대답합니다. 감사!
Bertvan

2
불행히도 .baseOffset은 웹킷에서만 작동합니다 (내 생각에). 또한 캐럿의 imediate 부모로부터의 오프셋 만 제공합니다 (<div> 안에 <b> 태그가있는 경우 <div>의 시작이 아니라 <b>의 시작부터 오프셋을 제공합니다. . 표준 기반 범위는 range.endOffset range.startOffset range.endContainer 및 range.startContainer를 사용 하여 선택 항목 의 상위 노드 와 노드 자체 (텍스트 노드 포함) 에서 오프셋을 가져올 수 있습니다. IE는 범위 .offsetLeft를 제공합니다. 픽셀 단위로 왼쪽에서 오프셋 되므로 쓸모가 없습니다.
Nico Burns

범위 객체 자체를 저장하고 window.getSelection (). addrange (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,
    }
} 

다음은 keyup에서 발생 하는 jsfiddle 입니다. 그러나 빠른 방향 키 누름과 빠른 삭제는 이벤트를 건너 뛰는 것 같습니다.


나를 위해 작동합니다! 정말 고맙습니다.
dmodo

이 텍스트를 사용하면 축소되어 더 이상 선택할 수 없습니다. 가능한 시나리오 : 모든 keyUp 이벤트에 대해 평가해야 함
hschmieder

0

endContainer에 도달 할 때까지 contenteditable div의 모든 하위 항목을 반복하는 간단한 방법입니다. 그런 다음 끝 컨테이너 오프셋을 추가하고 문자 인덱스를 얻습니다. 여러 중첩과 함께 작동해야합니다. 재귀를 사용합니다.

주 : 필요 폴리 채우기를 지원 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.