Fancy jQuery Slide-Out Effects for Large Page Elements

Wed, Aug 25, 2010 - 9:04am -- Isaac Sukin

Sometimes, there are things in my blog posts that just don't fit nicely into the width of the content area. This is a problem with code snippets and images in particular; I only have a certain amount of horizontal space, but often that's not enough.

Inspired by a solution I witnessed in action at Lullabot.com (and the place Lullabot discovered it, Viget.com) I finally decided to solve this problem using some fancy jQuery. Now, all code blocks on this site fit correctly into the content area, with any excess text hidden until your mouse hovers over the code block. All images are automatically shrunk, until your mouse hovers over them, at which point they will enlarge to their original size. Pretty sweet! And it's all cross-browser-compatible.

Let's get started with the code block magic. First, set this CSS rule for your code blocks:

div.codeblock {
  overflow: auto;
  white-space: pre;
}

div.codeblock is a DOM selector that, on this site, selects all code blocks (all div elements with class codeblock). Set it appropriately for your site. white-space: pre correctly formats white space and line breaks so that the code formatting and style are clear. overflow: auto makes the code block automatically fit within the content area. If needed, there will be a horizontal scroll bar on the code block so users can scroll to the right to see the rest of the code.

By the way, if you're fine with your lines of code wrapping, then you don't need any of this! The technique described here is only necessary if you want your code to look like it would in a code editor -- with no line wrapping. You may want to skip down to the image part.

Anyway, here's the jQuery that powers the code block expanding:

  $('div.codeblock').each(function() {
    var contentwidth = $(this).contents().width();
    var blockwidth = $(this).width();
    var mainwidth = 929;
    if (contentwidth > blockwidth) {
      $(this).wrap('<div />');
      $(this).parent().height($(this).height());
      var origPos = $(this).css('position');
      $(this).hover(function() {
        $(this).css('position', 'absolute').animate({width: mainwidth}, 'fast');
      }, function() {
        $(this).animate({width: blockwidth}, 'fast', function() {
          $(this).css('position', origPos);
        });
      });
    }
  });

Let's break this down.

  $('div.codeblock').each(function() {

This line cycles through each code block found on the page. Adjust the DOM selector (div.codeblock) as necessary for your site.

    var contentwidth = $(this).contents().width();
    var blockwidth = $(this).width();
    var mainwidth = 929;

Get the width of the code block div, the full width it would be if there was enough room, and the maximum width we want to allow. Adjust the mainwidth calculation to your site as necessary (fluid width themes will need to do some calculations). In my case, I don't want to allow code blocks to be wider than the content area plus the sidebar minus the extra space on each side for padding and the border.

    if (contentwidth > blockwidth) {

We only need to apply the fancy sliding technique if the code is too wide to fit in the content area.

      $(this).wrap('<div />');
      $(this).parent().height($(this).height());
      var origPos = $(this).css('position');

The theme for this website happens to have a right sidebar that comes after the content area in the HTML. So if the content area happened to overlap the sidebar, the sidebar would display "above" the content area. To fix this, we use a trick involving the CSS position attribute to bring the code block to the foreground. In order to keep the page layout the same when we change the position, we use a placeholder div that is the same height as the code block. (There is probably a more elegant way to solve this problem; if you can think of one, please let me know in the comments. Before you ask, z-index won't work here because the sidebar is floated.)

If you don't have this "sidebar problem," you don't need this trick, although it probably won't hurt anything.

      $(this).hover(function() {
        $(this).css('position', 'absolute').animate({width: mainwidth}, 'fast');
      }, function() {
        $(this).animate({width: blockwidth}, 'fast', function() {
          $(this).css('position', origPos);
        });
      });

This is the real magic here. jQuery has a nice hover listener that lets us react on the mouseenter and mouseleave events. For the layman, this means we can do stuff when someone hovers the mouse over the code. In this case, what we're doing is expanding (and contracting) the width of the code block. We could just set the CSS to have it expand and contract instantly, but we might as well use some nice jQuery eye candy in the form of the animate function to make it a little clearer what's going on.

There you go -- dynamically resizing code blocks! Try it out:

This is a really long block of code that expands beyond the width of the content area. It should display a scroll bar and expand on hover.
This is a really long block of code that expands beyond the width of the content area. It should display a scroll bar, and it should expand when the mouse hovers over it.

The image resizing jQuery is basically the same, with some tweaks to make sure the width-height ratio stays consistent:

  $('#content img').each(function() {
    var imgwidth = $(this).width();
    var imgheight = $(this).height();
    var contentwidth = 550; // $('#content').width();
    var origPos = $(this).css('position');
    if (imgwidth > contentwidth) {
      $(this).width(contentwidth);
      var newHeight = imgheight * (contentwidth / imgwidth);
      $(this).wrap('<div />');
      $(this).parent().height(newHeight);
      $(this).hover(function() {
        $(this).css('position', 'absolute').animate({width: imgwidth, height: imgheight}, 'fast', function() {
          $(this).parent().height(imgheight);
        });
      }, function() {
        $(this).animate({width: contentwidth, height: newHeight}, 'fast', function() {
          $(this).css('position', origPos).parent().height(newHeight);
        });
      });
    }
  });

Let's break it down.

  $('#content img').each(function() {

Cycles through each image in the content region. Adjust the selector to your site as necessary.

    var imgwidth = $(this).width();
    var imgheight = $(this).height();
    var contentwidth = $('#content').width();

Get the original height and width of the image, as well as the maximum allowable default width. (content is the ID of my content area; adjust to your site as necessary. If your content area is fixed with, it's more efficient to use the number of pixels directly.)

    var origPos = $(this).css('position');

Store the original position attribute. We will need to change this so that the expanded image is displayed above the sidebar. (If you don't have a sidebar on the right that occurs after the content area in the HTML, you can ignore the position parts.)

    if (imgwidth > contentwidth) {

We only apply our magic to images that are too big.

      $(this).width(contentwidth);
      var newHeight = imgheight * (contentwidth / imgwidth);

Resize the image to fit within the content region by default.

      $(this).wrap('<div />');
      $(this).parent().height(newHeight);

When we change the position attribute of the image, the image will overlay the text that was previously beneath it. We don't want that, so we're using a placeholder div to keep track of how tall the image is and keep text from accidentally sliding up under it.

      $(this).hover(function() {
        $(this).css('position', 'absolute').animate({width: imgwidth, height: imgheight}, 'fast', function() {
          $(this).parent().height(imgheight);
        });
      }, function() {
        $(this).animate({width: contentwidth, height: newHeight}, 'fast', function() {
          $(this).css('position', origPos).parent().height(newHeight);
        });
      });

Again, this is the real magic. Basically, we're just changing some CSS properties when the user hovers or un-hovers over the image. Specifically, we're changing the position, width, and height of the image (and adjusting our placeholder div appropriately) to expand and contract the image.

Go ahead, test it out:

CTF-CBP3-Krodan

Awesome.

Here's the final code, in the format used by Drupal modules. If you're not a Drupal user, just replace the first line with the standard $(document).ready(function() { and remove the two instances of the "context" variable from the rest of the code.

Drupal.behaviors.fancyPants = function(context) {
  $('div.codeblock', context).each(function() {
    var contentwidth = $(this).contents().width();
    var blockwidth = $(this).width();
    var mainwidth = 980; // Adjust this to fit your site
    if (contentwidth > blockwidth) {
      $(this).wrap('<div />');
      $(this).parent().height($(this).height());
      var origPos = $(this).css('position');
      $(this).hover(function() {
        $(this).css('position', 'absolute').animate({width: mainwidth}, 'fast');
      }, function() {
        $(this).animate({width: blockwidth}, 'fast', function() {
          $(this).css('position', origPos);
        });
      });
    }
  });
  $('#content img', context).each(function() {
    var imgwidth = $(this).width();
    var imgheight = $(this).height();
    var contentwidth = 628; // Adjust this to fit your site
    var origPos = $(this).css('position');
    if (imgwidth > contentwidth) {
      $(this).width(contentwidth);
      var newHeight = imgheight * (contentwidth / imgwidth);
      $(this).wrap('<div />');
      $(this).parent().height(newHeight);
      $(this).hover(function() {
        $(this).css('position', 'absolute').animate({width: imgwidth, height: imgheight}, 'fast', function() {
          $(this).parent().height(imgheight);
        });
      }, function() {
        $(this).animate({width: contentwidth, height: newHeight}, 'fast', function() {
          $(this).css('position', origPos).parent().height(newHeight);
        });
      });
    }
  });
};

Enjoy!