Thursday, July 17, 2008

Animated Scrolling

The graphic designer thought it would be great to replace the default scroll-bars with two buttons which control the scrolling of the content in a div. There were several problesm bundled up inside of this.

  • The CSS would be a hassle
  • There would be multiple instances of these two scrollers on one page
  • The scrollers would need to listen for events like mousewheel
  • The scrollers would need to be overlaid in the right place
  • and stay in the right place if the browser is resized
  • The way the scrollers look should come from CSS
  • So, Javascript would need to read the CSS rules for the paddings etc.


A fun problem! Here is what I came up with (it sits on the YUI framework):


/* only vertical scrolling supported */
cos.scrollers = function() {

var scrollEvents;
var buildCtrl = function(parent, name) {

var ctrl = cos.newEl({
'el' : 'a',
'parent' : parent,
'href' : '#',
'class' : [name]
});

Event.on(ctrl, 'mouseover', function(){
Dom.addClass(this, name + '_hover');
});

Event.on(ctrl, 'mouseout', function(){
Dom.removeClass(this, name + '_hover');
});

return ctrl;
};

var setup = function() {

Dom.batch($$('div#bd div.scrollable'), function(o) {

var r = Dom.getRegion(o);

if(o.scrollHeight <= (r.bottom - r.top)) {
return null;
}

/* first turn off the scroll bars */
cos.setStyles(o, {
'overflow' : 'hidden'
});

/* add the scroll controls */
var scrollerClasses = ['scroller'];
if(Dom.hasClass(o, 'black')) {
scrollerClasses.push('black');
}

/* scroller div element */
var scroller = cos.newEl({
'el' : 'div',
'parent' : document.body,
'class' : scrollerClasses
});

/* build the two buttons for vertcal scroll control */
var up_ctrl = buildCtrl(scroller, 'up_ctrl');
var dn_ctrl = buildCtrl(scroller, 'dn_ctrl');

/* assign scroll events */
var scrollObj = {
'content' : o,
'height' : o.scrollHeight,
'h' : ((r['bottom'] - r['top']) * (3/4))
};

/* assign click events */
Event.on(up_ctrl, 'click', scrollUp, scrollObj, true);
Event.on(dn_ctrl, 'click', scrollDown, scrollObj, true);

/* assign wheel events */
Event.on(o, 'DOMMouseScroll', wheelHandler, scrollObj, true);
Event.on(o, 'mousewheel', wheelHandler, scrollObj, true);

var fadeObj = {
'scroller' : scroller,
'scrollableDiv' : o
};

/* fade in right away */
repositionAndFadeIn(null, null, fadeObj);

/* prepare scrollers for vertical page adjustment */
cos.events.pageAdjust.vertical.begin.subscribe(fadeOut, fadeObj);
cos.events.pageAdjust.vertical.end.subscribe(repositionAndFadeIn, fadeObj);
});
};

var fadeOut = function(e, arr, obj) {
cos.setStyles(obj['scroller'], {
'opacity' : 0,
'visibility' : 'hidden'
});
};

var repositionAndFadeIn = function(e, arr, obj) {

var region = Dom.getRegion(obj['scrollableDiv']);
var scroller = obj['scroller'];

cos.safeCenter(scroller, region);

cos.setStyles(scroller, {
'opacity' : 0,
'visibility' : 'visible'
});

var appear = new Anim(scroller, {
'opacity' : { 'to' : 1 }
}, 1.0, YAHOO.util.Easing.easeOut);

appear.animate();
};

var wheelHandler = function(e, obj) {

/* if we do not stop the default scrolling
* it will scroll the whole page */
Event.stopEvent(e);

var delta;
/* normalize the delta
* from andrewdupont.net/2007/11/07/pseudo-custom-events-in-prototype-16/ */
if (e.wheelDelta) {/* IE & Opera */ delta = e.wheelDelta / 120; }
else if (e.detail) { /* W3C */ delta = -e.detail / 3; }

if (!delta) { return; }
if(delta > 0) { /* up */ scrollUp(null, obj); }
else { /* down */ scrollDown(null, obj); }
};

var scrollUp = function(e, obj, custom) {
scrollEvents.begin.fire();

if(e != null) {
Event.stopEvent(e);
}

var slideDown = new YAHOO.util.Scroll(obj['content'], {
'scroll': { 'by': [0, -obj['h']]}
}, 0.3, YAHOO.util.Easing.easeOut);

slideDown.onComplete.subscribe(function(){
scrollEvents.end.fire();
});
slideDown.animate();
};

var scrollDown = function(e, obj) {
scrollEvents.begin.fire();

if(e != null) {
Event.stopEvent(e);
}

var slideUp = new YAHOO.util.Scroll(obj['content'], {
'scroll': { 'by': [0, obj['h']]}
}, 0.3, YAHOO.util.Easing.easeOut);

slideUp.onComplete.subscribe(function(){
scrollEvents.end.fire();
});
slideUp.animate();
};

return function() {
Event.onDOMReady(setup);
scrollEvents = cos.events.newEventSet();

return {
'scrollEvents' : scrollEvents
}
}();
}();

Here is a what it looks like in action:


What I like about this code:
  • The process:
  1. The scroller class self-invokes at the early script execution time
  2. The onDOMReady waits to call the initializer (this way there will be html elements to init)
  3. Then, the divs with scrollable class get batched
  4. for each div.scrollable a new set of scrollers is made
  5. They are placed
  6. The event listeners are assigned
  • Also, the scrollers are overlays, meaning they have a non-default z-index and they are positioned absolutely
  • There is a special function which I used to place them safely: cos.safeCenter(scroller, region);

Here is that method:

cos.safeCenter = function(item, region) {
var halfWidth = Dom.getViewportWidth() / 2;
var sWidth = cos.stripPx(Dom.getStyle(item, 'width'));
var sHeight = cos.stripPx(Dom.getStyle(item, 'height'));
var right = region['right'];

/* item on right half of window : or item on left half */
var marginLeft = ((right-sWidth) > halfWidth) ? ((right-sWidth) - halfWidth) : (0 - (halfWidth - (right-sWidth)));

var marginalAdjustments = {
'vert' : cos.stripPx(Dom.getStyle(item, 'marginBottom')),
'horz' : cos.stripPx(Dom.getStyle(item, 'marginRight'))
};

cos.setStyles(item, {
'top' : (region['bottom'] - sHeight - marginalAdjustments['vert']) + 'px',
'marginLeft' : (Math.floor(marginLeft) - marginalAdjustments['horz']) + 'px'
});
};

I am currently writing a blog article which covers this type of centering more in depth.

Labels: , ,

0 Comments:

Post a Comment

<< Home