contentEditableキャレットインデックスの位置を取得します。


119

私はカーソルまたはキャレットインデックスの位置を設定する方法について、たくさんの優れたクロスブラウザ回答を見つけています contentEditable要素がありますが、そのインデックスをまたは検索する方法については何も見つかりません...

私がしたいのは、このdiv内のキャレットのインデックスを知っていることです keyupです。

したがって、ユーザーがテキストを入力しているときは、いつでもカーソル内のカーソルのインデックスを知ることができます contentEditable要素。

編集:カーソルの座標ではなく、divの内容(テキスト)内のINDEXを探しています。

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

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

テキスト内のその位置を見てください。次に、その位置の前にある最後の '@'を調べます。だから、ほんの一部のテキストロジック。
Bertvan

また、<diV>内で他のタグを許可するつもりはありません。テキストのみ
Bertvan

OK、はい私は午前の<div>内の他のタグを必要として。<a>タグがありますが、ネストはありません...
Bertvan

@Bertvan:キャレットが内の<a>要素内にある<div>場合、どのオフセットが必要ですか?<a>?内のテキスト内のオフセット
Tim Down

<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
@リチャード:まあ、keyupこれは間違ったイベントである可能性が高いですが、元の質問で使用されたものです。getCaretPosition()自身の制限内で問題ありません。
Tim Down

3
Enterキーを押して新しい行に進むと、JSFIDDLEデモは失敗します。位置は0が表示されます
giorgio79

5
@ giorgio79:はい、改行によって<br>or <div>要素が生成されるため、回答に記載されている最初の仮定に違反します。もう少し一般的なソリューションが必要な場合は、stackoverflow.com
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から数えます)。
Ian

太字や斜体の単語がある場合、正しく機能しません。
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

ちょっとパーティーに遅れましたが、誰か他の人が苦労している場合に備えて。過去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をご覧ください。乾杯!:)


1
タグまたは子html要素の<div contenteditable> some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text </div> 前にカーソルを置くたびにカーソルがある場合 、カーソルの位置は0から始まります。この再起動カウントをエスケープする方法はありますか?idiv
-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 2018

ありがとうございますが、新しい行で{x:0、y:0}も返します。
ヒチャムカザン

これは、文字オフセットではなくピクセル位置を返します
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())

これらの2つのケースでは、ターゲットインデックスを追加することにより、ブレークラインを扱います


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でも動作します)


1
ありがとうございます。ただし、divコンテンツのキャレット位置の「インデックス」を正確に取得するにはどうすればよいですか。
Bertvan '19年

わかりました。.getSelection()で.baseOffsetを呼び出すとうまくいきます。これがあなたの答えとともに私の質問に答えます。ありがとう!
Bertvan '19年

2
残念ながら、.baseOffsetはWebkitでのみ機能します(私はそう思います)。また、キャレットの直前の親からのオフセットのみを提供します(<div>内に<b>タグがある場合、<div>の開始ではなく、<b>の開始からのオフセットを提供します標準ベースの範囲では、range.endOffset range.startOffset range.endContainerとrange.startContainerを使用して、選択範囲の親ノードとノード自体(これにはテキストノードが含まれます)からのオフセットを取得できます。IE はrange.offsetLeftを提供しますピクセル単位での左からのオフセットなので、役に立たない
Nico Burns

範囲オブジェクト自体を保存し、window.getSelection()。addrange(range);を使用するのが最善です。<-標準とrange.select(); <-カーソルを同じ場所に再配置するためのIE。range.insertNode(nodetoinsert); <-standards and 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です。ただし、方向キーをすばやく押したり、すばやく削除したりすると、スキップイベントになるようです。


私のために働く!どうもありがとうございます。
dmodo

このテキストは折りたたまれているため、選択することはできません。考えられるシナリオ:すべてのkeyUpイベントを評価する必要がある
hschmieder

0

endeditに到達するまで、contenteditable divのすべての子を繰り返し処理する単純な方法。次に、終了コンテナのオフセットを追加すると、文字インデックスが作成されます。ネストの数に関係なく機能するはずです。再帰を使用します。

注:サポートするには、ポリゴンフィルが必要です。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.