SOURCE CODE: Uize.Widget.Options.Accordion
VIEW REFERENCE

/*______________
|       ______  |   U I Z E    J A V A S C R I P T    F R A M E W O R K
|     /      /  |   ---------------------------------------------------
|    /    O /   |    MODULE : Uize.Widget.Options.Accordion Class
|   /    / /    |
|  /    / /  /| |    ONLINE : http://www.uize.com
| /____/ /__/_| | COPYRIGHT : (c)2007-2012 UIZE
|           |__ |   LICENSE : Available under MIT License or GNU General Public License
|_______________|             http://www.uize.com/license.html
*/

/* Module Meta Data
  type: Class
  importance: 4
  codeCompleteness: 100
  testCompleteness: 0
  docCompleteness: 2
*/

/*?
  Introduction
    The =Uize.Widget.Options.Accordion= class extends its superclass by adding an accordion style behavior for revealing the contents of the selected tab.

    *DEVELOPERS:* `Ben Ilegbodu`
*/

Uize.module ({
  name:'Uize.Widget.Options.Accordion',
  required:[
    'Uize.Node',
    'Uize.Fade'
  ],
  builder:function (_superclass) {
    /*** Variables for Scruncher Optimization ***/
      var
        _true = true,
        _false = false,

        _Uize = Uize,
        _Uize_Node = _Uize.Node,
        _Uize_Fade = _Uize.Fade
      ;

    /*** Class Constructor ***/
      var
        _class = _superclass.subclass (
          null,
          function () {
            var
              _this = this,
              _previousTabNodeHeight,
              _newTabNodeHeight
            ;

            function _tabBodyStart(_tabNo) {
              _this.isWired
                // prepare the node to animate its height by 1) making sure it's visible
                // and 2) it's in the flow of the the container
                && _this.setNodeStyle(
                  _this._getTabBodyNode(_tabNo),
                  {
                    display:'block',
                    position:'relative',
                    overflow:'hidden',
                    visibility:'visible'
                  }
                )
            }
            function _setTabBodyHeight(_tabNo, _height) {
              _this.isWired
                && _this.setNodeStyle(
                  _this._getTabBodyNode(_tabNo),
                  {height:_height}
                )
            }
            function _tabBodyDone(_tabNo, _mustDisplay) {
              if (_this.isWired) {
                var _tabBodyNode = _this._getTabBodyNode(_tabNo);

                _this.displayNode(_tabBodyNode, _mustDisplay);

                // undo the explicit height & overflow we set
                _this.setNodeStyle(
                  _tabBodyNode,
                  {
                    height:'',
                    overflow:''
                  }
                );
              }
            }

            (_this.fade = new _Uize_Fade).wire({
              Start:function() {
                _tabBodyStart(_this._previousTabNo);
                _tabBodyStart(_this.get('valueNo'));
                _previousTabNodeHeight = _Uize_Node.getDimensions(
                  _this._getTabBodyNode(_this._previousTabNo)
                ).height;
                _newTabNodeHeight = _this.fade.get('endValue');
              },
              'Changed.value':function() {
                var _newHeight = +_this.fade;

                // Since we have only one fade object, the previous tab body node height
                // needs to change inversely proportional to the change of the new tab body node
                _previousTabNodeHeight
                  && _setTabBodyHeight(
                    _this._previousTabNo,
                    (1 - (_newHeight / _newTabNodeHeight)) * _previousTabNodeHeight
                  )
                ;
                _setTabBodyHeight(_this.get('valueNo'), _newHeight);
              },
              Done:function() {
                var _newValueNo = _this.get('valueNo');
                _tabBodyDone(_this._previousTabNo, _false);
                _tabBodyDone(_newValueNo, _true);
                _this._previousTabNo = _newValueNo;
              }
            });

            _this.wire (
              'Changed.valueNo',
              function () { _this._updateUiTabBodies() }
            );
          }
        ),
        _classPrototype = _class.prototype
      ;

    /*** Private Instance Methods ***/
      _classPrototype._resolveToValueNo = function (_valueOrValueNo) {
        return _Uize.isNumber (_valueOrValueNo) ? _valueOrValueNo : this.getValueNoFromValue (_valueOrValueNo)
      };

      _classPrototype._getTabBodyNode = function (_valueOrValueNo) {
        return this.getNode ('option' + this._resolveToValueNo (_valueOrValueNo) + 'TabBody')
      };

      _classPrototype._updateUiTabBodies = function() {
        var
          _this = this,
          _previousTabNo = _this._previousTabNo,
          _newTabNo = _this.get('valueNo')
        ;

        if (_this.isWired) {
          if (_newTabNo > -1 && _newTabNo != _previousTabNo) {
            var
              _newTabBodyNode = _this._getTabBodyNode(_newTabNo),
              _newTabBodyNodeStyleHeight = _this.getNodeStyle(_newTabBodyNode, 'height')
            ;

            _this.setNodeStyle(
              _newTabBodyNode,
              {
                display:'block',
                height:'auto',
                position:'absolute',
                visibility:'visible'
              }
            );

            var
              // If an explicit height is set then we want that to be the max value not the calculated
              // height. That way you can still have fixed height accordions
              _newTabHeight = parseInt(_newTabBodyNodeStyleHeight)
                || _Uize_Node.getDimensions(_newTabBodyNode).height
            ;

            _newTabHeight
              && _this.fade.start({
                curve:_this._animationCurve || _Uize_Fade.celeration (0,1),
                duration:_this._animationDuration,
                startValue:0,
                endValue:_newTabHeight
              })
            ;
          }
          else
            _this.forAll(
              function(_optionButton, _optionNo) {
                // hide all the tab bodies that should be hidden
                _this.displayNode(
                  _this._getTabBodyNode(_optionNo),
                  _optionNo === _previousTabNo
                )
              }
            )
          ;
        }
      };

    /*** Public Instance Methods ***/
      _classPrototype.enableTab = function (_value,_mustEnable) {
        this.getOptionButton (_value).set ({enabled:_mustEnable ? 'inherit' : false});
        this._updateUiTabBodies ();
      };

      _classPrototype.getOptionButton = function (_valueOrValueNo) {
        return this.children ['option' + this._resolveToValueNo (_valueOrValueNo)]
      };

      _classPrototype.tabExists = function (_valueOrValueNo) {
        var _optionButton = this.getOptionButton (_valueOrValueNo);
        return (
          _optionButton && (_optionButton.getNode () || this._getTabBodyNode (_valueOrValueNo))
            ? _true
            : _false
        );
      };

      _classPrototype.updateUi = function () {
        var _this = this;
        if (_this.isWired) {
          var _rootNode = _this.getNode();

          // Ensure that the root node is at least positioned relative
          _this.getNodeStyle(_rootNode, 'position') == 'static'
            && _this.setNodeStyle(_rootNode, {position:'relative'})
          ;

          _this._updateUiTabBodies();

          _superclass.prototype.updateUi.call (_this);
        }
      };

      _classPrototype.wireUi = function () {
        var _this = this;
        if (!_this.isWired) {
          _this._previousTabNo = _this.get('valueNo');

          _superclass.prototype.wireUi.call (_this);
        }
      };

    /*** Register Properties ***/
      _class.registerProperties ({
        _animationCurve:'animationCurve',
        _animationDuration:{
          name:'animationDuration',
          value:500
        }
      });

    return _class;
  }
});