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.
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')
andbounds('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')
andbounds('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 likerng.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
will find all email addresses and display them.for (rng = bililiteRange(element).find(emailRE); rng.match; rng.find(emailRE, true)){ alert('User: '+rng.match[1]+' Server: '+rng.match[2]); rng.bounds('endbounds'); }
- 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
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 asbounds([0,0])
and ifn
is past the end of the text, it is the same asbounds('all').bounds('endbounds')
. live([{Boolean}])
range.live()
orrange.live(true)
makes the range live; it listens forinput
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 andaddEventListener
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
istrue
, then auto-indent the insertedString
: copythis.indentation()
after every newline character in the inserted text. The other uses oftext()
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 oftabSize
spaces. IftabSize
is not set, usesthis.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).
Hacking at 0300 : bililiteRange data says:
[…] bililiteRange utilities redefine text() to allow "autoindenting" with a third parameter. We can implement this in an […]
February 26, 2014, 5:05 pm