[英]Get contentEditable caret position
我找到了大量關於如何在contentEditable
元素中設置插入符號 position 的跨瀏覽器的好答案,但沒有關於如何首先獲取插入符號 position 的答案。
我想要做的是知道keyup
上的 div 中的插入符 position。 因此,當用戶鍵入文本時,我可以隨時知道contentEditable
元素中的插入符號 position。
<div id="contentBox" contentEditable="true"></div>
$('#contentbox').keyup(function() {
// ... ?
});
以下代碼假設:
<div>
始終只有一個文本節點,沒有其他節點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>
我在其他答案中沒有看到的一些皺紋:
這是一種獲取開始和結束位置作為元素 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]];
}
$("#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>
參加聚會有點晚,但以防萬一其他人正在掙扎。 我在過去兩天找到的所有 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 了解更多詳情。 干杯! :)
這個對我有用:
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())
在這兩種情況下,我通過添加目標索引來處理中斷線
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
};
}
//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)
}
});
注意:range 對象本身可以存儲在一個變量中,並且可以隨時重新選擇,除非 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)
由於這讓我花了很長時間才弄清楚使用新的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 。 但是請注意,快速方向鍵按下以及快速刪除似乎是跳過事件。
一種直接的方式,它遍歷 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;
}
}
如果將可編輯的 div 樣式設置為“display:inline-block; white-space: pre-wrap”,則在輸入新行時不會獲得新的子 div,只會獲得 LF 字符(即 
); .
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 456 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 的開頭(從而捕獲其間的所有文本)。
這個建立在@alockwood05 的答案之上,並為在 contenteditable div 內帶有嵌套標簽的插入符號以及節點內的偏移量提供 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;
}
這個適用於角度
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;
}
此答案使用遞歸函數處理嵌套文本元素。 🪄
獎勵:將插入符號位置設置為保存位置。
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 ⤴</div>
我使用了John Ernest的優秀代碼,並根據我的需要對其進行了一些修改:
在研究它時,我偶然發現了鮮為人知(或很少使用)的 TreeWalker,並進一步簡化了它,因為它允許擺脫遞歸。
一種可能的優化可能是遍歷樹一次以找到開始節點和結束節點,但是:
相反,我處理了開始與結束相同的情況(只是一個插入符號,沒有真正的選擇)。
這是代碼:
export type CountingState = {
countBeforeNode: number;
offsetInNode: number;
node?: Node;
};
export type RangeOffsets = {
start: CountingState;
end: CountingState;
offsets: { start: number; end: number; }
};
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, range.startOffset);
const end = range.collapsed ? start : countUntilEndNode(container, range.endContainer, 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: Node,
offset: number,
countingState: CountingState = { countBeforeNode: 0, offsetInNode: 0 },
): CountingState {
const treeWalker = document.createTreeWalker(parent, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT);
while (treeWalker.nextNode()) {
const node = treeWalker.currentNode;
if (node === endNode) {
// We found the target node, memorize it.
countingState.node = node;
countingState.offsetInNode = offset;
break;
}
if (isTextNode(node)) {
// A simple text node in the way, we add its length to the total until 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;
if (isTextNode(node)) {
if (countingState.countBeforeNode <= offset && offset < countingState.countBeforeNode + node.length) {
countingState.offsetInNode = offset - countingState.countBeforeNode;
countingState.node = node;
break;
}
countingState.countBeforeNode += node.length;
}
}
return countingState;
}
function isTextNode(node: Node): node is Text {
return node.nodeType === Node.TEXT_NODE;
}
因此,根據 Chris Sullivan 提供的答案,我設法創建了一個在通過鍵盤進行選擇時不會重置的版本,並且能夠檢測到列號和行號。
在這種方法中,您首先必須找出一個解決方案來獲取所有文本,直到克拉。 您可以通過獲取當前選擇(即插入符號)、克隆它的第一個范圍、折疊它,然后將范圍的起始節點更改為元素的開頭來做到這一點。 從那里,您可以通過簡單地在范圍上運行 toString 來提取直到克拉的所有文本。 現在您有了文本,我們可以對其進行一些簡單的計算以確定行號和列。
對於行號,您只需計算文本字符串中的換行符數。 這可以使用一些簡單的正則表達式來完成,可以在下面的代碼中看到。
對於列號,可以通過三種方式獲取“列號”。
range.endOffset
)。廢話不多說,現在是表演時間:
// Caret
function getCaretPosition(element) {
// Check for selection
if (window.getSelection().type == "None") {
return {
"ln": -1,
"col": -1
}
}
// Copy range
var selection = window.getSelection();
var range = selection.getRangeAt(0).cloneRange();
// Collapse range
range.collapse();
// Move range to encompass everything
range.setStart(element.firstChild, 0);
// Calculate position
var content = range.toString();
var text = JSON.stringify(content);
var lines = (text.match(/\\n/g) || []).length + 1;
// Return caret position (col - 2 due to some weird calculation with regex)
return {
"ln": lines,
// "col": range.endOffset + 1 // Method 1
"col": text.replace(/\\n/g, " ").length - 2 // Method 2
// "col": text.length -2 // Method 3
}
}
現在通過這種方法,如果您願意,您可以在每次更新選擇時獲取插入符號 position:
document.addEventListener("selectionchange", function(e) {
console.log(getCaretPosition(document.getElementById("text-area")));
});
我希望這對某人有所幫助,我花了幾個小時試圖弄清楚如何做到這一點
下面的代碼通過獲取當前元素的偏移量然后導航回contenteditable
內的所有元素並計算字符總數來計算插入符號的位置。
這會:
如果您遇到問題,請告訴我,以便我更新代碼。
function getRowTextLength(currentNode) {
let previousSibling;
let textLength = 0;
//this means we are outside our desired scope
if (currentNode?.contentEditable == "true") {
return textLength;
}
while (currentNode) {
//get the previous element of the currentNode
previousSibling =
currentNode.previousSibling || //UNFORMATTED text case
//avoid targetting the contenteditable div itself
(currentNode.parentNode.nodeName != "DIV"
? currentNode.parentNode.previousSibling //FORMATTED text case
: null);
//count the number of characters in the previous element, if exists
textLength = previousSibling
? textLength + previousSibling.textContent.length
: textLength;
//set current element as previous element
currentNode = previousSibling;
//continue looping as long as we have a previous element
}
return textLength;
}
//pass e.target from an eventListener as argument
function getCaretPosition(element) {
let selection = getSelection(element);
//caret position at current row
let caretPosition = selection.anchorOffset;
let currentNode = selection.baseNode;
caretPosition += getRowTextLength(currentNode);
//get closest div parent node
if (caretPosition != 0) {
do {
currentNode = currentNode.parentNode;
} while (currentNode.nodeName != "DIV");
}
caretPosition += getRowTextLength(currentNode);
//console.log("CARET POSITION ", caretPosition);
return caretPosition;
}
獲取相對於可編輯內容的插入符號索引 position:
const getCaretPosition = () => {
var selection = document.getSelection();
if (!selection || !divRef) return 0;
selection.collapseToEnd();
const range = selection.getRangeAt(0);
const clone = range.cloneRange();
clone.selectNodeContents(divRef);
clone.setEnd(range.startContainer, range.startOffset);
return clone.toString().length;
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.