Blind Up/Down Using YUI Animation


The Prototype Library extension, Scriptaculous has some really nice animation widgets, but it is built on a library that I do not like using. My favorite animations from scriptaculous are 'blindUp' and 'blindDown'. Today we will be using the YUI animation package to write a widget that simplified writing 'blind' animations. The goal is to take a DOM node and instantiate a widget that manages scrolling the height of the DOM node up/down.

Here is my first pass at a blind up/down animation widget:

Example 1: BlindAnimator Widget

Core.Widget.BlindAnimator = function(id, cfg) {
    // local namespace
    var Anim = YAHOO.util.Anim,
        config = Object.is(cfg) ? cfg : {},
        Dom = YAHOO.util.Dom,
        F = function() {},
        isDown = false,
        that = null;

    // DOM namespace
    var dom = {
        node : Dom.get(id)
    };

    if (! dom.node) {return;} // node is required

    // update config
    if (! config.animSpeedDown) {config.animSpeedDown = 0.5;}
    if (! config.animSpeedUp) {config.animSpeedUp = 0.5;}
    if (! config.bottomPadding) {config.bottomPadding = 0;}
    if (! config.minHeight) {config.minHeight = 0;}
    if (! config.maxHeight) {
        var r = Dom.getRegion(dom.node);
        config.maxHeight = r.bottom - r.top;
        isDown = true;
    }
    
    // initialize animations
    var anim = {
        blindDown: new Anim(dom.node, {height: {from: config.minHeight, to: config.maxHeight}}, config.animSpeedDown, config.easing),
        blindUp: new Anim(dom.node, {height: {from: config.maxHeight, to: config.minHeight}}, config.animSpeedUp, config.easing)
    };

    // setup animations
    anim.blindDown.onStart.subscribe(function() {Dom.setStyle(dom.node, 'overflow', 'hidden');});
    anim.blindUp.onComplete.subscribe(function() {Dom.setStyle(dom.node, 'overflow', 'visible');});
    anim.blindDown._onComplete.subscribe(function() {isDown = true;});
    anim.blindUp._onComplete.subscribe(function() {isDown = false;});

    // scrolls the page as the bind opens
    if (config.scrollToViewport) {
        anim.blindDown._onTween.subscribe(function() { // handles scrolling the window to fit during animation
            var dim = Dom.getRegion(dom.node),
                offset = Core.Client.getScrollOffset(),
                view = Core.Client.getViewportSize(),
                pos = dim.bottom + config.bottomPadding,
                bottom = offset.y + view.y;

            if (pos > bottom) {
                window.scroll(0, offset.y + pos - bottom);
            }
        });
    }

    // public namespace
    F.prototype = {

        blindDown: function() {
            that.stop(false);
            anim.blindDown.animate();
        },

        blindUp: function() {
            that.stop(true);
            anim.blindUp.animate();
        },

        stop: function(isDown) {
            anim.blindDown.stop(isDown);
            anim.blindUp.stop(! isDown);
        },

        toggle: function() {
            that[isDown ? 'blindUp' : 'blindDown']();
        },

        subscribe: function(name, fn, isUp, o) {
            var a = isUp ? anim.blindUp : anim.blindDown;

            if (a[name] && a[name].subscribe) {
                a[name].subscribe(fn, o);
            }
        }
    };

    that = new F();
    return that;
};

BlindAnimation.js

Instantiate a new BlindAnimator by passing it the ID of the DOM node you wish to animation; you may also provide a configuration object. If no configuration is passed, the widget assumes that the DOM node is currently in the blindDown (opened) state and that ZERO is the blindUp (closed) state. There are currently 7 configuration objects:

Example 2: Configuration

{
    animSpeedDown: a number where 1 represents 1000ms (default is 0.5),
    animSpeedUp: a number where 1 represents 1000ms (default is 0.5),
    bottomPadding: the bottom margin when using scrollToViewport (default is ZERO),
    minHeight: the blindUp height  (default is ZERO),
    maxHeight: the blindDown height  (default is node height),
    scrollToViewport: true|false, when true will scroll the page to fit blindDown animation (default is false),
    easing: YAHOO.util.Easing.EASING_TYPE (default is none)
}

Each instance of BlindAnimator has 5 public methods: 'blindUp', 'blindDown', 'stop', 'toggle', and 'subscribe'. The 'blindUp' and 'blindDown' methods provide public access to the animations, while also ensuring that any previous animation has stopped, calling the 'stop' method that terminates both animations. The 'toggle' method calls the 'blindUp' or 'blindDown' method, with the opposite state that the widget is not currently in. And lastly, the 'subscribe' method exposes the Custom Events that are attached to the animations, allowing additional customization.

The key to any blind animation is the proper application of 'overflow' style. As you can see, we toggle the 'overflow' to 'hidden' just before we scroll down, which hides the content of the DOM node outside of the node's height. As the height increases we get the blind opening effect. The same is true, when we decrease the height and we get the blind close effect. After the 'blindUp' animates, we then reset the 'overflow' to 'visible'.

So far I am pleased with this widget, but I see three areas for improvement: 1) when frequently triggering an event that calls 'blindUp' or 'blindDown', the animation jumps around a bit; 2) YUI does not attempt to determine dimensions of elements that are initially displayed 'none'; and 3) I can foresee situations when min/max height need to be dynamic. The first issue can be addressed by better handling of the 'isDown' state when animations start and stop. The second issue can be solved by a method that temporarily displays the element off the screen, while we attempt to determine its dimensions (to be discussed in our next article). The third issue is tricky and will most likely involve additional configuration properties.

Here is a test page, where you can play around with BlindAnimator.