ContentEditable DIVでカーソルまたはキャレットの位置を設定する方法について、たくさんの優れたクロスブラウザの答えを見つけていますが、その位置を取得または見つける方法については何もありません...
私がやりたいのは、キーアップでこのdiv内のキャレットの位置を知ることです。
そのため、ユーザーがテキストを入力しているときに、div内のカーソルの位置をいつでも知ることができます。
編集:カーソルの座標ではなく、divの内容(テキスト)内のINDEXを探しています。
<div id="contentBox" contentEditable="true"></div>
$('#contentbox').keyup(function() {
// ... ?
});
次のコードは想定しています:
<div>
内には常に単一のテキストノードがあり、他のノードはありませんwhite-space
プロパティがpre
に設定されていませんコード:
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>
$("#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>
他の回答で対処されていないいくつかのしわ:
要素の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]];
}
ちょっと遅くなりましたが、他の誰かが苦労している場合に備えて。過去2日間に見つけたGoogle検索では、機能するものは見つかりませんでしたが、ネストされたタグの数に関係なく常に機能する簡潔でエレガントなソリューションを思い付きました。
cursor_position() {
var sel = document.getSelection();
sel.modify("extend", "backward", "paragraphboundary");
var pos = sel.toString().length;
console.log('pos: '+pos);
if(sel.anchorNode != undefined) sel.collapseToEnd();
return pos;
}
段落の先頭までさかのぼって選択し、文字列の長さをカウントして現在の位置を取得し、選択を取り消してカーソルを現在の位置に戻します。文書全体(複数の段落)に対してこれを行う場合は、paragraphboundary
をdocumentboundary
に変更するか、ケースの粒度を変更します。 詳細 のAPIを確認してください。乾杯! :)
//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も)
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
};
}
これは私のために働く:
function getCaretCharOffsetInDiv(element) {
var caretOffset = 0;
if (typeof window.getSelection != "undefined") {
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 (typeof document.selection != "undefined" && 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;
}
呼び出し線はイベントタイプに依存します。キーイベントの場合は次を使用します。
getCaretCharOffsetInDiv(e.target) + ($(window.getSelection().getRangeAt(0).startContainer.parentNode).index());
マウスイベントの場合は、これを使用します。
getCaretCharOffsetInDiv(e.target.parentElement) + ($(e.target).index())
これらの2つのケースでは、ターゲットインデックスを追加することでブレークラインを処理します
これにより、新しい 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,
};
}
// in case there is nested doms inside editable
while(selectedNode.parentNode !== editable) {
selectedNode = selectedNode.parentNode;
}
// 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 です。ただし、迅速な方向キーの押下、および迅速な削除はスキップイベントのようです。
EndContainerに到達するまで、コンテンツ編集可能なdivのすべての子を繰り返し処理する単純な方法。次に、コンテナ終了オフセットを追加し、文字インデックスを取得します。任意の数のネスティングで動作するはずです。再帰を使用します。
注:Element.closest('div[contenteditable]')
をサポートするには、 poly fill が必要です。
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;
}
}