157

要素にキャレットの位置を設定する方法については、クロスブラウザーの優れた回答がたくさん見つかりましたが、そもそもキャレットの位置を取得する方法については何も見つかりませんでした。contentEditable

私がやりたいのは、のdiv内のキャレットの位置を知ることですkeyupcontentEditableしたがって、ユーザーがテキストを入力しているときは、いつでも要素内のキャレットの位置を知ることができます。

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

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

15 に答える 15

151

次のコードは次のことを前提としています。

  • 編集可能なノード内には常に単一のテキストノードがあり、<div>他のノードはありません
  • white-space編集可能なdivの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>

于 2010-10-20T08:49:42.760 に答える
45

他の回答では対処されていないいくつかのしわ:

  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]];
}
于 2018-11-03T05:12:18.930 に答える
24

$("#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>

于 2016-04-05T10:28:51.877 に答える
17

ちょっとパーティーに遅れましたが、誰かが苦労している場合に備えて。過去2日間に私が見つけた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を確認してください。乾杯!:)

于 2019-01-23T18:55:12.007 に答える
14

これを試して:

Caret.jsキャレットの位置とテキストフィールドからのオフセットを取得します

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

デモ: http: //ichord.github.com/Caret.js

于 2014-06-02T08:29:51.070 に答える
12

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

これらの2つのケースでは、ターゲットインデックスを追加してブレークラインを処理します

于 2015-05-22T15:11:06.117 に答える
11
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
    };
}
于 2015-06-23T04:02:58.580 に答える
5

これは私が新しいwindow.getSelectionAPIを使用して理解するのに永遠にかかったので、後世のために共有するつもりです。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です。ただし、方向キーをすばやく押すこと、および削除をすばやく行うことは、スキップイベントのように見えることに注意してください。

于 2019-04-07T11:11:29.030 に答える
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(mozillaドキュメントですが、コードはchrome、safari、opera、ie9でも機能します)

于 2010-10-19T20:16:35.410 に答える
2

編集可能なdivスタイルを「display:inline-block; white-space:pre-wrap」に設定すると、新しい行を入力したときに新しい子divは取得されず、LF文字(つまり&#10)が取得されます。 。

function showCursPos(){
    selection = document.getSelection();
    childOffset = selection.focusOffset;
    const range = document.createRange();
    eDiv = document.getElementById("eDiv");
    range.setStart(eDiv, 0);
    range.setEnd(selection.focusNode, childOffset);
    var sHtml = range.toString();
    p = sHtml.length; 
    sHtml=sHtml.replace(/(\r)/gm, "\\r");
    sHtml=sHtml.replace(/(\n)/gm, "\\n");
    document.getElementById("caretPosHtml").value=p;
    document.getElementById("exHtml").value=sHtml;   
  }
click/type in div below:
<br>
<div contenteditable name="eDiv" id="eDiv"  
     onkeyup="showCursPos()" onclick="showCursPos()" 
     style="width: 10em; border: 1px solid; display:inline-block; white-space: pre-wrap; "
     >123&#13;&#10;456&#10;789</div>
<p>
html caret position:<br> <input type="text" id="caretPosHtml">
<p>  
html from start of div:<br> <input type="text" id="exHtml">

私が気付いたのは、編集可能なdivで「Enter」を押すと新しいノードが作成されるため、focusOffsetがゼロにリセットされることです。これが、範囲変数を追加し、それを子ノードのfocusOffsetからeDivの先頭に拡張する必要がある理由です(したがって、その間のすべてのテキストをキャプチャします)。

于 2021-01-08T23:17:16.367 に答える
2

これは@alockwood05の回答に基づいており、contenteditable div内のネストされたタグとノード内のオフセットを使用して、caretのget機能とset機能の両方を提供するため、オフセットによってシリアル化と逆シリアル化の両方が可能なソリューションが得られます。

私はこのソリューションをクロスプラットフォームのコードエディターで使用しています。クロスプラットフォームのコードエディターでは、レクサー/パーサーを介して構文を強調表示する前にカレットの開始/終了位置を取得し、その後すぐに元に戻す必要があります。

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

function countUntilOffset(parent, offset, countingState = {count: 0}) {
    for (let node of parent.childNodes) {
        if (countingState.done) break;
        if (node.nodeType === Node.TEXT_NODE) {
            if (countingState.count <= offset && offset < countingState.count + node.length)
            {
                countingState.offsetInNode = offset - countingState.count;
                countingState.node = node; 
                countingState.done = true; 
                return countingState; 
            }
            else { 
                countingState.count += node.length; 
            }
        } else if (node.nodeType === Node.ELEMENT_NODE) {
            countUntilOffset(node, offset, countingState);
        } else {
            countingState.error = true;
        }
    }
    return countingState;
}

function getCaretPosition()
{
    let editor = document.getElementById('editor');
    let sel = window.getSelection();
    if (sel.rangeCount === 0) { return null; }
    let range = sel.getRangeAt(0);    
    let start = countUntilEndContainer(editor, range.startContainer, range.startOffset);
    let end = countUntilEndContainer(editor, range.endContainer, range.endOffset);
    let offsetsCounts = { start: start.count + start.offsetInNode, end: end.count + end.offsetInNode };
    let offsets = { start: start, end: end, offsets: offsetsCounts };
    return offsets;
}

function setCaretPosition(start, end)
{
    let editor = document.getElementById('editor');
    let sel = window.getSelection();
    if (sel.rangeCount === 0) { return null; }
    let range = sel.getRangeAt(0);
    let startNode = countUntilOffset(editor, start);
    let endNode = countUntilOffset(editor, end);
    let newRange = new Range();
    newRange.setStart(startNode.node, startNode.offsetInNode);
    newRange.setEnd(endNode.node, endNode.offsetInNode);
    sel.removeAllRanges();
    sel.addRange(newRange);
    return true;
}
于 2021-02-06T21:53:48.040 に答える
1

簡単な方法で、contenteditable divのすべての子を、endContainerに到達するまで繰り返します。次に、エンドコンテナオフセットを追加すると、文字インデックスが作成されます。任意の数のネストで機能するはずです。再帰を使用します。

注:サポートするには、つまりポリフィルが必要です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;
    }
  }
于 2019-04-16T19:01:05.543 に答える
1

これはAngularで機能します

private getCaretPosition() {
   let caretRevCount = 0;
   if (window.getSelection) {
      const selection = window.getSelection();
      const currentNode = selection.focusNode.parentNode;
      caretRevCount = selection.focusOffset;
      let previousNode = currentNode.previousSibling;
      while(previousNode && previousNode.nodeName === 'SPAN') { 
      // you can check specific element
      caretRevCount += previousNode.textContent.length;
      previousNode = previousNode.previousSibling;
      }
    }
    return caretRevCount;
}
于 2021-04-02T14:37:39.130 に答える
0

この回答は、再帰関数を使用して、ネストされたテキスト要素で機能します。

ボーナス:キャレットの位置を保存された位置に設定します。

function getCaretData(elem) {
  var sel = window.getSelection();
  return [sel.anchorNode, sel.anchorOffset];
}

function setCaret(el, pos) {
  var range = document.createRange();
  var sel = window.getSelection();
  range.setStart(el,pos);
  range.collapse(true);
  sel.removeAllRanges();
  sel.addRange(range);
}


let indexStack = [];

function checkParent(elem) {
  
  let parent = elem.parentNode;
  let parentChildren = Array.from(parent.childNodes);
  
  let elemIndex = parentChildren.indexOf(elem);
  
  indexStack.unshift(elemIndex);
  
  if (parent !== cd) {
    
    checkParent(parent);
    
  } else {
    
    return;
    
  }
  
}

let stackPos = 0;
let elemToSelect;

function getChild(parent, index) {
  
  let child = parent.childNodes[index];
  
  if (stackPos < indexStack.length-1) {
    
    stackPos++;
        
    getChild(child, indexStack[stackPos]);
    
  } else {
    
    elemToSelect = child;
    
    return;
    
  }
  
}


let cd = document.querySelector('.cd'),
    caretpos = document.querySelector('.caretpos');

cd.addEventListener('keyup', () => {
  
  let caretData = getCaretData(cd);
  
  let selectedElem = caretData[0];
  let caretPos = caretData[1];
  
  
  indexStack = [];
  checkParent(selectedElem);
    
  
  cd.innerHTML = 'Hello world! <span>Inline! <span>In inline!</span></span>';
  
  
  stackPos = 0;
  getChild(cd, indexStack[stackPos]);
  
  
  setCaret(elemToSelect, caretPos);
  
  
  caretpos.innerText = 'indexStack: ' + indexStack + '. Got child: ' + elemToSelect.data + '. Moved caret to child at pos: ' + caretPos;
  
})
.cd, .caretpos {
  font-family: system-ui, Segoe UI, sans-serif;
  padding: 10px;
}

.cd span {
  display: inline-block;
  color: purple;
  padding: 5px;
}

.cd span span {
  color: chocolate;
  padding: 3px;
}

:is(.cd, .cd span):hover {
  border-radius: 3px;
  box-shadow: inset 0 0 0 2px #005ecc;
}
<div class="cd" contenteditable="true">Hello world! <span>Inline! <span>In inline!</span></span></div>
<div class="caretpos">Move your caret inside the elements above ⤴&lt;/div>

Codepen

于 2021-06-30T08:50:25.790 に答える
0

John Ernestの優れたコードを使用し、ニーズに合わせて少し作り直しました。

  • TypeScriptの使用(Angularアプリケーションで);
  • わずかに異なるデータ構造を使用します。

そして、それに取り組んでいる間、私はあまり知られていない(またはほとんど使用されていない)TreeWalkerに出くわし、再帰性を取り除くことができるので、コードをさらに単純化しました。

考えられる最適化は、ツリーを1回ウォークして、開始ノードと終了ノードの両方を見つけることですが、次のようになります。

  • 巨大で複雑なページの最後であっても、速度の向上がユーザーに認識されるとは思えません。
  • これにより、アルゴリズムがより複雑になり、読みにくくなります。

代わりに、開始が終了と同じである場合を扱いました(単なる注意、実際の選択はありません)。

[編集]範囲のノードは常にテキストタイプのようですので、コードをもう少し簡略化して、キャストせずにノードの長さを取得できるようにしました。

コードは次のとおりです。

export type CountingState = {
    countBeforeNode: number;
    offsetInNode: number;
    node?: Text; // Always of Text type
};

export type RangeOffsets = {
    start: CountingState;
    end: CountingState;
    offsets: { start: number; end: number; }
};

export function isTextNode(node: Node): node is Text {
    return node.nodeType === Node.TEXT_NODE;
}

export function getCaretPosition(container: Node): RangeOffsets | undefined {
    const selection = window.getSelection();
    if (!selection || selection.rangeCount === 0) { return undefined; }
    const range = selection.getRangeAt(0);
    const start = countUntilEndNode(container, range.startContainer as Text, range.startOffset);
    const end = range.collapsed ? start : countUntilEndNode(container, range.endContainer as Text, range.endOffset);
    const offsets = { start: start.countBeforeNode + start.offsetInNode, end: end.countBeforeNode + end.offsetInNode };
    const rangeOffsets: RangeOffsets = { start, end, offsets };
    return rangeOffsets;
}

export function setCaretPosition(container: Node, start: number, end: number): boolean {
    const selection = window.getSelection();
    if (!selection) { return false; }
    const startState = countUntilOffset(container, start);
    const endState = start === end ? startState : countUntilOffset(container, end);
    const range = document.createRange(); // new Range() doesn't work for me!
    range.setStart(startState.node!, startState.offsetInNode);
    range.setEnd(endState.node!, endState.offsetInNode);
    selection.removeAllRanges();
    selection.addRange(range);
    return true;
}

function countUntilEndNode(
    parent: Node,
    endNode: Text,
    offset: number,
    countingState: CountingState = { countBeforeNode: 0, offsetInNode: 0 },
): CountingState {
    const treeWalker = document.createTreeWalker(parent, NodeFilter.SHOW_TEXT);
    while (treeWalker.nextNode()) {
        const node = treeWalker.currentNode as Text;
        if (node === endNode) {
            // We found the target node, memorize it.
            countingState.node = node;
            countingState.offsetInNode = offset;
            break;
        }
        // Add length of text nodes found in the way, until we find the target node.
        countingState.countBeforeNode += node.length;
    }
    return countingState;
}

function countUntilOffset(
    parent: Node,
    offset: number,
    countingState: CountingState = { countBeforeNode: 0, offsetInNode: 0 },
): CountingState {
    const treeWalker = document.createTreeWalker(parent, NodeFilter.SHOW_TEXT);
    while (treeWalker.nextNode()) {
        const node = treeWalker.currentNode as Text;
        if (countingState.countBeforeNode <= offset && offset < countingState.countBeforeNode + node.length) {
            countingState.offsetInNode = offset - countingState.countBeforeNode;
            countingState.node = node;
            break;
        }
        countingState.countBeforeNode += node.length;
    }
    return countingState;
}
于 2021-11-23T08:37:29.250 に答える