Digg-Style Animated Sliding Comment Box

Awhile back on Digg there were a couple posts about animated sliding boxes similar to what Digg uses for its comment system. The two more popular ones, How to Create Digg Comment Style Sliding DIVs with Javascript and CSS and Re: How to Create Digg Comment Style Sliding DIVs with Javascript and CSS, failed to take one thing into account that annoyed me a bit. If you compare the two examples with Digg’s comment boxes you’ll notice that Digg’s fix the content of the box to the bottom where the other two fix the content to the top. Maybe it’s just me be anal, but I decided to figure out how to do it both ways.

CSS

We'll start with the cascading style sheets as they're the key to making this work correctly. The main container, the one that will actually expand and collapse, is defined below as "collapse". This needs to have a "overflow: hidden" so that during the expand and collapse the content will not display outside the bounds of the container and won't display scroll bars. Since I want my container to start out hidden, I'm also setting "display: none".

In order for this to work correctly the content container, defined below as "collapseContent" must have "position: relative".

  1. .collapse {
  2.         position: relative;
  3.         display: none;
  4.         background-color: #eeeeee;
  5.         overflow: hidden;
  6. }
  7. .collapseContent {
  8.         position: relative;
  9.         display: block;
  10. }

Events

The fireEvent calls and the example at the end both depend on my previous articles Custom JavaScript Event Listeners and JavaScript Built-In Listeners and Memory Leaks so be sure to check those out.

Constructor

The constructor "CollapsibleBox" takes 3 arguments - duration, fixbottom, and startopen. duration is the amount of time in milliseconds for the container to open and close. fixbottom is a boolean value to determine whether the contents are fixed to the bottom of the main container (true) or not (false). startopen is a boolean value to determine whether the whether the box starts open (true) or closed (false).

I took the approach of creating the main container, "collapse", and content container, "content", programmatically. I'm sure there are plenty of arguments out there against this approach, but it's the one I prefer because it give me complete control over the behavior of the primary DOM element.

  1. function CollapsibleBox(duration,fixbottom,startopen)
  2. {
  3.         this.fixbottom = fixbottom;
  4.         this.open = startopen;
  5.         if(duration !==
  6.         this.duration = duration;
  7.         this.currentframe = 0;
  8.         this.animationState = 'end';
  9.         this.collapse = document.createElement('div');
  10.         this.collapse.className = 'collapse';
  11.         this.content = document.createElement('div');
  12.         this.content.className = 'collapseContent';
  13.         this.collapse.appendChild(this.content);
  14. }

Retrieval of Containers

To place content inside the content container simply call getContent and then add your content to the returned object. getContainer has been included so you can place the main container wherever you like in the page, but also in case you need to modify the main container's styles or get information related to its layout.

  1. CollapsibleBox.prototype.getContainer = function()
  2. {
  3.         return this.collapse;
  4. };
  5. CollapsibleBox.prototype.getContent = function()
  6. {
  7.         return this.content;
  8. };

Animate

The animate method is the core of this object so I'll try to step through this will a little more detail.

var height = this.collapse.scrollHeight;

This line gets the total scrollable height of the content container. This is used to determine the final height of the main container.

var fps = 30/1000;

This line sets the frames per second to animate the open/close action. I chose the de facto standard of 30 frames per second.

var fpd = this.duration * fps;

This line determines how many total frames to show over the set animation duration.

var pxPerFrame = height/fpd;

This line determines how many frames to adjust the height of the container for each frame.

The following five lines make the calculation for the new container height based on the current frame number and whether the box is opening or closing, and then set the height.

var st = newheight - height;

This line determines the difference between the new main container height and the content height. This is important for two reasons. The first is if you want the content fixed to the bottom of the container then this.content.style.top gets set to this value. The second is to check whether the animation is complete or not. If st is less than 0 and the absolute value of st is less than the content height then continue on with the animation, otherwise finish up.

The last few lines sets the animationState to 'end' to make sure the code doesn't try to open or close again then, if the box was closing, it sets display to none and fires a close event, otherwise it fires a open event.

  1. CollapsibleBox.prototype.animate = function()
  2. {
  3.         if(this.animationState != 'end')
  4.         {
  5.                 var height = this.collapse.scrollHeight;
  6.                 var fps = 30/1000;
  7.                 var fpd = this.duration * fps;
  8.                 var pxPerFrame = height/fpd;
  9.                 if(this.animationState == 'closing')
  10.                         var newheight = Math.round(height - (pxPerFrame * this.currentframe));
  11.                 else
  12.                         var newheight = Math.round(pxPerFrame * this.currentframe);
  13.  
  14.                 this.collapse.style.height = newheight + 'px';
  15.                 var st = newheight - height;
  16.                 if(this.fixbottom)
  17.                         this.content.style.top = st + 'px';
  18.  
  19.                 this.currentframe++;
  20.                 if(st < 0 && Math.abs(st) < height)
  21.                         SetTimeout(this.animate.bind(this),fps);
  22.                 else
  23.                 {
  24.                         var state = this.animationState;
  25.                         this.animationState = 'end';
  26.  
  27.                         if(state == 'closing')
  28.                         {
  29.                                 this.collapse.style.display = 'none';
  30.                                 gEVENT.fireEvent(null,this,'close',this);
  31.                         }
  32.                         else if(state == 'opening')
  33.                                 gEVENT.fireEvent(null,this,'open',this);
  34.                 }
  35.         }
  36. };

Show/Hide and isOpen

To start an open animation the box first needs to be set to a positive, but very small, pixel value (1 in this case) and set display to block. Some object parameters also need to be set to make sure the animation performs properly; open needs to be set to true, currentframe needs to be set to 1, and animationState needs to be set to 'opening'. Finally, before starting the animation an 'opening' event gets fired.

To start a hide animation almost the exact opposite needs to happen. Since we want to see the animation the size and display values are changed at the end of the animation, but open needs to be set to false, currentframe needs to be set to 1, and animationstate needs to be set to 'closing'. Again, before starting the animation a 'closing' event gets fired.

The isOpen method is used to determine whether the box is open or closed.

  1. CollapsibleBox.prototype.showBox = function()
  2. {
  3.         this.collapse.style.height = '1px';
  4.         this.collapse.style.display = 'block';
  5.         this.open = true;
  6.         this.currentframe = 1;
  7.         this.animationState = 'opening';
  8.         gEVENT.fireEvent(null,this,'opening',this);
  9.         window.setTimeout(this.animate.bind(this),1);
  10. };
  11. CollapsibleBox.prototype.hideBox = function()
  12. {
  13.         this.open = false;
  14.         this.currentframe = 1;
  15.         this.animationState = 'closing';
  16.         gEVENT.fireEvent(null,this,'closing',this);
  17.         window.setTimeout(this.animate.bind(this),1);
  18. };
  19. CollapsibleBox.prototype.isOpen = function()
  20. {
  21.         return this.open;
  22. };

An Example

The example is a little verbose, but it's the easiest way to get everything in at once. To your body tag add onload="load()" which will call the following function.

  1. function load()
  2. {
  3.         var CB = new CollapsibleBox(1000,true,false);
  4.         var container = CB.getContainer();
  5.         var content = CB.getContent();
  6.         content.appendChild(document.createTextNode('Line 1'));
  7.         content.appendChild(document.createElement('br'));
  8.         content.appendChild(document.createTextNode('Line 2'));
  9.         content.appendChild(document.createElement('br'));
  10.         content.appendChild(document.createTextNode('Line 3'));
  11.  
  12.         var open = document.createElement('a');
  13.         open.appendChild(document.createTextNode('Open'));
  14.         gEVENT.addBuiltinListener(open,'click',CB.showBox,CB);
  15.         document.body.appendChild(open);
  16.         document.body.appendChild(document.createTextNode(' '));
  17.         var close = document.createElement('a');
  18.         close.appendChild(document.createTextNode('Close'));
  19.         gEVENT.addBuiltinListener(close,'click',CB.hideBox,CB);
  20.         document.body.appendChild(close);
  21.         document.body.appendChild(document.createElement('br'));
  22.         document.body.appendChild(container);
  23. }

First create a new CollapsibleBox instance and then get the container and content nodes. To the content container add some, well, content. Next create two links - one which calls showBox and the other which calls hideBox. Finally append the container to body. Now when you click "Open" or "Close" you'll see the container open or close with the content fixed to the bottom of the container.

The Final Result

  1. /**
  2.  * @memberOf CollapsibleBox
  3.  * @param {number} duration Time in milliseconds to open/close
  4.  * @param {boolean} fixbottom Whether to fix the content to the bottom (true) of the container or not (false)
  5.  * @param {boolean} startopen Start open (true) or closed (false)
  6.  * @constructor
  7.  */
  8. function CollapsibleBox(duration,fixbottom,startopen)
  9. {
  10.         this.fixbottom = fixbottom;
  11.         this.open = startopen;
  12.         this.duration = duration;
  13.         this.currentframe = 0;
  14.         this.animationState = 'end';
  15.  
  16.         this.collapse = document.createElement('div');
  17.         this.collapse.className = 'collapse';
  18.         this.collapse.style.display = startopen?'block':'none';
  19.         this.content = document.createElement('div');
  20.         this.content.className = 'collapseContent';
  21.         this.collapse.appendChild(this.content);
  22. }
  23. /**
  24.  * Returns the main div of the collapsible box
  25.  *
  26.  * @memberOf CollapsibleBox
  27.  * return {DOMNode}
  28.  */
  29. CollapsibleBox.prototype.getContainer = function()
  30. {
  31.         return this.collapse;
  32. };
  33. /**
  34.  * Returns the content div of the collapsible box
  35.  *
  36.  * @memberOf CollapsibleBox
  37.  * return {DOMNode}
  38.  */
  39. CollapsibleBox.prototype.getContent = function()
  40. {
  41.         return this.content;
  42. };
  43. /**
  44.  * Shows the contents
  45.  *
  46.  * @memberOf CollapsibleBox
  47.  */
  48. CollapsibleBox.prototype.showBox = function()
  49. {
  50.         this.collapse.style.height = '1px';
  51.         this.collapse.style.display = 'block';
  52.         this.open = true;
  53.         this.currentframe = 1;
  54.         this.animationState = 'opening';
  55.         gEVENT.fireEvent(null,this,'opening',this);
  56.         window.setTimeout(this.animate.bind(this),1);
  57. };
  58. /**
  59.  * Animates the opening/closing of the contents
  60.  *
  61.  * @memberOf CollapsibleBox
  62.  */
  63. CollapsibleBox.prototype.animate = function()
  64. {
  65.         if(this.animationState != 'end')
  66.         {
  67.                 var height = this.collapse.scrollHeight;
  68.                 var fps = 30/1000;
  69.                 var fpd = this.duration * fps;
  70.                 var pxPerFrame = height/fpd;
  71.                 if(this.animationState == 'closing')
  72.                 {
  73.                         var newheight = Math.round(height - (pxPerFrame * this.currentframe));
  74.                 }
  75.                 else
  76.                 {
  77.                         var newheight = Math.round(pxPerFrame * this.currentframe);
  78.                 }
  79.  
  80.                 this.collapse.style.height = newheight + 'px';
  81.                 var st = newheight - height;
  82.                 if(this.fixbottom)
  83.                 {
  84.                         this.content.style.top = st + 'px';
  85.                 }
  86.                 this.currentframe++;
  87.                 if(st < 0 && Math.abs(st) < height)
  88.                 {
  89.                         window.setTimeout(this.animate.bind(this),fps);
  90.                 }
  91.                 else
  92.                 {
  93.                         var state = this.animationState;
  94.                         this.animationState = 'end';
  95.  
  96.                         if(state == 'closing')
  97.                         {
  98.                                 this.collapse.style.display = 'none';
  99.                                 this.collapse.style.height = 'auto';
  100.                                 gEVENT.fireEvent(null,this,'close',this);
  101.                         }
  102.                         else if(state == 'opening')
  103.                         {
  104.                                 this.collapse.style.height = 'auto';
  105.                                 gEVENT.fireEvent(null,this,'open',this);
  106.                         }
  107.                 }
  108.         }
  109. };
  110. /**
  111.  * Hides the contents
  112.  *
  113.  * @memberOf CollapsibleBox
  114.  */
  115. CollapsibleBox.prototype.hideBox = function()
  116. {
  117.         this.open = false;
  118.         this.currentframe = 1;
  119.         this.animationState = 'closing';
  120.         gEVENT.fireEvent(null,this,'closing',this);
  121.         window.setTimeout(this.animate.bind(this),1);
  122. };
  123. /**
  124.  * Returns whether the box is open or not
  125.  *
  126.  * @memberOf CollapsibleBox
  127.  * @return {boolean}
  128.  */
  129. CollapsibleBox.prototype.isOpen = function()
  130. {
  131.         return this.open;
  132. };

EDIT: 4-25-2007

I discovered a couple of bugs/omissions that should be noted.

The first is the omission of changing the display property within the constructor based on whether startopen is true or false. The following code should be included after the creation of this.collapse.

  1. this.collapse.style.display = startopen?'block':'none';

The second is a bug which presents itself if the content of the box changes sizes. The design of the box is to change size to fit it's contents (I may want to make this a parameter at some point to determine whether it's a fixed size or not). However, during the animation stage the height of the box has to be set to an explicit value. Because of this if the content changes size then the box will not resize to show all the contents.

This code:

  1. if(state == 'closing')
  2. {
  3.         this.collapse.style.display = 'none';
  4.         gEVENT.fireEvent(null,this,'close',this);
  5. }
  6. else if(state == 'opening')
  7. {
  8.         gEVENT.fireEvent(null,this,'open',this);
  9. }

should be changed to this:

  1. if(state == 'closing')
  2. {
  3.         this.collapse.style.display = 'none';
  4.         this.collapse.style.height = 'auto';
  5.         gEVENT.fireEvent(null,this,'close',this);
  6. }
  7. else if(state == 'opening')
  8. {
  9.         this.collapse.style.height = 'auto';
  10.         gEVENT.fireEvent(null,this,'open',this);
  11. }

The "Final Result" code has been modified to reflect these changes.

Example

Here are the source files:

sliding-box.html
sliding-box.js