Archive for the ‘bililiteRange’ Category

It seems like a long time ago that I started updating bililiteRange but it was only 3 months ago. And I think I'm done (though projects are never done!). bililiteRange 3.2 is released, and requires history.js, toolbar.js, jquery.status.js and jquery.keymap.js for all the functionality, though really the core only requires history.js.

The documentation is at github.bililite.com/bililiteRange, with the documentation for the other repositories linked to at github.bililite.com.

I haven't updated bililiteRange for years, but it's time to drop support for Internet Explorer and use modern constructs like Promise and input events. It should make things more simple. From here on in, I will only support Chrome, Firefox and Edge (the chromium-based one; I don't have Edge Legacy. I'm also working on moving all the documentation into the git repositories themselves, as github pages (see github.bililite.com, which is an alias for dwachss.github.io).

I've updated jquery.status.js, jquery.keymap.js, and timer events. The documentation for the timer events and status are done.

bililiteRange.text() works well to insert text into ranges, but I wanted to be able to simulate other keys, ala Microsoft's SendKeys. bililiteRange.sendkeys() does exactly that. It basically executes text(string, 'end') but interprets any text between braces ('{key}') as a command representing a special key. For security reasons, the browser won't let you do anything outside of the text of the page itself, but I've implemented the following (the key names are from the proposed DOM3 standard)::

Backspace
Delete backwards
Delete
Delete forwards
ArrowRight
Move the insertion point to the right
ArrowLeft
Move the insertion point to the left
Enter
Insert a newline, with bililiteRange.insertEOL(). Warning: In contenteditable elements, Enter is flaky and inconsistent across browsers. This is due to the flakiness of contenteditable itself; I can't figure out what to do about this.

For backwards-compatibility with older versions, the following synonyms also work: backspace, del, rightarrow, leftarrow and enter.

So, for example, bililiteRange(el).sendkeys('foo') replaces the current range with 'foo' and sets the range to just after that string. bililiteRange(el).sendkeys('foo{Delete}{ArrowLeft}{ArrowLeft}') replaces the current range with 'foo', removes the character just after that string and sets the range to between the 'f' and the 'o'.

To manipulate the selection, use the usual bililiteRange methods. Thus, to simulate a backspace key, use bililiteRange(el).bounds('selection').sendkeys('{Backspace}').select().
To insert a '{', use an unmatched brace, bililiteRange(el).sendkeys('this is a left brace: {'), or {{}, as in bililiteRange(el).sendkeys('function() {{} whatever }');.

If anyone knows how to implement an up or down arrow, or page up/down, please let me know.

Other Commands

To make life easier for me, there are a few other "keys" that implement specific actions:

selectall
Select the entire field
tab
Insert a '\t' character. $().sendkeys('\t') would work just as well, but there are circumstances when I wanted to avoid having to escape backslashes.
newline
Insert a '\n' character, without the mangling that {enter} does.
selection
Inserts the text of the original selection (useful for creating "wrapping" functions, like "<em>{selection}</em>").
mark
Remembers the current insertion point and restores it after the sendkeys call. Thus "<p>{mark}</p>" inserts <p></p> and leaves the insertion point between the tags.

So to wrap the text of a range in HTML tags, use range.sendkeys('<strong>{selection}</strong>'). To create a hyperlink, use range.sendkeys('<a href="{mark}">{selection}</a>') which leaves the range between the quote marks rather than at the end.

Plugins

Adding new commands is easy. All the commands are in the bililiteRange.sendkeys object, indexed by the name of the command in braces (since that made parsing easier). The commands are of the form function (rng, c, simplechar) where rng is the target bililiteRange, c is the command name (in braces), and simplechar is a function simplechar (range, string) that will insert string into the range. range.data().sendkeysOriginalText is set to the original text of the range, and rng.data().sendkeysBounds is the argument for bililiteRange.bounds() that will be used at the end.

So, for example:

bililiteRange.sendkeys['{tab}'] = function (range, c, simplechar) { simplechar(rng, '\t') };
bililiteRange['{Backspace}'] = function (range, c, simplechar){
  var b = rng.bounds();
  if (b[0] == b[1]) rng.bounds([b[0]-1, b[0]]); // no characters selected; it's just an insertion point. Remove the previous character
  rng.text('', 'end'); // delete the characters and update the selection
};
bililiteRange.sendkeys['{selectall}'] = function (range, c, simplechar) { rng.bounds('all') };

So to have a reverse-string command:

bililiteRange['{reverse}'] = function (range, c, simplechar){
  simplechar(range, range.sendkeysOriginalText.split('').reverse().join(''));
};

Or, to annoy the anti-WordPress crowd, a Hello, Dolly command:

bililiteRange['{dolly}'] = function (range, c, simplechar){
  var lyrics = [
    "Hello, Dolly",
    "Well, hello, Dolly",
    "It's so nice to have you back where you belong",
    "You're lookin' swell, Dolly",
    "I can tell, Dolly",
    "You're still glowin', you're still crowin'",
    "You're still goin' strong"];
  simplechar (range, lyrics[Math.floor(Math.random() * lyrics.length)];
};

Events

After each printing character (not the specials, unless they call simplechar) it triggers a keydown event with event.which, event.keyCode and event.charCode set to the Unicode value of the character.
After the entire string is processed, it triggers a custom sendkeys event with event.which set to the original string.

There have been lots of times that I've wanted to be able to keep my hand on the keyboard when editing, rather than running off to the mouse all the time. There's an implementation of VIM in Javascript but I figured I would learn something by doing it myself. My goal is vi, not vim, since I don't need anything that sophisticated.

The first step is implementing the line-oriented part of vi, called ex, based on the manual from the sourceforge project. My version is based on bililiteRange, and depends on the bililiteRange utilities and undo plugin.

Use it simply as bililiteRange(textarea).ex('%s/foo/bar/');, passing the ex command to the ex() function. The biggest difference from real ex is that this uses javascript regular expressions, rather than the original ex ones. Thus s/\w/x/ rather than s/[:class:]/x/, and use ?/.../ rather than ?...? to search backwards (the question mark is used in Javascript regular expressions so I don't want to use it as a delimiter).

See a demo.

See the code on github.

Continue reading ‘New bililiteRange plugin, ex’ »

Turns out Internet Explorer is even worse than imagined. It's right at the bottom of the Uncanny Valley--close enough to a real browser to make it look like it works, but lots of near-impossible-to-track-down bugs that make life miserable. Turns out that Node.normalize is broken (see the bug reports and a workaround), so I had to add a test in bililiteRange to check for that, and not bother normalizing text if it's broken. Normalization just means merging adjacent text nodes, so losing it makes things less efficient but it should still work. I'm not going to lose sleep over Internet Explorer's inefficiencies.

Well, I ran the test code for bililiteRange and got "132 assertions of 151 passed, 19 failed." Better than none passed, I suppose, but it means I've got some work ahead of me. Or I could just give up on IE, but IE11 is supposed to be standards-compliant, so the errors might actually reflect a problem.

Some of the results are weird: the expected and actual results look identical, so I imagine there's some '\r's rearing their ugly heads, even in IE11. The selection is not being retained on losing focus in input elements; that might be a real problem. And then some tests are failing with a "WrongDocumentError". Never seen that before.

I'll add this to my list of things to get to eventually.

There have been a lot of times I have needed some information for a bililiteRange plugin that was associated with the underlying element, rather than a specific range. For instance, in bililiteRange(element).text('foo') then bililiteRange(element).undo() the undo needs to know about the previous text change. jQuery has a data() method that attaches an object to the element and you can add fields to that object. Actually, it only attaches an index that points to the actual object, since at least in some browsers the garbage collector had trouble with Javascript objects attached to DOM elements and you ended up with memory leaks. I'm not sure if that is still a problem, but it's an easy enough pattern to implement so I used it.

I didn't want to be jQuery-dependent and I wanted to be able to some more sophisticated things with my data, so I implemented my own. At its simplest, just use:

var data = bililiteRange(element).data();
data.foo = 'bar';
assert(bililiteRange(element).data().foo == 'bar');

bililiteRange(element).data() returns an object that you can add fields to and they will be saved across multiple calls to bililiteRange.

Continue reading ‘bililiteRange data’ »

I liked the way Dabblet does autoindenting (entering a new line copies the whitespace from the beginning of the current line, so you keep the same level of indentation). So I added an option to bililiteRange(element).text() to do that. Now bililiteRange(element).text('text to insert', select, true) with true passed as the last option will autoindent. My Prism editor now has a check box to implement that.

The code to do this is in the bililiteRange utilities, not the original code.

I also added two more bililiteRange plugins:

bililiteRange(element).indent(tabs)
Prepends the string tabs to each line that contains part of the range. Thus bililiteRange(element).bounds('selection').indent('\t') to indent by one tab (and if you want spaces, use those instead; I won't get into Holy Wars).
bililiteRange(element).unindent(n, tabSize)
Removes n tab characters or sequences of tabSize spaces from the start of each line.

And, inspired by jQuery data, added a bililiteRange(element).data() that returns an object tied to element that can be used to store any data on that element (not the bililiteRange) without memory leaks. Thus bililiteRange(element).data().tabSize = 4 can be used in future calls: assert(bililiteRange(element).data().tabSize == 4). In fact, unindent above does exactly that if tabSize is not passed in.

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.

I've been looking at the DOM 3 event model and decided that the bililiteRange.text() andbililiteRange.select methods ought to implement those, even though they're not in browsers yet. Nobody implements beforeinput, or event.data in the input event handler, and select only works in <input> and <textarea> (not in contenteditable). And that's in modern browsers.

But that's no excuse for me. beforeinput is now dispatched before changing the text, and input after, with event.data set to the text to be inserted, and event.bounds set to the bounds array in the original text (this last is not part of the spec, but it's hard to work with changing text without it). This is an incompatible change from the way it used to work (with event.detail being set to {text: text, bounds: bounds}), but I don't think that many people are using that aspect of the library.

I also moved a few methods from the utilities library into the main code, since I used them so much.

It doesn't look like my workplace is ever going to get past IE8 (many of the machines are still on Windows XP!), so I really want to support those. There are polyfills for getting the event listeners to work, and I incorporated a quick and cheap version of that into the code, and event listenders to dispatch input events on keystrokes, drag and drop, and cut/paste.

bililiteRange is now at version 2.0.

I will be forever grateful when I can drop IE8.