This should work on most current browsers. Tested with Safari v2.0.3.
BACKGROUND
One of the issues with browser compatibility is in how the HTML TEXTAREA behaves within Safari. I will cover that topic later in detail.
An odd behavior with Apple's Safari browser is the way the HTML tag TEXTAREA deals with text that overflows the TEXTAREA's visible box. The issue is that when the TEXTAREA has text that overflows, it does not display the scroll bars until two lines of text have overflowed. Thus the first line of text is unaccessible or visible when typing at the end of the text, or the last line when typing any where else when the overflow condition first occurs. This can lead to text that is not viewable by the users, or at least difficult to view. This even manifests itself when displaying the data for the first time to the user: if the data overflows by one line, it will not display the scroll bars and the user will not be able to view that last line of data.
One solution to this problem is to have the users click within the TEXTAREA and either move the cursor with the arrow keys, or drag the cursor with the mouse to force the TEXTAREA to scroll the text manually even without the scroll bars. As you can guess, this would not be viable solution that the users would even accept (or at least lets hope the users won't accept this kind of a solution!).
Where this can become an issue, even if the scroll bars function as expected, is if the users are printing the contents of a page that makes use of a TEXTAREA with overflowing text. All the text will not be printed.
THE CODE - First the Basics
There are three parts to the code. The first is the JavaScript that performs the adjustment in the height. The second is putting the hooks within the TEXTAREA, with the last being the general call to preset the TEXTAREAs when the page is initially loaded.
First lets start off with the primary JavaScript. This should be placed within the document's header section.
This function updateTextareaHeight() will take the contents of the TEXTAREA that is being passed to this function and calculate the "theoretical" number of rows it should have. If the TEXTAREA element is displaying the text in a fixed width font, then it would be fairly accurate in the length, but if it is using a variable width font, then the number of rows may be overstated, but all text should be displayed.
The way it works is by counting not only the number of linefeeds (new lines), but it also takes (or tries to) in to consideration wrapping text, which becomes the extraRows value. The way it determines if text may wrap in this example, is to take to total length of the text in the TEXT area and divides it by the number of columns the TEXTAREA is configured for. If the sum of the extraRows and linefeed count does not match was the current value is for the TEXTAREA's rows attribute, then it is updated.
Please keep in mind that this is very simplistic in nature and will not perform well under all conditions. Matter of fact it can be broken quite easily. One technique that works well, is by using very large word sizes such that there are many word wraps with large amounts of white space to the right of the words, but very few, if any, linefeeds are used. This first example just illustrates what can be done in terms of simplicity.
<script>
/*
* Copyright 2006 by Scott Tabar
* Usage is granted as long as this credit remains in place.
* If used in a production/commerical product, then please drop
* me a line at http://scott-tabar-safari.blogspot.com
*/
function updateTextareaHeight(obj)
{
if ( obj == null ) {
return false;
}
// NOTE: This is using regular expressions to count spaces and linefeeds
var objText = obj.value;
var linefeedsArray = objText.match(/[\n\r]/g);
var linefeeds = ( linefeedsArray == null ? 0 : linefeedsArray.length);
var extraRows = Math.round( objText.length / obj.cols );
var newRows = linefeeds + extraRows;
if ( newRows != obj.rows ) {
obj.rows=newRows;
}
}
</script>
An example of how the JavaScript would be used within the TEXTAREA tag is as follows:
<textarea cols="20" rows="4" onkeyup="updateTextareaHeight(this);"
id="ta01">Some text goes here!</textarea>
To precondition the resizing of the text area, you can either attach a JavaScript function to the BODY's onload event which will get called when the page is done being loaded. Or just place some JavaScript in-line with the document, but at the end after all referenced TEXTAREAs. This will resize them while the page is still loading (ie... loading images). NOTE: Many TEXTAREAs can be pre-sized this way.
<script>
{
updateTextareaHeight( document.getElementById( "ta01" ) );
}
</script>
That is the basics on what mechanisms need to be in place to auto size a TEXTAREA. See the next section on discussion about how to improve the sizing to eliminate overflow conditions or large amount of white space at the bottom of the text.
EXAMPLE - More Complex But More Accurate
This example combines all of the three code fragments listed above. To reset the example, just reload the page and start anew.
NOTE: Sorry... This blog will not support the use of the script tag so I can not in-line an example. So until I can find an alternative way of presenting it, you can just copy and paste the following into a new text document and open with your browser of choice.
If you done any playing around with the example above, you will be quick to find that it does not work too well. The concept may work great, but trying to guess what the user has typed in and how the browser is displaying the information is yet another thing altogether. The reason why this is so difficult, is that the TEXTAREA object does not report the actualRows that the text actually occupies. if it did, then this task would be quite simple.
Why is it so difficult to predict how many rows of text there will be? The answer lies in the complexity that is given by modern browsers: you can configure or change just about any aspect of how the TEXTAREA is displayed or how the font is rendered. You no longer are limited to just the plain font that the browser defaults to, but you can specify a specific font (and numerous backups if that one does not exist on the client's computer), font size, the font weight, kerning, tracking, horizontal and vertical spacing, font stretching, margins, and etc... Matter of fact, you can even write a JavaScript function that can take one character at a time and perform progressive or random changes so that a paragraph could contain a rainbow of colors, or each character could be a different size and font! So to put it mildly, the sky is the limit on how we can present the text in a TEXTAREA. Don't forget word wrapping in the TEXTAREA for that may be out of our control (aside from hinting that can be performed).
In the following example, I have added a few more matrices to try and better tune the control of how the TEXTAREA is sized. There are a few areas that I have tried to quantify to see if better control can be attainable. Next is a list of these measures and how they are "supposed" to help! ;-)
1. fontFamilyAdj - This is a very simplistic implementation. It assumes that the Courier font is the only mono-spaced font you will be using, and all other fonts will be proportionally spaced. This does fit our example so keep it in mind if you need to expand upon it. It assumes that for a monospaced font, that the weight should be greater than 1.0 for less characters can fit on a line in a TEXTAREA compared to a proportionally spaced font. For example, enter all lowercase i's. Conversely though, try entering all uppercase W's too. The point being, on average, more characters will fit per line with proportionally spaced characters.
2. linfeeds - Since we are using RegularExpressions to count the linefeeds and also the spaces and large words, there exists the strong chance that none exists, and as such a null will be returned. So in the cases were we are needing to count the elements of the result array to get our numbers, there is used an intermediate variable to hold such a value. This prevents the need to perform the Regular Expression twice: first to see if there are any results, second to get the count of the results. Anyway for linefeeds, we are only counting newlines and carriage returns. We are not interested in vertical tabs, form feeds, or others.
3. spaces - Counting the number of spaces can give us the average word size. We can also use the spaces and linefeeds when the density get very low to improve the accuracy, but in this example, it is not implemented as such (ie... low density conditions with a high ratio of linefeeds + spaces implies that there is really nothing to the document except for the linefeeds so using just the linefeeds may be the most accurate result).
4. avgWordSize - This really is not being used in the example below. Perhaps in combination with largeWordSize and adjColumns you can find an equation that would estimate the amount amount of white space that is caused by word wrapping. This could improve the estimation of the number of rows quite considerably.
5. adjColumns - Uses the fontFamilyAdj value to modify the column size to more accurately express how many characters really does fit on one line in a TEXTAREA.
6. largeWordSize - This is the smallest size for a large word, which can cause a large waste of white space if a large word is actually wrapped. This is the factor that tries to keep the TEXTAREA from overflowing if many big words are used.
7. largeWordCount - The number of large words that exist in the document.
8. density - Takes the adjColumns and multiplies it by the number of linefeeds and the extraRows (as calculated up to that point) to arrive at a theoretical number of characters that can fit in the TEXTAREA. It then uses that number as the divisor for the total character count of the text. The density, when very high indicates there are less linefeeds or other conditions involved and that the number of rows can be a few less. When the density appears to fall between .2 and .7, then there tends to be a larger number of linefeeds and/or larger words which can increase the likelihood of word wrapping and as such, more lines are needed to prevent an overflow condition. The value of linefeeds are also backed off a little under these conditions too. But when the density is less than .2, then you will find a high number of linefeeds.
9. newRows - is the combined total of the linefeeds and the final value of extraRows. Only if this value changes will the TEXTAREA.rows be modified.
One thing to note, is that currently, this code is not perfect. It behaves much better for medium to large amounts of text, but if you start typing words it will start resizing it self all over the place. Yes it does need some fine tuning, but the point I was wanting to make is that it is NOT an easy task and that there are many variables involved. The addition of
&& objText.length > adjColumns && linefeeds > 0to the density check really adds a fair amount of stability to keep the box from jumping. Try removing it to see what happens.
<html>
<head>
<script>
/*
* Copyright 2006 by Scott Tabar
* Usage is granted as long as this credit remains in place.
* If used in a production/commerical product, then please drop
* me a line at http://scott-tabar-safari.blogspot.com
*/
function updateTextareaHeight(obj)
{
if ( obj == null ) {
return false;
}
// NOTE: This is using regular expressions to count spaces and linefeeds
var objText = obj.value;
var fontFamilyAdj = obj.style.fontFamily == "Courier" ? 1.1 : .9;
var linefeedsArray = objText.match(/[\n\r]/g);
var linefeeds = ( linefeedsArray == null ? 0 : linefeedsArray.length);
var spacesArray = objText.match(/[ \t]/g);
var spaces = ( spacesArray == null ? 1 : spacesArray.length + 1);
var avgWordSize = Math.round( obj.value.length / spaces + .5);
// A large word is greater than 35% of the adj cols
var adjColumns = Math.round(obj.cols / fontFamilyAdj);
var largeWordSize = Math.round( adjColumns * .35 );
var regExpStr = "\\w{" + largeWordSize + ",}";
var largeWordArray = objText.match( new RegExp( regExpStr, "g" ));
var largeWordCount = (largeWordArray == null ? 0 : largeWordArray.length);
var extraRows = Math.round( obj.value.length / adjColumns) + 1;
extraRows += Math.round( largeWordCount * .5 );
// Density tries to measure large white space vs ideal fill
var density = Math.round( objText.length / (adjColumns *
(linefeeds + extraRows)) * 100 ) / 100;
if ( density < .7 && density > .2 &&
objText.length > adjColumns && linefeeds > 0 ) {
extraRows += Math.round( fontFamilyAdj / density + .5);
extraRows -= Math.round( linefeeds * (linefeeds < 20 ? .2 : .1) );
}
var newRows = linefeeds + extraRows;
var debugStr = "TEXTAREA id=" + obj.id + " newRows= " + newRows +
" linefeeds=" + linefeeds + " extraRows=" + extraRows +
" density=" + density +
" fontFamilyAdj=" + fontFamilyAdj +
" spaces=" + spaces +
" avgWordSize=" + avgWordSize + " cols=" + obj.cols +
" adjColumns=" + adjColumns +
" largeWordsize=" + largeWordSize +
" largeWordCount=" + largeWordCount;
document.getElementById( "debugText" ).innerText = debugStr;
if ( newRows != obj.rows ) {
obj.rows=newRows;
}
}</script>
</head>
<body>
<form>
<h1>>Dynamically Sized TEXTAREA</h1>
<textarea cols="20" rows="4" onkeyup="updateTextareaHeight(this);" id="textarea01" style="font-family:Times;">This is an example of a Proportional Font! Some text goes here! I have taken the liberty to add a little more text for this example to show how the TEXTAREA can be dyanmically adjusted in height when the page is initially loaded. Please remember from the example above that the original TEXTAREA was only 20 columns wide (and still is) but was only 4 rows high. There should be no scroll bars!</textarea>
<textarea cols="20" rows="4" onkeyup="updateTextareaHeight(this);" id="textarea02" style="font-family:Courier;">This is an example of a Fixed Width Font! Some text goes here! I have taken the liberty to add a little more text for this example to show how the TEXTAREA can be dyanmically adjusted in height when the page is initially loaded. Please remember from the example above that the original TEXTAREA was only 20 columns wide (and still is) but was only 4 rows high. There should be no scroll bars!</textarea>
<br /><br />
<b>Debug Information</b>
<div id="debugText" />
</form>
<script>
{
updateTextareaHeight( document.getElementById( "textarea01" ) );
updateTextareaHeight( document.getElementById( "textarea02" ) );
}
</script>
</body>
</html>
OTHER CONSIDERATIONS
Please be advised that if you use
line-heightand change the browser's default, then it will effect the behavior in one of two ways. 1) If the value is set too hight, say around 190% or greater, it will increase the likelihood of the text overflowing the TEXTAREA so scroll bars will appear. 2) If it is set to a very small value, say 10%, then more than one line can overflow before the scroll bars appear. Remember, the whole purpose of the Dynamically Sized TEXTAREA to to display the full text in a TEXTAREA element without the use of the scroll bars and to minimize the amount of white space between the end of the text and the bottom of the box.
Also note that proportional fonts will be a bit more tricky to trying to reduce the amount of white space at the end of the text and the prevention of the scroll bars from appearing. This may require a bit more fine tuning of the equation/technique involved.
The biggest issue you may find with fixed width fonts, is the way they tend to wrap text on a word boundry. This is fine, unless your column width is too narrow and there are large words such that there there tends to be a fair amount of white space to the right of a line when that lines contains no linefeed.
One alternative would be to use the TEXTAREA only for editing. If there is a page that will be read-only or used for printing, then it may be a better idea to use a DIV tag instead with a style="border:2px solid #000000;width=20em;", for example. This will place all contained text within a box. Do not set the height and this box will adjust for the height of the text. This especially would be best for printing.
REFERENCES
Information on Regular Expressions. Some good information on JavaScript too.
5 comments:
Very Cool, I found you on google searching for
"javascript counting line feeds in a textarea"
I used part of you code to enhance my web page editor
To see a demo go to:
http://clearimageonline.com/projects/texteditorexample.html
I thought it interesting what you were doing with textareas, since my text editor takes a slightly different approach and makes the editor fit the window.
Thanks Anonymous for your comment. I guess I'll just call you "Terry" to give you a name for now.
I checked out your URL and I saw in the source for the TEXTAREA that it is constrained to 400x600 but for some reason it was expanding to the full size of the text content. I did not get a chance to dig in to find the reason, but I do assume it works for you.
I was using Safari v2.0.4.
Since I wrote that blog post, I have thought of a few other "Ways" to achieve the same effect with probably more accuracy and less CPU overhead. I'll research it, test it, then post it some day! ;-)
Terry, once again, thanks for the comment!
Hi Scott,
I like what you did here but I have some suggestionns and then I'll also include my code which include some changes to allow this to work more universally across other browsers and platforms.
You can increase the accuracy of the script considerably by doing the following:
1. Change font-family to monospace this way you ensure that each charater uses one real space in the textarea. Also, we need to make sure scrollbars are not displayed in the textarea so we need to hide them. This can be done by simply adding style="font-family:monospace;overflow:hidden" to the textarea tag.
2. Calculate the wrap point in the textarea to count lines properly. Unfortunately, this is different for different browsers so you need to have code the tells you which browser is being used. Since this is beyond the scope of what I'm trying to show here I won't elaborate but I will say there are many techniques to accomplish this goal.
That said, here's my code:
/*
The function seems to do pretty well under "normal"
conditions but there is a single case for firefox where it fails
to behave in a reasonable fashion and that's when the user enters
a line that extends beyond the width of the textarea and there is
no white space in the line. Firefox simply treats this a one long
word and does not revert to character wrapping as it should.
*/
function updateTextareaHeight( obj ) {
if ( obj == null ) { return false; }
// force the font size so this always works
obj.style.fontSize = '12px';
var newRows = 0;
var objText = obj.value;
var lines = objText.split("\n");
/* This may need to be enhanced to handle other browsers
right now I'm only supporting with ie and firefox
The factor below is to compensate for the fact that
firefox adds space for a scrollbar */
if ( btype == "ie" ) {
var factor = 0;
var rlen = factor + obj.cols;
} else {
var factor = 3; // this works well for a font size of 12px but should be changed for other sizes
var rlen = factor + obj.cols + 1;
//[extra columns in firefox] + [textarea columns] + [1 char because firefox wrapping rules differ]
}
var len = factor + obj.cols;
// Deal with the lines of text
if ( lines ) {
for ( i=0; i < lines.length; i++ ) {
// get rid of any trailing \r
ridx = lines[i].indexOf("\r");
if ( ridx >= 0 ) {
var line = lines[i].substring( 0, ridx );
} else {
var line = lines[i];
}
if ( line.length <= len ) {
newRows++;
continue;
}
var start = 0;
var lastPart = line.substr( line.length - rlen );
while ( start < line.length ) {
/* This may need to be enhanced to handle other browsers
right now I'm only supporting ie and firefox */
var part = line.substr( start, len );
var rpart = line.substr( start, rlen );
/* if this part of the line is smaller then the width of
the textarea then we're on the last line and we're done */
if ( rpart.length < rlen ) {
newRows++;
break;
}
// find the last space in this part of the line
ls = rpart.lastIndexOf(" ");
/* If there's no space then just add the len to
start to get the next len part of the line */
if ( ls == -1 ) {
start += len;
} else {
if ( ls != rlen ) {
/* if the last char in this part is not a space
but it is the last char in the line then
we're done with this line. */
if ( start + len == line.length ) {
start = line.length;
} else {
/* set start to get the next part of the
string that will appear on the next line
of the textarea */
start += ls + 1;
}
}
}
newRows++;
}
}
}
if ( newRows != obj.rows ) {
if ( newRows == 0 )
obj.rows = 1;
else
obj.rows = newRows;
}
}
Thanks man... pity on coders.. they need to look into two/more type of browsers following different standards
It has solved my issue on Safari
Thank you for posting helpful article.
Keep it up.
Post a Comment