19

I'd like to track the movement of the caret/cursor in a contenteditable. I'm not sure what's the best way to do this, though.

I'm currently listening for click, keydown, keyup. (keypress of course doesn't even fire for things like arrow keys or ctrl-x.)

While click works fine, the problem with keydown is that it's fired before the caret actually moves, so when I query the current document selection range, I get the old position and not the new one. But if I rely on keyup to get the updated position, it fires too late: the caret moves as soon as the key is pressed down, but the key is released an arbitrary time later.

This must be possible because things like CKeditor are able to do this. Any hints?

Yang
  • 16,037
  • 15
  • 100
  • 142
  • That's odd, it's exactly what I was working on today! – MaxArt Jul 11 '12 at 22:34
  • @MaxArt Then you should upvote/star the question :) – Yang Jul 11 '12 at 22:40
  • You're right, I did it right away. I'm answering with my results, by the way. – MaxArt Jul 11 '12 at 22:41
  • Why do you think keyup may be too late? What could happen in between? – Bergi Jul 11 '12 at 23:29
  • @Bergi Too late as in the caret moves as soon as the key is pressed down - the key is released an arbitrary time later. Clarified my question. – Yang Jul 12 '12 at 02:20
  • I'd say you could put this down as impossible to do. Tell us why you want to and maybe we can suggest alternative strategies. – Morgan T. Jul 12 '12 at 18:27
  • @MorganTiley I'm trying to make an editor that tracks the caret / currently focused node so that it can update various controls on the screen, e.g. a bounding box (with various buttons/controls/handles) surrounding the focused node, a status bar showing the currently focused node's path from the root ("body > div > p"), the various toolbar button states ("bold" is depressed), etc. How do you think CKeditor manages to pull this off? – Yang Jul 12 '12 at 19:44
  • I've been working with Telerik RadEditor and they basically handle keyup event (along with others). It doesn't update as quickly as CKEditor e.g. hold down left arrow and going through bolded text it doesn't update until keyup but I'd say it's not that important. CK probably does it with a timer which if could be a very simple way of doing it. Just use Rangy (Tim Down's lib), get the Selection object, the Range object from that, and then the nodes from that to inspect. – Morgan T. Jul 13 '12 at 15:48
  • @MorganTiley Yeah, timer sounds like the way to go. Thanks to all the great answers here! – Yang Jul 13 '12 at 23:48

4 Answers4

10

It's nice to read that people are talking about CKEditor :). I'm one of its developers and I haven't been working much on selection, but I'll try to help.

What I know is that we've got an internal selectionChange event. So when do we check if it changed? ... ... At least once per every 200ms :) See:

http://dev.ckeditor.com/browser/CKEditor/trunk/_source/plugins/selection/plugin.js#L39

We also check selection every time we know that it could have been changed (e.g. by the API or on keyup/mouseup or on native selectionchange - http://dev.ckeditor.com/browser/CKEditor/trunk/_source/plugins/selection/plugin.js#L554). So... pretty much all the time :) AFAIK we do some tricks, so it doesn't burn your CPU, but it's still heavy. However, if we did this, then it's the only possible way to have this working so nicely.

Unfortunately, selection handling is by far the worst task in wysiwygs world. It's so broken in all - old and new browsers. More than 75% LOC in the file I linked above are hacks and tricks. That's complete madness.

Reinmar
  • 21,729
  • 4
  • 67
  • 78
5

In Mozilla and Opera, the nasty business of handling key and mouse events is your only option. Not only is it fiddly, it also doesn't cover every case: it's possible to change the selection via the edit and context menus (via Select All, for example). To cover that, you'd also need to add some kind of polling of the selection object.

However, in IE (all the way back to at least 5.5) and recent-ish WebKit, there is a selectionchange event that fires on the document whenever the selection changes.

document.onselectionchange = function() {
    alert("Selection changed!");
};

There is a chance that Mozilla will support it in the future: https://bugzilla.mozilla.org/show_bug.cgi?id=571294

Tim Down
  • 318,141
  • 75
  • 454
  • 536
0

It's not an easy task for the reasons you said. I came up with some kludge like this:

var caretInterval, caretOffset;
document.addEventListener("keydown", function(e) {
    if (!e.target.contentEditable || caretInterval) return;
    if (e.keyCode !== 37 && e.keyCode !== 39) // Left and right
        return;
    var sel = getSelection();
    caretInterval = setInterval(function() {
        if (sel.type === "Caret") caretOffset = sel.baseOffset;
    }, 50);
});
document.addEventListener("keyup", function(e) {
    if (e.keyCode !== 37 && e.keyCode !== 39) // Left and right
        return;
    clearInterval(caretInterval);
    caretInverval = null;
    var sel = getSelection();
    if (sel.type === "Caret") caretOffset = sel.baseOffset;
});

There could be a small problem if someone tries to press left and right at the same time. For ctrl-X and ctrl-V, you should catch the cut and paste event, and that's actually another pain in the bollocks.

In the end, I decided it wasn't worth the effort for my purposes. Maybe you have different needs.

MaxArt
  • 22,200
  • 10
  • 82
  • 81
0

WRT catching the event after the selection is updated: I simply wrap my handler functions in timeouts:

editor.onkeydown = function() {
  window.setTimeout( function(){
    // Your handler code here
  }, 0 );
};

This registers your handler to be executed in the browser's event loop as soon as possible, but after the current (eg click) event is processed. But be aware of possible races if you have other scripts modifying the content; there is no guarantee that your timeout is the next in line to be run.

Siemen
  • 101