Monthly Archives: April 2010

jQuery plugin: working scrollbar on left side of div

I’ve been building JSF views and creating custom components for a while and one issue we worked on was making scrollable tables, where the header and footer stay stationary and the data scrolls.

I’m still cleaning this up, but the general idea is that I wrapped the table in DIVs, which are set up to scroll vertically or horizontally based on the dynamic content of the table. This seems to be the only solution that is cross-browser, and it uses jQuery to dissect the table and move the header and footer so that the scrolling works correctly. Once I finalize that, i’ll post it.

But one thing was missing: the content of the tables sometimes pushes the table off the right side of the view, and the horizontal scrollbar appears. At this point, the vertical scrollbar is way off to the right and pretty much makes the table useless. So I wanted to move the vertical scrollbar to the left side, and I had trouble finding a cross-browser solution … until now.

I just found a jQuery plugin written by Brian Reavis at his website: 3rd Route. I tested it out and it worked immediately. The only thing i had to do to it was swap out all of the ‘$’ for ‘jQuery’ because of my use of the jQuery.noConflict() flag. I originally found it via stackoverflow, but the version on the website looks like it was cleaned up to enable chaining.

I tried other things, like multiple versions of style="direction:trl", but these felt messy when i read about them, and did not work when I implemented them.

Thanks Brian!

Update: I ended up doing a lot of changes because of some issues i found:

  • For very large tables, this code was really slow in IE.  I narrowed it down to the jQuery ‘append’ function and refactored the plugin to not use it at all
  • The original copied all components into the poser div.  I removed all of that and the poser div is basically now just the scrollbar.  Something major was required for using the plugin with JSF, because it was causing duplicate ID issues.  I decided to get rid of all of the extra components instead of just renaming the IDs.
  • I created a second version that has the scrollbar on both sides
  • I added a unique identifier class so that multiple instances could be on the same page.
  • I changed how the scroll updating worked, but the old way wasn’t broken so it wasn’t really an improvement.

So, based on Brian’s original design and implementation, here is what I am using now:

/**
 * jQuery plugin to add a scrollbar to the left side of a div.  It does this by
 * creating a false div on the left side of the table, and then having that div's scroll
 * position set on the original div every scroll event.
 * 
 * @link edited from http://thirdroute.com/css-js-left-vertical-scrollbars/, but heavily changed from the original version.
 */
jQuery.fn.leftAndRightScrollBar = function(){
	var items = jQuery(this);
	
	var randomString = function() {
		var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
		var string_length = 8;
		var randomstring = '';
		for (var i=0; i<string_length; i++) {
			var rnum = Math.floor(Math.random() * chars.length);
			randomstring += chars.substring(rnum,rnum+1);
		}
		return randomstring;
	}

	jQuery(function(){
		items.each(function(){
			// create unique classes for targeting the poser div
			var poserTargetingClass = randomString();
			
			var e = jQuery(this);
			var contentHeight = e.children('table:first').height();
			var content = e.html();
			var ie = !jQuery.support.boxModel;
			var w = e[ie?'innerWidth':'width'](), h = e[ie?'innerHeight':'height']();
			
			//calculate paddings
			var pad = {};
			jQuery(['top', 'right', 'bottom', 'left']).each(function(i, side){
				pad[side] = parseInt(e.css('padding-' + side).replace('px',''));
			});
			//detect scrollbar width
			var xfill = jQuery('<div class="xFill">').css({margin:0, padding:0, height:'1px'});
			e.append(xfill);
			var contentWidth = xfill.width();
			var scrollerWidth = e.innerWidth() - contentWidth - pad.left - pad.right;
			e.css('padding', '0');
			e.children('.xFill').remove();
			
			var poserHeight = h - pad.top - pad.bottom;
			var poser = jQuery('<div class="leftAndRightScrollPoser '+poserTargetingClass+'">')
				// create a div that forces height without copying the content to do it.
				.html('<div style="visibility:hidden;height:'+contentHeight+'px">.</div>')
				.css('overflow','auto')
				.height(poserHeight+(ie?pad.top+pad.bottom:0))
				.width(scrollerWidth-(ie?0:pad.left*2)) // only as wide as the scrollbar.
			;
			
			e
				.css({
					width: w+(ie?0:scrollerWidth)-(ie?0:pad.right+pad.left),
					height: h-(ie?0:pad.bottom+pad.top),
					marginTop: -poserHeight-pad.top*2,
					marginLeft: scrollerWidth
				})
				.css('overflow-y', 'auto')
				.css('overflow-x', 'hidden')
			;
				
			jQuery(['top', 'right', 'bottom', 'left']).each(function(i, side){
				 poser.css('padding-'+side, pad[side]);
				 e.css('padding-'+side, pad[side]);
			});
			poser.insertBefore(e);
			
			var hRatio = (e.innerHeight()+pad.bottom) / poser.innerHeight();
			// Set up scrolling update events
			jQuery("." + poserTargetingClass).scroll(function(){e.scrollTop(poser.scrollTop()*hRatio)});
			e.scroll(function(){poser.scrollTop(e.scrollTop()*hRatio)});
		});
	});
	return items;
};


/**
 * jQuery plugin to move the scrollbar to the left side of a div -- no right scrollbar
 * @link http://thirdroute.com/css-js-left-vertical-scrollbars/, but heavily changed from the original version.
 */
jQuery.fn.leftScrollbar = function(){
	var items = jQuery(this);
	
	var randomString = function() {
		var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
		var string_length = 8;
		var randomstring = '';
		for (var i=0; i<string_length; i++) {
			var rnum = Math.floor(Math.random() * chars.length);
			randomstring += chars.substring(rnum,rnum+1);
		}
		return randomstring;
	}
	
	jQuery(function(){
		items.each(function(){
			// create unique classes for targeting the poser div
			var poserTargetingClass = randomString();
			
			var e = jQuery(this);
			var content = e.html();
			var contentHeight = e.children('table:first').height();
			var ie = !jQuery.support.boxModel;
			var w = e[ie?'innerWidth':'width'](), h = e[ie?'innerHeight':'height']();
			//calculate paddings
			var pad = {};
			jQuery(['top', 'right', 'bottom', 'left']).each(function(i, side){
				pad[side] = parseInt(e.css('padding-' + side).replace('px',''));
			});
			//detect scrollbar width
			var xfill = jQuery('<div>').css({margin:0, padding:0, height:'1px'});
			e.append(xfill);
			var contentWidth = xfill.width();
			var scrollerWidth = e.innerWidth() - contentWidth - pad.left - pad.right;
			e.css('padding', '0');
			e.children('.xFill').remove();
			
			var poserHeight = h - pad.top - pad.bottom;
			var poser = jQuery('<div class="leftScrollPoser '+poserTargetingClass+'">')
				.html('<div style="visibility:hidden;height:'+contentHeight+'px">.</div>')
				.css('overflow','auto')
				.height(poserHeight+(ie?pad.top+pad.bottom:0))
				.width(scrollerWidth-(ie?0:pad.left*2)) // only as wide as the scrollbar
			;
			
			e
				.css({
					width: w/*-scrollerWidth*/-(ie?0:pad.right+pad.left),
					height: h-(ie?0:pad.bottom+pad.top),
					overflow: 'hidden',
					marginTop: -poserHeight-pad.top*2,
					marginLeft: scrollerWidth
				});
				
			jQuery(['top', 'right', 'bottom', 'left']).each(function(i, side){
				 poser.css('padding-'+side, pad[side]);
				 e.css('padding-'+side, pad[side]);
			});
			poser.insertBefore(e);
			
			var hRatio = (e.innerHeight()+pad.bottom) / poser.innerHeight();
			// Set up scrolling update events
			jQuery("." + poserTargetingClass).scroll(function(){e.scrollTop(poser.scrollTop()*hRatio)});
			e.scroll(function(){poser.scrollTop(e.scrollTop()*hRatio)}); // so mouse wheel scrolls table
		});
	});
	return items;
};