290

I have this simple HTML as an example:

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

I want simple thing - when I click the button, I want to place caret(cursor) into specific place in the editable div. From searching over the web, I have this JS attached to button click, but it doesn't work (FF, Chrome):

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

Is it possible to set manually caret position like this?

Kostas Minaidis
  • 4,681
  • 3
  • 17
  • 25
Frodik
  • 14,986
  • 23
  • 90
  • 141
  • For everyone who wants to set the cursor simply to the end, see https://stackoverflow.com/a/69727327/1066234 – Avatar Nov 04 '22 at 06:19

13 Answers13

376

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/

vsync
  • 118,978
  • 58
  • 307
  • 400
Tim Down
  • 318,141
  • 75
  • 454
  • 536
  • 3
    Your solution works perfectly. Thanks a lot. Is there a chance it could be made to work in "text context" - that means position #5 would be the fifth letter on a screen and not the fifth letter in a code ? – Frodik Jun 06 '11 at 08:46
  • 3
    @Frodik: You could use the `setSelectionRange()` function from the answer I wrote here: http://stackoverflow.com/questions/6240139/highlight-text-range-using-javascript/6242538#6242538. As I noted in the answer, there are various things it won't handle correctly/consistently but it may be good enough. – Tim Down Jun 06 '11 at 08:51
  • 7
    how about set the caret inside a span tag like this : <
    test1
    test2
    – Med Akram Z Mar 29 '12 at 01:21
  • Will the given code barf on IE<9, or just do nothing? – MalcolmOcean May 20 '15 at 03:59
  • 1
    @MalcolmOcean: Barf, because IE < 9 has no `document.createRange` (or `window.getSelection`, but it won't get that far). – Tim Down May 20 '15 at 08:30
  • This solution works great, but does not in fire fox 38.0.1. Any workaround to that? – Alex Jun 04 '15 at 14:50
  • 1
    @undroid: The jsfiddle works fine for me in Firefox 38.0.5 on Mac. – Tim Down Jun 04 '15 at 17:03
  • @TimDown you're right, I had a problem in my browser. Perfect solution. – Alex Jun 08 '15 at 07:06
  • For me, I needed to give the element focus `el.focus()` beforehand in order for this to work in Firefox 40 on Mac OS X. – mareoraft Sep 15 '15 at 19:46
  • 1
    It wouldn't work if you have an `inline text`. You can remove all tags `
    ` and try again.
    – Tân Jul 13 '16 at 06:16
  • In order to be able to test your answer quickly, could you edit your answer into a runnable code snippet? Thank you in advance. – Basj Jun 12 '18 at 11:40
  • Why does your fiddle work only when I set the value of childNodes to [0],[2], and [4]? Why don't 1 or 3 work? – Spero Oct 18 '18 at 17:58
  • 1
    @Spero: child nodes 1 and 3 are
    elements and it doesn't make sense to try to place a selection boundary within one of those so I think the browser ignores it.
    – Tim Down Oct 19 '18 at 15:40
  • I suggest checking out Rangy if you're looking for a more consistent solution across browsers: https://github.com/timdown/rangy I'm using it within a React project on a controlled contenteditable, and it's been working like a dream. – hsrob Mar 25 '19 at 18:54
  • For me, with just a comma separated string inside the div and no `
    `s, i had to use `1` as the child instead of 5, and then it worked perfectly
    – Ali Nov 14 '19 at 13:23
  • 1
    Small note: this solution won't work for anything created with Web Components. This is because the selection API isn't aware of anything in the Shadow DOM: https://twitter.com/bocoup/status/1459120679220092928 It's a known issue, though. And folks are working on a solution: https://github.com/WICG/webcomponents/issues/79 – paceaux Jul 25 '22 at 17:49
100

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!

Liam
  • 5,033
  • 2
  • 30
  • 39
  • 3
    Could you provide a jsfiddle of this working please? I'm struggling to figure out how this works as I'm not sure what `node.id` and `parentId` relate to without an example. Thanks :) – Bendihossan Dec 12 '16 at 16:11
  • 4
    @Bendihossan - try this https://jsfiddle.net/nrx9yvw9/5/ - for some reason the content editable div in this example is adding in some characters and a carriage return at the start of the text (it might even be jsfiddle itself doing it as it doesn;t do the same on my asp.net server). – Liam Dec 12 '16 at 16:46
  • @Bendihossan - the html elements within the contenteditable div get broken down into a tree structure with one node for each html element. The getCurrentCursorPosition gets the current selection position and goes back up the tree counting how many plain text characters there are. Node.id is the html element id, while parentId refers to the html element id it should stop counting back to – Liam Dec 12 '16 at 16:53
  • This was really great, I wrapped the methods into a jQuery plugin for my usage, but then re-read your comment and you said you had one already. Do you have it shared in a repo somewhere that I can reference and provide credit? – Dom Hastings Apr 05 '17 at 16:23
  • 1
    It's on my todo list to write one that is completely separate from my UI code - I'll post it when I have a sec. – Liam Apr 06 '17 at 17:03
  • I was struggling with html nodes + contenteditable + caret position, this was exactly what I needed, and worked for me in React without much modification, thank yo uso much. – Flion May 23 '17 at 04:57
  • for me it was not working in firefox. to make it work i just added to the setCurrentCursorPosition-method before the setting of the ranges a focus on the div like so: document.getElementById("test").focus(). – sanyooh Jun 12 '17 at 12:35
  • you are actually amazing. I was creating a rich text editor and this is just what i needed. works like a charm! wish i could double upvote! – notrota Feb 18 '18 at 06:26
  • 1
    In order to be able to test your different solutions quickly, could you edit your answer into runnable code snippets? Thank you in advance. – Basj Jun 12 '18 at 11:40
  • I've experienced an error that I couldn't manage to get rid of : `
    someText specialText someMoreText
    ` If the user deletes the first char after a closing node (here the ` ` after ``), the caret jumps to a wrong position before the first node in the current div. (I used a trick to get rid of all that - and didn't use this code) Note that the html was modified when the user typed (coloring keywords, identifiers, numbers and quotes)
    – Jean-Marc Zimmer Nov 02 '18 at 09:55
  • Yes, in fact even "plain" text is going to needing a more sophisticated solution like this if it has new lines. These will either be represented by BRs, DIVs or occasionally Ps, which will be interspersed between stretches of text. – mike rodent Jul 26 '20 at 12:40
  • Anyone managed to fix the issue with cursor when you hit enter? – bgplaya Jul 28 '20 at 17:54
  • I also have issues with newlines when pressing enter – Stephen Eckels Jun 28 '21 at 16:53
  • 1
    I made a codepen demo of this https://codepen.io/jeffward/pen/OJjPKYo with minor changes - I don't like using `id`, so you pass the functions a container (aka the root contenteditable div) instead. – Jeff Ward Oct 12 '21 at 17:00
  • @JeffWard You are a LIFE SAVER. Thanks so much! – dwenr Jun 09 '23 at 14:27
33

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);
// insert code here that does 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;
    }
}
RedDragonWebDesign
  • 1,797
  • 2
  • 18
  • 24
  • 1
    Is there another way to handle hitting `Enter` in the code above? In my case, it is something really not handy. – bgplaya Jul 28 '20 at 17:53
  • 2
    @bgplaya I actually made a separate question with this code and offered a bounty to fix the enter bug. Nobody was able to fix it. – RedDragonWebDesign Jul 29 '20 at 00:28
  • 1
    Life saver! I adapted it to typescipt and integrated it with braft-editor and react. Awesome, thx – Chai Oct 31 '22 at 16:06
10

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('')
}
Nikolay Makhonin
  • 1,087
  • 12
  • 18
8
  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);
  }
Sagar M
  • 972
  • 8
  • 13
7

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.

Jon
  • 136
  • 3
  • 12
6

If you don't want to use jQuery you can try this approach:

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

editableDiv you editable element, don't forget to set an id for it. Then you need to get your innerHTML from the element and cut all brake lines. And just set collapse with next arguments.

Farsad Fakhim
  • 148
  • 1
  • 6
Volodymyr Khmil
  • 1,078
  • 15
  • 13
4

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>

It is very hard set caret in proper position when you have advance element like (p) (span) etc. The goal is to get (object text):

<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>
dale landry
  • 7,831
  • 2
  • 16
  • 28
Jalaluddin Rumi
  • 71
  • 1
  • 1
  • 8
  • 1
    In order to be able to test your answer quickly, could you edit your answer into a runnable code snippet? Thank you in advance. – Basj Jun 12 '18 at 11:39
3
var sel = window.getSelection();
sel?.setPosition(wordDiv.childNodes[0], 5);
event.preventDefault();
Diego Díaz
  • 389
  • 4
  • 4
2

I've readed and tried some cases from here and just put here what is working for me, considering some details according dom nodes:

  focus(textInput){
    const length = textInput.innerText.length;
    textInput.focus();

    if(!!textInput.lastChild){
      const sel = window.getSelection();
      sel.collapse(textInput.lastChild, length);
    }
  }
  • 1
    Thanks for the trick, turns out both calling `focus()` and using `lastChild` (even if the editable div is just plain text) is necessary to make it work. – Petr Bela May 23 '23 at 17:07
1

I think it's not simple to set caret to some position in contenteditable element. I wrote my own code for this. It bypasses the node tree calcing how many characters left and sets caret in needed element. I didn't test this code much.

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

I also wrote code to get current caret position (didn't test):

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

You also need to be aware of range.startOffset and range.endOffset contain character offset for text nodes (nodeType === 3) and child node offset for element nodes (nodeType === 1). range.startContainer and range.endContainer may refer to any element node of any level in the tree (of course they also can refer to text nodes).

vitaliydev
  • 420
  • 4
  • 7
1

Based on Tim Down's answer, but it checks for the last known "good" text row. It places the cursor at the very end.

Furthermore, I could also recursively/iteratively check the last child of each consecutive last child to find the absolute last "good" text node in the 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>
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
0
move(element:any,x:number){//parent
    let arr:Array<any>=[];
    arr=this.getAllnodeOfanItem(this.input.nativeElement,arr);
    let j=0;
    while (x>arr[j].length && j<arr.length){
        x-=arr[j].length;
        j++;
    }
    

    
    var el = arr[j];
    var range = document.createRange();
    var sel = window.getSelection();
    range.setStart(el,x );
    range.collapse(true);
    if (sel)sel.removeAllRanges();
    if (sel)sel.addRange(range);
}   

getAllnodeOfanItem(element:any,rep:Array<any>){
    let ch:Array<any>=element.childNodes;
    if (ch.length==0 && element.innerText!="")
        rep.push(element);
    else{
        for (let i=0;i<ch.length;i++){
            rep=this.getAllnodeOfanItem(ch[i],rep)
        }
    }
    return rep;
}
Tina
  • 9
  • 2
  • 2
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Oct 16 '21 at 10:49