I am using Summernote plugin to create a nice textarea and Mark.js plugin to highlight a defined keyword (for example foo) inside that textarea.
I have this HTML code:
<div id="text"></div>
And Javascript code:
$('#text').summernote({
height: 150,
callbacks: {
onChange: function (contents, $editable){
$('.note-editable').unmark({"done": function () {
$('.note-editable').mark(
'foo',
{'separateWordSearch': false })
}})
}
}
}
);
JSFiddle
And it's working well. Every time the user writes foo in the textarea the word is being highlighted, but I have this issue: when the user writes foo the cursor moves to the beginning of the foo word and I don't want this. How can I fix this?
The editable div(s) are not the ideal types to handle inputs. Similar cursor behavior is common where the value of an editable div is being updated explicitly using JS or jQuery. To solve this, you either need to use suitable default input types like <input> and <textarea> tags.
And that doesn't solve your use case as you wont be able to change the properties of the selected text. So the solution is as follows at client side:
Create a global variable to store the updated value of cursor for each such editable div.
Update the value of this global variable on each onblur event.
Retrieve this global value before highlighting your keyword.
Update the cursor to the retrieved value after making the necessary keyword highlights.
Following are the function to retrieve and update the cursor value :
function doGetCaretPosition (id, storeTo) {
var oField = document.getElementById(id);
// Initialize
var iCaretPos = -1;
// IE Support
if (document.selection) {
// Set focus on the element
oField.focus();
// To get cursor position, get empty selection range
var oSel = document.selection.createRange();
// Move selection start to 0 position
oSel.moveStart('character', -oField.value.length);
// The caret position is selection length
iCaretPos = oSel.text.length;
}
// Firefox support
else if (oField.selectionStart || oField.selectionStart == '0')
iCaretPos = oField.selectionStart;
// Return results
if( window[storeTo] !== undefined || window[storeTo] !== '' )
{
//if position is not updated
if( $(oField).val().length == iCaretPos )
window[storeTo] = -1;
else
window[storeTo] = iCaretPos;
console.log("[doGetCaretPosition Updated] "+$(oField).attr('id')+" in : "+window[storeTo]);
}
else
console.log("[doGetCaretPosition : failed]");
}
//Set caret position
function setCaretPosition(elemId, caretPos) {
var elem = document.getElementById(elemId);
if(elem != null) {
if(elem.createTextRange) {
var range = elem.createTextRange();
range.move('character', window[caretPos]);
range.select();
}
else {
if(elem.selectionStart) {
elem.focus();
elem.setSelectionRange(window[caretPos], window[caretPos]);
}
else
elem.focus();
}
}
console.log( "[setCaretPosition Updated]" );
}
Thats because your highlighted text will get a <markjs highlighted="true">foo</markjs> tag become. You have to setup your cursor at the end of that tag. If you have time I will update this post in a hour with the solution.
Related
I m having some troubles while getting the cursor caret position inside of a div with contenteditable attribute.
Actually, I m using the following function :
onBlurArea (field, ev) {
ev.preventDefault()
const editable = ev.target.childNodes[1].childNodes[2]
this.positions[field] = {start: this.getCaretCharacterOffsetWithin(editable), editable}
}
getCaretCharacterOffsetWithin (element) {
let ie = (typeof this.doc.selection != 'undefined' && this.doc.selection.type != 'Control') && true
let w3 = (typeof this.win.getSelection != 'undefined') && true
let caretOffset = 0
if (w3) {
let range = this.win.getSelection().getRangeAt(0)
console.log(range)
let preCaretRange = range.cloneRange()
preCaretRange.selectNodeContents(element)
preCaretRange.setEnd(range.endContainer, range.endOffset)
console.log(preCaretRange)
caretOffset = preCaretRange.toString().length
console.log(caretOffset)
}
else if (ie) {
let textRange = this.doc.selection.createRange()
let preCaretTextRange = this.doc.body.createTextRange()
preCaretTextRange.expand(element)
preCaretTextRange.setEndPoint('EndToEnd', textRange)
caretOffset = preCaretTextRange.text.length
}
return caretOffset
}
The function onBlurArea is triggered when I blur a text-angular field :
<text-angular ng-model="moduleManage.currentModule.description" required ng-blur="moduleManage.onBlurArea('description', $event)"></text-angular>
Moreover :
this.doc = $document
this.win = $window
I need to get the cursor position to add some external stuff not managed by the library.
My problem is that when I blur my field, my carretOffset is not the same if I focus on a precise field or on another one.
The function above always gives me 0
When I log the range, I m always having a div item that is completely out of my current focus, even out of text-angular module
EDIT : I think my problem is on the following line :
let range = this.win.getSelection().getRangeAt(0)
That give me something that I don't understand at all...
When blur event is fired, u have already lost your focus and the range created.
To get the caret offset just before the div lost focus, I would recommend you save the caret offset every time it changes. That is to say, update it every time keyup or mouseup is fired or when any other timing u would like to update it.
Check this plunkr
BTW, in this plunkr i used your getCaretCharacterOffsetWithin function to calculate the offset. But this function may be not working as expected. It will add some blanks to the content and the caret offset is larger than real.
I have a div (#recommendTextArea) which is editable, in which that I try to modify the innerHTML of this div when a user clicks on a list (this is called .display_box), the function looks like this. Basically it appends a span to the innerHTML of the div and then it hides the friendList, upon hiding it also tries to restoreTheSelection and before appending the extra span I called saveSelection.
$(".display_box").live("click",function()
{
selRange = saveSelection();
console.log(selRange);
var username = $(this).attr('title');
var old = $("#recommendTextArea").html();
var content = old.replace(word, " "); //replacing #abc to (" ") space
var E ="<span contenteditable='false'>"+ username + "</span> ";
content = [content.slice(0, start), E, content.slice(start)].join('');
$("#recommendTextArea").html(content);
$("#friendsList").hide(function(){
restoreSelection(selRange);
});
});
I have the following function to restore and save selection:
function saveSelection() {
if (window.getSelection) {
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
return sel.getRangeAt(0);
}
} else if (document.selection && document.selection.createRange) {
return document.selection.createRange();
}
return null;
}
function restoreSelection(range) {
if (range) {
if (window.getSelection) {
sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
} else if (document.selection && range.select) {
range.select();
}
}
}
However this doesn't work as expected, the cursor is no where to be seen when I click on an item. What am I doing wrong here?
You have a few issues:
1) Timing: the "click" event is way too late to grab selection (ALWAYS debug this, it's super easy to see the editable DIV has long lost focus and selection by that time). Use "mousedown" instead.
2) You can't store selection range like this - changing the selection context (in your case the innerHTML of the commonAncestorContainer) will wipe that range (for some reason even cloned range objects get wiped for me). If you manage to get a copy (via jQuery.extend for example) it will become invalid because the text node inside is not guaranteed to remain the same. My best guess is to go with storing start/end offset and if needed the related nodes as required by the range. Restore the range properties after the HTML is modified.
3) As with 1) focus is crucial to maintain selection, so that click on the list.. make sure you prevent the default before exiting the handler so focus and you new selection will remain in the DIV.
Can't figure out the exact use case from your code but this is my test sample and you can adjust from here as needed: http://jsfiddle.net/damyanpetev/KWDf6/
How to show a little image after selecting some text when we using the window.getSelection() code?
The most similar thing I found was this How can I position an element next to user text selection? but cant seem to get it to work properly :-s
Being kinda new to this also doesn't help.
Just need some simple code to do it, doesn't matter much where the image shows up, aslong as its near the selected text
-edit-
As I said in comment, the idea is to show a button (thought of image first but a button is better) floating near the selected text (after the selection is made), with link to quote what was selected, and if we clear what was selected the button doesn't show anymore.
And would this be possible by pulling mouse coords when finishing text selection and adding the x,y coords to the style of the button to be shown?
-edit-
got it working just like I wanted, having that coords idea in mind. Found this http://motyar.blogspot.pt/2010/02/get-user-selected-text-with-jquery-and.html and with it I came up with this:
function getSelected() {
if (window.getSelection) {
return window.getSelection();
}
else if (document.getSelection) {
return document.getSelection();
}
else {
var selection = document.selection && document.selection.createRange();
if (selection.text) {
return selection.text;
}
return false;
}
return false;
}
$(document).ready(function() {
var blank = '',
selectionImage;
$('#mensagem').mouseup(function(e) {
var selection = getSelected();
if (!selectionImage) {
selectionImage = $('<button>').attr({
type: 'button',
title: 'Citar Texto seleccionado',
id: 'quote-place'
}).html("Citar").css({
"color": "red"
}).hide();
$(document.body).append(selectionImage);
}
$("#quote-place").click(function quote() {
var txt = '';
if (window.getSelection) {
txt = window.getSelection();
}
else if (document.getSelection) {
txt = document.getSelection();
}
else if (document.selection) {
txt = document.selection.createRange().text;
}
else {
return;
}
document.aform.selectedtext.value = txt;
}).mousedown(function() {
if (selectionImage) {
selectionImage.fadeOut();
}
});
selectionImage.css({
top: e.pageY - 30,
//offsets
left: e.pageX - 13 //offsets
}).fadeIn();
});
});
http://jsfiddle.net/ordhor/2Gc8c/
The problem now lies on when we click on the <div> without selecting text the button keeps showing. It should only appear when selecting text and I cant find how to fix it...
Let's suppose that image is the image element you want to insert. The idea is to place a place-holder <span> element after the selected text, since we can't compute the position of text nodes. So, we start with this CSS:
.place-holder {position: relative;}
.quote-image {
position: absolute;
top: 0;
left: 0;
}
The place-holder class is for our <span> element, and the position property for the image we want to put in, which is absolutely positioned in order to keep a zero-width <span>.
Now we want to check whether a selection is made or not. Unfortunately, there's no standard event raised when a selection is made. The onselect event is fired only if a selection is made in text fields, and not even when the selection is canceled.
But since the selection is usually made using mouse and keyboard events, we can listen to them and check if a selection is made.
var prevRng;
function checkSelection() {
var s = getSelection(), r = s.rangeCount && s.getRangeAt(0);
if (prevRng) {
if (!r || r.endContainer !== prevRng.endContainer
|| r.endOffset !== prevRng.endOffset) {
// The selection has changed or been cleared
selectionClear();
}
}
if (r) setPlaceHolder(r);
}
document.addEventListener("mouseup", checkSelection);
// mousedown usually clears the selection
document.addEventListener("mousedown", checkSelection);
// You can use the keyboard to extend the selection, or select all (Ctrl+A)
document.addEventListener("keyup", checkSelection);
prevRng is a variable in the function scope to store the selection made. Now we can make our working code:
function setPlaceHolder(range) {
if (range.startContainer === range.endContainer
&& range.startOffset === range.endOffset) {
// The selection is clear
prevRng = null;
return;
}
prevRng = range;
var endc = range.endContainer, span = document.createElement("span");
span.className = "place-holder";
span.appendChild(image);
if (endc.nodeType === Node.TEXT_NODE) { // Node.TEXT_NODE === 3
var p1 = endc.nodeValue.substring(0, range.endOffset),
p2 = endc.nodeValue.substring(range.endOffset);
endc.nodeValue = p1;
if (p2)
endc.parentNode.insertBefore(document.createTextNode(p2),
endc.nextSibling);
}
endc.parentNode.insertBefore(image, endc.nextSibling);
}
function selectionClear() {
if (!prevRng) return;
var endc = prevRng.endContainer;
if (endc.nextSibling.className === "place-holder") {
endc.parentNode.removeChild(endc.nextSibling);
if (endc.nodeType === Node.TEXT_NODE
&& endc.nextSibling.nodeType === Node.TEXT_NODE) {
// Joining previously divided text nodes
endc.nodeValue += endc.nextSibling.nodeValue;
endc.parentNode.removeChild(endc.nextSibling);
}
}
}
Edit: it seems I've misunderstood your question. I thought you wanted to insert the image after the selection... So, you want to know when the selection is actually made?
Edit 2: changed more or less everything to match the request.
For example:
$("div.rte").on("mouseup",function(){ // event binding on the div
if(window.getSelection().toString().length>0) //check length if >0 then only proceed
{
// do processing
}
this should resolve your problem from my understanding of the problem
I have been trying to implement similar functionality , my problem is that the document can be edited as well.
The way of saving the annotation is a challenge, currently Im thinking to save the start and end index of the annotion in the db, when document content changes , using mutation observers , I will calculate the new char count of the annotation and then save again in db
Please let me know are there any other ways of storing and retrieving from db
I currently have a textarea which I requires control over text that has been pasted in,
essentially I need to be able to take whatever the user wants to paste into a textarea and place it into a variable.
I will then work out the position in which they pasted the text and the size of the string to remove it from the textarea,
Then at the end deal with the text thats is in the variable in my own way.
My question: how would I go about getting a copy of the text in a variable that was just pasted in by the user?
Thanks.
I answered a similar question a few days ago: Detect pasted text with ctrl+v or right click -> paste. This time I've included quite a long function that accurately gets selection boundaries in textarea in IE; the rest is relatively simple.
You can use the paste event to detect the paste in most browsers (notably not Firefox 2 though). When you handle the paste event, record the current selection, and then set a brief timer that calls a function after the paste has completed. This function can then compare lengths to know where to look for the pasted content. Something like the following:
function getSelectionBoundary(el, start) {
var property = start ? "selectionStart" : "selectionEnd";
var originalValue, textInputRange, precedingRange, pos, bookmark, isAtEnd;
if (typeof el[property] == "number") {
return el[property];
} else if (document.selection && document.selection.createRange) {
el.focus();
var range = document.selection.createRange();
if (range) {
// Collapse the selected range if the selection is not a caret
if (document.selection.type == "Text") {
range.collapse(!!start);
}
originalValue = el.value;
textInputRange = el.createTextRange();
precedingRange = el.createTextRange();
pos = 0;
bookmark = range.getBookmark();
textInputRange.moveToBookmark(bookmark);
if (/[\r\n]/.test(originalValue)) {
// Trickier case where input value contains line breaks
// Test whether the selection range is at the end of the
// text input by moving it on by one character and
// checking if it's still within the text input.
try {
range.move("character", 1);
isAtEnd = (range.parentElement() != el);
} catch (ex) {
log.warn("Error moving range", ex);
isAtEnd = true;
}
range.moveToBookmark(bookmark);
if (isAtEnd) {
pos = originalValue.length;
} else {
// Insert a character in the text input range and use
// that as a marker
textInputRange.text = " ";
precedingRange.setEndPoint("EndToStart", textInputRange);
pos = precedingRange.text.length - 1;
// Delete the inserted character
textInputRange.moveStart("character", -1);
textInputRange.text = "";
}
} else {
// Easier case where input value contains no line breaks
precedingRange.setEndPoint("EndToStart", textInputRange);
pos = precedingRange.text.length;
}
return pos;
}
}
return 0;
}
function getTextAreaSelection(textarea) {
var start = getSelectionBoundary(textarea, true),
end = getSelectionBoundary(textarea, false);
return {
start: start,
end: end,
length: end - start,
text: textarea.value.slice(start, end)
};
}
function detectPaste(textarea, callback) {
textarea.onpaste = function() {
var sel = getTextAreaSelection(textarea);
var initialLength = textarea.value.length;
window.setTimeout(function() {
var val = textarea.value;
var pastedTextLength = val.length - (initialLength - sel.length);
var end = sel.start + pastedTextLength;
callback({
start: sel.start,
end: end,
length: pastedTextLength,
text: val.slice(sel.start, end),
replacedText: sel.text
});
}, 1);
};
}
window.onload = function() {
var textarea = document.getElementById("your_textarea");
detectPaste(textarea, function(pasteInfo) {
var val = textarea.value;
// Delete the pasted text and restore any previously selected text
textarea.value = val.slice(0, pasteInfo.start) +
pasteInfo.replacedText + val.slice(pasteInfo.end);
alert(pasteInfo.text);
});
};
You might now use FilteredPaste.js (http://willemmulder.github.com/FilteredPaste.js/) instead. It will let you control what content gets pasted into a textarea or contenteditable and you will be able to filter/change/extract content at will.
A quick search shows me that there are different methods for different browsers. I'm not sure if jQuery has a solution. Prototype.js does not appear to have one. Maybe YUI can do this for you?
You can also use TinyMCE, since it does have a gazillion of different event triggers. It is a full fledged word processor, but you can use it as plain text if you want. It might be a bit too much weight to add though. For example, upon initiation, it turns your <textarea> into an iFrame with several sub. But it will do what you ask.
--Dave
I am after a definitive, cross-browser solution to set the cursor/caret position to the last known position when a contentEditable='on' <div> regains focus. It appears default functionality of a content editable div is to move the caret/cursor to the beginning of the text in the div each time you click on it, 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.
If anybody has any thoughts, working code snippets or samples I'd be happy to see them.
I don't really have any code yet but here is 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 have tried this resource but it appears it does not work for a <div>. Perhaps only for textarea (How to move cursor to end of contenteditable entity)
This solution works in all major browsers:
saveSelection() is attached to the onmouseup and onkeyup events of the div and saves the selection to the variable savedRange.
restoreSelection() is attached to the onfocus event of the div and reselects the selection saved in savedRange.
This works perfectly unless you want the selection to be restored when the user clicks the div aswell (which is a bit unintuitative as normally you expect the cursor to go where you click but code included for completeness)
To achieve this the onclick and onmousedown events are canceled by the function cancelEvent() which is a cross browser function to cancel the event. The cancelEvent() function also runs the restoreSelection() function because as the click event is cancelled the div doesn't receive focus and therefore nothing is selected at all unless this functions is run.
The variable isInFocus stores whether it is in focus and is changed to "false" onblur and "true" onfocus. This allows click events to be cancelled only if the div is not in focus (otherwise you would not be able to change the selection at all).
If you wish to the selection to be change when the div is focused by a click, and not restore the selection onclick (and only when focus is given to the element programtically using document.getElementById("area").focus(); or similar then simply remove the onclick and onmousedown events. The onblur event and the onDivBlur() and cancelEvent() functions can also safely be removed in these circumstances.
This code should work if dropped directly into the body of an html page if you want to test it quickly:
<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>
This is compatible with the standards-based browsers, but will probably fail in IE. I'm providing it as a starting point. IE doesn't support DOM Range.
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);
};
Update
I've written a cross-browser range and selection library called Rangy that incorporates an improved version of the code I posted below. You can use the selection save and restore module for this particular question, although I'd be tempted to use something like #Nico Burns's answer if you're not doing anything else with selections in your project and don't need the bulk of a library.
Previous answer
You can use IERange (http://code.google.com/p/ierange/) to convert IE's TextRange into something like a DOM Range and use it in conjunction with something like eyelidlessness's starting point. Personally I would only use the algorithms from IERange that do the Range <-> TextRange conversions rather than use the whole thing. And IE's selection object doesn't have the focusNode and anchorNode properties but you should be able to just use the Range/TextRange obtained from the selection instead.
I might put something together to do this, will post back here if and when I do.
EDIT:
I've created a demo of a script that does this. It works in everything I've tried it in so far except for a bug in Opera 9, which I haven't had time to look into yet. Browsers it works in are IE 5.5, 6 and 7, Chrome 2, Firefox 2, 3 and 3.5, and Safari 4, all on Windows.
http://www.timdown.co.uk/code/selections/
Note that selections may be made backwards in browsers so that the focus node is at the start of the selection and hitting the right or left cursor key will move the caret to a position relative to the start of the selection. I don't think it is possible to replicate this when restoring a selection, so the focus node is always at the end of the selection.
I will write this up fully at some point soon.
I had a related situation, where I specifically needed to set the cursor position to the END of a contenteditable div. I didn't want to use a full fledged library like Rangy, and many solutions were far too heavyweight.
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 theory is simple: append a span to the end of the editable, select it, and then remove the span - leaving us with a cursor at the end of the div. You could adapt this solution to insert the span wherever you want, thus putting the cursor at a specific spot.
Usage is simple:
$('#editable').focusEnd();
That's it!
I took Nico Burns's answer and made it using jQuery:
Generic: For every div contentEditable="true"
Shorter
You'll need jQuery 1.6 or higher:
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();
}
});
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();
}
});
div[contenteditable] {
padding: 1em;
font-family: Arial;
outline: 1px solid rgba(0,0,0,0.5);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div contentEditable="true"></div>
<div contentEditable="true"></div>
<div contentEditable="true"></div>
After playing around I've modified eyelidlessness' answer above and made it a jQuery plugin so you can just do one of these:
var html = "The quick brown fox";
$div.html(html);
// Select at the text "quick":
$div.setContentEditableSelection(4, 5);
// Select at the beginning of the contenteditable div:
$div.setContentEditableSelection(0);
// Select at the end of the contenteditable div:
$div.setContentEditableSelection(html.length);
Excuse the long code post, but it may help someone:
$.fn.setContentEditableSelection = function(position, length) {
if (typeof(length) == "undefined") {
length = 0;
}
return this.each(function() {
var $this = $(this);
var editable = this;
var selection;
var range;
var html = $this.html();
html = html.substring(0, position) +
'<a id="cursorStart"></a>' +
html.substring(position, position + length) +
'<a id="cursorEnd"></a>' +
html.substring(position + length, html.length);
console.log(html);
$this.html(html);
// 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
}
};
// Slight delay will avoid the initial selection
// (at start or of contents depending on browser) being mistaken
setTimeout(function() {
var cursorStart = document.getElementById('cursorStart');
var 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();
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);
}
}
// Register selection again
captureSelection();
}, 10);
});
};
You can leverage selectNodeContents which is supported by modern browsers.
var el = document.getElementById('idOfYoursContentEditable');
var selection = window.getSelection();
var range = document.createRange();
selection.removeAllRanges();
range.selectNodeContents(el);
range.collapse(false);
selection.addRange(range);
el.focus();
In Firefox you might have the text of the div in a child node (o_div.childNodes[0])
var range = document.createRange();
range.setStart(o_div.childNodes[0],last_caret_pos);
range.setEnd(o_div.childNodes[0],last_caret_pos);
range.collapse(false);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
I may be late to the party with this one, but maybe instead of just manipulating the dom on the frontend you could store whatever the current value of the tag you are edititing in a variable that is not the value that is being rendered to the dom. Then you can detect a page refresh and change the value of the innerHTML of the tag then, or in my case you just need to keep track of the changes and send this to a back end. Perhaps my words won't do any justice so I will write a code example that has worked for my use case.
import React, { ChangeEvent, useEffect, useState } from "react";
interface IParentProps {}
const ParentComp: React.FC<IParentProps> = (props) => {
const [innerValue, setInnerValue] = useState<string>();
const [ghostValue, setGhostValue] = useState<string>();
// create some boolean to detect when the enter key was pressed in the input field so that you
//can remove the input field and add the child component
const handleChange = (event: ChangeEvent<HTMLDivElement>) => {
setInnerValue(event.currentTarget.innerHTML);
setGhostValue(event.currentTarget.innerHTML);
};
const handleGhostChange = (event: ChangeEvent<HTMLDivElement>) => {
setGhostValue(event.currentTarget.innerHTML);
};
//handle screen refresh, or send the ghost value to the backend
useEffect(() => {}, []);
return (
<div>
<input type="text" onChange={handleChange} />
<ChildComponent handleChange={handleGhostChange}>
{innerValue}
</ChildComponent>
</div>
);
};
interface IChildProps {
handleChange: (e: ChangeEvent<HTMLDivElement>) => void;
children: React.ReactNode;
}
const ChildComponent: React.FC<IChildProps> = (props) => {
return (
<p
contentEditable="true"
suppressContentEditableWarning={true}
onInput={props.handleChange}
>
{props.children}
</p>
);
};
I hope this makes sense if you would like me to revise the answer without the typescript bloat I am willing and able. If this works for you guys please let me know I think this is a much simplier solution then trying to reconfigure the cursor how you want it personally.