Edited 2014-02-27 to add the indentation code.

I created a collection of useful plugins for my bililiteRange, for more sophisticated manipulations. Now you can search the element with a regular expression, and you can keep the range "live", adjusting it when the text is edited.

Download the code.

See the demo. (The dynamic highlighting is off by a few pixels in iOS Safari. I'm still working on that)

The Methods

bounds ('startbounds' | 'endbounds' | 'line' | 'BOL' | 'EOL' )
(Other uses of bounds remain unchanged). Modifies the bounds of the range. bounds('startbounds') and bounds('endbounds') collapse the range to its boundaries.
'line' changes the range to encompass the entire line that contains the start of the existing range. A line is defined as /^.*$/m; that is, they are delimited by newlines. This works well for <textarea>s but <div contenteditable>s are flaky and inconsistent between browsers about whether newlines are added or <br>s or new <div>s are created. So use at your own risk.
bounds('BOL') and bounds('EOL') mean "beginning of line" and "end of line"; they move the bounds to the start or end of the line containing the start of the range; they act like rng.bounds('line').bounds('startbounds' or 'endbounds').
find (re {Regular Expression}[, nowrap {Boolean} [, backwards {Boolean}]])
Search for the next occurence of re in the entire text of the element from the start of the current bounds until the end and set the bounds to the found string. If nowrap is true, then the search will fail if it not found; otherwise the search continues from the start of the element. If backwards is true, then the search goes backwards from the start of the current bounds to the start of the element, possibly wrapping around to the end of the element.
If re is not found, the bounds remain the same. If the found string will never be identical to the current bounds; that allows for a "Find Again" functionality; range.find(/foo/).find(/foo/) finds the second occurrence of foo.

range.match is set to the result of re.exec, so you can test for success or failure and get the captured groups. If you use emailRE = /(\S+)@(\w+\.\w+)/ as a primitive email matcher, then
for (rng = bililiteRange(element).find(emailRE); rng.match; rng.find(emailRE, true)){
  alert('User: '+rng.match[1]+' Server: '+rng.match[2]);
  rng.bounds('endbounds');
}
will find all email addresses and display them.
findBack (re {Regular Expression}[, nowrap {Boolean}])
Just a shorthand for find(re, nowrap, true).
line()
Return the line number (1-indexed!) of the line that contains the start of the range. Lines are defined as for bounds('line') above.
line(n)
Set the range to line n, 1-indexed. It tries to emulate a "down arrow" by setting the range to the character position on the new line corresponding to the character position of the end of the current bounds. line(n).bounds('line') will select the entire line. line(0) is the same as bounds([0,0]) and if n is past the end of the text, it is the same as bounds('all').bounds('endbounds').
live([{Boolean}])
range.live() or range.live(true) makes the range live; it listens for input events and attempts to update the bounds of the range so it still refers to the same text, they way bookmarks do in Microsoft Word. Unfortunately, input events do not include any information about what was changed, so the function compares the old and the new text and tries to figure out what was added. Usually that's straightforward, but pasting text that already exists may confuse it. Inserted text within or at the borders of a range expand the range; deleted text that includes part of the range shrinks it. If all the text of a range is deleted, the range is set to a zero-length range in at the start of the inserted text (ranges are never deleted).
Since it relies on input events and addEventListener it will fail in older versions of IE.

Indentation

Some of the plugins add indentation (adding tabs or spaces at the beginning of a line). Whether you should use tabs or spaces is one of those geek religious wars, and this package is agnostic.

text(String, String, Boolean)
If the last Boolean is true, then auto-indent the inserted String: copy this.indentation() after every newline character in the inserted text. The other uses of text() and the other parameters remain the same.
indentation()
Returns the whitespace (anything that matches /\s*/) at the beginning of the line that contains the start of the range. Does not include any newline.
indent(String)
Inserts the String at the beginning of every line of the range (including the line that the range starts on, even if the range does not include the start of the line).
unindent(n {Number}, tabSize {Number})
Removes n "tabs" from the beginning of every line of the range (including the line that the range starts on, even if the range does not include the start of the line). A "tab" is either a '\t' or a sequence of tabSize spaces. If tabSize is not set, uses this.data().tabSize || 8.

For a demo of autoindenting, see the Prism editor.

A Note on the Demo

To illustrate the location of each of the ranges, I needed some way to highlight or mark up a textarea. Which is of course impossible. But someone with a screen name of Trinithis created an awesome hack (explained in a ddforum post) that uses absolutely positioned <pre> elements behind the textarea, with the textarea itself having a transparent background. I ended up needing to use a timer for the redrawing, which is inelegant, but works in most browsers and avoids issues of synchronicity when listening for events. I packaged this up into a jQuery plugin, though it's still pretty flaky (as in, doesn't handle text wrapping well).

One Comment

  1. Hacking at 0300 : bililiteRange data says:

    […] bililiteRange utilities redefine text() to allow "autoindenting" with a third parameter. We can implement this in an […]

Leave a Reply


Warning: Undefined variable $user_ID in /home/public/blog/wp-content/themes/evanescence/comments.php on line 75