Is there a way to set the cursor position to a known index inside CKEditor?
I want to do this because when I change the html inside the editor it resets the cursor to the start of the inserted element, which is a problem as I'm changing the content on the fly as the user types.
If I know that I want to set the cursor back to a known character position, say 100, inside the editor, is this possible?
(I asked a related question but I think I was overcomplicating the issue with example code.)
The basic way of setting selection is by creating a Range, setting its position and selecting it.
Note: if you don't know the Range API (or at least the idea which stands behind ranges), you won't be able to use selection. Here's a pretty good introduction - DOM Range spec (yep, it is a spec, but it's good). CKEditor's Range API is very similar, but a little bit bigger.
For example:
// Having this HTML in editor:
// <p id="someId1">foo <em id="someId2">bar</em>.</p>
var range = editor.createRange();
range.setStart( editor.document.getById( 'someId1' ), 0 ); // <p>^foo
range.setEnd( editor.document.getById( 'someId2' ).getFirst(), 1 ); // <em>b^ar</em>
editor.getSelection().selectRanges( [ range ] );
// Will select:
// <p id="someId1">[foo <em id="someId2">b]ar</em>.</p>
Or other case:
// Having this HTML in editor:
// <p>foo bar.</p>
var range = editor.createRange();
range.moveToElementEditablePosition( editor.editable(), true ); // bar.^</p>
editor.getSelection().selectRanges( [ range ] );
// Will select:
// <p>foo bar.^</p>
But very often you don't want to select a new range, but to restore an old selection or range. First thing you need to know is that it is impossible to correctly restore selection if you made an uncontrolled DOM changes. You need to be able to keep track of the containers and offsets of the selection's start and end.
Range keeps the references to its start and end containers (in startContainer
and endContainer
properties). Unfortunately, this references may be violated by:
innerHTML
,The same may happen with offsets (startOffset
and endOffset
properties) - if you removed one of start/end container's child nodes these offsets may need to be updated.
So in some situations range instance is not helpful when we want to remember a selection position. I'll explain three basic ways to deal with this problem.
First, this is our plan:
Note: From now on I use "ranges" in plural form because Firefox supports multiple range selections - one selection can contain more than one range (try e.g. to use CTRL key while making selections).
var ranges = editor.getSelection().getRanges();
// Make DOM changes.
editor.getSelection().selectRanges( ranges );
This is the simplest solution. It will work only if the DOM changes which we made haven't outdated ranges or we know how to update them.
var bookmarks = editor.getSelection().createBookmarks();
// Make DOM changes.
editor.getSelection().selectBookmarks( bookmarks );
Bookmarks created by the createBookmarks
method insert invisible <span>
elements with special attributes (including data-cke-bookmark
) at the selection's ranges start and end points.
If you can avoid uncontrolled innerHTML
changes and instead append/remove/move some nodes, then just remember that you have to preserve these <span>
elements and this method will work perfectly. You can also move bookmarks' elements if your modifications should change the selection as well.
By default bookmarks keep references to their <span>
elements, but you can also create serializable bookmarks passing true
to the createBookmarks
method. This kind of bookmarks will keep references to nodes by ids, so you can overwrite entire innerHTML
.
Note: This method is also available in a Range API.
This is the most popular method, because you have the full control over selection and you can change DOM, although you need to take care of bookmarks' spans
.
var bookmarks = editor.getSelection().createBookmarks2();
// Make DOM changes.
editor.getSelection().selectBookmarks( bookmarks );
Note: In this solution we use createBookmarks
2
method.
Here we also create an array of bookmarks objects, but we do not insert any elements into DOM. These bookmarks store their positions by the addresses. Address is an array of ancestors' indexes in their parents.
This solution is very similar to solution 1, but you can overwrite entire innerHTML
, because it (most likely ;>) won't change the addresses of bookmarks' nodes. Although, in such a case you should pass true
to createBookmarks2
to get normalized addresses because adjacent text nodes will be joined and empty ones removed when setting innerHTML
.
... Working with DOM and selection isn't trivial. You need to know what you're doing, you need to know DOM and you need to pick the right solution for your problem. Most often it will be the second one, but it depends on a case.