我有一个简单的HTML作为例子:

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

我想要简单的东西-当我点击按钮时,我想在可编辑的div中放置插入符号(光标)到特定的地方。从网上搜索,我有这个JS附加到按钮点击,但它不起作用(FF, Chrome):

const range = document.createRange();
const myDiv = document.getElementById("editable");
range.setStart(myDiv, 5);
range.setEnd(myDiv, 5);

是否可以像这样手动设置插入符号的位置?


当前回答

基于Tim Down的答案,但它会检查最后一个已知的“好”文本行。它把光标放在最后。

此外,我还可以递归/迭代地检查每个连续的最后一个子节点的最后一个子节点,以找到DOM中绝对最后一个“好”文本节点。

function onClickHandler() { setCaret(document.getElementById("editable")); } function setCaret(el) { let range = document.createRange(), sel = window.getSelection(), lastKnownIndex = -1; for (let i = 0; i < el.childNodes.length; i++) { if (isTextNodeAndContentNoEmpty(el.childNodes[i])) { lastKnownIndex = i; } } if (lastKnownIndex === -1) { throw new Error('Could not find valid text content'); } let row = el.childNodes[lastKnownIndex], col = row.textContent.length; range.setStart(row, col); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); el.focus(); } function isTextNodeAndContentNoEmpty(node) { return node.nodeType == Node.TEXT_NODE && node.textContent.trim().length > 0 } <div id="editable" contenteditable="true"> text text text<br>text text text<br>text text text<br> </div> <button id="button" onclick="onClickHandler()">focus</button>

其他回答

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

function setCaret() var el =文档。getElementById(“编辑”) 咖啡壶。 var = window.getSelection() 太阳城setStart (el。childNodes [2], 5) 太阳城崩溃(真正的)。 细胞removeAllRanges()。 细胞addRange(射程)。 的 <div id=“编辑”contenteble =“真”> 文本文本<br>文本文本<br>文本文本<br> < / div > <巴顿id=“巴顿”onclick=“setCaret”()巴顿”focus > < - >

IE < 9的工作方式完全不同。如果需要支持这些浏览器,则需要不同的代码。

jsFiddle示例:http://jsfiddle.net/timdown/vXnCM/

我为我的简单文本编辑器做了这个。

与其他方法的区别:

高性能 适用于所有空间

使用

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

selection.ts

/** 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('')
}

基于Tim Down的答案,但它会检查最后一个已知的“好”文本行。它把光标放在最后。

此外,我还可以递归/迭代地检查每个连续的最后一个子节点的最后一个子节点,以找到DOM中绝对最后一个“好”文本节点。

function onClickHandler() { setCaret(document.getElementById("editable")); } function setCaret(el) { let range = document.createRange(), sel = window.getSelection(), lastKnownIndex = -1; for (let i = 0; i < el.childNodes.length; i++) { if (isTextNodeAndContentNoEmpty(el.childNodes[i])) { lastKnownIndex = i; } } if (lastKnownIndex === -1) { throw new Error('Could not find valid text content'); } let row = el.childNodes[lastKnownIndex], col = row.textContent.length; range.setStart(row, col); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); el.focus(); } function isTextNodeAndContentNoEmpty(node) { return node.nodeType == Node.TEXT_NODE && node.textContent.trim().length > 0 } <div id="editable" contenteditable="true"> text text text<br>text text text<br>text text text<br> </div> <button id="button" onclick="onClickHandler()">focus</button>

function set_mouse() { var as = document.getElementById("editable"); el = as.childNodes[1].childNodes[0]; //goal is to get ('we') id to write (object Text) because it work only in object text var range = document.createRange(); var sel = window.getSelection(); range.setStart(el, 1); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); document.getElementById("we").innerHTML = el; // see out put of we id } <div id="editable" contenteditable="true">dddddddddddddddddddddddddddd <p>dd</p>psss <p>dd</p> <p>dd</p> <p>text text text</p> </div> <p id='we'></p> <button onclick="set_mouse()">focus</button>

当你有(p) (span)等高级元素时,将插入符号设置在合适的位置是非常困难的。目标是获取(对象文本):

<div id="editable" contenteditable="true">dddddddddddddddddddddddddddd<p>dd</p>psss<p>dd</p>
    <p>dd</p>
    <p>text text text</p>
</div>
<p id='we'></p>
<button onclick="set_mouse()">focus</button>
<script>

    function set_mouse() {
        var as = document.getElementById("editable");
        el = as.childNodes[1].childNodes[0];//goal is to get ('we') id to write (object Text) because it work only in object text
        var range = document.createRange();
        var sel = window.getSelection();
        range.setStart(el, 1);
        range.collapse(true);
        sel.removeAllRanges();
        sel.addRange(range);

        document.getElementById("we").innerHTML = el;// see out put of we id
    }
</script>

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

//Set offset in current contenteditable field (for start by default or for with forEnd=true)
function setCurSelectionOffset(offset, forEnd = false) {
    const sel = window.getSelection();
    if (sel.rangeCount !== 1 || !document.activeElement) return;

    const firstRange = sel.getRangeAt(0);

    if (offset > 0) {
        bypassChildNodes(document.activeElement, offset);
    }else{
        if (forEnd)
            firstRange.setEnd(document.activeElement, 0);
        else
            firstRange.setStart(document.activeElement, 0);
    }



    //Bypass in depth
    function bypassChildNodes(el, leftOffset) {
        const childNodes = el.childNodes;

        for (let i = 0; i < childNodes.length && leftOffset; i++) {
            const childNode = childNodes[i];

            if (childNode.nodeType === 3) {
                const curLen = childNode.textContent.length;

                if (curLen >= leftOffset) {
                    if (forEnd)
                        firstRange.setEnd(childNode, leftOffset);
                    else
                        firstRange.setStart(childNode, leftOffset);
                    return 0;
                }else{
                    leftOffset -= curLen;
                }
            }else
            if (childNode.nodeType === 1) {
                leftOffset = bypassChildNodes(childNode, leftOffset);
            }
        }

        return leftOffset;
    }
}

我还写了代码来获取当前插入符号的位置(没有测试):

//Get offset in current contenteditable field (start offset by default or end offset with calcEnd=true)
function getCurSelectionOffset(calcEnd = false) {
    const sel = window.getSelection();
    if (sel.rangeCount !== 1 || !document.activeElement) return 0;

    const firstRange     = sel.getRangeAt(0),
          startContainer = calcEnd ? firstRange.endContainer : firstRange.startContainer,
          startOffset    = calcEnd ? firstRange.endOffset    : firstRange.startOffset;
    let needStop = false;

    return bypassChildNodes(document.activeElement);



    //Bypass in depth
    function bypassChildNodes(el) {
        const childNodes = el.childNodes;
        let ans = 0;

        if (el === startContainer) {
            if (startContainer.nodeType === 3) {
                ans = startOffset;
            }else
            if (startContainer.nodeType === 1) {
                for (let i = 0; i < startOffset; i++) {
                    const childNode = childNodes[i];

                    ans += childNode.nodeType === 3 ? childNode.textContent.length :
                           childNode.nodeType === 1 ? childNode.innerText.length :
                           0;
                }
            }

            needStop = true;
        }else{
            for (let i = 0; i < childNodes.length && !needStop; i++) {
                const childNode = childNodes[i];
                ans += bypassChildNodes(childNode);
            }
        }

        return ans;
    }
}

你还需要注意范围。starttooffset和range。endOffset包含文本节点(nodeType === 3)的字符偏移量和元素节点(nodeType === 1)的子节点偏移量。startContainer和range。endContainer可以引用树中任何级别的任何元素节点(当然它们也可以引用文本节点)。