A Low Tech Approach to Wraparound Scrolling Lists
Some time ago, I had cause to build a web page that contained a wraparound scrolling list. A visitor to the page could see all of the items in the list, and when an item was clicked the whole list scrolled to bring that item to the top. Given that the direction of scrolling didn't matter in this case, I wrote the list in the way described in this post. The sample layout given here will work in modern browsers, but not quite correctly in IE6 due to the margin doubling float bug - there are a couple of ways to fix that, which I leave as an exercise for the reader rather than clutter up the examples in this post.
Firstly, you will need two nested divs to hold the contents of your list. In the example below my list is just the letters A to L, which you'll note I'm repeating twice. You'll want to set the outer frame to hide its overflowed content, and to be half as wide as your list when the size is measured in pixels. That is accomplished below with CSS element size declarations, alongside a few extra look and feel items for clarity:
<style type="text/css"> #stationary-frame { overflow-x: hidden; width:460px; border: 1px solid #000; } #moving-frame { width:920px; } #moving-frame div { border: 2px solid #990000; float: left; margin: 3px; padding: 5px; text-align: center; width: 20px; cursor: pointer; } .top { background-color: #ffcccc; } .clear { clear: both; } </style> <div id="stationary-frame"> <div id="moving-frame"> <div class="first top">A</div> <div class="first">B</div> <div class="first">C</div> <div class="first">D</div> <div class="first">E</div> <div class="first">F</div> <div class="first">G</div> <div class="first">H</div> <div class="first">I</div> <div class="first">J</div> <div class="first">K</div> <div class="first">L</div> <div class="second">A</div> <div class="second">B</div> <div class="second">C</div> <div class="second">D</div> <div class="second">E</div> <div class="second">F</div> <div class="second">G</div> <div class="second">H</div> <div class="second">I</div> <div class="second">J</div> <div class="second">K</div> <div class="second">L</div> </div> <div class="clear"></div> </div>
The two repeated lists are differentiated from one another by a class assignment - that will come in useful later. Sizing the frame and list elements is a pain to do by hand, given that (a) you're probably going to be tinkering with your list throughout development and (b) there will no doubt be multiple layers of padding and margins involved along the way. So why not let the code do it for you? Here is an example, using jQuery:
var frameWidth = 0; jQuery(document).ready(function () { jQuery("#moving-frame div").each(function() { frameWidth += jQuery(this).outerWidth(true); }); jQuery("#stationary-frame").css("width", frameWidth / 2); jQuery("#moving-frame").css("width", frameWidth); });
So now you have something that looks like this:
Only the first half of the doubled list is displayed, and the initial bad guesses at div width in the hand-written CSS have been replaced with calculated values via Javascript. This isn't terribly exciting or dynamic, however, as the scrolling mechanism has yet to be added. So how to scroll the list in response to user actions? I'll take the easy way out and use jQuery's built in animation function. In the following code, I'm applying an order to the list elements in an extra attribute, creating the animation function, and binding it to click events on the items in the list:
var currentListPosition = new Number(0); var listLocked = false; // this function will be bound to click events and called in a jQuery // context, meaning I can make use of "this" as the element clicked var animateFunction = function animateFunction(event) { if( listLocked ) { return; } listLocked = true; // make sure this is a number; otherwise string comparisons will be made var clickPosition = new Number(jQuery(this).attr("no").substring(2)); if( clickPosition == currentListPosition ) { // the list is where it should be, do nothing. listLocked = false; return; } // note that distance is calculated by looking at all the elements // so this will still work for a list of elements with different widths var animationParam = "+=0"; var distance = 0; if(currentListPosition < clickPosition) { // move the list left for(i = currentListPosition; i < clickPosition; i++) { distance += jQuery("#moving-frame div[no=n_" + i + "]").outerWidth(true); } animationParam = "-=" + distance; } else if (currentListPosition > clickPosition) { // move the list right for(i = clickPosition; i < currentListPosition; i++) { distance += jQuery("#moving-frame div[no=n_" + i + "]").outerWidth(true); } animationParam = "+=" + distance; } // move the highlight - note that we're highlighting two different // elements in the double list, as either or both may be visible. jQuery("#moving-frame div.top").removeClass("top"); jQuery("#moving-frame div[no=n_" + clickPosition + "]").addClass("top"); // roll the animation jQuery("#moving-frame").animate({ marginLeft: animationParam }, 1000, 'swing', function() { currentListPosition = clickPosition; listLocked = false; } ); } var count1 = 0; var count2 = 0; jQuery(document).ready(function () { jQuery("#moving-frame div").each(function() { if( jQuery(this).hasClass("first") ) { jQuery(this).attr("no", "n_" + count1); count1 = ++count1; } else { jQuery(this).attr("no", "n_" + count2); count2 = ++count2; } jQuery(this).click(animateFunction); }); });
The functional result is shown below: click a few boxes, and you'll see how it works.
One caveat if you intend to adapt and use this technique: Javascript-enabled animation of DOM elements is prone to poor visual quality, with ragged motion instead of smooth motion. A number of different browser versions exhibit this undesirable behavior if any other significant Javascript activity is taking place at the same time as the animation. Additionally, some browsers cannot under any circumstances produce smooth animations when moving complex nested trees of DOM elements. So:
- Keep the contents of the list as simple as possible.
- Test your implementation on a variety of browsers and versions.
If you must run another animation or costly Javascript function concurrently with the list animation - for example if your list is a selector, and you want to alter other parts of the page in addition to the list itself - you might try a callback strategy, such as:
// roll the animation with a callback function jQuery("#moving-frame").animate({ marginLeft: animationParam }, 1000, 'swing', doOtherStuffFunction); var doOtherStuffFunction = function doOtherStuff() { ... }
Using the callback function to trigger other work you might have for Javascript avoids the mess made by many browsers when they try to run costly tasks concurrently.