.eternal
ad infinitum
In the field of observation, chance favors only the prepared mind.
![]()
Building a Tab control using Activator – Part I
I have been planning a site design update for quite a while: the first quick designs were done in the middle of January. One of the things I have built into the design is a toolbar menu type control where rolling over each main item shows the sub-items similar to the one used here. Using my immense prowess as a thinker, I quickly realized that this is basically a tab control using mouseovers instead of mouseclicks. I could, of course, simply have used the one from Livepipe, but where would be the fun in that?
The Widget.Activator control is extremely adept at things involving highlighting and selecting of elements. It is intended not only to be used as a button control but also as a basis for other controls. It makes sense to use it as the basis of a tab control which is essentially a toolbar which shows and hides various content panes.
Traditionally, tab views are developed using an unordered list <ul> with the list items <li> containing anchors <a> which are set to link to the id’s of the various panels (href=’#panelId’). I will stick to this design but also allow it to be overridden in the options passed to the tab view widget.
Because this control is going to use existing markup, I decided to start with some HTML. It’s always easiest to start with something you already have, so I stole the demo from the image transitions test because it already contains a layout similar to what I want. I modified the navigation div so that the markup is what we want:
<div id='navigation' class='center'> <ul> <li><a href='#content-ground' id='nav-ground'>Ground</a></li> <li><a href='#content-sky' id='nav-sky'>Sky</a></li> <li><a href='#content-orange' id='nav-orange'>Orange</a></li> </ul> </div>
which follows the standard pattern. I added a few more styles:
#navigation ul {
margin: 0px;
padding: 0px;
}
#navigation li {
display: inline;
line-height: 24px;
height: 24px;
padding: 3px;
margin: 0px;
}
#navigation a {
color: #eee;
padding-left: 4px;
padding-right: 4px;
}
.selected {
background: red;
}
The #navigation styles make the unordered list items appear in a single row with no bullets rather than as a normal unordered list. The .selected class is used by the activator to style the selected element.
Finally, I removed the image transitions scripts, added the ids used in the hrefs of the navigation div to the first 3 paragraphs and deleted the other 2, and added a script near the top with a src=’tabs.js’. Whether that was less work than writing a new test page from scratch, I’m not sure, but it felt like it. As a point of interest, I get my Lorem Ipsum text from the Lorem Ipsum generator.
The next step is to create the tabs.js file and start with a new widget. When I develop, I use Firefox with Firebug. This allows me to inspect the markup, gives me good JavaScript debugging capabilities, and has the console, which is handy to write debug messages to. To start with, I just wanted the tab control to create the activator and make sure that the tabs were picked up, and that each tab points to the correct content:
var Widget = window.Widget || {};
Widget.Tabs = Class.create({
initialize: function(element, options) {
this.element = $(element);
this.activator = new Widget.Activator("li", {
container: this.element
});
this.activator.getElements("*").each(function(e) {
console.log(e.memo.element);
$$(e.down("a[href]").getAttribute("href")).each(function(li) {
console.log(li)
});
}.bind(this));
},
}
First this code creates an activator telling it that it should activate li elements and that it has a container which is the element passed into the tabs widget. The getElements method returns an array of all the elements that the activator has found. It then iterates over those using each to log the element. The down method uses a CSS selector to get the specified child of the element, in this case an <a> tag with a href attribute. The $$ function finds all elements that match the CSS selector. In this case we are using getAttribute to get the href. So, essentially, the $$ method is passed “#content-ground” which is the href of the first <a> element and it returns all elements that match that, in this case the first paragraph. I did this just to make sure that everything is working as advertised, and satisfied that it was I continued. I simply changed the contents of the outer loop over the activator elements:
this.activator.getElements("*").each(function(e) {
$$(e.down("a[href]").getAttribute("href")).invoke("hide");
}.bind(this));
The invoke method invokes a method on each element of the array. In this case hide and you can imagine what that does.
Now we need a show method. I want to be able to show the contents of a tab by passing the index or the id of the tab. Additionally, the show method must be able to handle the activator event to change the tab. The activator has a getElement method which can take either an index or id (in ‘#identity’ form), so that part is easy, and the activator passes the element of an event in the memo of the event. So the show method looks like this:
show: function(e) {
var tab;
if(Object.isNumber(e) || Object.isString(e)) {
tab = this.activator.getElement(e);
} else {
tab = this.activator.isElement(e.memo.element);
}
if(!tab) return;
if(this.selected) {
this.activator.setSelected(this.selected, false);
$$(this.selected.down("a[href]").getAttribute("href")).invoke("hide");
}
this.selected = tab;
this.activator.setSelected(this.selected, true);
$$(this.selected.down("a[href]").getAttribute("href")).invoke("show");
}
The method first checks the type of the parameter e by using Object.isNumber and Object.isString. If it is a number or string, it attempts to get the tab from the activator using getElement. The isElement method, checks if an element is in fact one of the elements that the activator is observing. If it is, the element is returned. If not it recursively checks the parent element, until it runs out of parents (and returns null) or finds an element that it is observing (and returns that).
If no tab is found, exit the function. The tab view will keep track of the selected element in this.selected which is null by default. If there is a selected tab, the method deselects it and hides the content using the same logic as used in the constructor. It then sets the selected tab, selects it, and shows it’s content by invoking show in the same way as hide was used before.
Finally, adding these two lines to the bottom of the constructor, sets the whole thing in motion:
this.activator.observe(this.activator.options.events.mouseover, this.show.bind(this)); this.show(0);
The first line observes the mouseover event so that the selected tab and content will change as the user hovers over the tabs. The second line just shows the first tab.
Now, as you can see, we have a tab widget that meets my requirements perfectly in just over 30 lines of code, some markup and a bit of styling. Still, meeting the requirements does not a reusable widget make. In the next parts, I will add options that can change the behaviour of the tab widget, add events that can be observed and are cancellable, add documentation and wrap the whole thing up with minified versions and a proper demo page with downloads.

In the field of observation, chance favors only the prepared mind.



January 11th, 2009 at 11:15
[...] the first part in this series we built a very simple tab widget which met the initial requirements, and then was [...]