Unobtrusive & Persistant Script.aculo.us Effects
Nov 01 2005 10:07 PM
Update
Thanks to some pointers from Kyle, I've updated the code to use some of the functions built into Prototype (such as '$" (Dollar), addClassName, & removeClassName) and to use Behaviour's addLoadEvent. I also created a more generic example that isn't quite as specific to this site.
As part of the design of this site, I'm using the script.aculo.us library to make the boxes in the sidebar expand and contract when you click them. The important part to me was to make the effect unobtrusive (no extra markup in the HTML) and keep the position of each box persistant from page to page.
This turned into quite a long post that can really be divided into two sections:
The Box
First let's take a look at some example HTML markup.
-
<div id="recent">
-
<h3>Recently</h3>
-
<ul id="recent-body">
-
<li>Recent Post 1</li>
-
<li>Recent Post 2</li>
-
</ul>
-
</div>
A div with an id, a header, and an unordered list with an id - that's it for each box!
Now think of the box in 3 sections: the top (the header of each section), the middle (content), and the bottom.
Top:
The header background actually contains 2 box tops for the different positions, one with a minus (-) and one with a plus (+).
![]()
Look at the measurements for CSS purposes. The width of each box is 160px and height is 21px.
![]()
Notice in the CSS that the height + padding-top should equal 21px and the width + padding-left should equal 160px. Also, set the cursor to a pointer so the user knows it is clickable.
-
#sidebar h3 {
-
background: url(i/sidebar-h3-bg.jpg) no-repeat 0 0;
-
margin: 0;
-
cursor: pointer;
-
-
/* height + padding-top = 21px */
-
height: 14px;
-
padding-top: 7px;
-
-
/* width + padding-left = 160px */
-
width: 153px;
-
padding-left: 7px;
-
}
Create a CSS class to use for when the box is contracted and you need to show the other box top (the one with the plus). I use .invisible. Measure how many pixels it is the the beginning of the second box. In my example, it's 165 pixels.
-
#sidebar h3.invisible {
-
background: url(i/sidebar-h3-bg.jpg) no-repeat -165px 0;
-
}
Middle:
The content needs to have a border on each side with a little padding to seperate the text. I threw in a little gradient background for kicks as well. I use unordered lists for all the content in my sidebar, but with a few changes you can make the middle section whatever you like.
-
#sidebar ul {
-
background: url(i/sidebar-ul-bg.jpg) no-repeat 0 0;
-
border-left: 1px solid #A6B6C3;
-
border-right: 1px solid #A6B6C3;
-
padding: 4px 2px 0 2px;
-
}
Bottom:
I designed the bottom of the box to "fit in" with the top when it's contracted.
![]()
Position the bottom of the box at the bottom of the surrounding div. Set the padding-bottom equal to the height of the box bottom image.
-
#sidebar div {
-
background: url(i/sidebar-div-bg.jpg) no-repeat bottom left;
-
margin: 2px 0 10px 0;
-
padding-bottom: 5px;
-
border: none;
-
}
Files you'll need
Download the following and extract all the JavaScript files into a folder on your webserver. I like to keep all of my JavaScript files in a folder called "js."
Makin' the magic
Look again at the HTML for the "Recent Posts" box:
-
<div id="recent">
-
<h3>Recently</h3>
-
<ul id="recent-body">
-
<li>Recent Post 1</li>
-
<li>Recent Post 2</li>
-
</ul>
-
</div>
Think about it logically. We need to listen to each h3 element in the sidebar for the onclick event. Then when one is clicked, see if that h3 element has a class name of "invisible." If it does, then we'll run the scriptaculous effect "BlindDown" to bring it back into view and set the class to empty; if the class is already empty, we'll run the "BlindUp" effect to hide it, then set the class to "invisible." Easy enough right?
You can read the Behaviour instructions, but basically it allows you to create "rules" that specify elements to add JavaScript events to. This means that we don't have any JavaScript code in our HTML markup - totally unobtrusive baby! So whip out your favorite JavaScript editor and let's get to work!
Here's what the JavaScript "rule" looks like:
-
'#recent h3' : function(el){
-
el.onclick = function(){
-
if (Element.hasClassName(this, 'invisible')) {
-
new Effect.BlindDown('recent-body');
-
Element.removeClassName(this, 'invisible');
-
setCookie(this.parentNode.id, '', 365);
-
} else {
-
new Effect.BlindUp('recent-body');
-
Element.addClassName(this, 'invisible');
-
setCookie(this.parentNode.id, 'invisible', 365);
-
}
-
}
-
}
We're setting the onclick event for #recent h3 unobtusively. If it has the class name invisible, run the BlindDown effect; if it doesn't then run the BlindUp effect. Notice the Element.hasClassName, Element.removeClassName, and Element.addClassName. These are custom Prototype functions that'll work with multiple class names.
Create a new JavaScript file to store the code for your rules. Here's an example with two "boxes":
-
var theRules = {
-
'#box1 h3' : function(el){
-
el.onclick = function(){
-
if (Element.hasClassName(this, 'invisible')) {
-
new Effect.BlindDown('box1-body');
-
Element.removeClassName(this, 'invisible');
-
setCookie(this.parentNode.id, '', 365);
-
} else {
-
new Effect.BlindUp('box1-body');
-
Element.addClassName(this, 'invisible');
-
setCookie(this.parentNode.id, 'invisible', 365);
-
}
-
}
-
},
-
'#box2 h3' : function(el){
-
el.onclick = function(){
-
if (Element.hasClassName(this, 'invisible')) {
-
new Effect.BlindDown('box2-body');
-
Element.removeClassName(this, 'invisible');
-
setCookie(this.parentNode.id, '', 365);
-
} else {
-
new Effect.BlindUp('box2-body');
-
Element.addClassName(this, 'invisible');
-
setCookie(this.parentNode.id, 'invisible', 365);
-
}
-
}
-
}
-
};
-
-
Behaviour.register(theRules);
Simply create all the rules you need based on your HTML markup, then call Behaviour.register(Your Rules Variable) at then end.
Bakin' Cookies
Notice the setCookie function in there? We need to be able to read and set a cookie to store the each box's status (expanded or contracted) from page to page so add the following functions under your rules. Code adopted from here:
-
function setCookie(name,value,days) {
-
if (days) {
-
var date = new Date();
-
date.setTime(date.getTime()+(days*24*60*60*1000));
-
var expires = ";expires="+date.toGMTString();
-
} else {
-
expires = "";
-
}
-
document.cookie = name+"="+value+expires+";path=/";
-
}
-
-
function readCookie(name) {
-
var needle = name + "=";
-
var cookieArray = document.cookie.split(';');
-
for(var i=0;i <cookieArray.length;i++) {
-
var pair = cookieArray[i];
-
while (pair.charAt(0)==' ') {
-
pair = pair.substring(1, pair.length);
-
}
-
if (pair.indexOf(needle) == 0) {
-
return pair.substring(needle.length, pair.length);
-
}
-
}
-
return null;
-
}
Persistance Is Key
Now that we're storing the position in a cookie everytime the user clicks a box, we need to read those values when each page is generated in order to maintain each box's status as the user browses the site.
Create a variable boxIds that uses the $ (dollar) function with the id name of each div (box) that needs to be checked. The hideBoxes() function will check each cookie and "position" the boxes accordingly.
-
function hideBoxes() {
-
-
// Id names of all the "boxes"
-
boxIds = $("box1","box2");
-
-
for (i = 0; i <boxIds.length; i++) {
-
if (boxIds[i]) {
-
cookieValue = readCookie(boxIds[i].id);
-
if (cookieValue == 'invisible') {
-
var h3 = boxIds[i].getElementsByTagName('h3');
-
Element.addClassName(h3[0], 'invisible');
-
var kids = boxIds[i].childNodes;
-
for (j = 1; j <kids.length; j++) {
-
if (kids[j].id) {
-
Element.hide(kids[j]);
-
}
-
}
-
}
-
}
-
}
-
}
Last but not least, make sure the hideBoxes() function runs when the browser window loads. Behaviour includes a load event already, so you can just use that.
-
Behaviour.addLoadEvent(hideBoxes);
Final Thoughts
A lot of the code in this example is specific to this site, but the idea and framework is universal and easily adaptable to any site. There's a more generic example which you can view the source of to see how it works.
I really just wanted to show that it's pretty easy to add cool JavaScript effects from the script.aculo.us library to your site without mucking up all your HTML code.






wow
you’ve made excellent use of those unobtrusive scripts. I really, really like that!