Javascript 如何在contenteditable元素(div)中设置插入符号(光标)位置?

Javascript 如何在contenteditable元素(div)中设置插入符号(光标)位置?,javascript,jquery,contenteditable,caret,cursor-position,Javascript,Jquery,Contenteditable,Caret,Cursor Position,我以这个简单的HTML为例: <div id="editable" contenteditable="true"> text text text<br> text text text<br> text text text<br> </div> <button id="button">focus</button> 可以这样手动设置插入符号位置吗?在大多数浏览器中,您需要和对象。将每个选择边界指定为节点

我以这个简单的HTML为例:

<div id="editable" contenteditable="true">
  text text text<br>
  text text text<br>
  text text text<br>
</div>
<button id="button">focus</button>

可以这样手动设置插入符号位置吗?

在大多数浏览器中,您需要和对象。将每个选择边界指定为节点,并在该节点内指定偏移。例如,要将插入符号设置为第二行文本的第五个字符,请执行以下操作:

函数setCaret(){
var el=document.getElementById(“可编辑”)
var range=document.createRange()
var sel=window.getSelection()
range.setStart(el.childNodes[2],5)
范围。折叠(真)
选择removeAllRanges()
选择添加范围(范围)
}

文本
文本
文本
文本
焦点
功能集\鼠标(){
var as=document.getElementById(“可编辑”);
el=as.childNodes[1].childNodes[0];//目标是获取要写入的('we')id(对象文本),因为它只在对象文本中工作
var range=document.createRange();
var sel=window.getSelection();
范围。设置开始(el,1);
范围。塌陷(真);
选择removeAllRanges();
选择添加范围(范围);
document.getElementById(“we”).innerHTML=el;//请参阅输出we id
}
dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd
dd

PSS dd

dd

文本文本文本


聚焦
您在内容可编辑光标定位上找到的大多数答案都相当简单,因为它们只适合普通文本的输入。一旦在容器中使用html元素,输入的文本将被分割成节点,并在树结构中自由分布

要设置光标位置,我有一个函数,该函数在提供的节点内循环所有子文本节点,并设置从初始节点开始到chars.count字符的范围:

function createRange(node, chars, range) {
    if (!range) {
        range = document.createRange()
        range.selectNode(node);
        range.setStart(node, 0);
    }

    if (chars.count === 0) {
        range.setEnd(node, chars.count);
    } else if (node && chars.count >0) {
        if (node.nodeType === Node.TEXT_NODE) {
            if (node.textContent.length < chars.count) {
                chars.count -= node.textContent.length;
            } else {
                range.setEnd(node, chars.count);
                chars.count = 0;
            }
        } else {
           for (var lp = 0; lp < node.childNodes.length; lp++) {
                range = createRange(node.childNodes[lp], chars, range);

                if (chars.count === 0) {
                    break;
                }
            }
        }
    } 

    return range;
};
range.collapse(false)将光标设置为范围的末尾。我已经用最新版本的Chrome、IE、Mozilla和Opera对它进行了测试,它们都运行良好

注:如果有人感兴趣,我可以使用以下代码获取当前光标位置:

function isChildOf(node, parentId) {
    while (node !== null) {
        if (node.id === parentId) {
            return true;
        }
        node = node.parentNode;
    }

    return false;
};

function getCurrentCursorPosition(parentId) {
    var selection = window.getSelection(),
        charCount = -1,
        node;

    if (selection.focusNode) {
        if (isChildOf(selection.focusNode, parentId)) {
            node = selection.focusNode; 
            charCount = selection.focusOffset;

            while (node) {
                if (node.id === parentId) {
                    break;
                }

                if (node.previousSibling) {
                    node = node.previousSibling;
                    charCount += node.textContent.length;
                } else {
                     node = node.parentNode;
                     if (node === null) {
                         break
                     }
                }
           }
      }
   }

    return charCount;
};
代码的作用与set函数相反-它获取当前window.getSelection().focusNode和focusOffset,并向后计算遇到的所有文本字符,直到它到达id为containerId的父节点。isChildOf函数只是在运行之前检查所提供的节点实际上是所提供的parentId的子节点


代码应该可以直接工作而不做任何更改,但我刚刚从我开发的jQuery插件中获取了它,所以我已经破解了几个这样的插件-如果有任何东西不起作用,请告诉我

如果不想使用jQuery,可以尝试以下方法:

public setCaretPosition() {
    const editableDiv = document.getElementById('contenteditablediv');
    const lastLine = this.input.nativeElement.innerHTML.replace(/.*?(<br>)/g, '');
    const selection = window.getSelection();
    selection.collapse(editableDiv.childNodes[editableDiv.childNodes.length - 1], lastLine.length);
}
public setCaretPosition(){
const editableDiv=document.getElementById('contenteditablediv');
const lastLine=this.input.nativeElement.innerHTML.replace(/.*?(
)/g',); const selection=window.getSelection(); 折叠(editableDiv.childNodes[editableDiv.childNodes.length-1],lastLine.length); }

editableDiv
在编辑元素时,不要忘记为它设置一个
id
。然后,您需要从元素中获取
innerHTML
,并剪切所有制动线。然后用下一个参数设置collapse。

我正在编写一个语法高亮(和基本代码编辑器),我需要知道如何自动键入单引号字符并将插入符号向后移动(就像现在很多代码编辑器一样)

这是我的解决方案的一个片段,多亏了这个线程、MDN文档和大量moz控制台的帮助

//onKeyPress event

if (evt.key === "\"") {
    let sel = window.getSelection();
    let offset = sel.focusOffset;
    let focus = sel.focusNode;

    focus.textContent += "\""; //setting div's innerText directly creates new
    //nodes, which invalidate our selections, so we modify the focusNode directly

    let range = document.createRange();
    range.selectNode(focus);
    range.setStart(focus, offset);

    range.collapse(true);
    sel.removeAllRanges();
    sel.addRange(range);
}

//end onKeyPress event
这在contenteditable div元素中


我在这里留下这个作为感谢,意识到已经有了一个公认的答案。

我认为在contenteditable元素中将插入符号设置到某个位置并不简单。我为此编写了自己的代码。它绕过节点树,计算剩余字符数,并在所需元素中设置插入符号。我没有对这段代码进行太多测试

  const el = document.getElementById("editable");
  el.focus()
  let char = 1, sel; // character at which to place caret

  if (document.selection) {
    sel = document.selection.createRange();
    sel.moveStart('character', char);
    sel.select();
  }
  else {
    sel = window.getSelection();
    sel.collapse(el.lastChild, char);
  }
//在当前contenteditable字段中设置偏移量(用于默认启动或用于with forEnd=true)
函数setCurSelectionOffset(偏移量,forEnd=false){
const sel=window.getSelection();
if(sel.rangeCount!==1 | |!document.activeElement)返回;
常数firstRange=sel.getRangeAt(0);
如果(偏移量>0){
绕过子节点(document.activeElement,offset);
}否则{
if(forEnd)
setEnd(document.activeElement,0);
其他的
firstRange.setStart(document.activeElement,0);
}
//纵深绕道
函数节点(el、leftOffset){
const childNodes=el.childNodes;
对于(设i=0;i=leftOffset){
if(forEnd)
setEnd(childNode,leftOffset);
其他的
setStart(childNode,leftOffset);
返回0;
}否则{
leftOffset-=卷曲;
}
}否则
if(childNode.nodeType==1){
leftOffset=bypassChildNodes(childNode,leftOffset);
}
}
返回左偏移量;
}
}
我还编写了获取当前插入符号位置的代码(未测试):

//获取当前contenteditable字段中的偏移量(默认情况下为起始偏移量或calcEnd=true的结束偏移量)
函数GetCurseElectionOffset(calcEnd=false){
const sel=window.getSelection();
if(sel.rangeCount!==1 | |!document.activeElement)返回0;
常数firstRange=sel.getRangeAt(0),
startContainer=calcEnd?firstRange.endContainer:firstRange.startContainer,
startOffset=calcEnd?firstRange.endOffset:firstRange.startOffset;
让我们停下来
//onKeyPress event

if (evt.key === "\"") {
    let sel = window.getSelection();
    let offset = sel.focusOffset;
    let focus = sel.focusNode;

    focus.textContent += "\""; //setting div's innerText directly creates new
    //nodes, which invalidate our selections, so we modify the focusNode directly

    let range = document.createRange();
    range.selectNode(focus);
    range.setStart(focus, offset);

    range.collapse(true);
    sel.removeAllRanges();
    sel.addRange(range);
}

//end onKeyPress event
  const el = document.getElementById("editable");
  el.focus()
  let char = 1, sel; // character at which to place caret

  if (document.selection) {
    sel = document.selection.createRange();
    sel.moveStart('character', char);
    sel.select();
  }
  else {
    sel = window.getSelection();
    sel.collapse(el.lastChild, char);
  }
// get current selection
const [start, end] = getSelectionOffset(container)

// change container html
container.innerHTML = newHtml

// restore selection
setSelectionOffset(container, start, end)

// use this instead innerText for get text with keep all spaces
const innerText = getInnerText(container)
const textBeforeCaret = innerText.substring(0, start)
const textAfterCaret = innerText.substring(start)
/** return true if node found */
function searchNode(
    container: Node,
    startNode: Node,
    predicate: (node: Node) => boolean,
    excludeSibling?: boolean,
): boolean {
    if (predicate(startNode as Text)) {
        return true
    }

    for (let i = 0, len = startNode.childNodes.length; i < len; i++) {
        if (searchNode(startNode, startNode.childNodes[i], predicate, true)) {
            return true
        }
    }

    if (!excludeSibling) {
        let parentNode = startNode
        while (parentNode && parentNode !== container) {
            let nextSibling = parentNode.nextSibling
            while (nextSibling) {
                if (searchNode(container, nextSibling, predicate, true)) {
                    return true
                }
                nextSibling = nextSibling.nextSibling
            }
            parentNode = parentNode.parentNode
        }
    }

    return false
}

function createRange(container: Node, start: number, end: number): Range {
    let startNode
    searchNode(container, container, node => {
        if (node.nodeType === Node.TEXT_NODE) {
            const dataLength = (node as Text).data.length
            if (start <= dataLength) {
                startNode = node
                return true
            }
            start -= dataLength
            end -= dataLength
            return false
        }
    })

    let endNode
    if (startNode) {
        searchNode(container, startNode, node => {
            if (node.nodeType === Node.TEXT_NODE) {
                const dataLength = (node as Text).data.length
                if (end <= dataLength) {
                    endNode = node
                    return true
                }
                end -= dataLength
                return false
            }
        })
    }

    const range = document.createRange()
    if (startNode) {
        if (start < startNode.data.length) {
            range.setStart(startNode, start)
        } else {
            range.setStartAfter(startNode)
        }
    } else {
        if (start === 0) {
            range.setStart(container, 0)
        } else {
            range.setStartAfter(container)
        }
    }

    if (endNode) {
        if (end < endNode.data.length) {
            range.setEnd(endNode, end)
        } else {
            range.setEndAfter(endNode)
        }
    } else {
        if (end === 0) {
            range.setEnd(container, 0)
        } else {
            range.setEndAfter(container)
        }
    }

    return range
}

export function setSelectionOffset(node: Node, start: number, end: number) {
    const range = createRange(node, start, end)
    const selection = window.getSelection()
    selection.removeAllRanges()
    selection.addRange(range)
}

function hasChild(container: Node, node: Node): boolean {
    while (node) {
        if (node === container) {
            return true
        }
        node = node.parentNode
    }

    return false
}

function getAbsoluteOffset(container: Node, offset: number) {
    if (container.nodeType === Node.TEXT_NODE) {
        return offset
    }

    let absoluteOffset = 0
    for (let i = 0, len = Math.min(container.childNodes.length, offset); i < len; i++) {
        const childNode = container.childNodes[i]
        searchNode(childNode, childNode, node => {
            if (node.nodeType === Node.TEXT_NODE) {
                absoluteOffset += (node as Text).data.length
            }
            return false
        })
    }

    return absoluteOffset
}

export function getSelectionOffset(container: Node): [number, number] {
    let start = 0
    let end = 0

    const selection = window.getSelection()
    for (let i = 0, len = selection.rangeCount; i < len; i++) {
        const range = selection.getRangeAt(i)
        if (range.intersectsNode(container)) {
            const startNode = range.startContainer
            searchNode(container, container, node => {
                if (startNode === node) {
                    start += getAbsoluteOffset(node, range.startOffset)
                    return true
                }

                const dataLength = node.nodeType === Node.TEXT_NODE
                    ? (node as Text).data.length
                    : 0

                start += dataLength
                end += dataLength

                return false
            })

            const endNode = range.endContainer
            searchNode(container, startNode, node => {
                if (endNode === node) {
                    end += getAbsoluteOffset(node, range.endOffset)
                    return true
                }

                const dataLength = node.nodeType === Node.TEXT_NODE
                    ? (node as Text).data.length
                    : 0

                end += dataLength

                return false
            })

            break
        }
    }

    return [start, end]
}

export function getInnerText(container: Node) {
    const buffer = []
    searchNode(container, container, node => {
        if (node.nodeType === Node.TEXT_NODE) {
            buffer.push((node as Text).data)
        }
        return false
    })
    return buffer.join('')
}
let richText = document.getElementById('rich-text');
let offset = Cursor.getCurrentCursorPosition(richText);
// insert code here that does stuff to the innerHTML, such as adding/removing <span> tags
Cursor.setCurrentCursorPosition(offset, richText);
richText.focus();
// Credit to Liam (Stack Overflow)
// https://stackoverflow.com/a/41034697/3480193
class Cursor {
    static getCurrentCursorPosition(parentElement) {
        var selection = window.getSelection(),
            charCount = -1,
            node;
        
        if (selection.focusNode) {
            if (Cursor._isChildOf(selection.focusNode, parentElement)) {
                node = selection.focusNode; 
                charCount = selection.focusOffset;
                
                while (node) {
                    if (node === parentElement) {
                        break;
                    }

                    if (node.previousSibling) {
                        node = node.previousSibling;
                        charCount += node.textContent.length;
                    } else {
                        node = node.parentNode;
                        if (node === null) {
                            break;
                        }
                    }
                }
            }
        }
        
        return charCount;
    }
    
    static setCurrentCursorPosition(chars, element) {
        if (chars >= 0) {
            var selection = window.getSelection();
            
            let range = Cursor._createRange(element, { count: chars });

            if (range) {
                range.collapse(false);
                selection.removeAllRanges();
                selection.addRange(range);
            }
        }
    }
    
    static _createRange(node, chars, range) {
        if (!range) {
            range = document.createRange()
            range.selectNode(node);
            range.setStart(node, 0);
        }

        if (chars.count === 0) {
            range.setEnd(node, chars.count);
        } else if (node && chars.count >0) {
            if (node.nodeType === Node.TEXT_NODE) {
                if (node.textContent.length < chars.count) {
                    chars.count -= node.textContent.length;
                } else {
                    range.setEnd(node, chars.count);
                    chars.count = 0;
                }
            } else {
                for (var lp = 0; lp < node.childNodes.length; lp++) {
                    range = Cursor._createRange(node.childNodes[lp], chars, range);

                    if (chars.count === 0) {
                    break;
                    }
                }
            }
        } 

        return range;
    }
    
    static _isChildOf(node, parentElement) {
        while (node !== null) {
            if (node === parentElement) {
                return true;
            }
            node = node.parentNode;
        }

        return false;
    }
}
var sel = window.getSelection();
sel?.setPosition(wordDiv.childNodes[0], 5);
event.preventDefault();