Thursday, April 1, 2010

A mootools based horizontal scrollable list

Hi,
today I write about a nice javascript class that creates a typical horizontal scrollable list starting from an html unordered list. The final result is like many others in the web. The LI elements of a list are used to create the items of this scrollable list. The number of items visible at a time is a paramether, and if is greater than the maximum possible one, is reduced in order to get the items to stay in their space. The controller are two arrow buttons on the left and right of the slide. They have three states: ON, OFF and OVER controlled through javascript and personalizable by css. The width of the entire object and of the single item are needed, the height of the single item is not necessary because is calculated by javascript (through an iteration along all items), but clearly for a good result all items would rather have the same height.
Let's see the details now.
Imagine we have an unordered list where the LI elements are images set as css background of a DIV, for example:
<ul id="hlistVideo">
  <li>
    <div title="Durata: 00:00:10" style="background-color:#fff;background-image:url('upload/img/images.jpg'); background-position:center center;background-repeat:no-repeat; width:130px;height:100px;margin:10px;cursor:pointer;" /></div>
  </li>
  <li>
    <div title="Durata: 00:02:29" class="tooltip" style="background-color:#fff;background-image:url('upload/img/wildThings.png'); background-position:center center;background-repeat:no-repeat; width:130px;height:100px;margin:10px;cursor:pointer;" /></div>
  </li>
  <li>
    <div title="Durata: 00:02:18" class="tooltip" style="background-color:#fff;background-image:url('upload/img/trucker.png'); background-position:center center;background-repeat:no-repeat; width:130px;height:100px;margin:10px;cursor:pointer;" /></div>
  </li>
</ul>
Now, we want to transform it in a classical scrollable horizontal list, like the one represented in the image (the same one).
Well, here comes my mootools based javascript class which coupled with some css realizes it. First the code, then the usage
/*
 * hScrollingList class written by abidibo, 01/04/2010
 *
 * hScrollingList method: constructor
 *   Syntax
 *      var myInstance = new hScrollingList(list, vpItems, scrollableWidth, itemWidth, [options]);
 *   Arguments
 *      1. list - (string|Object) The UL element or its id attribute to be transformed
 *      2. vpItems - (int) The number of element showed in a viewport (the viewport changes (scrolls) when clicking on the arrows)
 *      3. scrollableWidth - (int) The width in px of the scrollable object
 *      4. itemWidth - (int) The width in px of a list element
 *    5. options - (object, optional) The options object.
 *   Options
 *    - id (string: default to null) The id of the object
 *      ........ maybe many more options in the future.........
 *
 */
var hScrollingList = new Class({

    Implements: [Options],
    options: {
        id: null
        // maybe more options in the future here
    },
        initialize: function(list, vpItems, scrollableWidth, itemWidth, options) {
  
        if($defined(options)) this.setOptions(options);

        this.list = $type(list)=='element'? list:$(list);
        this.listElements = this.list.getChildren('li');

        this.setWidths(scrollableWidth, itemWidth);
        this.vpItems = vpItems;
      
        this.setSlide();
        this.setStyles();  // vpItems property may change!
        this.setWrapper();

        this.vps = 1;
        this.tots = Math.ceil(this.listElements.length/this.vpItems);
        this.updateCtrl();
        this.tr = new Fx.Tween(this.slide, {
                'duration': 1000,
                'transition': 'quad:out',
                'onComplete' : function() {this.busy=false}.bind(this)
            });


    },
    setWidths: function(tw, iw) {
        this.width = tw;
        this.ctrlWidth = 24;
        this.cWidth = this.width - 2*this.ctrlWidth;
        this.iWidth = iw;
    },
    setSlide: function() {
        var clear = new Element('div', {'styles':{'clear':'both'}});
        this.slide = new Element('div', {
            'styles': {'position':'relative', 'width':'10000em', 'background-color':'#ccc'}  
        });
        this.slide.inject(this.list, 'before');
        this.slide.grab(this.list);
        clear.inject(this.slide, 'bottom');
    },
    setWrapper: function() {
        this.wrapper = new Element('div', {
                'styles':{'width': this.width}
        });      
        var ctrlHeight = this.listElements[0].getCoordinates().height;
        for(var i=1; i<this.listElements.length; i++)
            if(this.listElements[i].getCoordinates().height > ctrlHeight)
                ctrlHeight = this.listElements[i].getCoordinates().height;

        this.leftCtrl = new Element('div', {
            'styles': {'float': 'left', 'width': this.ctrlWidth+'px', 'height':ctrlHeight+'px'}
        })
        this.rightCtrl = new Element('div', {
            'styles': {'float': 'right', 'width': this.ctrlWidth+'px', 'height':ctrlHeight+'px'}      
        })
        this.itemContainer = new Element('div', {
            'styles': {'float': 'left', 'width': this.cWidth+'px', 'overflow':'hidden'}      
        })
        this.wrapper.adopt(this.leftCtrl, this.itemContainer, this.rightCtrl);
        this.wrapper.inject(this.slide, 'before');
        this.itemContainer.adopt(this.slide);
    },
    setStyles: function () {
        this.list.setStyles({'margin': '0', 'padding': '0', 'list-style-type':'none', 'list-style-position':'outside'});

        var esw = this.vpItems*this.iWidth;
        while(esw>this.cWidth) esw = --this.vpItems*this.iWidth;
        var margin = (this.cWidth - esw)/2;

        for(var i=0; i<this.listElements.length; i++) {
            var item = this.listElements[i];
            var r = i%this.vpItems;
            item.setStyles({
                'float':'left',
                'width': this.iWidth+'px',
                'margin-left': !i ? margin+'px' : r ? '0px' : 2*margin+'px'  
            })
        }
    },
    scroll: function(d) {
      
        if(this.busy) return false;

        this.busy = true;
        if(d=='right')
            this.tr.start('left', '-'+(this.cWidth*this.vps++)+'px');
        else if(d=='left')
            this.tr.start('left', '-'+(this.cWidth*(--this.vps-1))+'px');
  
        this.updateCtrl();
    },
    updateCtrl: function() {

        var lclass = this.vps == 1 ? 'leftCtrlOff':'leftCtrl';
        var rclass = this.vps == this.tots ? 'rightCtrlOff':'rightCtrl';
        this.leftCtrl.setProperty('class', lclass);          
        this.rightCtrl.setProperty('class', rclass);      

        if(this.vps==1) {
            this.leftCtrl.removeEvents('mouseover');
            this.leftCtrl.removeEvents('mouseout');
            this.leftCtrl.removeEvents('click');
            this.le = false;
        }
        else if(!this.le) {
            this.leftCtrl.addEvent('mouseover', function() {this.setProperty('class', 'leftCtrlOver')});
            this.leftCtrl.addEvent('mouseout', function() {this.setProperty('class', 'leftCtrl')});
            this.leftCtrl.addEvent('click', this.scroll.bind(this, 'left'));
            this.le = true;
        }

        if(this.vps == this.tots) {
            this.rightCtrl.removeEvents('mouseover');
            this.rightCtrl.removeEvents('mouseout');
            this.rightCtrl.removeEvents('click');
            this.re = false;
      
        }
        else if(!this.re) {
            this.rightCtrl.addEvent('mouseover', function() {this.setProperty('class', 'rightCtrlOver')});
            this.rightCtrl.addEvent('mouseout', function() {this.setProperty('class', 'rightCtrl')});
            this.rightCtrl.addEvent('click', this.scroll.bind(this, 'right'));
            this.re = true;
        }

    }
});
Now the css styles used in my example (if you wanna try this code remember to make your own arrows images)
div.leftCtrl {
    background: #b8b7b7 url('../img/arrowSX.png') no-repeat center center;
    cursor:pointer;
}
div.rightCtrl {
    background: #b8b7b7 url('../img/arrowDX.png') no-repeat center center;
    cursor:pointer;
}
div.leftCtrlOver {
    background: #929191 url('../img/arrowSX.png') no-repeat center center;
    cursor:pointer;
}
div.rightCtrlOver {
    background: #929191 url('../img/arrowDX.png') no-repeat center center;
    cursor:pointer;
}

div.leftCtrlOff {
    background: #b8b7b7 url('../img/arrowSX.png') no-repeat center center;
    opacity: 0.8;
    filter: alpha(opacity = 80);

}
div.rightCtrlOff {
    background: #b8b7b7 url('../img/arrowDX.png') no-repeat center center;
    opacity: 0.8;
    filter: alpha(opacity = 80);
}
Basically these css styles contains the three states of the two buttons: button ON, button OFF, and button OVER.
Now let's see how to use this class, very simple:
<script>var hList = new hScrollingList('hlistVideo', 5, 600, 150)</script>
So simply we instanciate the class hScrollingList, passing the paramethers:
  • 'hlistVideo': the id of the unordered list (we may pass even the DOM Object)
  • 5: the number of items 'for page', that is the number of elements in the viewport for each scrolling action
  • 600: the total width of the object
  • 150: the width of a single item (a LI element)
Observe that the class is able to auto-reduce the number of items 'for page', because it tries to put them in the space  allowed, and if thay can't stay correctly in the space their number is reduced by one untill they will.
So you only have to take this code, personalize the arrows images, the three states of the buttons through css and obtain your own personalized horizontal scrollable list.
For info or problems write at abidibo@gmail.com
Bye!