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 nodes 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.