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

JavascriptJqueryContenteditableCaretCursor Position

Javascript Problem Overview


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

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

Is it possible to set manually caret position like this ?

Javascript Solutions


Solution 1 - Javascript

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/

Solution 2 - Javascript

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!

Solution 3 - Javascript

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

Solution 4 - Javascript

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.

Solution 5 - Javascript

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

Solution 6 - Javascript

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

Solution 7 - Javascript

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.

Solution 8 - Javascript

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>

Solution 9 - Javascript

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

Solution 10 - Javascript

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>

Solution 11 - Javascript

var sel = window.getSelection();
sel?.setPosition(wordDiv.childNodes[0], 5);
event.preventDefault();

Solution 12 - Javascript

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

Solution 13 - Javascript

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionFrodikView Question on Stackoverflow
Solution 1 - JavascriptTim DownView Answer on Stackoverflow
Solution 2 - JavascriptLiamView Answer on Stackoverflow
Solution 3 - JavascriptRedDragonWebDesignView Answer on Stackoverflow
Solution 4 - JavascriptJonView Answer on Stackoverflow
Solution 5 - JavascriptSagar MView Answer on Stackoverflow
Solution 6 - JavascriptNikolay MakhoninView Answer on Stackoverflow
Solution 7 - JavascriptVolodymyr KhmilView Answer on Stackoverflow
Solution 8 - JavascriptJalaluddin RumiView Answer on Stackoverflow
Solution 9 - JavascriptvitaliydevView Answer on Stackoverflow
Solution 10 - JavascriptMr. PolywhirlView Answer on Stackoverflow
Solution 11 - JavascriptDiego DíazView Answer on Stackoverflow
Solution 12 - JavascriptTinaView Answer on Stackoverflow
Solution 13 - JavascriptAndrew ZagarichukView Answer on Stackoverflow