How to set caret(cursor) position in contenteditable element (div)?

后端 未结 10 1655
被撕碎了的回忆
被撕碎了的回忆 2020-11-22 01:41

I have this simple HTML as an example:

text text text
text text text
text text
10条回答
  •  谎友^
    谎友^ (楼主)
    2020-11-22 02:17

    I made this for my simple text editor.

    Differences from other methods:

    • High performance
    • Works with all spaces

    usage

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

提交回复
热议问题