Monday, April 14, 2008

Cross Browser Rounded Borders Using Sprites

I was reviewing techniques for rounded borders and came across this. It uses four background images for each of the corners; however, they're not using a sprite. So, we'll do it with a single sprite. Once we're done, we'll have something like this that works on all the major browsers ( IE6/IE7, Firefox, Safari and Opera ) --

First, we'll take the four corner GIF images that they have, make them transparent ( using a bitmap editor ) and then use a sprite generator to create our sprite as well as the supporting CSS. I like to use the one from Website Performance.

Next, we'll take the layered approach which means that we'll nest absolutely positioned elements in a relatively positioned container. So, the four corners, the two horizontal and vertical lines are absolutely positioned.

Here's the structure --
    <div class="b2">
<div class="border-container">
<div class="sprite-tl2"></div><div class="sprite-tr2"></div>
<div class="horz-line top-line1"></div>
<div class="horz-line top-line2"></div>
<div class="content">
We're using sprites for rounded borders. This implementation works in IE6/IE7, Firefox 2.0.x, Safari 3.1 and Opera 9.27. <a href="">Read more.</a>
<div class="border-container">
<div class="sprite-bl2"></div><div class="sprite-br2"></div>
<div class="horz-line bottom-line1"></div>
<div class="horz-line bottom-line2"></div>
<div class="vert-line left-line1"></div>
<div class="vert-line left-line2"></div>
<div class="vert-line right-line1"></div>
<div class="vert-line right-line2"></div>

and here's the CSS --
.sprite-bl2{background: url(roundsSprite3.gif) 0 -30px no-repeat; position:absolute;width:11px;height:10px;top:0;left:0;}
.sprite-br2{background: url(roundsSprite3.gif) 0 -70px no-repeat; position:absolute;width:11px;height:10px;top:0;right:0;}
.sprite-tl2{background: url(roundsSprite3.gif) 0 -110px no-repeat; position:absolute;width:11px;height:10px;top:0;left:0;}
.sprite-tr2{background: url(roundsSprite3.gif) 0 -150px no-repeat; position:absolute;width:11px;height:10px;top:0;right:0;}
.content{padding:10px 10px;font-family:verdana;font-size:93%;}
.horz-line{position:absolute;border-top:1px solid #EC9E3C;width:50%;}
.vert-line{position:absolute;border-left:1px solid #EC9E3C;height:50%;_height:2em;}

It's all pretty self-explanatory. You can view the sample and the source here. Feel free to use and improve it!
Have fun!

Sunday, April 06, 2008

Persistent iGoogle Dashboards :: Redux

From time to time, I get questions about the iGoogle imitation example. The most often asked question is how to persist the layout once you've moved the containers. One way to do it is to use cookies. Because the original example used the YUI library to handle drag and drop and because YUI 2.5.1 now supports a beta cookie library, we'll take the cookie approach.

Keep in mind that this is very simple example. Our containers don't contain data and we don't have a backend ( i.e. PHP, JSP, etc. ). We're just going to persist layout and that's pretty easy to do with cookies.

Here's the

The general idea is that if the layout cookie isn't set, we'll show the default positions ( the default layout is already there; we've just set the display to "none" ). Otherwise, we'll layout based on what's found in the cookie.

What does the cookie look like?

Well, all of our containers ( "Container 1", "Container 2", etc. ) have static positions and are found in three columns. So, we'll use three subcookies ( c1, c2 and c3 ) where each subcookie contains a string consisting of element IDs. The cookie's name is "igoogimi" and expires every 14 days. If we were to look at the cookie on April 6, 2008, we'd see something like --

Name :: igoogimi

Value :: c1=Rec1%2CRec2&c2=Rec3%2CRec4%2CAbout&c3=Rec5%2CRec6
Expires :: Sun, 20 Apr 2008 05:41:55 GMT

Setting and getting a subcookie is pretty straightforward and it's
well documented. Setting our subcookie is interesting because we use the method YAHOO.util.Dom.getChildrenBy to query the DOM looking for the column IDs. Here's our setCookies method --
        var setCookies = function() {
// BEGIN :: Calculate cookie expiration to 14 days from today
var date = new Date();
// END :: Calculate cookie expiration to 14 days from today
var getNode = function(node) {
return ("Rec1"||"Rec2"||"Rec3"||"Rec4"||"Rec5"||"Rec6"||"About");
var createString = function(colId) {
var nodes = YAHOO.util.Dom.getChildrenBy(document.getElementById(colId), getNode);
var list = [];
var l = nodes.length;
for(var i=0;i<l;i++) {
list[i] = nodes[i].id;
return list.toString();
YAHOO.util.Cookie.setSub("igoogimi", "c1", createString("Column1"), {expires: date});
YAHOO.util.Cookie.setSub("igoogimi", "c2", createString("Column2"), {expires: date});
YAHOO.util.Cookie.setSub("igoogimi", "c3", createString("Column3"), {expires: date});

Notice that the method
createString creates the subcookies values by looking for all children of the node with the matching column ID which pass the boolean test defined as the getNode method. In this method, we check to see if any of the children have IDs equal to the containers. If so, then those children ( these are nodes ) are returned. From those children, a string consisting of IDs in the order of their appearance in the DOM is created.

We set the igoogimi cookie when the unload event is fired. This occurs when we navigate to another page, close the browser, etc. This ensures that the cookie is always set.

When we read the cookie, a number of things happen.

The entire page is hidden through CSS ( the body's display is set to "none" ). We do this because the default layout is always there. We don't want it to appear and then be replaced by the layout found in the cookie.

Once we've determined whether the cookie exists, we remove all the nodes while keeping a reference to them. Then, we'll add them back based on the order that they're found in the igoogimi cookie --

          var containerRef = [];
var node;

// BEGIN :: Removing the nodes
var cArray1 = c1.split(",");
var len = cArray1.length;
for(var i=0;i<len;i++){
node = document.getElementById(cArray1[i]);
node?containerRef[cArray1[i]] = node.parentNode.removeChild(node):"";
var cArray2 = c2.split(",");
len = cArray2.length;
for(var i=0;i<len;i++){
node = document.getElementById(cArray2[i]);
node?containerRef[cArray2[i]] = node.parentNode.removeChild(node):"";
var cArray3 = c3.split(",");
len = cArray3.length;
for(var i=0;i<len;i++){
node = document.getElementById(cArray3[i]);
node?containerRef[cArray3[i]] = node.parentNode.removeChild(node):"";
// END :: Removing the nodes

// BEGIN :: Adding the nodes
len = cArray1.length;
var col = document.getElementById("Column1");
var tmpCR;
for(var i=0;i<len;i++){
tmpCR = containerRef[cArray1[i]];
len = cArray2.length;
var col = document.getElementById("Column2");
for(var i=0;i<len;i++){
tmpCR = containerRef[cArray2[i]];
len = cArray3.length;
var col = document.getElementById("Column3");
for(var i=0;i<len;i++){
tmpCR = containerRef[cArray3[i]];
// END :: Adding the nodes

Keep in mind that we're passing through the DOM twice -- once for removal and another for insertion. So, it's not terribly efficient. Note that the removal and the insertions dynamically update the DOM and if we didn't hide the body, then the page would be jumping all over the place ( this is known as reflow where DOM manipulations are done when rendering is completed ). Once the last node has been added, we set the body's display to "block" and all the containers appear.

You can see our
example here. To review the code, simply view the source.

Feel free to use and make it better.

Have fun!