One of those ongoing unsolved problems with web apps is how to keep track of the selection or insertion point in an editing element, when the focus has moved onto some other element. For instance, in a rich-text editor, you want to be able to select a word, then click the "bold" button and make that word bold. But once you click the button, the original word is no longer selected!
One solution that is used by Yahoo mail (and I believe Gmail as well) is to put the editing element in its own <iframe>
. That acts as a separate window, and the selection is kept even when that "window" is no longer active. You can then get the selection with window.getSelection()
and the like (look at the source for bililiteRange._nativeGetSelection()
for the variations on that in different browsers and elements). That's a perfectly good solution, but I would like to be able to use any element on the current page.
There are two related issues we need to deal with:
- We want to know what the selection was when the element was active, after the element loses focus.
- We want to be able to restore the selection when focus returns to the element.
In each case, we have to deal with standards-based browsers in input and textarea elements (which behave identically) and other elements, and with Internet Explorer 8 ("we" here means just me, unfortunately).
Finding the selection
For input elements, that's easy. The browsers maintains element.selectionStart
and element.selectionEnd
. For the others, we have to somehow catch the element before it loses focus and grab the selection then. Internet Explorer actually does this right; it fires a beforedeactivate
event, so we can use element.attachEvent('onbeforedeactivate', function() { saveSelection(document.selection) }
. I don't like IE's nonstandard nomenclature, but at least the event exists.
In standard browsers, there's nothing. The blur
event fires after the focus is lost, so the selection is lost. I can't find anything else to do but listen to every user interaction and record the selection each time, and remember to programmatically save the selection whenever it is changed in code: element.addEventListener('mouseup', function() { saveSelection(window.getSelection() })
and similarly for keyup
. I'm pretty sure that those are the only two user interactions that browsers fire; all the others, like cut
or drop
, also fire one of those. It's inefficient to have to check the selection so often, but I don't see another option.
Restoring the focus
With the selection in hand, we can manipulate the text as desired, but when we return the focus to the text, we would like the selection to be in the right place. It's easy enough to do element.addEventListener('focus', function() {window.getSelection().addRange (savedSelection)})
or the equivalent in the other use cases, but that sets the selection even when we don't want it to.
There are three ways to put the focus in an element: click in it, tab into it, and in script with element.focus()
. With a mouse click, we want the selection to be where the user clicked, not where it used to be. The effect of tabbing into an element varies (see below), but I would like the selection to be restored. So the only question is whether the focus
event happened as the result of a click or not.
Unfortunately, the focus
event happens before the mouseup
or click
events, and the change of the selection happens before that (even for input events that record their own selections), so I can't listen for mouseup
. The only solution I found was to check whether the mouse button is down at the time of the focus
event:
element.addEventListener('focus', function() {
if (mouseButtonIsDown) window.getSelection().addRange (savedSelection)
})
But how to get mouseButtonIsDown
? There is no way in Javascript in modern browsers. It's a hack, but I had to track mousedown
and mouseup
:
mouseButtonIsDown= false;
document.addEventListener ('mousedown', function() {
mouseButtonIsDown = true;
});
document.addEventListener ('mouseup', function() {
mouseButtonIsDown = false;
});
Ugly, and fails in all sorts of edge cases when the user hits more than one mouse button or drags from one window to the next. But it's not critical; the user gets immediate feedback where the selection/insertion point is.
The other subtlety is tabbing in; I'd like to restore the selection rather than use the browser's default of selecting the whole thing or placing the insertion point at the beginning. With the focus
event listener above, there's a flash as the insertion point goes to the default position then moves to where I want it. I don't know how to avoid that.
Conclusion
Putting the pieces together, I can keep the selection in the right place even with other elements programmatically manipulating it. See the source code for bililiteRange.select()
and bililiteRange.bounds('selection')
, along with all the event listeners in the bililiteRange
constructor.
To watch it in action, see the sendkeys
demo. Enter text in each box, then click or tab between them or click the radio buttons above them to change the focus with Javascript.
Postscript
Browsers vary in how they handle focussing in response to a tab key or an element.focus()
. I tested it on my Windows XP machine:
<input> |
<textarea> |
<div contenteditable> |
|
Firefox tab | all | saved | start |
Firefox focus() |
saved | saved | start |
Chrome tab | all | start | start |
Chrome focus() |
saved | saved | start |
IE 8 tab | all | end | start |
IE 8 focus() |
start | start | start |
"All" means the entire text is selected, "start" means the insertion point is placed before the first character, and "saved" means that the selection is restored to where it was when the element lost focus.