Problem
When a contentEditable=’on’ div> regains focus, I’m looking for a cross-browser solution to set the cursor/caret position to the last known position. When you click on a content editable div, it looks that the default behaviour is to relocate the caret/cursor to the beginning of the text in the div, which is undesirable.
I believe I would have to store in a variable the current cursor position when they are leaving focus of the div, and then re-set this when they have focus inside again, but I have not been able to put together, or find a working code sample yet.
I’d appreciate hearing from anyone with ideas, working code snippets, or samples.
I don’t have much code right now, but here’s what I do have:
<script type="text/javascript">
// jQuery
$(document).ready(function() {
$('#area').focus(function() { .. } // focus I would imagine I need.
}
</script>
<div id="area" contentEditable="true"></div>
PS. I tried this resource, but it doesn’t appear to work with a div>. Maybe only for the textarea (How to move cursor to end of contenteditable entity)
Asked by GONeale
Solution #1
This approach is compatible with all major web browsers:
saveSelection() is called from the div’s onmouseup and onkeyup events, and it saves the selection to the savedRange variable.
restoreSelection() is called from the div’s onfocus event and reselects the savedRange selection.
Unless you want the selection to be restored when the user clicks the div as well, this works properly (which is a bit unintuitative as normally you expect the cursor to go where you click but code included for completeness)
The onclick and onmousedown events are canceled using the cancelEvent() function, which is a cross-browser function for canceling events. Because the click event is cancelled, the restoreSelection() function is also called because the div does not receive focus and so nothing is selected unless this function is called.
The variable isInFocus keeps track of whether it’s in focus and toggles between “false” and “true” onblur and “true” onfocus. Only if the div is not in focus can click events be terminated (otherwise you would not be able to change the selection at all).
If you want the selection to change when the div is focused by a click rather than being restored onclick (and only when the element is focused programmatically using document.getElementById), you’ll need to use document.getElementById (“area”). Remove the onclick and onmousedown events by using focus(); or something similar. In these cases, the onblur event, as well as the onDivBlur() and cancelEvent() routines, can be safely omitted.
If you want to test it quickly, you may drop this code directly into the body of an html page:
<div id="area" style="width:300px;height:300px;" onblur="onDivBlur();" onmousedown="return cancelEvent(event);" onclick="return cancelEvent(event);" contentEditable="true" onmouseup="saveSelection();" onkeyup="saveSelection();" onfocus="restoreSelection();"></div>
<script type="text/javascript">
var savedRange,isInFocus;
function saveSelection()
{
if(window.getSelection)//non IE Browsers
{
savedRange = window.getSelection().getRangeAt(0);
}
else if(document.selection)//IE
{
savedRange = document.selection.createRange();
}
}
function restoreSelection()
{
isInFocus = true;
document.getElementById("area").focus();
if (savedRange != null) {
if (window.getSelection)//non IE and there is already a selection
{
var s = window.getSelection();
if (s.rangeCount > 0)
s.removeAllRanges();
s.addRange(savedRange);
}
else if (document.createRange)//non IE and no selection
{
window.getSelection().addRange(savedRange);
}
else if (document.selection)//IE
{
savedRange.select();
}
}
}
//this part onwards is only needed if you want to restore selection onclick
var isInFocus = false;
function onDivBlur()
{
isInFocus = false;
}
function cancelEvent(e)
{
if (isInFocus == false && savedRange != null) {
if (e && e.preventDefault) {
//alert("FF");
e.stopPropagation(); // DOM style (return false doesn't always work in FF)
e.preventDefault();
}
else {
window.event.cancelBubble = true;//IE stopPropagation
}
restoreSelection();
return false; // false = IE style
}
}
</script>
Answered by Nico Burns
Solution #2
This is compatible with standards-based browsers, however it will most likely fail on Internet Explorer. It’s just a beginning point for you. DOM Range is not supported by Internet Explorer.
var editable = document.getElementById('editable'),
selection, range;
// Populates selection and range variables
var captureSelection = function(e) {
// Don't capture selection outside editable region
var isOrContainsAnchor = false,
isOrContainsFocus = false,
sel = window.getSelection(),
parentAnchor = sel.anchorNode,
parentFocus = sel.focusNode;
while(parentAnchor && parentAnchor != document.documentElement) {
if(parentAnchor == editable) {
isOrContainsAnchor = true;
}
parentAnchor = parentAnchor.parentNode;
}
while(parentFocus && parentFocus != document.documentElement) {
if(parentFocus == editable) {
isOrContainsFocus = true;
}
parentFocus = parentFocus.parentNode;
}
if(!isOrContainsAnchor || !isOrContainsFocus) {
return;
}
selection = window.getSelection();
// Get range (standards)
if(selection.getRangeAt !== undefined) {
range = selection.getRangeAt(0);
// Get range (Safari 2)
} else if(
document.createRange &&
selection.anchorNode &&
selection.anchorOffset &&
selection.focusNode &&
selection.focusOffset
) {
range = document.createRange();
range.setStart(selection.anchorNode, selection.anchorOffset);
range.setEnd(selection.focusNode, selection.focusOffset);
} else {
// Failure here, not handled by the rest of the script.
// Probably IE or some older browser
}
};
// Recalculate selection while typing
editable.onkeyup = captureSelection;
// Recalculate selection after clicking/drag-selecting
editable.onmousedown = function(e) {
editable.className = editable.className + ' selecting';
};
document.onmouseup = function(e) {
if(editable.className.match(/\sselecting(\s|$)/)) {
editable.className = editable.className.replace(/ selecting(\s|$)/, '');
captureSelection();
}
};
editable.onblur = function(e) {
var cursorStart = document.createElement('span'),
collapsed = !!range.collapsed;
cursorStart.id = 'cursorStart';
cursorStart.appendChild(document.createTextNode('—'));
// Insert beginning cursor marker
range.insertNode(cursorStart);
// Insert end cursor marker if any text is selected
if(!collapsed) {
var cursorEnd = document.createElement('span');
cursorEnd.id = 'cursorEnd';
range.collapse();
range.insertNode(cursorEnd);
}
};
// Add callbacks to afterFocus to be called after cursor is replaced
// if you like, this would be useful for styling buttons and so on
var afterFocus = [];
editable.onfocus = function(e) {
// Slight delay will avoid the initial selection
// (at start or of contents depending on browser) being mistaken
setTimeout(function() {
var cursorStart = document.getElementById('cursorStart'),
cursorEnd = document.getElementById('cursorEnd');
// Don't do anything if user is creating a new selection
if(editable.className.match(/\sselecting(\s|$)/)) {
if(cursorStart) {
cursorStart.parentNode.removeChild(cursorStart);
}
if(cursorEnd) {
cursorEnd.parentNode.removeChild(cursorEnd);
}
} else if(cursorStart) {
captureSelection();
var range = document.createRange();
if(cursorEnd) {
range.setStartAfter(cursorStart);
range.setEndBefore(cursorEnd);
// Delete cursor markers
cursorStart.parentNode.removeChild(cursorStart);
cursorEnd.parentNode.removeChild(cursorEnd);
// Select range
selection.removeAllRanges();
selection.addRange(range);
} else {
range.selectNode(cursorStart);
// Select range
selection.removeAllRanges();
selection.addRange(range);
// Delete cursor marker
document.execCommand('delete', false, null);
}
}
// Call callbacks here
for(var i = 0; i < afterFocus.length; i++) {
afterFocus[i]();
}
afterFocus = [];
// Register selection again
captureSelection();
}, 10);
};
Answered by eyelidlessness
Solution #3
Update
Rangy, a cross-browser range and selection library that incorporates an enhanced version of the code I posted below, was written by myself. For this query, you can use the selection save and restore module, though if you’re not doing anything else with selections in your project and don’t require the bulk of a library, I’d be tempted to use something like @Nico Burns’ response.
Previous answer
You can utilize IERange (http://code.google.com/p/ierange/) to transform IE’s TextRange into a DOM Range, which you can then use with something like eyelidlessness’ starting point. Rather than using the entire package, I would only utilize the IERange techniques that accomplish the Range -> TextRange conversions. The focusNode and anchorNode properties aren’t available in IE’s selection object, but you should be able to utilize the selection’s Range/TextRange instead.
I plan to write this up in its entirety at some point in the near future.
Answered by Tim Down
Solution #4
I encountered a similar problem where I needed to set the cursor position to the END of a contenteditable div. I didn’t want to utilize a full-fledged library like Rangy, and many of the other options were far too cumbersome.
In the end, I came up with this simple jQuery function to set the carat position to the end of a contenteditable div:
$.fn.focusEnd = function() {
$(this).focus();
var tmp = $('<span />').appendTo($(this)),
node = tmp.get(0),
range = null,
sel = null;
if (document.selection) {
range = document.body.createTextRange();
range.moveToElementText(node);
range.select();
} else if (window.getSelection) {
range = document.createRange();
range.selectNode(node);
sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
tmp.remove();
return this;
}
The concept is straightforward: add a span to the end of the editable, select it, and then delete the span, leaving us with a cursor at the div’s end. This technique could be modified to place the span wherever you like, setting the pointer in a certain location.
Usage is simple:
$('#editable').focusEnd();
That’s it!
Answered by Zane Claes
Solution #5
I used jQuery to create Nico Burns’ response:
jQuery 1.6 or above is required:
savedRanges = new Object();
$('div[contenteditable="true"]').focus(function(){
var s = window.getSelection();
var t = $('div[contenteditable="true"]').index(this);
if (typeof(savedRanges[t]) === "undefined"){
savedRanges[t]= new Range();
} else if(s.rangeCount > 0) {
s.removeAllRanges();
s.addRange(savedRanges[t]);
}
}).bind("mouseup keyup",function(){
var t = $('div[contenteditable="true"]').index(this);
savedRanges[t] = window.getSelection().getRangeAt(0);
}).on("mousedown click",function(e){
if(!$(this).is(":focus")){
e.stopPropagation();
e.preventDefault();
$(this).focus();
}
});
Answered by Gatsbimantico
Post is based on https://stackoverflow.com/questions/1181700/set-cursor-position-on-contenteditable-div