Archive for January, 2014

I've finally decided to join the 21st century and use automated testing on my bililiteRange code; I'm using QUnit, with tests on github. I want to bililiteRange test on all three types of editable elements, <input type=text>, <textarea>, and <div contenteditable>, so I created a convenience function to run each test on each type of element, with

var elements = ['div', 'textarea', 'input'];
function multitest (message, fn){
  elements.forEach( function(name){
    test(message, function(){
      var $el = $('<'+name'>').attr('contenteditable','').appendTo('#qunit-fixture');
      fn($el); // the testing function
    });
  });
}

And the testing code calls multitest rather than test.

I feel so modern; maybe I'll start complaining about waterfalls next.

Playing with the kavanot editor, I wanted some way to encapsulate the process of noting that the text is "dirty"—meaning has been changed but not yet saved, and using a Promise to express the idea that the text is soon-to-be-saved, then saved.

I created a plugin that simply represents a state machine with four states, clean, dirty, pending, and failed. So you can do

var monitor = $.savemonitor();
$('textarea').on('input', function() { monitor.dirty() }); // track when the text area changes
// call this function to save (say, as a click handler for a Save button
function save(){
  var text = $('textarea').val();
  monitor.clean($.post('saveit.php', {data: text})); // jQuery Deferred's like $.post can be cast to Promises
}
// and keep track of the status
monitor.on('clean', function() {console.log('saved')})
monitor.on('failed', function() {console.log('not saved')})

See the code on github.

See a simple demo.

See a demo that uses FontAwesome icons and randomly fails to simulate a network failure.

Continue reading ‘New jQuery plugin, savemonitor’ »

I use Range and Selection extensively in bililiteRange in regular elements in standards-based browsers. I initially implemented scrolling a range by inserting an empty element, using scrollIntoView on that, then deleting the element.

But it turns out adding and removing elements messes up the selection. So I had to change it to manipulate scrollTop directly.

Continue reading ‘Odd bug with document ranges and selections’ »

With my status plugin, I'd generally like the input element to take up the whole line, or at least the part that's not taken up by the prompt and by other messages. Evidently, that's a common concern, with a pure CSS solution that relies on overflow: hidden to create a new block formatting context that makes width: 100% mean "100% of the space not taken up by floats".

So the solution is to make sure that everything that is not the input element is floated, and that the input element is wrapped with an element that is display: block and overflow: hidden, and the input element is width: 100% (and box-sizing: border-box to make sure the border doesn't overflow).

It's unfortunate that we need the non-semantic wrapper to set the size that the input can be 100% of, but I don't see another option. It also requires that the floated elements come before the input element in the DOM, so my status plugin has to use prependTo for messages and appendTo for the input element.

As an example (the text is all editable):


<div>
  <span style="float: left" contenteditable>Prompt String:</span>
  <span style="float: right; color: red" contenteditable>Important Message</span>
  <span style="overflow: hidden; display: block; padding: 0 1em"> <!-- the padding isn't essential; it's just decorative -->
    <input style="width: 100%; box-sizing: border-box; -moz-box-sizing: border-box"/>
  </span>
</div>

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:

  1. We want to know what the selection was when the element was active, after the element loses focus.
  2. 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.