jMar"s Blog DevSmash Developer Portal

Wednesday, February 27, 2008

Blogger Trick: Style Author Comments Differently with jQuery

No doubt you've encountered a blog at one time and noticed the custom styling applied to comments left by the author. It certainly adds a nice touch - and I wanted to be able to do the same here on Blogger. My first step towards finding a solution was to examine the comment markup generated by blogger. Unfortunately, however, it seems that Blogger doesn't indicate author comments in any way that can be accessed through a CSS rule. Well, I'm not that easily deterred, and eventually I came up with the following approach.

Before I explain how to implement this solution, let me briefly explain how it works. If you have ever looked inside your template's html, you have likely noticed some custom tags being used. In our case, we're concerned with the data tag. If this already sounds unfamiliar to you, I would advice you look at this page on using the data tag.

The particular piece of data that we want to access is the data.userUrl member. This member is only made available within the Profile widget, which unfortunately means that if you have disabled the profile on your blog, you're out of luck. This member is also not available if you have a team blog (sorry). For the rest of you bloggers (which is still the vast majority of you) this solution will work just great.

So now, with the preliminaries out of the way, we're going to tell the script to loop through all of the comments on the page. If the member link on the current comment matches the member link in the profile, then it must be an author comment. The script will then apply an additional class to the comment, allowing you to style it separately. All clear?

Step 1: Import the jQuery Library

In order for this script to work, you will have to import the jQuery library. Since jQuery let's you directly link to the source, this step is quite trivial. From your blog's dashboard, you will want to go to the layout tab, and then click "Edit HTML". Search for the <head> tag, and insert the following line directly below it:

<script src='http://code.jquery.com/jquery-latest.min.js' type='text/javascript'/>

Click "Save Template". Leave this page open because we'll be needing it in the following steps.

Step 2: Insert the Script

Now click the check box that says "Expand Widget Templates". Since the data.userUrl member is only available within the Profile widget, that is where we need to insert the script.

If are the only author of your blog, do this:

Search for the following tag:

<b:widget id='Profile1' locked='false' title='xxx' type='Profile'>.

Inside of this tag you will find the following line:

<b:else/> <!-- normal blog profile -->

Directly below this line, insert the following script:

<script type='text/javascript'>
$().ready(function() {
 $('dl#comments-block dt a').each(function(i) {
  if($(this).attr('href') == '<data:userUrl/>') {
   $(this).parent('dt').addClass('author-comment').next('dd.comment-body').addClass('author-comment').next('dd.comment-footer').addClass('author-comment');
  }
 });
});
</script>

If you have co-authors on your blog, do this:

Search for the following tag:

<b:widget id='Profile1' locked='false' title='xxx' type='Profile'>.

Inside this tag, you will find the following tag:

<b:if cond='data:team == "true"'>.

Immediately beneath this tag, insert the following code:

<script type="text/javascript">
 var author_urls = new Array();
</script>

Next, find the following line: <b:loop values='data:authors' var='i'>

Immediately below this line, insert the following code:

<script type="text/javascript">
 author_urls[author_urls.length] = '<data:i.userUrl/>';
</script>

Next, insert the following code right below the closing ul tag.

<script type='text/javascript'>
$().ready(function() {
 var urlsReg = new RegExp(author_urls.join("|"));
 $('dl#comments-block dt a').each(function(i) {
  if(urlsReg.test($(this).attr('href'))) {
   $(this).parent('dt').addClass('author-comment').next('dd.comment-body').addClass('author-comment').next('dd.comment-footer').addClass('author-comment');
  }
 });
});
</script>

That takes care of all the JavaScript parts. Now all that's left to do is to style it.

Step 3: Update your CSS

If you look at the markup that Blogger generates, you will notice that each comment is made up of a dt tag followed by two dd tags. At the time of writing this, I am using the following additional CSS:

dl#comments-block dt.author-comment {
  background-image: none;
  background-color: #EDE5BE;
  margin-bottom: 0px;
  padding: 6px 0 6px 10px;
  border: 1px solid #ccc;
  border-bottom: 1px solid #FFF7CF;
}

dl#comments-block dd.comment-body.author-comment {
  color: #593622;
  background-color: #EDE5BE;
  margin-top: 0px;
  margin-bottom: 0px;
  padding: 10px;
  border: 1px solid#ccc;
  border-top: 1px solid #CBC4AC;
}

dl#comments-block dd.comment-footer.author-comment {
  font-size: .8em;
  background-color: #CBC39C;
  padding: 3px;
  margin-top: 0px;
  float: right;
  border: 1px solid #ccc;
  border-top: none;
}

To add these styles, simply paste them right above the </style> tag.

You're Done!

That's it! Just hit "Save Template" and view your blog. To see the script in action, view my comment below this post.

Tuesday, February 26, 2008

jTruncate in Action

This is the demonstration page for the jTruncate plugin for jQuery. If you are looking for the jTruncate homepage, go here.

Plain Jane Example

jTruncate is called in the same way that most other jQuery plugins are called. The example below is using the following code, that accepts all the default options.

$().ready(function() {
 $('#example1').jTruncate();
});

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Etiam fringilla, purus a ultrices blandit, odio ante scelerisque neque, vitae imperdiet odio velit ac nisl. Sed tortor metus, placerat condimentum, feugiat in, feugiat adipiscing, mi. Donec pulvinar sem vitae leo. Vestibulum eget lectus et ligula hendrerit pharetra. Sed porta justo ac nisl. Aliquam nisi erat, pellentesque sed, sagittis eu, fringilla sit amet, dolor. Nam ac mi. Pellentesque pede purus, mattis aliquet, semper nec, cursus a, orci. Duis bibendum nibh ac massa. Integer eu tortor. Aenean convallis quam at nunc. Nunc mollis tincidunt nisi. Suspendisse mauris odio, iaculis ut, feugiat vitae, ultrices in, tortor. Quisque at elit. In hac habitasse platea dictumst.
jTruncate Options

jTruncate allows you to customize nearly every aspect of the truncation operation. The following options are provided:

length: Defaults to 300
The number of characters to display before truncating.
minTrail: Defaults to 20
The minimum number of "extra" characters required to truncate. This option allows you to prevent truncation of a section of text that is only a few characters longer than the specified length.
moreText: Defaults to "more"
The text to use for the "more" link.
lessText: Defaults to "less"
The text to use for the "less" link.
ellipsisText: Defaults to "..."
The text to append to the truncated portion.
moreAni: Defaults to an empty string
The speed argument for the show() method (as specified here).
lessAni: Defaults to an empty string
The speed argument for the hide() method (as specified here).
Custom jTruncate Example

The following example demonstrates how to override the defaults described above.

$().ready(function() {
 $('#example2').jTruncate({
  length: 200,
  minTrail: 0,
  moreText: "[see all]",
  lessText: "[hide extra]",
  ellipsisText: " (truncated)",
  moreAni: "fast",
  lessAni: 2000
 });
});

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Etiam fringilla, purus a ultrices blandit, odio ante scelerisque neque, vitae imperdiet odio velit ac nisl. Sed tortor metus, placerat condimentum, feugiat in, feugiat adipiscing, mi. Donec pulvinar sem vitae leo. Vestibulum eget lectus et ligula hendrerit pharetra. Sed porta justo ac nisl. Aliquam nisi erat, pellentesque sed, sagittis eu, fringilla sit amet, dolor. Nam ac mi. Pellentesque pede purus, mattis aliquet, semper nec, cursus a, orci. Duis bibendum nibh ac massa. Integer eu tortor. Aenean convallis quam at nunc. Nunc mollis tincidunt nisi. Suspendisse mauris odio, iaculis ut, feugiat vitae, ultrices in, tortor. Quisque at elit. In hac habitasse platea dictumst.
Special Considerations

Note that if you override the default animation options, you will notice a "new line" inserted at the point of truncation. This is because the hide/show methods require the animated element to be block level, and thus will begin on its own line.

Also note that jTruncate chooses the split location by starting at the length you specify (or the default) and then finds the next space. This is to prevent truncation in the middle of an html tag. This implies that the text needs to have spaces in it (duh), and that any tags within the truncated text cannot contain a space (i.e. <p id="myP"> = bad).

Thank you for visiting, and if you have any questions/suggestions at all, just let me know in the comments!

jTruncate - Text Truncation for jQuery

Welcome and thanks for visiting! This is the official home page for the jTruncate plugin for jQuery. In a nutshell, jTruncate provides simple yet customizable truncation for text entities in your web page.

Download

The latest version of jTruncate can be found at the following links:

Instructions/Examples

For usage instructions and working examples of the jTruncate plugin, look here.

Testers wanted!

To date I have tested jTruncate in FF2, IE6+, and Safari 3 (for windows). If you are running a browser other than what I have listed, please let me know how it performs! Due to the limited testing thus far, I am currently releasing jTruncate at version 0.8 - but it is a simple script and (until proven otherwise) I consider it to be stable.

Friday, February 8, 2008

Easy Multi Select Transfer with jQuery

I'm sure that at some point or another you've encountered a form widget like the one below, allowing options to be traded from one multi select to another.

add >>

I recently encountered a tutorial over at Quirks Mode on creating such a widget. While not a bad script, when all was said and done it was coming up on 40 lines of JS. I suppose that's not horrible, but we're talking about some simple functionality.

This struck me as a perfect example to demonstrate the simple and compact nature of jQuery coding. The widget operating above is running off of the following code:

$().ready(function() {
 $('#add').click(function() {
  return !$('#select1 option:selected').remove().appendTo('#select2');
 });
 $('#remove').click(function() {
  return !$('#select2 option:selected').remove().appendTo('#select1');
 });
});

That's it... 8 lines.

If you'd like to try it out, here's a working test page:

<html>
<head>
 <script src="js/jquery.js" type="text/javascript"></script>
 <script type="text/javascript">
  $().ready(function() {
   $('#add').click(function() {
    return !$('#select1 option:selected').remove().appendTo('#select2');
   });
   $('#remove').click(function() {
    return !$('#select2 option:selected').remove().appendTo('#select1');
   });
  });
 </script>
 
 <style type="text/css">
  a {
   display: block;
   border: 1px solid #aaa;
   text-decoration: none;
   background-color: #fafafa;
   color: #123456;
   margin: 2px;
   clear:both;
  }
  div {
   float:left;
   text-align: center;
   margin: 10px;
  }
  select {
   width: 100px;
   height: 80px;
  }
 </style>
 
</head>

<body>
 <div>
  <select multiple id="select1">
   <option value="1">Option 1</option>
   <option value="2">Option 2</option>
   <option value="3">Option 3</option>
   <option value="4">Option 4</option>
  </select>
  <a href="#" id="add">add &gt;&gt;</a>
 </div>
 <div>
  <select multiple id="select2"></select>
  <a href="#" id="remove">&lt;&lt; remove</a>
 </div>
</body>
</html>

As was mentioned in the comments below, the following (slightly modified) code can be used to automatically select all options in the second select box before submitting (thanks Charlie!).

$('form').submit(function() {
 $('#select2 option').each(function(i) {
  $(this).attr("selected", "selected");
 });
});

Just make sure you include that snippet inside the $().ready() method.

Update! (2/28/2008)

To prevent the page from scrolling to the top whenever a user clicks a button (in some browsers), I added return false; to the click event handlers.

(3/07/2008)

I included Charlie's suggestion from the comments into the main article. Thanks again Charlie! I also updated the the click event handlers to return false on the same line. Now it's only 8 lines!

(6/24/2008)

David Walsh has done an excellent port of this script for the Mootools library. You'll notice that the libraries allow for very similar syntax and nearly identical flow of logic. Check it out!

Wednesday, February 6, 2008

Java, Reflection, Hibernate Proxy Objects, and Confetti

After spending the last 3 days scratching my head and pushing Google to its theoretical limits, I have just experienced the ever-satisfying thrill of success. It was one of those moments where a little hole appears in the clouds and a beam of light illuminates my desk while gold and silver confetti float down from the overhead sprinklers. It really was that good; I nearly made an announcement over the intercom.

So here's the setup: we have a web application running on a Weblogic Portal server and we're using Hibernate and Spring. Due to application growth and an expanding data model, we decided it was time to turn lazy loading on (with respect to Hibernate). Meanwhile I'm working on some form processing logic that (for the sake of this discussion) needed to be a runtime annotation consumer.

The section of code was to determine whether a form had values for all of the "Recommended Fields". The recommended fields were designated via a RecommendedField annotation in the model class. All this method knows is that the particular model object will extend MySuperClass. So it's pretty straight forward. Here's a basic outline of what I had going:

public static boolean isComplete(MySuperClass object) {

 Method[] methods = object.getClass().getMethods();
 for(Method method : methods) {
  RecommendedField rField = method.getAnnotation(RecommendedField.class);
  if( null != rField) {
   // do some logic, then return true or false 
  }
 }
 
 return true;
}

Nothing seems glaringly wrong in the code above, but to my consternation, method.getAnnotation() always returned null. Around this time I came to understand one of the side effects of Hibernate lazy loading. In order to allow lazy access to any relational objects, Hibernate returns a "proxy" object that essentially wraps and mimics the object that you wanted. This way when you call a getter, instead of throwing a NullPointerException, Hibernate can retrieve the result from the database.

Great... so what now? While the proxy object will suffice whenever you need to access a public property or method, it quickly becomes apparent that introspection and reflection present a problem. Since the proxy object is not truly an instance of the class that it is wrapping, metadata (such as annotations) are not there.

In truth, this realization was the key to solving the problem. Understanding that the annotations simply didn't exist on the object that I was inspecting prompted me to explore how to retrieve the non-proxy version of the object. Here is the solution that I ultimately came up with:

public static boolean isComplete(MySuperClass object) {
 /*
  *  The type casting below is necessary in order to read the annotations on the object's methods.
  *  The "MySuperClass" object that is passed into this method is really a Hibernate proxy object 
  *  that wraps the MySuperClass object (due to Hibernate's lazy loading).
  */
 if (object instanceof HibernateProxy) {
  object = (MySuperClass)((HibernateProxy)object).getHibernateLazyInitializer().getImplementation();
 }
  
 Method[] methods = object.getClass().getMethods();
 for(Method method : methods) {
  RecommendedField rField = method.getAnnotation(RecommendedField.class);
  if( null != rField) {
   // do some logic, then return true or false 
  }
 }
 
 return true;
}

*Cue confetti*

You'll notice that the commented addition is rather peculiar. It essentially takes the proxy object that was passed in and casts it as a HibernateProxy object. Calling getImplementation() returns a generic object, which I can then recast as a MySuperClass object (which is what it claimed to be in the first place). Since the object I am working with is now a real and true instance of MySuperClass, I can inspect the methods and their annotations without any problems.

There is a word of warning to throw in about this solution: since I am replacing the proxy object with the real object, my assumption is that it is now detached from the Hibernate session. Attempts at lazy loading on the object would most likely return null or throw a LazyLoadException (I have not tested to verify). In my case I don't need to access any more data, so it's not a problem. If your circumstances are different, you can simply create a new instantiation out of the casted object, or attempt to reattach the object to the Hibernate session when you're done.

Sunday, February 3, 2008

Building Your First jQuery Plugin

So you were out on your quest to find the answer to life, the universe, and everything, when blam, you found jQuery. Yes, I know, you were expecting 42, but too all of our surprise, it was jQuery. So what's next? Build your own plugin!

While some are intimidated by the thought of creating their own plugin, the truth is that jQuery is built with an infinitely friendly plugin architecture. If you've gotten comfortable with the basics of jQuery coding, then you're certainly ready to develop your own plugin. This tutorial will take you step by step through creating your very own truncation plugin. Say, for example, you have a "tip of the day" widget on your home page. This plugin will let you truncate it to a specified length, with a link to expand it to view it's full content. Here's a working example:

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Etiam fringilla, purus a ultrices blandit, odio ante scelerisque neque, vitae imperdiet odio velit ac nisl. Sed tortor metus, placerat condimentum, feugiat in, feugiat adipiscing, mi. Donec pulvinar sem vitae leo. Vestibulum eget lectus et ligula hendrerit pharetra. Sed porta justo ac nisl. Aliquam nisi erat, pellentesque sed, sagittis eu, fringilla sit amet, dolor. Nam ac mi. Pellentesque pede purus, mattis aliquet, semper nec, cursus a, orci. Duis bibendum nibh ac massa. Integer eu tortor. Aenean convallis quam at nunc. Nunc mollis tincidunt nisi. Suspendisse mauris odio, iaculis ut, feugiat vitae, ultrices in, tortor. Quisque at elit. In hac habitasse platea dictumst.

Note that if JavaScript is disabled (or not supported) the content will simply display in it's entirety.

You ready? Let's dig in...

Step 1

The first step is to extend the actual jQuery object with the function that we wish to add. In our case, we wish to add "truncation" functionality. So here's where to start: create a jquery.truncate.js file and save it with the following code:

$.fn.truncate = function(options) {

   return this.each(function() {

   });
};

Now you may have heard that plugin developers should not user the $ alias, as this can result in conflicts with other libraries. This is only partially true. The following snippet is the same as the one above, except that we pass jQuery into the function, allowing us to use an alias we want. We'll stick with $.

(function($){
 $.fn.truncate = function() {

    return this.each(function() {

    });
 };
})(jQuery);
Step 2

Before we go any further, let's create a simple test page that we can use to test our plugin. Create a page and call it whatever_you_want.html. Insert the following code. As you can see I placed both the jQuery library and the plugin inside a folder named js. Note that we are already invoking our plugin in this snippet, although we have not yet coded any behavior.

<html>
<head>
 <title>Truncation Plugin Test</title>
 <script src="js/jquery.js" type="text/javascript"></script>
 <script src="js/jquery.truncate.js" type="text/javascript"></script>
 
 <script type="text/javascript">
  $().ready(function() {
   $('.tip').truncate();
  });
 </script>
</head>
<body>
 <div class="tip" style="width:200px;background-color:#ccc;padding:10px;">
  Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Etiam fringilla, purus a ultrices blandit,
  odio ante scelerisque neque, vitae imperdiet odio velit ac nisl. Sed tortor metus, placerat condimentum,
  feugiat in, feugiat adipiscing, mi. Donec pulvinar sem vitae leo. Vestibulum eget lectus et ligula hendrerit
  pharetra. Sed porta justo ac nisl. Aliquam nisi erat, pellentesque sed, sagittis eu, fringilla sit amet,
  dolor. Nam ac mi. Pellentesque pede purus, mattis aliquet, semper nec, cursus a, orci. Duis bibendum nibh
  ac massa. Integer eu tortor. Aenean convallis quam at nunc. Nunc mollis tincidunt nisi. Suspendisse mauris
  odio, iaculis ut, feugiat vitae, ultrices in, tortor. Quisque at elit. In hac habitasse platea dictumst.
 </div>
</body>
</html>
Step 3

The next thing we want to do is provide a mechanism for the user to customize the plugin. While we want to make the plugin as flexible as possible, we should also provide defaults so that the user isn't forced into providing a long list of parameters. We can easily do this using jQuery's extend method. Update your plugin to match the following:

(function($){
 $.fn.truncate = function(options) {

  var defaults = {
   length: 300,
   minTrail: 20,
   moreText: "more",
   lessText: "less",
   ellipsisText: "..."
  };
  var options = $.extend(defaults, options);
    
  return this.each(function() {

  });
 };
})(jQuery);

For now we won't override any of the defaults in our test page, but we'll demonstrate this later.

Step 4

So that takes care of all the preliminary considerations. Let's get to coding the plugin's functionality. As you've already seen, the plugin is returning this.each(...). This line will execute the contained anonymous function on each item in the jQuery array. So, if for example we called $('p').truncate(), the code we're about to write would execute on every p tag.

Since I'm assuming a comfortable understanding of jQuery, I won't explain in detail how the function's code actually works. If anything in the code is not obvious, you should refer to the documentation or ask a question in the comments. To complete your plugin, update it to match the following:

(function($){
 $.fn.truncate = function(options) {
    
  var defaults = {
   length: 300,
   minTrail: 20,
   moreText: "more",
   lessText: "less",
   ellipsisText: "..."
  };
  
  var options = $.extend(defaults, options);
    
  return this.each(function() {
   obj = $(this);
   var body = obj.html();
   
   if(body.length > options.length + options.minTrail) {
    var splitLocation = body.indexOf(' ', options.length);
    if(splitLocation != -1) {
     // truncate tip
     var splitLocation = body.indexOf(' ', options.length);
     var str1 = body.substring(0, splitLocation);
     var str2 = body.substring(splitLocation, body.length - 1);
     obj.html(str1 + '<span class="truncate_ellipsis">' + options.ellipsisText + 
      '</span>' + '<span  class="truncate_more">' + str2 + '</span>');
     obj.find('.truncate_more').css("display", "none");
     
     // insert more link
     obj.append(
      '<div class="clearboth">' +
       '<a href="#" class="truncate_more_link">' +  options.moreText + '</a>' + 
      '</div>'
     );

     // set onclick event for more/less link
     var moreLink = $('.truncate_more_link', obj);
     var moreContent = $('.truncate_more', obj);
     var ellipsis = $('.truncate_ellipsis', obj);
     moreLink.click(function() {
      if(moreLink.text() == options.moreText) {
       moreContent.show('normal');
       moreLink.text(options.lessText);
       ellipsis.css("display", "none");
      } else {
       moreContent.hide('normal');
       moreLink.text(options.moreText);
       ellipsis.css("display", "inline");
      }
      return false;
       });
    }
   } // end if
   
  });
 };
})(jQuery);

You'll notice that whenever I needed to select an element within the plugin, I always used obj as my context (e.g., moreLink = $('.truncate_more_link', obj)). This is necessary to constrain any selections to the current truncated element. Without setting the context like this, you will get unpredictable results.

So that's it - your first jQuery plugin! We're not quite finished though, since I promised we'd demonstrate overriding the default options. In the following example, every option has been over ridden, although it is perfectly valid to override fewer. Just replace the script in your test page with this:

$().ready(function() {
 $('.tip').truncate( {
  length: 120,
  minTrail: 10,
  moreText: 'show more',
  lessText: 'show less',
  ellipsisText: " [there's more...]"
 });
});

This code will give you something like this:

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Etiam fringilla, purus a ultrices blandit, odio ante scelerisque neque, vitae imperdiet odio velit ac nisl. Sed tortor metus, placerat condimentum, feugiat in, feugiat adipiscing, mi. Donec pulvinar sem vitae leo. Vestibulum eget lectus et ligula hendrerit pharetra. Sed porta justo ac nisl. Aliquam nisi erat, pellentesque sed, sagittis eu, fringilla sit amet, dolor. Nam ac mi. Pellentesque pede purus, mattis aliquet, semper nec, cursus a, orci. Duis bibendum nibh ac massa. Integer eu tortor. Aenean convallis quam at nunc. Nunc mollis tincidunt nisi. Suspendisse mauris odio, iaculis ut, feugiat vitae, ultrices in, tortor. Quisque at elit. In hac habitasse platea dictumst.

Well... I hope you found this helpful. If you have any questions beyond what I've explained, please just ask in the comments. Thanks for reading!

Update! (2/26/2008)

The plugin that we just developed above has now been added to the jQuery plugin repository under the name of jTruncate. The home page is located here on my blog. It is now an evolving beast, so check there for any updates.