This is such a common problem; I’m surprised that every language doesn’t have a native library to deal with it. A web designer or client has data they want to present, and they don’t want side scrolling, which is a perfectly understandable desire. The decision is made that data must fit within a certain width using a defined font size. The width is discussed for however long and a final decision is made…without ever checking the data to see if all of it fits. The initial page layout will include an example such as this:
Figure 1: A Standard Table with Scrolling
(Please pardon the lack of styling, I only have so much time to write these articles and I am a visual developer, not a visual designer, so thinking up something pretty takes me longer than coding something pretty that someone else came up with.)
As you see, everything lines up perfectly with the designer’s representative text. The page is built and the stubs return similar dummy text and everything still works fine. Then, you start getting the live data and you see something like this:
Figure 2: Don’t You Hate when This Happens?
You all know what happens next: More meetings with designers and/or clients. I’ve never met a developer who likes meetings, so the developer may not even be in these meetings, although he/she will be used as an absolute authority by the team lead that there is nothing that can be done (even if that isn’t what they said). It is very common that the person that writes the CSS and the one who writes the dynamic page code are different people, with different perspectives and agreement that this cannot be fixed. Design changes are made to accommodate and the client and/or designer is dissatisfied. Not a happy ending. I know; I have been that team lead and both dynamic coder and CSS developer.
If It Is Broke, You Must Fix It
Then came the project where the client did me the ultimate favor. They refused to change the design. For those who don’t know why that is a favor, I will elaborate. On almost every project, there is a time line, and missing that time line has consequences, ranging from very long hours, to unemployment, and several subtle degrees in between. The experienced developer know that once the end of the time line has been set, the only response to the question “can we do this?” that will allow them to avoid consequences is “no.” The seasoned consultant who is hoping for another project will say “probably, but it will take a long time.” The result of both these answers is that the client (who usually also has consequences of missed dates) will give in most of the time, and the times when they don’t can generally be squeezed in with some extra hours if the seasoned developer wasn’t seasoned enough to add in some extra time in estimating because these things always happen. Sometimes, that “no” or “will take a long time” is the absolute truth. These are the times when the client insists on doing it anyway; the developer gets to stretch themselves and either learn or create something new. For developers who do it because they like it, this is a good thing.
So, here you are faced with an immovable layout and an irresistible string. How can you fix this? The easy answer is to count characters and stick a space in at every n character to allow it to wrap. However, because character sizes vary, this can be less exact than you would think. It would be less visually pleasing or accurate to have a line break when it doesn’t have to. So, instead, you can combine string parsing with CSS and create the opportunity for the line to break without the necessity for it.
The Simple Solution
First, you begin with the CSS. It looks like this:
.stringBreak_nbsp{font-size : 1px;letter-spacing : 0px;}
Although you would prefer that the font size be 0 pixels, some browsers ignore this as an invalid value, thus creating a full-sized space. If you were to manually break up your problem string, for example purposes, it would look like this:
This<span class="stringBreak_nbsp"> </span> Is<span class="stringBreak_nbsp"> </span> Some<span class="stringBreak_nbsp"> </span> Real<span class="stringBreak_nbsp"> </span> Important<span class="stringBreak_nbsp"> </span> Database<span class="stringBreak_nbsp"> </span> Value
You use a space because of the different browser behaviors. If there were no space, some browsers won’t allow the text to break. If you were to use the notation, you either get too large of a gap or a forced break, again depending on the browser. The result of this approach in all browsers would look like this:
Figure 3: Repairing Alignment with CSS
The 1 pixel space is too small for most to discern. Obviously, you aren’t going to change all of your data to be split up this way, so instead you will do some basic parsing to insert this. The method I used the first time worked as follows. First, define the capital letters (another of those items I would expect in standard libraries):
private static final String CAPS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
Also handy is an array of common characters where people will expect to see a line break, so you can add this char array:
private static final char[] BREAK_CHARS = new char[] {'', '-', '/', '_', '#', '~', '@', '$', '%', '^', '&', '*', '(', ')', '+' };
Finally, you can create a static constant for the HTML and CSS that creates the space.
private static final String HTML_FIX = "<span class='stringBreak_nbsp'> </span>";
The static constant is to conserve memory (obviously), though it should be noted that with a table with a large amount of data that needs to be parsed in this manner, the page response time will be slowed due to the increase of characters being returned. Finally, you take the data as a string as a method parameter and break it up with a nested loop:
/* * Wrap on natural break characters in HTML * @param string the string to be wrapped */ public static String htmlWrap(String string) { StringBuffer stringFix = new StringBuffer(); boolean fixChar = false; char lastChar = SPACE_CHAR; char thisChar = string.charAt(0); int strLength = string.length(); for (int i = 0; i < strLength; i++) { thisChar = string.charAt(i); for (int c = 0; c < BREAK_CHARS.length; c++) { if (thisChar == BREAK_CHARS[c]) { fixChar = true; break } } // break a capital letter, but if there are two capital // letters in a row, don't break them. if (!fixChar && i > 0 && CAPS.indexOf(thisChar) >= 0 && lastChar != SPACE_CHAR && CAPS.indexOf(lastChar) < 0) { stringFix.append(HTML_FIX + string.charAt(i)); } else if (fixChar) stringFix.append(HTML_FIX + string.charAt(i) + HTML_FIX); else stringFix.append(string.charAt(i)); fixChar = false; lastChar = string.charAt(i); } return stringFix.toString().trim(); }
This could definitely be done more elegantly by using a regular expression. In the case of both the project and this article, I didn’t have the time to be elegant and went for effective instead.
When The Going Gets Tough, The Tough Fake It
The preceding code works great for single pieces of data. You can add characters to split on as your data requires. The example above was for a field where breaking on the capital letters was sufficient. In another project, we had a description field populated by research data, which tends to have long words. Even though the following method isn’t perfect, it sufficed to keep the table from breaking:
/* * For every word in the string, split the word that is longer * than maxWordLength into smaller words separated by space. * * @param string the string to be wraped @param maxWordLength * the maximum length of a word */ public static String wrapString(String string, int maxWordLength) { if (maxWordLength <= 0 || string == null) { return string; } StringBuffer result = new StringBuffer(); StringTokenizer st = new StringTokenizer(string, " "); String emptyString = "<span class='stringBreak_nbsp'> </span>"; while (st.hasMoreTokens()) { String word = st.nextToken(); if (word.length() > maxWordLength) {// if word length is too long, split the word into // smaller words int loop = word.length() / maxWordLength; for (int i = 0; i <= loop; i++) { if (i * maxWordLength + maxWordLength <= word.length()) { result.append(word.substring(i * maxWordLength, i * maxWordLength + maxWordLength)).append(emptyString); } else if (i * maxWordLength <= word.length()) { result.append(word.substring(i * maxWordLength, word.length())).append( emptyString); } } } else { result.append(word).append(" "); } } return result.toString(); }
When All Else Fails, Compromise
There was one situation where, in even the previous solution, the text was still too long, taking up so many rows with wrapping that the cells were too tall. Here, a compromise had to made; agreeing to truncate the text at such times. The following method is a simple and effective solution:
/* * Calculate the max string length based on the specified column * width, font size, and number of rows to display. * If the string length exceeds this number, truncate it. * * @param String the string to be truncated * @param columnWidth width of the display column in pixels * @param fontSize font size used * @param numberOfRows number of lines to be displayed * @return String */ public static String truncateString(String string, int columnWidth, int fontSize, int numberOfRows) { if (string == null) { return ""; } String result = new String(string); float VARIANCE = 1.6f; Float maxDisplayLength = new Float(numberOfRows * (columnWidth / fontSize) * VARIANCE); int maxLen = maxDisplayLength.intValue(); if (result.length() > maxLen) { // -2 accommodates >> result = result.substring(0, maxLen - 2); if (!result.endsWith(" ") && (result.lastIndexOf(" ") > 0)) { result = result.substring(0, result.lastIndexOf(" ")); } } return result; }
Conclusion
This article covered one issue that is common and sometimes thought to be insolvable. In addition to providing a solution for this particular issue, it is probably more useful to notice that the solutions were arrived at by having the option of not solving them removed. This shows the disservice of the frequent use of the word “issue” when what you mean is “problem.” “Issue” has become the office politic-speak for a problem. Issues can be stated, described, assigned, and postponed. Problems must be solved, preferably now. I’d rather have a problem than an issue any day, because a week from now most problems will be solved and the issues will still be there.
About the Author
Scott Nelson is a Senior Principal Web Application Consultant with well over 10 years of experience designing, developing, and maintaining web-based applications for manufacturing, pharmaceutical, financial services, non-profit organizations, and real estate agencies for use by employees, customers, vendors, franchisees, executive management, and others who use a browser. For information on how he can help with your web applications, please visit http://www.fywservices.com/ He also blogs all of the funny emails forwarded to him at Frequently Unasked Questions.