How-to: Toggling text with preview

In my years of development, when I come to a point where I stop and scratch my head and say, “Hmm, how am I going to do this”, the obvious next step is to look and see if anyone else has already done solved the problem. Chances are they have. All developers know this.

The hard part is remembering to pass along the favor. Of course, once you solve the problem, and the solution is staring you in the face, it seems so obvious that it hardly merits mentioning or blogging. Isn’t it obvious how to do that now? Well, here’s one of those. I was working on this problem, and when I tried to see if anyone else had already done it, I didn’t immediately find an answer. It’s time to patch that little hole.

I was working on a website that was aimed at mobile devices. When you’re working with mobile devices, you’re inevitably trying to fit a lot of content onto a small screen and you have to hide things. In this case it was text. I decided to have a teaser of text displayed upon initial loading, and if the viewer decided to read it, they could simply toggle a link to display or hide the bulk of the text. I needed the text to stop dynamically in the middle of a sentence, and when the user clicked the “ Show me more” link, the rest of the text magically appeared, seamless with the teaser text that was there before. Here’s how I did it.

I created a couple of functions to truncate the text.

#truncate description
@contracted_description = truncate_text(@location.description,100)
@remaining_description = truncate_remainder(@location.description, 100)

With my helper functions looking like this..

def truncate_text(text, len)
  chars = text.mb_chars #converts string to char
  while(chars[len] != ' ') #make sure the break doesn't happen in the middle of a word
    len = len-1
  end
  trunc_text = (chars.length > len ? chars[0...len] : text).to_s
end
def truncate_remainder(text, len)
  chars = text.mb_chars #converts string to char
  l = chars.length
  if l < len #if text does not need truncated
    return ""
  end
  while(chars[len] != ' ') #make sure the break doesn't happen in the middle of a word
    len = len-1
  end
  trunc_text = (chars.length > len ? chars[len...l] : text).to_s
end

Now that I’m a little wiser, I could have done it like this. (I haven’t actually tested this code so caveat emptor.)

@contracted_description = truncate(@location.description, :length => 100, :separator => ' ', :omission => '')
@remaining_description = @location.description.gsub(/#{@contracted_description}(.*)/m, '\1')

Basically what going on here is I pass these functions some text and the amount of characters I want to show up in the teaser text. The first one returns my teaser text, the second returns the remainder text to be toggled on or off, and both of them will “back up” in the text provided to find the first white space to ensure we don’t break off the teaser text in the middle of a word.

Ok, we have our two pieces of text, now to display them. This site already was using scriptaculous effects in the menu to toggle nested menus, so we stuck with that. Assuming you have the scriptaculous library in your app, our view looks something like:

<div id = "contracted_description" >
	<%= @contracted_description %>
	<div id = "full_description" style = "display:none;">
		<%= @remaining_description %>
		<%= link_to_function("Less Text","toggleDescription()", :id =>"less_link") %>
	</div>
	<% unless @remaining_description.length == 0%>
		<%= link_to_function("...More Text","toggleDescription()", :id =>"more_link") %>
	<% end %>
</div>

Or if you prefer slim:

div id = "contracted_description"
        = @contracted_description
	div id = "full_description" style = "display:none;"
		= @remaining_description
		= link_to_function("Less Text","toggleDescription()", :id =>"less_link")
	-unless @remaining_description.length == 0
		= link_to_function("...More Text","toggleDescription()", :id =>"more_link")

And our toggleDescription() javascript function looks something like:

function toggleDescription(){
	Effect.toggle("full_description", 'blind', {duration:0.50});
	Effect.toggle("more_link", 'appear');
}

Now we have text that expands and contracts. It’s not seamless yet, but where almost there. As for our “Less Text” and “More Text” links, you could easily make them the same link, and change the text rather than have two different ones.

All that’s left is to make it all display like seamless text when expanded, rather than text separated by two divs. The text I was pulling from the database was already formatted with paragraphs that I wanted to preserve, so I used psuedo elements to display:inline the last element of type paragraph in the contracted text, as well as the first element of type paragraph in the remaining full description text. So our stylesheet looks like:

#contracted_description{
	margin-left: 10px;
}
 
#contracted_description > p:last-of-type{
        display: inline;
}
 
#full_description {
	display: inline;
	margin-right: 10px;
}
 
#full_description p:first-child{
	display: inline;
	margin-left: 0px;
}
 
#full_description p:nth-child(2){
        display: block;
}
 
#more_link{
	display: inline;
	text-decoration: underline;
}
 
#less_link{
        text-decoration: underline;
}