SOURCE CODE: Uize.Widget.Tree
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.Tree Class
|   /    / /    |
|  /    / /  /| |    ONLINE : http://www.uize.com
| /____/ /__/_| | COPYRIGHT : (c)2003-2012 UIZE
|          /___ |   LICENSE : Available under MIT License or GNU General Public License
|_______________|             http://www.uize.com/license.html
*/

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

/*?
  Introduction
    The =Uize.Widget.Tree= class is a base class for hierarchical collapsible/expandable tree widgets of many kinds, including lists, drop-down menus, etc.

    *DEVELOPERS:* `Chris van Rensburg`
*/

Uize.module ({
  name:'Uize.Widget.Tree',
  builder:function (_superclass) {
    /*** Variables for Scruncher Optimization ***/
      var
        _undefined,
        _false = false
      ;

    /*** Class Constructor ***/
      var
        _class = _superclass.subclass (),
        _classPrototype = _class.prototype
      ;

    /*** Public Static Methods ***/
      _class.itemHasChildren = function (_item) {
        return !!(_item && _item.items && _item.items.length);
      };

      _class.itemIsDivider = function (_item) {
        return !!_item && _item.title == '-' && !_class.itemHasChildren (_item);
      };

    /*** Private Instance Methods ***/
      _classPrototype._canonicalizeItemSpecifier = function (_itemSpecifier) {
        return (
          typeof _itemSpecifier == 'string'
            ? _itemSpecifier
            : this.getItemInfoFromSpecifier (_itemSpecifier).itemSpecifier
        );
      };

    /*** Public Instance Methods ***/
      _classPrototype.getItemFromSpecifier = function (_itemSpecifier) {
        return this.getItemInfoFromSpecifier (_itemSpecifier).item;
      };

      _classPrototype.getItemInfoFromSpecifier = function (_itemSpecifier) {
        var
          _this = this,
          _item,
          _items = _this._items,
          _canonicalItemSpecifier = [],
          _titleParts = [],
          _itemSpecifierWasArray = Uize.isArray (_itemSpecifier),
          _itemDelimiter = _this._itemDelimiter,
          _itemSpecifierLevels = _itemSpecifierWasArray ? _itemSpecifier : _itemSpecifier.split (_itemDelimiter),
          _itemSpecifierLevelsLength = _itemSpecifierLevels.length
        ;
        for (var _levelNo = -1; ++_levelNo < _itemSpecifierLevelsLength;) {
          var _itemSpecifierForLevel = _itemSpecifierLevels [_levelNo];
          if (_itemSpecifierWasArray && typeof _itemSpecifierForLevel == 'string')
            _itemSpecifierForLevel = Uize.findRecordNo (_items,{title:_itemSpecifierForLevel})
          ;
          _item = _items [_itemSpecifierForLevel];
          if (_item) {
            _items = _item.items;
            _canonicalItemSpecifier.push (_itemSpecifierForLevel);
            _titleParts.push (_item.title);
          } else {
            break;
          }
        }
        return {
          item:_item,
          titleParts:_titleParts,
          itemSpecifier:_item ? _canonicalItemSpecifier.join (_itemDelimiter) : ''
        };
      };

      _classPrototype.setExpandedDepth = function (_expandedDepth,_itemSpecifier) {
        var _this = this;
        _this.traverseTree ({
          itemHandler:
            function (_item,_itemSpecifier,_depth) {
              _this.setItemExpanded (_itemSpecifier,_depth < _expandedDepth);
            },
          itemSpecifier:_itemSpecifier
        });
      };

      _classPrototype.setItemExpanded = function (_itemSpecifier,_expanded) {
        /* NOTE:
          - override the implementation of this method in a subclass
          - fall back to using this implementation in subclass implementation if widget is not yet wired
        */
        var _item = this.getItemFromSpecifier (_itemSpecifier);
        _item.expanded = typeof _expanded == 'boolean' ? _expanded : _item.expanded === _false;
      };

      _classPrototype.collapseAllBut = function (_expandedItemSpecifier) {
        var
          _this = this,
          _itemDelimiter = _this._itemDelimiter
        ;
        _expandedItemSpecifier = _this._canonicalizeItemSpecifier (_expandedItemSpecifier);
        _this.traverseTree ({
          itemHandler:
            function (_item,_itemSpecifier) {
              _this.setItemExpanded (
                _itemSpecifier,
                !(_expandedItemSpecifier + _itemDelimiter).indexOf (_itemSpecifier + _itemDelimiter)
              );
            }
        });
      };

      _classPrototype.traverseTree = function (_params) {
        var
          _this = this,
          _itemSpecifier = _params.itemSpecifier,
          _itemDelimiter = _this._itemDelimiter,
          _nop = Uize.nop,
          _itemHandler = _params.itemHandler || _nop,
          _beforeSubItemsHandler = _params.beforeSubItemsHandler || _nop,
          _afterSubItemsHandler = _params.afterSubItemsHandler || _nop
        ;
        function _traverseItem (_item,_itemSpecifier,_depth) {
          _itemHandler (_item,_itemSpecifier,_depth);
          var _itemItems = _item.items;
          if (_itemItems && _itemItems.length) {
            _beforeSubItemsHandler (_item,_itemSpecifier,_depth);
            _traverseItems (_itemItems,_itemSpecifier + _itemDelimiter,_depth + 1);
            _afterSubItemsHandler (_item,_itemSpecifier,_depth);
          }
        }
        function _traverseItems (_items,_itemSpecifierPrefix,_depth) {
          for (var _itemNo = -1, _itemsLength = _items.length; ++_itemNo < _itemsLength;)
            _traverseItem (_items [_itemNo],_itemSpecifierPrefix + _itemNo,_depth)
          ;
        }
        if (_itemSpecifier) {
          _itemSpecifier = _this._canonicalizeItemSpecifier (_itemSpecifier);
          _traverseItem (_this.getItemFromSpecifier (_itemSpecifier),_itemSpecifier,0);
        } else {
          _traverseItems (_this._items,'',0);
        }
      };

    /*** Register Properties ***/
      _class.registerProperties ({
        _itemDelimiter:{
          name:'itemDelimiter',
          value:'x'
        },
        _items:{
          name:'items',
          value:[],
          onChange:function () {
            var _this = this;
            if (_this.isWired) {
              _this.removeUi ();
              _this.insertUi ();
            }
          }
        },
        _value:{
          name:'value',
          value:[]
        }
      });

    /*** Override Initial Values for Inherited State Properties ***/
      _class.set ({
        built:_false
      });

    return _class;
  }
});