Set Caret Position in 'contenteditable' div that has children - javascript

I have an HTML structure like this:
<div contenteditable="true">This is some plain, boring content.</div>
I also have this function that allows me to set the caret position to anywhere I want within the div:
// Move caret to a specific point in a DOM element
function SetCaretPosition(object, pos)
{
// Get key data
var el = object.get(0); // Strip inner object from jQuery object
var range = document.createRange();
var sel = window.getSelection();
// Set the range of the DOM element
range.setStart(el.childNodes[0], pos);
range.collapse(true);
// Set the selection point
sel.removeAllRanges();
sel.addRange(range);
}
This code works completely fine until I start adding child tags (span, b, i, u, strike, sup, sub) to the div e.g.
<div contenteditable="true">
This is some <span class="fancy">plain</span>, boring content.
</div>
Things get more complicated when these child tags end up with child tags of their own e.g.
<div contenteditable="true">
This is some <span class="fancy"><i>plain</i></span>, boring content.
</div>
Essentially, what happens, is that setStart throws an IndexSizeError when I try to SetCaretPosition to an index higher than the start of a child tag. setStart only works until it reaches the first child tag.
What I need, is for the SetCaretPosition function to handle an unknown number of these child tags (and potentially an unknown number of nested child tags) so that setting the position works in the same way it would if there were no tags.
So for both this:
<div contenteditable="true">This is some plain, boring content.</div>
and this:
<div contenteditable="true">
This is <u>some</u> <span class="fancy"><i>plain</i></span>, boring content.
</div>
SetCaretPosition(div, 20); would place the caret before the 'b' in 'boring'.
What is the code I need? Many thanks!

So, I was experiencing the same issue and decided to write my own routine quickly, it walks through all the child nodes recursively and set the position.
Note how this takes a DOM node as argument, not a jquery object as your original post does
// Move caret to a specific point in a DOM element
function SetCaretPosition(el, pos){
// Loop through all child nodes
for(var node of el.childNodes){
if(node.nodeType == 3){ // we have a text node
if(node.length >= pos){
// finally add our range
var range = document.createRange(),
sel = window.getSelection();
range.setStart(node,pos);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
return -1; // we are done
}else{
pos -= node.length;
}
}else{
pos = SetCaretPosition(node,pos);
if(pos == -1){
return -1; // no need to finish the for loop
}
}
}
return pos; // needed because of recursion stuff
}
I hope this'll help you!

If you are going to position the caret based on the character index of the content editable, the easiest way would be using modify method:
const position = 6;
const el = document.querySelector('#editable');
const selection = document.getSelection();
if (!selection || !el) return;
// Set the caret to the beggining
selection.collapse(el, 0);
// Move the caret to the position
for (let index = 0; index < position; index++) {
selection.modify('move', 'forward', 'character');
}
No need to traverse the child nodes.

It only work for object Text childNodes(0).So you have to make it.Here is not so very standard code,but works.Goal is that (p) id of (we) will output object text.If it does then it might work.
<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)
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>

Related

Is there a way to select text through multiple elements?

var range = document.createRange();
var root_node = document.getElementById("test");
// Start at the `hello` element.
range.setStart(root_node.childNodes[0], 2);
// End in the `world` node
range.setEnd(root_node.childNodes[1], 2);
range.selectNodeContents(root_node);
let sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
<div id="test">
hello
<span>world</span>
</div>
I'd like to select text but because it's in different elements it hasn't been working. Is there a way to do this?
I don't mean to highlight both words in their entirety but portions of each word.
#Matt answer is in the right direction, but doesn't achieve what the OP wants, which is to "span" the range across multiple elements (nodes), while using offsets within those elements.
The following achieve it:
var range = document.createRange();
var root_node = document.getElementById("test");
range.setStart(root_node.querySelector('a').firstChild, 2);
range.setEnd(root_node.querySelector('span').firstChild, 3);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
<div id="test">
hello
<span>world</span>
</div>
Note how on both setStart and setEnd we pass a text node - this is the firstChild of the anchor and span elements.
For further reading on this subject, please refer to this excellent explanation.
You called setStart twice, instead of setStart followed by setEnd, and you are specifying an offset of 2 in each case, but I don't think you want an offset because that puts your node index out of range (and throws an error).
var range = document.createRange();
var root_node = document.getElementById("test");
// Start at the `hello` element.
range.setStart(root_node.childNodes[0], 0);
// End in the `world` node
range.setEnd(root_node.childNodes[1], 0);
range.selectNodeContents(root_node);
let sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
<div id="test">
hello
<span>world</span>
</div>

How to select text range within a contenteditable div that has no child nodes?

I'd like to select text within a content editable div. I'd like to provide a start index and an end index.
For example if I have a div:
<div id="main" contenteditable="true">
Hello World
</div>
I'd like a function to do something like "selectText('#main',6,10)" and it would select set the focus to main and select "World".
But all the examples that I see online assume that the container div has children. But mine don't have any children. Just the text within the div.
This is what I've tried so far to no avail:
$('#main').focus();
var mainDiv = document.getElementById("main");
var startNode = mainDiv;
var endNode = mainDiv;
var range = document.createRange();
range.setStart(startNode, 6);
range.setEnd(endNode, 10);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
But I get:
Uncaught IndexSizeError: Failed to execute 'setStart' on 'Range': There is no child at offset 6.
My jsfiddle:
http://jsfiddle.net/foreyez/h4bL5u4g/
But mine don't have any children. Just the text within the div.
The text within the div is a child – it's a text node. That's what you want to target.
You will also need to trim its nodeValue to get the proper offset. Otherwise, the leading spaces will be included.
This seems to do what you want:
function SelectText(obj, start, stop) {
var mainDiv = $(obj)[0],
startNode = mainDiv.childNodes[0],
endNode = mainDiv.childNodes[0];
startNode.nodeValue = startNode.nodeValue.trim();
var range = document.createRange();
range.setStart(startNode, start);
range.setEnd(endNode, stop + 1);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
} //SelectText
$('#main').focus();
SelectText('#main', 6, 10);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="main" contenteditable="true">
Hello World
</div>

Rangy (JS/jQuery) split node

How would I split a node/element at a position (selection).
Example I have this markup:
<p>This is a te|st, you like?</p>
(this pipe represents the position/selection)
I want to convert it to:
<p>This is a te</p>|<p>st, you like?</p>
Maintaining the selection.
Any ideas?
I and using the Rangy library, and also jQuery, but can use raw JS if applicable.
You could do this by creating a range that extends from the caret to the point immediately after the paragraph and using its extractContents() method.
Live demo: http://jsfiddle.net/timdown/rr9qs/2/
Code:
var sel = rangy.getSelection();
if (sel.rangeCount > 0) {
// Create a copy of the selection range to work with
var range = sel.getRangeAt(0).cloneRange();
// Get the containing paragraph
var p = range.commonAncestorContainer;
while (p && (p.nodeType != 1 || p.tagName != "P") ) {
p = p.parentNode;
}
if (p) {
// Place the end of the range after the paragraph
range.setEndAfter(p);
// Extract the contents of the paragraph after the caret into a fragment
var contentAfterRangeStart = range.extractContents();
// Collapse the range immediately after the paragraph
range.collapseAfter(p);
// Insert the content
range.insertNode(contentAfterRangeStart);
// Move the caret to the insertion point
range.collapseAfter(p);
sel.setSingleRange(range);
}
}

html - selection range - getting the range + starting node + ending node + distance

From my previous question for selecting specific html text, I have gone through this link to understand range in html string.
For selecting a specific text on html page. We need to follow this steps.
Assumed HTML:
<h4 id="entry1196"><a
href="http://radar.oreilly.com/archives/2007/03/call_for_a_blog_1.html"
class="external">Call for a Blogger's Code of Conduct</a></h4>
<p>Tim O'Reilly calls for a Blogger Code of Conduct. His proposals are:</p>
<ol>
<li>Take responsibility not just for your own words, but for the
comments you allow on your blog.</li>
<li>Label your tolerance level for abusive comments.</li>
<li>Consider eliminating anonymous comments.</li>
</ol>
java script to make selection by range
var range = document.createRange(); // create range
var startPar = [the p node]; // starting parameter
var endLi = [the second li node]; // ending parameter
range.setStart(startPar,13); // distance from starting parameter.
range.setEnd(endLi,17); // distance from ending parameter
range.select(); // this statement will make selection
I want to do this in invert way. I mean, assume that selection is done by user on browser (safari). My question is that How can we get starting node (as we have 'the p node' here) and ending node (as we have 'the second li node' here) and the range as well (as we have 13,17 here)?
Edit : my efforts (From this question)
var sel = window.getSelection();
if (sel.rangeCount < 1) {
return;
}
var range = sel.getRangeAt(0);
var startNode = range.startContainer, endNode = range.endContainer;
// Split the start and end container text nodes, if necessary
if (endNode.nodeType == 3) {
endNode.splitText(range.endOffset);
range.setEnd(endNode, endNode.length);
}
if (startNode.nodeType == 3) {
startNode = startNode.splitText(range.startOffset);
range.setStart(startNode, 0);
}
But, yet I am confused about getting like, if selected is first paragraph or second or third, or selected is in first heading or second heading or what....
Storing the selected range is simple. The following will return only the first selected range (Firefox at least supports multiple selections):
<script type="text/javascript">
function getSelectionRange() {
var sel;
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount) {
return sel.getRangeAt(0);
}
} else if (document.selection) {
return document.selection.createRange();
}
return null;
}
var range;
</script>
<input type="button" onclick="range = getSelectionRange();"
value="Store selection">
range will have properties startContainer (the node containing the start of the range), startOffset (an offset within the start container node: a character offset in the case of text nodes and child offset in elements), endContainer and endOffset (equivalent behvaiour to the start properties). Range is well documented by its specification and MDC.
In IE, range will contain a TextRange, which works very differently. Rather than nodes and offsets, TextRanges are concerned with characters, words and sentences. Microsoft's site has some documentation: http://msdn.microsoft.com/en-us/library/ms533042%28VS.85%29.aspx, http://msdn.microsoft.com/en-us/library/ms535872%28VS.85%29.aspx.

How to move cursor to end of contenteditable entity

I need to move caret to end of contenteditable node like on Gmail notes widget.
I read threads on StackOverflow, but those solutions are based on using inputs and they doesn't work with contenteditable elements.
Geowa4's solution will work for a textarea, but not for a contenteditable element.
This solution is for moving the caret to the end of a contenteditable element. It should work in all browsers which support contenteditable.
function setEndOfContenteditable(contentEditableElement)
{
var range,selection;
if(document.createRange)//Firefox, Chrome, Opera, Safari, IE 9+
{
range = document.createRange();//Create a range (a range is a like the selection but invisible)
range.selectNodeContents(contentEditableElement);//Select the entire contents of the element with the range
range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
selection = window.getSelection();//get the selection object (allows you to change selection)
selection.removeAllRanges();//remove any selections already made
selection.addRange(range);//make the range you have just created the visible selection
}
else if(document.selection)//IE 8 and lower
{
range = document.body.createTextRange();//Create a range (a range is a like the selection but invisible)
range.moveToElementText(contentEditableElement);//Select the entire contents of the element with the range
range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
range.select();//Select the range (make it the visible selection
}
}
It can be used by code similar to:
elem = document.getElementById('txt1');//This is the element that you want to move the caret to the end of
setEndOfContenteditable(elem);
If you don't care about older browsers, this one did the trick for me.
// [optional] make sure focus is on the element
yourContentEditableElement.focus();
// select all the content in the element
document.execCommand('selectAll', false, null);
// collapse selection to the end
document.getSelection().collapseToEnd();
There is also another problem.
The Nico Burns's solution works if the contenteditable div doesn't contain other multilined elements.
For instance, if a div contains other divs, and these other divs contain other stuff inside, could occur some problems.
In order to solve them, I've arranged the following solution, that is an improvement of the Nico's one:
//Namespace management idea from http://enterprisejquery.com/2010/10/how-good-c-habits-can-encourage-bad-javascript-habits-part-1/
(function( cursorManager ) {
//From: http://www.w3.org/TR/html-markup/syntax.html#syntax-elements
var voidNodeTags = ['AREA', 'BASE', 'BR', 'COL', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR', 'BASEFONT', 'BGSOUND', 'FRAME', 'ISINDEX'];
//From: https://stackoverflow.com/questions/237104/array-containsobj-in-javascript
Array.prototype.contains = function(obj) {
var i = this.length;
while (i--) {
if (this[i] === obj) {
return true;
}
}
return false;
}
//Basic idea from: https://stackoverflow.com/questions/19790442/test-if-an-element-can-contain-text
function canContainText(node) {
if(node.nodeType == 1) { //is an element node
return !voidNodeTags.contains(node.nodeName);
} else { //is not an element node
return false;
}
};
function getLastChildElement(el){
var lc = el.lastChild;
while(lc && lc.nodeType != 1) {
if(lc.previousSibling)
lc = lc.previousSibling;
else
break;
}
return lc;
}
//Based on Nico Burns's answer
cursorManager.setEndOfContenteditable = function(contentEditableElement)
{
while(getLastChildElement(contentEditableElement) &&
canContainText(getLastChildElement(contentEditableElement))) {
contentEditableElement = getLastChildElement(contentEditableElement);
}
var range,selection;
if(document.createRange)//Firefox, Chrome, Opera, Safari, IE 9+
{
range = document.createRange();//Create a range (a range is a like the selection but invisible)
range.selectNodeContents(contentEditableElement);//Select the entire contents of the element with the range
range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
selection = window.getSelection();//get the selection object (allows you to change selection)
selection.removeAllRanges();//remove any selections already made
selection.addRange(range);//make the range you have just created the visible selection
}
else if(document.selection)//IE 8 and lower
{
range = document.body.createTextRange();//Create a range (a range is a like the selection but invisible)
range.moveToElementText(contentEditableElement);//Select the entire contents of the element with the range
range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
range.select();//Select the range (make it the visible selection
}
}
}( window.cursorManager = window.cursorManager || {}));
Usage:
var editableDiv = document.getElementById("my_contentEditableDiv");
cursorManager.setEndOfContenteditable(editableDiv);
In this way, the cursor is surely positioned at the end of the last element, eventually nested.
EDIT #1: In order to be more generic, the while statement should consider also all the other tags which cannot contain text. These elements are named void elements, and in this question there are some methods on how to test if an element is void. So, assuming that exists a function called canContainText that returns true if the argument is not a void element, the following line of code:
contentEditableElement.lastChild.tagName.toLowerCase() != 'br'
should be replaced with:
canContainText(getLastChildElement(contentEditableElement))
EDIT #2: The above code is fully updated, with every changes described and discussed
It's possible to do set cursor to the end through the range:
setCaretToEnd(target/*: HTMLDivElement*/) {
const range = document.createRange();
const sel = window.getSelection();
range.selectNodeContents(target);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
target.focus();
range.detach(); // optimization
// set scroll to the end if multiline
target.scrollTop = target.scrollHeight;
}
A shorter and readable version using only selection (without range):
function setEndOfContenteditable(elem) {
let sel = window.getSelection();
sel.selectAllChildren(elem);
sel.collapseToEnd();
}
<p id="pdemo" contenteditable>
A paragraph <span id="txt1" style="background-color: #0903">span text node <i>span italic</i></span> a paragraph.
<p>
<button onclick="pdemo.focus(); setEndOfContenteditable(txt1)">set caret</button>
Quite useful: https://javascript.info/selection-range
Moving cursor to the end of editable span in response to focus event:
moveCursorToEnd(el){
if(el.innerText && document.createRange)
{
window.setTimeout(() =>
{
let selection = document.getSelection();
let range = document.createRange();
range.setStart(el.childNodes[0],el.innerText.length);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
,1);
}
}
And calling it in event handler (React here):
onFocus={(e) => this.moveCursorToEnd(e.target)}}
I had a similar problem trying to make a element editable. It was possible in Chrome and FireFox but in FireFox the caret either went to the beginning of the input or it went one space after the end of the input. Very confusing to the end-user I think, trying to edit the content.
I found no solution trying several things. Only thing that worked for me was to "go around the problem" by putting a plain old text-input INSIDE my . Now it works. Seems like "content-editable" is still bleeding edge tech, which may or may not work as you would like it to work, depending on the context.
The problem with contenteditable <div> and <span> is resolved when you start typing in it initially. One workaround for this could be triggering a focus event on your div element and on that function, clear, and refill what was already in the div element. This way the problem is resolved and finally you can place the cursor at the end using range and selection. Worked for me.
moveCursorToEnd(e : any) {
let placeholderText = e.target.innerText;
e.target.innerText = '';
e.target.innerText = placeholderText;
if(e.target.innerText && document.createRange)
{
let range = document.createRange();
let selection = window.getSelection();
range.selectNodeContents(e.target);
range.setStart(e.target.firstChild,e.target.innerText.length);
range.setEnd(e.target.firstChild,e.target.innerText.length);
selection.removeAllRanges();
selection.addRange(range);
}
}
In HTML code:
<div contentEditable="true" (focus)="moveCursorToEnd($event)"></div>

Categories