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

Fro*_*dik 172 javascript jquery caret contenteditable 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>
Run Code Online (Sandbox Code Playgroud)

我想要简单的事情 - 当我点击按钮时,我想将插入符号(光标)放入可编辑div中的特定位置.通过网络搜索,我将这个JS附加到按钮点击,但它不起作用(FF,Chrome):

var range = document.createRange();
var myDiv = document.getElementById("editable");
range.setStart(myDiv, 5);
range.setEnd(myDiv, 5);
Run Code Online (Sandbox Code Playgroud)

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

Tim*_*own 235

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

var el = document.getElementById("editable");
var range = document.createRange();
var sel = window.getSelection();
range.setStart(el.childNodes[2], 5);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
Run Code Online (Sandbox Code Playgroud)

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

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

  • 如何在这样的span标记内设置插入符:<< div id ="editable"contenteditable ="true"> test1 <br> test2 <br> <span> </ span> </ div> (7认同)
  • 小提示:此解决方案不适用于使用 Web 组件创建的任何内容。这是因为选择 API 不知道 Shadow DOM 中的任何内容:https://twitter.com/bocoup/status/1459120679220092928 不过,这是一个已知问题。人们正在研究解决方案:https://github.com/WICG/webcomponents/issues/79 (3认同)
  • 您的解决方案运行完美。非常感谢。是否有可能在“文本上下文”中工作-这意味着位置#5将是屏幕上的第五个字母,而不是代码中的第五个字母? (2认同)
  • @Frodik:您可以使用我在这里写的答案中的`setSelectionRange()`函数:http://stackoverflow.com/questions/6240139/highlight-text-range-using-javascript/6242538#6242538.正如我在答案中所指出的那样,有些东西它无法正确/一致地处理,但它可能已经足够好了. (2认同)

Lia*_*iam 54

您在可信任的光标定位上找到的大多数答案都相当简单,因为它们只能满足普通文本的输入.在容器中使用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;
};
Run Code Online (Sandbox Code Playgroud)

然后我用这个函数调用例程:

function setCurrentCursorPosition(chars) {
    if (chars >= 0) {
        var selection = window.getSelection();

        range = createRange(document.getElementById("test").parentNode, { count: chars });

        if (range) {
            range.collapse(false);
            selection.removeAllRanges();
            selection.addRange(range);
        }
    }
};
Run Code Online (Sandbox Code Playgroud)

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

PS.如果有人有兴趣,我会使用以下代码获取当前光标位置:

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;
};
Run Code Online (Sandbox Code Playgroud)

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

该代码应直接工作,没有变化,但我刚刚从一个jQuery插件,我已经开发等方面都进行破解它采取一对夫妇这才是 -让我知道如果有什么不工作!

  • 你能提供这个工作的jsfiddle吗?我正在努力弄清楚这是如何工作的,因为如果没有示例,我不确定 `node.id` 和 `parentId` 与什么相关。谢谢 :) (3认同)
  • @Bendihossan - 尝试这个https://jsfiddle.net/nrx9yvw9/5/ - 由于某种原因,这个例子中的内容可编辑div添加了一些字符和文本开头的回车符(它甚至可能是jsfiddle本身这样做,因为它没有;在我的asp.net服务器上做同样的事情). (3认同)

Nik*_*nin 11

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

与其他方法的区别:

  • 高性能
  • 适用于所有空间

用法

// 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)
Run Code Online (Sandbox Code Playgroud)

选择.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('')
}
Run Code Online (Sandbox Code Playgroud)


小智 7

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

这是我的解决方案的一个片段,感谢这个线程、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
Run Code Online (Sandbox Code Playgroud)

这是在 contenteditable div 元素中

我把这个留在这里作为感谢,意识到已经有一个被接受的答案。


Vol*_*mil 6

如果您不想使用 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);
}
Run Code Online (Sandbox Code Playgroud)

editableDiv您可编辑的元素,不要忘记id为它设置一个。innerHTML然后你需要从元件中取出并切断所有刹车线。并且只需使用下一个参数设置折叠即可。


Adm*_*ama 6

我重构了@Liam 的回答。我把它放在一个带有静态方法的类中,我让它的函数接收一个元素而不是#id,以及其他一些小的调整。

此代码特别适用于在您可能使用的富文本框中固定光标<div contenteditable="true">。在到达以下代码之前,我坚持了几天。

编辑:他的答案和这个答案有一个涉及回车的错误。由于 enter 不算作字符,因此在按 Enter 后光标位置会变得混乱。如果我能够修复代码,我会更新我的答案。

编辑 2:为自己省去很多麻烦,并确保您<div contenteditable=true>display: inline-block. 这修复了一些与 Chrome 放置相关的错误,<div>而不是<br>按回车键。

如何使用

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();
Run Code Online (Sandbox Code Playgroud)

代码

// Credit to Liam (Stack Overflow)
// /sf/answers/2872428821/
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;
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @bgplaya 实际上,我用这段代码提出了一个单独的问题,并提供了奖金来修复输入错误。没有人能够修复它。 (2认同)

Sag*_*r M 5

  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);
  }
Run Code Online (Sandbox Code Playgroud)