Source: widgets/commonCtrls/kekule.widget.nestedContainers.js

/**
 * @fileoverview
 * Base class of container that can hold a set of nested children (e.g., menu, tree view).
 * @author Partridge Jiang
 */

/*
 * requires /lan/classes.js
 * requires /utils/kekule.utils.js
 * requires /utils/kekule.domUtils.js
 * requires /xbrowsers/kekule.x.js
 * requires /widgets/kekule.widget.base.js
 */

(function(){
"use strict";

/** @ignore */
Kekule.Widget.HtmlClassNames = Object.extend(Kekule.Widget.HtmlClassNames, {
	/* Indicate that all children of current item is hidden. */
	STATE_COLLAPSED: 'K-State-Collapsed',
	STATE_EMPTY: 'K-State-Empty',
	NESTED_CONTAINER: 'K-NestedContainer',
	NESTED_CONTAINER_ITEM: 'K-NestedContainer-Item'
});

/**
 * An abstract nested container.
 * @class
 * @augments Kekule.Widget.BaseWidget
 *
 * @property {Bool} itemInitialExpanded
 */
Kekule.Widget.NestedContainer = Class.create(Kekule.Widget.BaseWidget,
/** @lends Kekule.Widget.NestedContainer# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Widget.NestedContainer',
	/** @private */
	DEF_CONTAINER_TAG: 'ul',
	/** @private */
	DEF_ITEM_TAG: 'li',
	/** @private */
	ITEM_DATA_FIELD: '__$itemData__',
	/** @construct */
	initialize: function($super, parentOrElementOrDocument)
	{
		$super(parentOrElementOrDocument);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('itemInitialExpanded', {'dataType': DataType.BOOL});
	},
	/** @ignore */
	doCreateRootElement: function(doc)
	{
		var result = doc.createElement('div');
		return result;
	},
	/** @ignore */
	doBindElement: function($super, element)
	{
		$super(element);
	},

	/** @private */
	getContainerElemTag: function()
	{
		return this.DEF_CONTAINER_TAG;
	},
	/** @private */
	getItemElemTag: function()
	{
		return this.DEF_ITEM_TAG;
	},
	/** @private */
	_createChildItemElem: function()
	{
		var result = this.doCreateChildItemElem();
		Kekule.HtmlElementUtils.addClass(result, Kekule.Widget.HtmlClassNames.NESTED_CONTAINER_ITEM);
		Kekule.HtmlElementUtils.addClass(result, Kekule.Widget.HtmlClassNames.STATE_EMPTY);
		return result;
	},
	/**
	 * Do actual work of _createChildItemElem.
	 * Descendants may override this method.
	 * @private
	 */
	doCreateChildItemElem: function()
	{
		return this.getDocument().createElement(this.getItemElemTag());
	},
	/** @private */
	_createChildContainerElem: function()
	{
		var result = this.doCreateChildContainerElem();
		Kekule.HtmlElementUtils.addClass(result, Kekule.Widget.HtmlClassNames.NESTED_CONTAINER);
		return result;
	},
	/**
	 * Do actual work of _createChildItemElem.
	 * Descendants may override this method.
	 * @private
	 */
	doCreateChildContainerElem: function()
	{
		return this.getDocument().createElement(this.getContainerElemTag());
	},
	/**
	 * Returns container element to hold all children in an item.
	 * Descendants can override this method.
	 * @param {HTMLElement} itemElem
	 * @param {Bool} canCreate If true, a new container element will be created if none exists there.
	 * @returns {HTMLElement}
	 * @private
	 */
	getChildContainerElem: function(itemElem, canCreate)
	{
		if (!itemElem)
			itemElem = this.getElement();
		var elems = itemElem.getElementsByTagName(this.getContainerElemTag());
		var result = elems.length? elems[0]: null;
		if (!result && canCreate)
		{
			result = this._createChildContainerElem();
			itemElem.appendChild(result);
		}
		return result;
	},

	/**
	 * Returns data previously set to itemElem.
	 * @param {HTMLElement} itemElem
	 * @returns {Object}
	 */
	getItemData: function(itemElem)
	{
		return itemElem[this.ITEM_DATA_FIELD];
	},
	/**
	 * Set data associated with itemElem.
	 * If data has an array field named "children", child items will automatically be added
	 */
	setItemData: function(itemElem, data)
	{
		this.doSetItemData(itemElem, data);
		itemElem[this.ITEM_DATA_FIELD] = data;
		var childrenData = data.children;
		if (childrenData && Kekule.ArrayUtils.isArray(childrenData))
		{
			this.clearChildItems(itemElem);
			for (var i = 0, l = childrenData.length; i < l; ++i)
			{
				var childData = childrenData[i];
				this.appendChildItem(itemElem, childData);
			}
		}
		return this;
	},
	/**
	 * Do actual work of method setItemData.
	 * Descendants should override this method.
	 * @param {HTMLElement} childItemElem
	 * @param {Object} data
	 * @private
	 */
	doSetItemData: function(itemElem, data)
	{
		// do nothing here
	},

	/**
	 * Check if there are child under parentItem.
	 * @param {HTMLElement} parentItem
	 * @returns {Bool}
	 */
	hasChildItem: function(parentItem)
	{
		return this.getChildItemCount(parentItem) > 0;
	},
	/**
	 * Returns child item count in a parent item.
	 * @param {HTMLElement} parentItem
	 * @returns {Int}
	 */
	getChildItemCount: function(parentItem)
	{
		return this.getChildren(parentItem).length;
	},
	/**
	 * Returns all child item elements/
	 * @param {HTMLElement} parentItem
	 * @returns {HTMLElementCollection}
	 */
	getChildren: function(parentItem)
	{
		var containerElem = this.getChildContainerElem(parentItem);
		if (containerElem)
		{
			var result = Kekule.DomUtils.getDirectChildElems(containerElem, this.getItemElemTag());
			return result;
		}
		else
			return [];
	},
	/**
	 * Returns child item element at index.
	 * @param {HTMLElement} parentItem
	 * @param {Int} index
	 * @returns {HTMLElement}
	 */
	getChildItemAt: function(parentItem, index)
	{
		var children = this.getChildren(parentItem);
		return (children.length && (children.length > index))? children[index]: null;
	},

	/**
	 * Insert a child item with data to parentItemElem.
	 * @param {HTMLElement} parentItemElem Set to null to insert to root directly.
	 * @param {Variant} childItemElemOrData HTML element or object of data.
	 * @param {Variant} indexOrRefItem Int or HTMLElement.
	 * @returns {HTMLElement} Element added.
	 */
	insertChildItem: function(parentItemElem, childItemElemOrData, indexOrRefItem)
	{
		var result;
		var containerElem = this.getChildContainerElem(parentItemElem, true);
		//if (childItemElemOrData instanceof HTMLElement)  // an element, HTMLElement may cause undefined class error in IE
		//if (childItemElemOrData && (childItemElemOrData.tagName && childItemElemOrData.nodeType))  // is element
		if (childItemElemOrData && Kekule.DomUtils.isElement(childItemElemOrData))
		{
			result = childItemElemOrData;
		}
		else  // data object
		{
			var result = this._createChildItemElem();
			if (!this.getItemInitialExpanded())
				Kekule.HtmlElementUtils.addClass(result, Kekule.Widget.HtmlClassNames.STATE_COLLAPSED);
			if (childItemElemOrData)
				this.setItemData(result, childItemElemOrData);
		}
		var refChild = DataType.isSimpleType(indexOrRefItem)? this.getChildren(parentItemElem)[index]: indexOrRefItem;
		if (refChild)
			containerElem.insertBefore(result, refChild);
		else
			containerElem.appendChild(result);
		this.childItemsChanged(parentItemElem);
		return result;
	},
	/**
	 * Append a child item with data to parentItemElem.
	 * @param {HTMLElement} parentItemElem Set to null to insert to root directly.
	 * @param {Variant} childItemElemOrData HTML element or object of data.
	 * @returns {HTMLElement} Element added.
	 */
	appendChildItem: function(parentItemElem, childItemElemOrData)
	{
		return this.insertChildItem(parentItemElem, childItemElemOrData);
	},
	/**
	 * Remove a child item from parent.
	 * @param {HTMLElement} parentItemElem
	 * @param {HTMLElement} childItemElem
	 */
	removeChildItem: function(parentItemElem, childItemElem)
	{
		var container = this.getChildContainerElem(parentItemElem);
		if (container)
		{
			container.removeChild(childItemElem);
			if (!this.hasChildItem(parentItemElem))  // parent empty, remove container element
				parentItemElem.removeChild(container);
			this.childItemsChanged(parentItemElem);
		}
		return this;
	},
	/**
	 * Remove a child item from parent.
	 * @param {HTMLElement} parentItemElem
	 * @param {Int} index
	 */
	removeChildItemAt: function(parentItemElem, index)
	{
		var child = this.getChildItemAt(parentItemElem, index);
		if (child)
			this.removeChildItem(parentItemElem, child);
		return this;
	},
	/**
	 * Clear all child items from parent.
	 * @param {HTMLElement} parentItemElem
	 */
	clearChildItems: function(parentItemElem)
	{
		var container = this.getChildContainerElem(parentItemElem);
		if (!parentItemElem)
			parentItemElem = this.getElement();
		if (container)
			parentItemElem.removeChild(container);
		this.childItemsChanged(parentItemElem);
	},

	/**
	 * Notify that the child items has been removed or added to parent item.
	 * Descendants can override this method.
	 * @param {HTMLElement} parentItemElem
	 */
	childItemsChanged: function(parentItem)
	{
		if (parentItem)  // not root
		{
			var c = Kekule.Widget.HtmlClassNames.STATE_EMPTY;
			if (this.hasChildItem(parentItem))
				Kekule.HtmlElementUtils.removeClass(parentItem, c);
			else
				Kekule.HtmlElementUtils.addClass(parentItem, c);
		}
	},

	/**
	 * Expand an item.
	 * @param {HTMLElement} item
	 */
	expandItem: function(item)
	{
		Kekule.HtmlElementUtils.removeClass(item, Kekule.Widget.HtmlClassNames.STATE_COLLAPSED);
		return this;
	},
	/**
	 * Collapse an item.
	 * @param {HTMLElement} item
	 */
	collapseItem: function(item)
	{
		Kekule.HtmlElementUtils.addClass(item, Kekule.Widget.HtmlClassNames.STATE_COLLAPSED);
		return this;
	},
	/**
	 * Collapse expanded item or expand collapsed item.
	 * @param {HTMLElement} item
	 */
	toggleExpandStateOfItem: function(item)
	{
		Kekule.HtmlElementUtils.toggleClass(item, Kekule.Widget.HtmlClassNames.STATE_COLLAPSED);
		return this;
	},

	/**
	 * Check if element is an child item of container.
	 * @param {HTMLElement} element
	 * @returns {Bool}
	 */
	isChildItem: function(element)
	{
		return Kekule.HtmlElementUtils.hasClass(element, Kekule.Widget.HtmlClassNames.NESTED_CONTAINER_ITEM);
	},
	/**
	 * Get nearest child item. Element is a child item itself or its child element.
	 * @param {HTMLElement} element
	 * @returns {HTMLElement}
	 */
	getBelongedChildItem: function(element)
	{
		var result = element;
		var root = this.getElement();
		var body = this.getDocument().body;
		while ((result !== root) && (result !== body))
		{
			result = result.parentNode;
			if (this.isChildItem(result))
				return result;
		}
		return null;
	},
	/**
	 * List all child items in order in rootItem.
	 * @param {HTMLElement} rootItem If not set, this method will returns all items in container.
	 * @returns {Array}
	 */
	getAllChildItems: function(rootItem)
	{
		var result = [];
		var containerElem = this.getChildContainerElem(rootItem, false);
		if (containerElem)
		{
			var children = Kekule.DomUtils.getDirectChildElems(containerElem);
			for (var i = 0, l = children.length; i < l; ++i)
			{
				var child = children[i];
				if (this.isChildItem(child))
				{
					result.push(child);
					var items = this.getAllChildItems(child);
					result = result.concat(items);
				}
			}
		}
		return result;
	},
	/**
	 * Returns all items between item1 and item2 (including item1 and item2).
	 * @param {HTMLElement} item1
	 * @param {HTMLElement} item2
	 * @param {HTMLElement} rootItem Set to null to search in whole widget.
	 * @returns {Array}
	 */
	getChildItemRange: function(item1, item2, root)
	{
		var allItems = this.getAllChildItems(root);
		var index1 = allItems.indexOf(item1);
		var index2 = allItems.indexOf(item2);
		if ((index1 >= 0) && (index2 >= 0))
		{
			if (index1 > index2)
			{
				var reversed = true;
				var temp = index1;
				index1 = index2;
				index2 = temp;
			}
			var result = allItems.slice(index1, index2 + 1);
			if (reversed)
				result = result.reverse();
			return result;
		}
		else
			return [];
	}
});
})();