Background...I'm building a content management type website and wanted to include a nice WYSIWYG editor. Unfortunately all the editors I tested are either too unpredictable and erratic, cost a fortune, or are not customizable enough for my needs (out of the box anyways).
In the end I just didn't want to deal with the awful (x)html that they produce. I also didn't want to deal with my end users, whom have never heard of HTML and won't want to "View Source" to fix the HTML when the WYSIWYG fails.
So, I decided to build my own. After a smidgen of research and testing I decided not to build my own. Now I'm going for a Wiki style code editor with a toolbar. With this I can generate clean consistent simple markup. It's not WYSIWYG but at least it will be consistent and predictable.
I quickly found that I'd need to be able to retrieve and adjust the current text selection in the textarea control. So, after not finding any satisfactory solutions via Google I've come up with my own.
Textarea Cursor Position in IEFirst off I'd like to give credit to the sites that provided good tips, suggestions, and solutions.
- parentNode - The best written and closest solution I found.
- Mishoo - A good idea but the comments were more helpful
- Scott Van Vliet - Also a very interesting approach. The comments were also helpful
- Thanks to WikkaWiki which has a good solution for their text editor.
- MSDN Library
Summary of the ImplementationMy solution works like this:
- Make 2 TextRanges.
- The first TextRange selects from the beginning of the textarea to the beginning of the selected text.
- The second TextRange is a duplicate of the selection.
- Restore missing new line characters in both TextRanges
- The TextRange.text property right trims all the \r\n characters from the selection. We must restore the new lines to calculate the correct cursor position.
- Calculate the start position which is the untrimmed length of the 1st TextRange.text property.
- Calculate the end position which is the untrimmed length of the 2nd TextRange.text property plus the start position.
- Calculate the complete untrimmed selection using textarea.value.substring(startPoint, endPoint).
Further Explanation and RepetitionInternet Explorer makes this whole process very difficult. The biggest issues are:
- It treats \r\n as one character when working with the TextRanges
- It trims \r\n from the end of the TextRange.text so you don't know the real selection length.
My solution discovers the correct index of the start and end positions of the selection which can't be done by just using the
TextRange.text.length. Once you have the positions you can use
textarea.value.substring(start,end) to get the actual selected text rather than the trimmed
TextRange.text property.
All the string methods (
substring,
indexOf,
length, etc.) treat
\r\n as 2 characters, however, the TextRange methods (
moveStart,
moveEnd, etc.) treat
\r\n as 1 character. So just moving the TextRange until we reach the beginning and counting how many times we moved will be off by exactly the number of new lines passed along the way.
A Word About Changing the Selection
The
TextRange.text \r\n trim issue also causes problems when attempting to change the selection's position. You need to count the newlines between the beginning and the target index and then subtract count from the target index. Then you can use this adjusted index with the
TextRange.moveStart and
TextRange.moveEnd methods to get the correct selection.
The CodeIn my code I use 3 TextRanges (instead of 2 as described above) so that I can re-assemble the entire string and compare it to the original
textarea.value. I do this to make sure I performed the untrimming correctly.
You will also notice that I only use a single
do..while loop to untrim all the TextRanges. I could have done this with 3 separate loops. Even better, I probably should move the untrim bit to a separate function. I purposely made these design decisions but it could be done differently.
Also note that this bit of code was
only tested in IE 6.0. Please don't complain because it doesn't work in Opera, Firefox, Safari or Lynx.
var textarea = document.getElementById("myTextArea");
textarea.focus();
var selection_range = document.selection.createRange().duplicate();
if (selection_range.parentElement() == textarea) { // Check that the selection is actually in our textarea
// Create three ranges, one containing all the text before the selection,
// one containing all the text in the selection (this already exists), and one containing all
// the text after the selection.
var before_range = document.body.createTextRange();
before_range.moveToElementText(textarea); // Selects all the text
before_range.setEndPoint("EndToStart", selection_range); // Moves the end where we need it
var after_range = document.body.createTextRange();
after_range.moveToElementText(textarea); // Selects all the text
after_range.setEndPoint("StartToEnd", selection_range); // Moves the start where we need it
var before_finished = false, selection_finished = false, after_finished = false;
var before_text, untrimmed_before_text, selection_text, untrimmed_selection_text, after_text, untrimmed_after_text;
// Load the text values we need to compare
before_text = untrimmed_before_text = before_range.text;
selection_text = untrimmed_selection_text = selection_range.text;
after_text = untrimmed_after_text = after_range.text;
// Check each range for trimmed newlines by shrinking the range by 1 character and seeing
// if the text property has changed. If it has not changed then we know that IE has trimmed
// a \r\n from the end.
do {
if (!before_finished) {
if (before_range.compareEndPoints("StartToEnd", before_range) == 0) {
before_finished = true;
} else {
before_range.moveEnd("character", -1)
if (before_range.text == before_text) {
untrimmed_before_text += "\r\n";
} else {
before_finished = true;
}
}
}
if (!selection_finished) {
if (selection_range.compareEndPoints("StartToEnd", selection_range) == 0) {
selection_finished = true;
} else {
selection_range.moveEnd("character", -1)
if (selection_range.text == selection_text) {
untrimmed_selection_text += "\r\n";
} else {
selection_finished = true;
}
}
}
if (!after_finished) {
if (after_range.compareEndPoints("StartToEnd", after_range) == 0) {
after_finished = true;
} else {
after_range.moveEnd("character", -1)
if (after_range.text == after_text) {
untrimmed_after_text += "\r\n";
} else {
after_finished = true;
}
}
}
} while ((!before_finished || !selection_finished || !after_finished));
// Untrimmed success test to make sure our results match what is actually in the textarea
// This can be removed once you're confident it's working correctly
var untrimmed_text = untrimmed_before_text + untrimmed_selection_text + untrimmed_after_text;
var untrimmed_successful = false;
if (textarea.value == untrimmed_text) {
untrimmed_successful = true;
}
// ** END Untrimmed success test
var startPoint = untrimmed_before_text.length;
var endPoint = startPoint + untrimmed_selection_text.length;
var selected_text = untrimmed_selection_text;
alert("Start Index: " + startPoint + "\nEnd Index: " + endPoint + "\nSelected Text\n'" + selected_text + "'");
}