Window text selection in a contenteditable - javascript

the text selection in a contenteditable causing me big problems...
I'm tryin to get begin and end selection point in the same way of this code part :
http://jsfiddle.net/TjXEG/1/
(Because in the contenteditable, there is differents tags and i need to reselect after a loss of focus the visible selected text (text node ?)
I'm really lost with that, someone know a tutorial or another thing to understand the selection in a web browser ?
Thanks,
Yeppao

Because I only use Chrome, I chopped off the else since it doesn't apply. So here's the Chrome solution:
Non-editable text. Editable is below:
<div id="test" contenteditable="true">Hello, some <b>bold</b> and <i>italic and <b>bold</b></i> text</div>
<div id="caretPos"></div>
<div id="caretPost"></div>
<script type="text/javascript">
function getCaretCharacterOffsetWithin(element) {
var begin = 0;
var end = 0;
if (typeof window.getSelection != "undefined") {
var range = window.getSelection().getRangeAt(0);
var preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(element);
preCaretRange.setEnd(range.startContainer, range.startOffset);
begin = preCaretRange.toString().length;
preCaretRange.setEnd(range.endContainer, range.endOffset);
end = preCaretRange.toString().length;
}
return "Selection start: " + begin + "<br>Selection end: " + end;
}
function showCaretPos() {
var el = document.getElementById("test");
var caretPosEl = document.getElementById("caretPos");
caretPosEl.innerHTML = getCaretCharacterOffsetWithin(el);
}
document.body.onkeyup = showCaretPos;
document.body.onmouseup = showCaretPos;
</script>
Once you understand it, the logic is pretty simple. There is a start and end container. If it is plain text: they will be the same; however, HTML tags fragment the sentence and will make start contain a portion and end contain a different portion.
If you use setStart() where I'm using setEnd() and reverse the parameters as well, it'll be the same as reversing the index (i.e. the end will be 0 instead of the beginning).
So in order to fetch the beginning, you still use setEnd(), except with the start parameters.

Related

How to place cursor at end of text in text input element in IE [duplicate]

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>

Rangy: word under caret (again)

I'm trying to create a typeahead code to add to a wysihtml5 rich text editor.
Basically, I need to be able to insert People/hashtag references like Twitter/Github/Facebook... do.
I found some code of people trying to achieve the same kind of thing.
http://jsfiddle.net/A9z3D/
This works pretty fine except it only do suggestions for the last word and has some bugs. And I want a select box like Twitter, not a simple "selection switching" using the tab key.
For that I tried to detect the currently typed word.
getCurrentlyTypedWord: function(e) {
var iframe = this.$("iframe.wysihtml5-sandbox").get(0);
var sel = rangy.getSelection(iframe);
var word;
if (sel.rangeCount > 0 && sel.isCollapsed) {
console.debug("Rangy: ",sel);
var initialCaretPositionRange = sel.getRangeAt(0);
var rangeToExpand = initialCaretPositionRange.cloneRange();
var newStartOffset = rangeToExpand.startOffset > 0 ? rangeToExpand.startOffset - 1 : 0;
rangeToExpand.setStart(rangeToExpand.startContainer,newStartOffset);
sel.setSingleRange(rangeToExpand);
sel.expand("word", {
trim: true,
wordOptions: {
includeTrailingSpace: true,
//wordRegex: /([a-z0-9]+)*/gi
wordRegex: /[a-z0-9]+('[a-z0-9]+)*/gi
// wordRegex: /([a-z0-9]+)*/gi
}
});
word = sel.text();
sel.removeAllRanges();
sel.setSingleRange(initialCaretPositionRange);
} else {
word = "noRange";
}
console.debug("WORD=",word);
return word;
This is only triggered when the selection is collapsed.
Notice I had to handle a backward move of the start offset because if the caret is at the end of the word (like it is the case most of the time when an user is typing), then the expand function doesn't expand around the currently typed word.
This works pretty nicely until now, the problem is that it uses the alpha release of Rangy 1.3 which has the TextRangeModule. The matter is that I noticed wysihtml5 is also using Rangy in a different and incompatible version (1.2.2) (problem with rangy.dom that probably has been removed).
As Rangy uses a global window.rangy variable, I think I'll have to use version 1.2.2 anyway.
How can I do an equivalent of the expand function, using only rangy 1.2.2?
Edit: by the way, is there any other solution than using the expand function? I think it is a bit strange and hakish to modify the current selection and revert it back just to know which word is currently typed. Isn't there a solution that doesn't involve selecting the currently typed word? I mean just based on ranges once we know the initial caret collapsed range?
As Rangy uses a global window.rangy variable, I think I'll have to use version 1.2.2 anyway.
Having read Rangy's code, I had the intuition that probably it would be feasible to load two versions of Rangy in the same page. I did a google search and found I was right. Tim Down (creator of Rangy) explained it in an issue report. He gave this example:
<script type="text/javascript" src="/rangy-1.0.1/rangy-core.js"></script>
<script type="text/javascript" src="/rangy-1.0.1/rangy-cssclassapplier.js"></script>
<script type="text/javascript">
var rangy1 = rangy;
</script>
<script type="text/javascript" src="/rangy-1.1.2/rangy-core.js"></script>
<script type="text/javascript" src="/rangy-1.1.2/rangy-cssclassapplier.js"></script>
So you could load the version of Rangy that your code wants. Rename it and use this name in your code, and then load what wysihtml5 wants and leave this version as rangy.
Otherwise, having to implement expand yourself in a way that faithfully replicates what Rangy 1.3 does is not a simple matter.
Here's an extremely primitive implementation of code that would expand selections to word boundaries. This code is going to be tripped by elements starting or ending within words.
var word_sep = " ";
function expand() {
var sel = rangy.getSelection();
var range = sel.getRangeAt(0);
var start_node = range.startContainer;
if (start_node.nodeType === Node.TEXT_NODE) {
var sep_at = start_node.nodeValue.lastIndexOf(word_sep, range.startOffset);
range.setStart(start_node, (sep_at !== -1) ? sep_at + 1 : 0);
}
var end_node = range.endContainer;
if (end_node.nodeType === Node.TEXT_NODE) {
var sep_at = end_node.nodeValue.indexOf(word_sep, range.endOffset);
range.setEnd(end_node, (sep_at !== -1) ? sep_at : range.endContainer.nodeValue.length);
}
sel.setSingleRange(range);
}
Here's a fiddle for it. This should work in rangy 1.2.2. (It would even work without rangy.)
For those interested, based in #Louis suggestions, I made this JsFiddle that shows a wysihtml5 integration to know the currently typed word.
It doesn't need the use of the expand function that is in rangy 1.3 which is still an alpha release.
http://jsfiddle.net/zPxSL/2/
$(function () {
$('#txt').wysihtml5();
var editor = $('#txt').data("wysihtml5").editor;
$(".wysihtml5-sandbox").contents().find("body").click(function(e) {
getCurrentlyTypedWord();
});
$(".wysihtml5-sandbox").contents().find("body").keydown(function(e) {
getCurrentlyTypedWord();
});
function getCurrentlyTypedWord() {
var iframe = this.$("iframe.wysihtml5-sandbox").get(0);
var sel = rangy.getIframeSelection(iframe);
var wordSeparator = " ";
if (sel.rangeCount > 0) {
var selectedRange = sel.getRangeAt(0);
var isCollapsed = selectedRange.collapsed;
var isTextNode = (selectedRange.startContainer.nodeType === Node.TEXT_NODE);
var isSimpleCaret = (selectedRange.startOffset === selectedRange.endOffset);
var isSimpleCaretOnTextNode = (isCollapsed && isTextNode && isSimpleCaret);
// only trigger this behavior when the selection is collapsed on a text node container,
// and there is an empty selection (this means just a caret)
// this is definitely the case when an user is typing
if (isSimpleCaretOnTextNode) {
var textNode = selectedRange.startContainer;
var text = textNode.nodeValue;
var caretIndex = selectedRange.startOffset;
// Get word begin boundary
var startSeparatorIndex = text.lastIndexOf(wordSeparator, caretIndex);
var startWordIndex = (startSeparatorIndex !== -1) ? startSeparatorIndex + 1 : 0;
// Get word end boundary
var endSeparatorIndex = text.indexOf(wordSeparator, caretIndex);
var endWordIndex = (endSeparatorIndex !== -1) ? endSeparatorIndex : text.length
// Create word range
var wordRange = selectedRange.cloneRange();
wordRange.setStart(textNode, startWordIndex);
wordRange.setEnd(textNode, endWordIndex);
console.debug("Word range:", wordRange.toString());
return wordRange;
}
}
}
});

Calculating the absolute start and finish of a range (highlighted text) within an editable div

I'm running an experiment to see if I can return that absolute start and end points of a highlighted block of test within a contentEditable (not actually important to the test) div. I'm not building a rich text editor or anything I just want to know how it's done! So all I want to return upon right click (not important, I'm just messing with that too) are two numbers, the absolute distance from the start of the wrapper div to the start of the selection and the absolute distance from the start of the wrapper div to the end of the selection.
I thought Mootools would make this easy but I could only get their implementation to work with forms (i.e. textarea, input etc). So I had a quick bash using Google, and it all worked fine when no tags were involved, e.g. He|llo Wor|ld (where the pipe, |, represents the highlighted range) would return [2, 9] which is correct. However, the moment I add tags to the div to allow colours / formatting these numbers do not make any sense as the range only gives position relative to text nodes and not an absolute value. Any ideas how to get this? I can only imagine it involves some form of horrendous DOM manipulation.
JS:
window.addEvent('domready', function()
{
document.body.addEvent('contextmenu',
function(e)
{
e.stop();
}
);
if(!window.SelectionHandler)
{
SelectionHandler = {};
}
SelectionHandler.Selector = {};
SelectionHandler.Selector.getSelected = function()
{
var userSelection = '';
if(window.getSelection)
{
userSelection = window.getSelection();
}
else if(document.getSelection)
{
userSelection = document.getSelection();
}
else if(document.selection)
{
userSelection = document.selection.createRange();
}
return userSelection;
}
SelectionHandler.Selector.getText = function(userSelection)
{
var selectedText = userSelection;
if(userSelection.text)
{
selectedText = userSelection.text;
}
return selectedText;
}
SelectionHandler.Selector.getRange = function(userSelection)
{
if(userSelection.getRangeAt && typeof(userSelection.getRangeAt) != 'undefined')
{
var selectedRange = userSelection.getRangeAt(0);
}
else
{
var selectedRange = document.createRange();
selectedRange.setStart(userSelection.anchorNode, userSelection.anchorOffset);
selectedRange.setEnd(userSelection.focusNode, userSelection.focusOffset);
}
return selectedRange;
}
$('mydiv').addEvent('mousedown',
function(event)
{
if(event.rightClick)
{
var userSelection = SelectionHandler.Selector.getSelected();
var selectedText = SelectionHandler.Selector.getText(userSelection);
var selectedRange = SelectionHandler.Selector.getRange(userSelection);
// New ranges to add identifiable nodes (must be in that order!?)
var endRange = document.createRange();
endRange.setStart(selectedRange.endContainer, selectedRange.endOffset);
endRange.insertNode(document.createTextNode('!~'));
var startRange = document.createRange();
startRange.setStart(selectedRange.startContainer, selectedRange.startOffset);
startRange.insertNode(document.createTextNode('~!'));
// Find the position of our new identifiable nodes (and account for their removal)
var div_content = $('mydiv').get('html');
var start = div_content.indexOf('~!');
var end = div_content.indexOf('!~') - 2;
// Remove our identifiable nodes (DOESN'T WORK)
//startRange.deleteContents();
//endRange.deleteContents();
// This does work, but obviously loses the selection
div_content = div_content.replace('~!', '').replace('!~', '');
$('mydiv').set('html', div_content);
console.log(start + ' vs ' + end);
}
}
);
}
);
HTML:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Edit Range Test</title>
</head>
<script type="text/javascript" src="mootools.js"></script>
<script type="text/javascript" src="edit_range.js"></script>
<style>
#mydiv {
width: 400px;
height: 400px;
border: 1px solid #a2a2a2;
padding: 5px;
}
</style>
<body>
<h1>Edit Range Test</h1>
<div id="mydiv" contentEditable="true"><span style="color: red;">Hello</span> World! <span style="color: red;">Hello</span> World! </div>
</body>
</html>
So when I now select He|llo Wor|ld (where the pipe, |, again represents the highlighted range) it would return [2, 4] when I want [28, 42].
EDIT: I've updated the code to clarify what I am trying to do. It does most of what I wanted to test, but loses the selection and is very scruffy!
Thanks in advance!
First, the information you get from Range objects are about as useful as you can get: for each of the start and end boundaries of the Range you get a DOM node and an offset within that node (a character offset inside a text or comment node or a child node offset otherwise), which completely describes the boundary. What you mean by "absolute start and end points" I imagine is two character offsets within the whole editable element, but that is a slippery concept: it seems simple, but is tricky to pin down. For example, how many characters does a paragraph break count for? A <br>? An <a> element with display: block? Elements such as <script> elements that contain text but are not visible to the user? Elements hidden via display: none? Elements outside the normal document flow, such as those positioned via position: absolute? Multiple consecutive whitespace characters that are rendered as a single visible character by the browser?
Having said all that, I have recently had a go at writing code to do this, and yes, it does involve DOM manipulation (although not that horrendous). It's extremely rudimentary and doesn't deal satisfactorily with any of the above issues, partly because I knocked it up quite quickly for a question on SO, and partly because in general I don't think it's possible to deal nicely in general with all those issues. The following answer provides functions for saving and restoring the selection as character indices within an editable element: replace innerHTML in contenteditable div

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