Saturday, March 31, 2012

Sliding / Expandable / Collapsible Box with max-height CSS Transition

Note


An improved solution can be found in this post.








Source code and demo

One common CSS3 Transition is to slide up (collapse) and slide down (expand) a box by manipulating its "height" attribute, e.g. changing 400px to 0. However, when either height is set to "auto", the transition won't work anymore. This topic has been discussed here.

The solution is to change the "max-height" instead of "height". Max-height is a CSS attribute supported in almost all modern browsers (see compatibility chart here). It defines the maximum height of an element. We can use it to "shrink" a box by setting max-height to 0, or expand a box by restoring its original height. In order to restore the original height, we need to retain the computed height of the box content.

Adam at stackoverflow.com provided a solution inspired by this same idea. I simplified the solution by removing some of the JavaScript code.

HTML markup


Here I create an item (div.item) with a title (<h2>) and content area (div.content). I want to make the content area slide down (expand) or slide up (collapse) once the item is clicked. The title will always be visible.

<div class="item">
    <!-- Title -->
    <h2>Click me to expand</h2> 

    <!-- Content wrapper -->
    <div class="content_w"> 

        <!-- Content -->
        <div class="content"> 
            Lorem ipsum dolor sit amet, consectetur adipisicing elit, 
            sed do eiusmod tempor incididunt ut labore et dolore 
            magna aliqua. Ut enim ad minim veniam, quis nostrud 
            exercitation ullamco laboris nisi ut aliquip ...
        </div>
    </div>
</div>

I put the content div inside a wrapper (div.content_w). Instead of changing the max-height of the real content div, we change the wrapper's. This way, we can achieve the sliding up/down effect while still retaining the computed height of the real content div.

Style sheet


Here is the style sheet. Please notice that the max-height and transition are set on the content wrapper rather than the content itself. The content will remain unchanged no matter if the wrapper is collapsed or expanded. The content wrapper needs to have "overflow: hidden" in order to hide its contained content when the wrappers's height becomes less than the content's.

.item {
    width: 400px;
}

/* Content wrapper */
.content_w {
    overflow: hidden;
    max-height: 0;
    -webkit-transition: max-height 0.5s;
       -moz-transition: max-height 0.5s;
         -o-transition: max-height 0.5s;
            transition: max-height 0.5s;
}

JavaScript


With a little help from jQuery, I toggle the max-height between 0 and the content height based on the "open" class which I use simply to mark the expanded and collapsed state.

(function($) {

  // max-height transition. 
  // Inspired by http://jsfiddle.net/adambiggs/MAbD3/
  function toggleContent($contentWrapper) {
    // Get the computed height of the content
    var contentHeight = $('.content', $contentWrapper).outerHeight(true);

    // Add or remove class "open"
    $contentWrapper.toggleClass('open');

    // Set max-height
    if ($contentWrapper.hasClass('open')) {
      $contentWrapper.css('max-height', contentHeight);
    }
    else {
      $contentWrapper.css('max-height', 0);
    }
  }

  // Listen to click events on the item element 
  $('.item').on('click', function(e) {
    e.preventDefault();

    toggleContent($('.content_w', this)); 
  });

})(jQuery);​

One thing to notice is that I didn't use jQuery to do the transition. The transition is done by CSS. jQuery is used only for selecting DOM elements, marking elements, and applying CSS styles. You can replace jQuery with any of your favorite JavaScript libraries.

Limitations


The max-height is set to the content height when it is expanded. So if the content changes or re-flows later, some content will be clipped. The extra code that Adam put there is to prevent this by setting max-height to a really big number at the end of the expand transition. However, if you need a simple slideup box whose content and layout won't change after expansion, then this solution should work fine for you.

An improved solution can be found in this post.

Saturday, March 24, 2012

Bring GVIM menu back in Ubuntu 11.10

GVIM menu bar disappeared in Ubuntu 11.10 with Unity if gvim is launched from command line. This post is a compilation of things that I tried to fix the issue.

First, you might need to remove Vim from the .gnome2 dir:

rm ~/.gnome2/Vim

Then, add the following line to ~/.bashrc

alias gvi='gvim -f'

Restart terminal. Type gvi, and you should see the gvim with menu in the unity top bar. People report that the "-f" should fix the menu problem.

If you don't want to block a terminal when opening gvim, replace the above alias line with this:

gvi() {
  gvim -f $@ &
}

Next time, when you type gvi, the gvim process will be put in background.

If you want to keep gvim running even after its terminal is closed, try this:

gvi() {
  gvim -f $@ &
  disown
}

References:


Friday, March 2, 2012

Don't use # to create empty links

An empty link is a <a> tag that doesn’t link to anywhere. Oftentimes, we have these links where we want to ignore the href attribute and customize the behavior for the click events. To make an empty link, usually, we put a “#” in the href attribute, like this:

<a href=”#”>link</a>

This approach is simple, however, the “#” href introduced two issues:

1. It creates an entry in browser history whenever users click on the link.

2. (This is caused by the first issue) If the link is in the middle of a page, after clicking the link, users will be brought back to the top of the page.

A # in a href attribute points to an anchor. If the anchor name is empty, it refers to the top of the current page itself. When user clicks on such # link, browsers treat # as a normal navigation, and put it to the history stack. When users click on the back button, they expect a previous page, however, what they will see is the current page again, because the # is at the top of the history stack, and it points to the current page. If users click the # link multiple times, a same amount of # entries will be put to the history stack, and users have to click back button several times in order to go back to the real previous page. This behavior certainly introduces confusions to users.

Since browser treats # as a normal navigation, and # refers to the top of the current page, when users click on the # links, browser will display the current page with the vertical scroll bar being reset to the top. User will lose their scrolling positions. This will be a big problem for long pages.

The correct way to create an empty link is:

<a href=”javascript:void(0);”>link</a>

This will make browser ignore the href attribute, and won’t introduce a new entry in browser history.