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

后端 未结 10 1521
被撕碎了的回忆
被撕碎了的回忆 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:14
      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);
      }
    
    0 讨论(0)
  • 2020-11-22 02:16

    In most browsers, you need the Range and Selection objects. You specify each of the selection boundaries as a node and an offset within that node. For example, to set the caret to the fifth character of the second line of text, you'd do the following:

    function setCaret() {
        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)
    }
    <div id="editable" contenteditable="true">
      text text text<br>text text text<br>text text text<br>
    </div>
    
    <button id="button" onclick="setCaret()">focus</button>

    IE < 9 works completely differently. If you need to support these browsers, you'll need different code.

    jsFiddle example: http://jsfiddle.net/timdown/vXnCM/

    0 讨论(0)
  • 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('')
    }
    
    0 讨论(0)
  • 2020-11-22 02:19

    Most answers you find on contenteditable cursor positioning are fairly simplistic in that they only cater for inputs with plain vanilla text. Once you using html elements within the container the text entered gets split into nodes and distributed liberally across a tree structure.

    To set the cursor position I have this function which loops round all the child text nodes within the supplied node and sets a range from the start of the initial node to the chars.count character:

    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;
    };
    

    I then call the routine with this function:

    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);
            }
        }
    };
    

    The range.collapse(false) sets the cursor to the end of the range. I've tested it with the latest versions of Chrome, IE, Mozilla and Opera and they all work fine.

    PS. If anyone is interested I get the current cursor position using this code:

    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;
    };
    

    The code does the opposite of the set function - it gets the current window.getSelection().focusNode and focusOffset and counts backwards all text characters encountered until it hits a parent node with id of containerId. The isChildOf function just checks before running that the suplied node is actually a child of the supplied parentId.

    The code should work straight without change, but I have just taken it from a jQuery plugin I've developed so have hacked out a couple of this's - let me know if anything doesn't work!

    0 讨论(0)
  • 2020-11-22 02:20

    I refactored @Liam's answer. I put it in a class with static methods, I made its functions receive an element instead of an #id, and some other small tweaks.

    This code is particularly good for fixing the cursor in a rich text box that you might be making with <div contenteditable="true">. I was stuck on this for several days before arriving at the below code.

    edit: His answer and this answer have a bug involving hitting enter. Since enter doesn't count as a character, the cursor position gets messed up after hitting enter. If I am able to fix the code, I will update my answer.

    edit2: Save yourself a lot of headaches and make sure your <div contenteditable=true> is display: inline-block. This fixes some bugs related to Chrome putting <div> instead of <br> when you press enter.

    How To Use

    let richText = document.getElementById('rich-text');
    let offset = Cursor.getCurrentCursorPosition(richText);
    // do stuff to the innerHTML, such as adding/removing <span> tags
    Cursor.setCurrentCursorPosition(offset, richText);
    richText.focus();
    

    Code

    // 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;
        }
    }
    
    0 讨论(0)
  • 2020-11-22 02:27

    I'm writting a syntax highlighter (and basic code editor), and I needed to know how to auto-type a single quote char and move the caret back (like a lot of code editors nowadays).

    Heres a snippet of my solution, thanks to much help from this thread, the MDN docs, and a lot of moz console watching..

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

    This is in a contenteditable div element

    I leave this here as a thanks, realizing there is already an accepted answer.

    0 讨论(0)
提交回复
热议问题