One of the most useful JavaScript animations is to animate a DOM node as it slides in from the top (or another side) of the browser viewport. I have implemented this animation several times for various projects, and thought that it would be a good idea to unify my implementations into one concise widget. I have used YUI as the Framework to power this widget, but unlike most of my other widgets, I have internalized all library references, so it would be easy to replace with another framework. In addition, I have not leveraged any of my own Framework files. By coding this way, the widget is completely standalone and more easily imported into your existing codebase. I intend to write all future widgets in this manner.
Anyway, this is my first version of ‘ViewportSlider’ and I am open to suggestions. Currently, it only supports animating out of the top of the viewport, but I have included a configuration object that I may later extend to support animations form any side of the viewport.
Example 1: Viewport Slider (source)
YAHOO.widget.ViewportSlider = function(elem, cfg) {
// library namespace
var YUI = YAHOO.util,
Dom = YUI.Dom,
E = YUI.Event,
A = YUI.Anim;
// local namespace
var config = cfg || {},
F = function() {},
isAnimated = false,
isSlideDown = false,
that = null;
// DOM namespace
var dom = {
node: Dom.get(elem)
};
if (! dom.node) {return null;} // missing required
// setup configuration
// config.useFixed
// config.center
if (! config.easing) {config.easing = YAHOO.util.Easing.easeBoth;}
if (! config.duration) {config.duration = 1;}
if (! config.zIndex) {config.zIndex = 101;}
// position for scroll
Dom.setStyle(dom.node, 'visibility', 'hidden');
Dom.setStyle(dom.node, 'display', 'block');
Dom.setStyle(dom.node, 'position', 'absolute');
Dom.setStyle(dom.node, 'z-index', config.zIndex);
var rect = Dom.getRegion(dom.node),
height = rect.bottom - rect.top;
Dom.setStyle(dom.node, 'top', '-' + height + 'px');
Dom.setStyle(dom.node, 'visibility', 'visible');
// event namespace
var evt = {
ieScroll: function() {
var reg = Dom.getClientRegion();
Dom.setStyle(dom.node, 'top', reg.top + 'px');
}
};
// public namespace
F.prototype = {
isAnimated: function() {
return isAnimated;
},
isSlideDown: function() {
return isSlideDown;
},
isSlideUp: function() {
return ! isSlideDown;
},
slideDown: function() {
if (! isAnimated) {
var rect = Dom.getRegion(dom.node),
reg = Dom.getClientRegion(),
vheight = Dom.getViewportHeight(),
vwidth = Dom.getViewportWidth(),
height = rect.bottom - rect.top,
width = rect.right - rect.left,
left = (vwidth - width) / 2;
if (0 > left || isNaN(left)) {left = 0;}
isAnimated = true;
isSlideDown = true;
Dom.setStyle(dom.node, 'top', reg.top - height + 'px');
Dom.setStyle(dom.node, 'position', 'absolute');
if (config.center) {Dom.setStyle(dom.node, 'left', left + 'px');}
// setup the animation
var anim = new A(dom.node, {top: {from: reg.top - height, to: reg.top}}, config.duration, config.easing);
anim._onComplete.subscribe(function() {
isAnimated = false;
// fix the position, when configured and smaller than viewport
if (config.useFixed && vheight > height) {
// special case IE < 7, becuase it improperly supports "position:fixed"
if (0 < YAHOO.env.ua.ie && 7 > YAHOO.env.ua.ie) {
E.on(window, 'scroll', evt.ieScroll);
}
else {
Dom.setStyle(dom.node, 'top', '0px');
Dom.setStyle(dom.node, 'position', 'fixed');
}
} // don't position fixed, if viewport is too small
});
anim.animate();
}
},
slideToggle: function() {
that[isSlideDown ? 'slideUp' : 'slideDown']();
},
slideUp: function() {
if (! isAnimated) {
var rect = Dom.getRegion(dom.node),
height = rect.bottom - rect.top,
reg = Dom.getClientRegion();
isAnimated = true;
isSlideDown = false;
Dom.setStyle(dom.node, 'top', reg.top + 'px'); // reposition to scroll offset + viewport before animating
Dom.setStyle(dom.node, 'position', 'absolute');
// setup the animation
var anim = new A(dom.node, {top: {from: reg.top, to: reg.top - height}}, config.duration, config.easing);
anim._onComplete.subscribe(function() {
isAnimated = false;
Dom.setStyle(dom.node, 'top', '-' + height + 'px');
// remove special case event handler for IE < 7
if (0 < YAHOO.env.ua.ie && 7 > YAHOO.env.ua.ie) {
E.removeListener(window, 'scroll', evt.ieScroll);
}
});
anim.animate();
}
}
};
that = new F();
return that;
};
To instantiate, call as follows:
Example 2: Viewport Slider Instantiation
YAHOO.widget.ViewportSlider('idOfElementToAnimate', {center: true, useFixed: true, duration: 0.5});
The widget requires that a DOM node is provided, but the second parameter (configuration object) is entirely optional. Once call, ‘ViewportSlider’ first creates shortcut references to YUI, some internal variables, and a DOM reference to the node. Then it goes through the configuration parameters, setting default values as necessary. You can currently configure the following: center (true|false) - centers the node horizontally in the viewport, useFixed (true|false) - fixes node to the top of the viewport, duration (number) - the length of animation in seconds, easing (YAHOO.util.Easing.*) - any YAHOO easing method to use when animating, and zIndex (number) - the zIndex to make the node. Next we position the node absolutely above the top of the viewport and display it as block (in case it wasn’t a block-level node already). There are several public methods, the names of which explain there purpose, but check the source-code for additional documentation. To animate, call ’slideDown’, ’slideUp’, or ’slideToggle’.
When ’slideDown’ is called, we compute the dimension of the viewport, the node, and how far the page has been scrolled. Then we update the ‘isAnimated’ and ‘isSlideDown’ states, before positioning the element to the top of the viewport plus any scroll offset. This is so that a user who has scrolled down the page still see the animation. We also center the node, if the ‘center’ configuration property was set to true. Next we setup the animation to scroll from the top position we just set, to the top of the viewport plus any scroll offset. We subscribe to the YUI ‘_onComplete’ custom event so that when the animation terminates, we can adjust the ‘isAnimated’ state. We also check to see if ‘useFixed’ is set and that the size of the node is smaller than the viewport. When both are true, we position the element statically to the top of the viewport (’0px’), except in IE less than 7, where we have to use a special ’scroll’ event monitor to simulate static position (the node stays absolutely positioned and the top is adjusted to the scroll offset).
When ’slideUp’ is called, like ’slideDown’, we compute dimensions and adjust ‘isAnimated’ and ‘isSlideDown’ states. Then we reposition absolutely to the top of the viewport plus any scroll offset. This is required, because when ‘useStatic’ is set to true, the ‘top’ position of the node was adjusted to ‘0px’ and now must be restored before animating. We animate from the top of the viewport plus any scroll offset, to that value minus the height of the node. Using the same animation custom event subscription, as ’slideDown’, we adjust the ‘isAnimated’ state and reposition the element off where it was first positioned when the widget loaded. Lastly, if the IE less than 7 special-case listener was used, we unsubscribe it now, freeing up the resources.
When ’slideToggle’ is called, it uses the ‘isSlideDown’ to determine whether to call ’slideUp’ or ’slideDown’.
That is pretty much it, simple instantiate a ViewportSlider object, and use ’slideUp’ and ’slideDown’ to do all the work. Keep in mind that any node you pass to this widget will have their ‘position’ set to ‘absolute’ and be visually removed from the page, until ’slideDown’ is called. If the nodes are not initially ‘hidden’ or “display:none” then there will be a flicker (as seen on the demo page) as the widget loads. Here is a ViewportSlider Demo Page so you can try it out. The demo is straight forward, except there is a third button hidden way at the bottom of the page, used to illustrate how the ViewportSlider works with scrolling.