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

/**
 * @fileoverview
 * Implementation of menu widgets.
 * Unfinished yet.
 * @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
 * requires /widgets/kekule.widget.containers.js
 * requires /widgets/commonCtrls/kekule.widget.images.js
 */

(function(){
"use strict";

var DU = Kekule.DomUtils;
var EU = Kekule.HtmlElementUtils;
var CNS = Kekule.Widget.HtmlClassNames;

/** @ignore */
Kekule.Widget.HtmlClassNames = Object.extend(Kekule.Widget.HtmlClassNames, {
	MENU: 'K-Menu',
	POPUPMENU: 'K-PopupMenu',
	MENUBAR: 'K-MenuBar',
	MENUITEM: 'K-MenuItem',
	MENUITEM_NORMAL: 'K-MenuItem-Normal',
	MENUITEM_SEPARATOR: 'K-MenuItem-Separator',
	SUBMENU_MARKER: 'K-SubMenu-Marker',
	CHECKMENU_MARKER: 'K-CheckMenu-Marker'
});

/**
 * Menu item in menu widget.
 * @class
 * @augments Kekule.Widget.Container
 *
 * @property {String} text Text on menu.
 * @property {Bool} checked Whether the menu item is checked.
 * @property {Bool} autoCheck If true, the menu item will be automatically checked/unchecked when clicking on it.
 * @property {Bool} isSeparator If true, the menu item is a static separator (single line).
 */
/**
 * Invoked when menu item is checked.
 *   event param of it has field: {widget}
 * @name Kekule.Widget.MenuItem#check
 * @event
 */
Kekule.Widget.MenuItem = Class.create(Kekule.Widget.Container,
/** @lends Kekule.Widget.MenuItem# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Widget.MenuItem',
	/** @private */
	BINDABLE_TAG_NAMES: ['li'],
	/** @private */
	SUB_MENU_TAGS: ['ol', 'ul'],
	/** @constructs */
	initialize: function($super, parentOrElementOrDocument, text)
	{
		//this.setPropStoreFieldValue('useCornerDecoration', true);
		$super(parentOrElementOrDocument);
		if (text)
			this.setText(text);
		this._subMenuMarker = null;
		this._checkMarker = null;
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('text', {'dataType': DataType.STRING,
			'getter': function() { return Kekule.HtmlElementUtils.getInnerText(this.getElement()); },
			'setter': function(value) {
				if (value === Kekule.Widget.MenuItem.SEPARATOR_TEXT)
					this.setIsSeparator(true);
				else
				{
					if (!!value)
						this.setIsSeparator(false);
					this.changeContentText(value);
				}
			}
		});
		this.defineProp('checked', {'dataType': DataType.BOOL,
			'setter': function(value)
			{
				if (this.getPropStoreFieldValue('checked') !== value)
				{
					this.setPropStoreFieldValue('checked', value);
					this.checkChanged();
				}
			}
		});
		this.defineProp('autoCheck', {'dataType': DataType.BOOL});
		this.defineProp('isSeparator', {'dataType': DataType.BOOL,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('isSeparator', value);
				this.isSeparatorChanged(value);
			}
		});
	},
	/** @ignore */
	initPropValues: function($super)
	{
		$super();
		this.setUseCornerDecoration(false);
		this.setIsSeparator(false);
	},
	/** @ignore */
	doGetWidgetClassName: function($super)
	{
		return $super() + ' ' + CNS.MENUITEM; // + ' ' + CNS.MENUITEM_NORMAL;
	},
	/** @ignore */
	doCreateRootElement: function(doc)
	{
		var result = doc.createElement('li');
		return result;
	},
	/** @private */
	changeContentText: function(newText)
	{
		this.getElement().innerHTML = newText || '';
	},
	/** @private */
	isSeparatorChanged: function(isSeparator)
	{
		if (isSeparator)
		{
			this.setText(' ');
			this.removeClassName(CNS.MENUITEM_NORMAL);
			this.addClassName(CNS.MENUITEM_SEPARATOR);
			this.setStatic(true);
		}
		else
		{
			this.removeClassName(CNS.MENUITEM_SEPARATOR);
			this.addClassName(CNS.MENUITEM_NORMAL);
			this.setStatic(false);
		}
	},

	/** @ignore */
	doBindElement: function($super, element)
	{
		$super(element);
		// check if there is sub menu items
		var children = DU.getDirectChildElems(element);
		for (var i = 0, l = children.length; i < l; ++i)
		{
			var child = children[i];
			if (child.nodeType === Node.ELEMENT_NODE)
			{
				this.bindSubMenu(child);
			}
			child = child.nextSibling;
		}
	},
	/** @private */
	doDomElemAdded: function(elem)
	{
		if (!Kekule.Widget.getWidgetOnElem(elem))  // elem is not a widget yet
			this.bindSubMenu(elem);
	},

	/** @private */
	checkChanged: function()
	{
		var checked = this.getChecked();
		if (checked)
		{
			this._createCheckMarker();
			this.addClassName(CNS.STATE_CHECKED);
			this.invokeEvent('check');
		}
		else
			this.removeClassName(CNS.STATE_CHECKED);
	},
	/** @private */
	_createCheckMarker: function()
	{
		var result = this._checkMarker;
		if (!result)
		{
			var result = this.getDocument().createElement('span');
			result.className = CNS.CHECKMENU_MARKER;
			this.getElement().appendChild(result);
			this._checkMarker = result;
		}
		return result;
	},
	/** @private */
	_removeCheckMarker: function()
	{
		if (this._checkMarker)
		{
			this._checkMarker.parentNode.removeChild(this._checkMarker);
			this._checkMarker = null;
		}
	},

	/** @ignore */
	childWidgetAdded: function($super, widget)
	{
		$super(widget);
		if (widget instanceof Kekule.Widget.Menu)
		{
			widget.setIsSubMenu(true);
		}
		else if (widget instanceof Kekule.Widget.MenuItem)  // child menu item should be put in sub menu
		{
			var menu = this.getSubMenu(true);
			(function()
				{
					widget.setParent(menu);
					widget.appendToWidget(menu);
				}).defer(10);  // Important: must may item to sub menu after usual child widget add routine
		}
	},
	/** @ignore */
	childrenModified: function($super)
	{
		$super();
		this.subMenuChanged();
	},

	/** @private */
	bindSubMenu: function(elem)
	{
		var tagName = elem.tagName.toLowerCase();
		if (tagName === 'ul' || tagName === 'ol')
		{
			var result = new Kekule.Widget.PopupMenu(elem);
			this.addSubMenu(result);
			return result;
		}
	},
	/**
	 * Called when sub menu is added or removed from item.
	 * @private
	 */
	subMenuChanged: function()
	{
		var subMenu = this.getSubMenu();
		if (subMenu)
		{
			this._createSubMenuMarker(subMenu);
		}
		else
			this._removeSubMenuMarker();
	},

	/**
	 * Returns sub menu of this menu item.
	 * @param {Bool} canCreate If no sub menu exists, create a new one.
	 * @returns {Kekule.Widget.Menu}
	 */
	getSubMenu: function(canCreate)
	{
		/*
		var menuItemElem = this.getElement();
		var elems;
		for (var i = 0, l = this.SUB_MENU_TAGS.length; i < l; ++i)
		{
			var tag = this.SUB_MENU_TAGS[i];
			elems = DU.getDirectChildElems(menuItemElem, tag);
			if (elems && elems.length)
				break;
		}
		if (elems.length)
		{
			for (var i = 0, l = elems.length; i < l; ++i)
			{
				var elem = elems[i];
				var w = Kekule.Widget.getWidgetOnElem(elem);
				if (w && (w instanceof Kekule.Widget.Menu))
					return w;
			}
		}
		return null;
		*/
		var children = this.getChildWidgets();
		for (var i = 0, l = children.length; i < l; ++i)
		{
			var w = children[i];
			if (w instanceof Kekule.Widget.Menu)
				return w;
		}
		if (canCreate)
		{
			return this.createSubMenu();
		}
		else
			return null;
	},
	/**
	 * Add sub menu to this item.
	 * @param {Kekule.Widget.Menu} subMenu
	 */
	addSubMenu: function(subMenu)
	{
		subMenu.appendToWidget(this);
		return this;
	},
	/**
	 * Remove sub menu from item.
	 * @param {Kekule.Widget.Menu} subMenu
	 */
	removeSubMenu: function(subMenu)
	{
		subMenu.setParent(null);
		return subMenu;
	},
	/**
	 * Create a new sub menu under this item.
	 * @returns {Kekule.Widget.Menu}
	 */
	createSubMenu: function()
	{
		var result = new Kekule.Widget.PopupMenu(this);
		this.addSubMenu(result);
		return result;
	},

	/** @private */
	_createSubMenuMarker: function(subMenu)
	{
		var result = this._subMenuMarker;
		if (!result)
		{
			var result = this.getDocument().createElement('span');
			result.className = CNS.SUBMENU_MARKER;
			/*
			var refElem = subMenu.getElement() || null;
			//this.getElement().insertBefore(result, refElem);  // cause error when sub menu has not been inserted to DOM
			*/
			this.getElement().appendChild(result);
			this._subMenuMarker = result;
		}
		return result;
	},
	/** @private */
	_removeSubMenuMarker: function()
	{
		if (this._subMenuMarker)
		{
			this._subMenuMarker.parentNode.removeChild(this._subMenuMarker);
			this._subMenuMarker = null;
		}
	},

	/**
	 * Returns whether this menu item has no sub menu.
	 * @returns {Bool}
	 */
	isLeafItem: function()
	{
		var elem = this.getElement();
		return !(elem.getElementsByTagName('ul').length || elem.getElementsByTagName('ol').length);
	},
	/** @private */
	isPeriodicalExecuting: function($super)
	{
		return $super() && this.getIsActive();
	},

	/** @private */
	doReactActiviting: function($super, e)
	{
		$super(e);
		if (this.isLeafItem())
			if (this.getEnablePeriodicalExec())
				this.startPeriodicalExec(e);
	},
	/** @private */
	doReactDeactiviting: function($super, e)
	{
		if (this.isLeafItem())
		{
			//if (this.getEnablePeriodicalExec())
			this.stopPeriodicalExec();  // stop it anyway
			if (this.getIsActive())  // meet a active-deactive event, clicked or key pressed on menu
			{
				this.execute(e);
				if (this.getAutoCheck())
					this.setChecked(!this.getChecked());
				this.notifyDeactivated();
			}
		}
	},
	/**
	 * Notify parent menu that this item is activated (executed)
	 * and the menu may be hidden now.
	 * @private
	 */
	notifyDeactivated: function()
	{
		var p = this.getParent();
		if (p && p.childDeactivated)
		{
			p.childDeactivated(this);
		}
	},
	/**
	 * Be notified by child that child has been activated.
	 * @param {Kekule.Widget.BaseWidget} childWidget
	 * @private
	 */
	childDeactivated: function(childWidget)
	{
		var p = this.getParent();
		if (p && p.childDeactivated)
		{
			p.childDeactivated(this);
		}
	}
});
/** @ignore */
Kekule.Widget.MenuItem.SEPARATOR_TEXT = '-';
/**
 * A class method to create menu item from a definition hash.
 * If input parameter is string rather than hash, it will be regarded as
 * menu text. If the text is '-', a separator will be created.
 * @param {Variant} parentOrElementOrDocument
 * @param {Hash} defHash
 * @returns {Kekule.Widget.MenuItem}
 */
Kekule.Widget.MenuItem.createFromHash = function(parentOrElementOrDocument, defHash)
{
	var def = defHash;
	if (DataType.isSimpleValue(def))  // simple string value
	{

		if (def === Kekule.Widget.MenuItem.SEPARATOR_TEXT)  // create separator
			def = {isSeparator: true};
		else
			def = {'text': def};
	}
	if (!def.widget && !def.widgetClass)
		def.widget = Kekule.Widget.MenuItem;
	return Kekule.Widget.createFromHash(parentOrElementOrDocument, def);
};

/**
 * Base class of menu. Do not use it directly. Using {@link Kekule.Widget.PopupMenu} or
 * {@link Kekule.Widget.MenuBar} instead.
 * @class
 * @augments Kekule.Widget.Container
 *
 * @property {Bool} isSubMenu Whether the object is a nested sub menu.
 */
Kekule.Widget.Menu = Class.create(Kekule.Widget.Container,
/** @lends Kekule.Widget.Menu# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Widget.Menu',
	/** @private */
	BINDABLE_TAG_NAMES: ['ol', 'ul'],
	/** @private */
	MENU_ITEM_TAG: 'li',
	/** @constructs */
	initialize: function($super, parentOrElementOrDocument)
	{
		//this.setPropStoreFieldValue('useCornerDecoration', true);
		$super(parentOrElementOrDocument);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('isSubMenu', {'dataType': DataType.BOOL, 'serializable': false,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('isSubMenu', value);
				if (value)  // sub menu should always be vertical
					this.setLayout(Kekule.Widget.Layout.VERTICAL);
			}
		});
	},
	/** @ignore */
	initPropValues: function($super)
	{
		$super();
		this.setUseCornerDecoration(true);
	},
	/** @ignore */
	doGetWidgetClassName: function($super)
	{
		return $super() + ' ' + CNS.MENU;
	},
	/** @ignore */
	doCreateRootElement: function(doc)
	{
		var result = doc.createElement('ul');
		return result;
	},
	/** @ignore */
	doBindElement: function($super, element)
	{
		$super(element);
		var children = Kekule.DomUtils.getDirectChildElems(element);
		for (var i = 0, l = children.length; i < l; ++i)
		{
			var child = children[i];
			if (child.nodeType === Node.ELEMENT_NODE)
			{
				this.bindMenuItem(child);
			}
			child = child.nextSibling;
		}
	},
	/** @private */
	doDomElemAdded: function(elem)
	{
		this.bindMenuItem(elem);
	},

	/**
	 * @private
	 */
	bindMenuItem: function(elem)
	{
		if (elem.tagName.toLowerCase() === 'li')
		{
			var result = new Kekule.Widget.MenuItem(elem);
			result.setParent(this);
			return result;
		}
	},

	/**
	 * Returns prefered menu list tag (ul or ol) to create new sub menu.
	 * @returns {String}
	 */
	getSubMenuTagName: function()
	{
		return this.getElement().tagName;
	},
	/**
	 * Returns menu item widget in a menu or sub menu element.
	 * @param {HTMLElement} rootMenuElem Set null to get all first level items.
	 * @returns {Array}
	 */
	getMenuItems: function(rootMenuElem)
	{
		if (!rootMenuElem)
			rootMenuElem = this.getElement();
		var elems = DU.getDirectChildElems(menuElem, this.MENU_ITEM_TAG);
		var result = [];
		for (var i = 0, l = elems.length; i < l; ++i)
		{
			var w = Kekule.Widget.getWidgetOnElem(elems[i]);
			if (w && (w instanceof Kekule.Widget.MenuItem))
				result.push(w);
		}
		return result;
	},

	/**
	 * Insert a menu item before refItem. If refItem is not set, new item will be appended.
	 * @param {Kekule.Widget.MenuItem} menuItem
	 * @param {Kekule.Widget.MenuItem} refItem
	 */
	insertMenuItem: function(menuItem, refItem)
	{
		menuItem.insertToWidget(this, refItem)
		return this;
	},
	/**
	 * Append a menu item.
	 * @param {Kekule.Widget.MenuItem} menuItem
	 */
	appendMenuItem: function(menuItem)
	{
		menuItem.appendToWidget(this);
		return this;
	},
	/**
	 * Remove a menu item.
	 * @param {Kekule.Widget.MenuItem} menuItem
	 * @param {Bool} doNotFinalize
	 */
	removeMenuItem: function(menuItem, doNotFinalize)
	{
		menuItem.setParent(null);
		if (!doNotFinalize)
			menuItem.finalize();
	},
	/**
	 * Remove all items in menu.
	 * @param {Bool} doNotFinalize If set to true, the child menu items will not be finalized.
	 */
	clearMenuItems: function(doNotFinalize)
	{
		this.clearWidgets(doNotFinalize);
		return this;
	},
	/**
	 * Returns whether current menu is the top level one.
	 * @returns {Bool}
	 */
	isTopLevel: function()
	{
		var p = this.getParent();
		return !p ||
			!((p instanceof Kekule.Widget.MenuItem) || (p instanceof Kekule.Widget.Menu));
	},

	/**
	 * Be notified by child that child has been activated.
	 * @param {Kekule.Widget.BaseWidget} childWidget
	 * @private
	 */
	childDeactivated: function(childWidget)
	{
		var p = this.getParent();
		if (p)
		{
			if (p.childDeactivated)
			{
				p.childDeactivated(this);
			}
		}
	},
	/**
	 * Create child menu items by hash definitions.
	 * @param {Array} defs Hash definition of child menu items.
	 */
	createChildrenByDefs: function(defs)
	{
		for (var i = 0, l = defs.length; i < l; ++i)
		{
			var def = defs[i];
			var item = Kekule.Widget.MenuItem.createFromHash(this, def);
		}
		return this;
	}
});


/**
 * An popup menu widget.
 * @class
 * @augments Kekule.Widget.Menu
 */
Kekule.Widget.PopupMenu = Class.create(Kekule.Widget.Menu,
/** @lends Kekule.Widget.Menu# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Widget.PopupMenu',
	/** @ignore */
	initPropValues: function($super)
	{
		$super();
		this.setLayout(Kekule.Widget.Layout.VERTICAL);
	},
	/** @ignore */
	doGetWidgetClassName: function($super)
	{
		return $super() + ' ' + CNS.POPUPMENU;
	},
	/** @ignore */
	childDeactivated: function(childWidget)
	{
		//this.hide();
	}
});

/**
 * Menu bar (main manu) widget.
 * This widget is usually the top level menu and will not hide automatically.
 * @class
 * @augments Kekule.Widget.Menu
 */
Kekule.Widget.MenuBar = Class.create(Kekule.Widget.Menu,
/** @lends Kekule.Widget.MenuBar# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Widget.MenuBar',
	/** @ignore */
	doGetWidgetClassName: function($super)
	{
		return $super() + ' ' + CNS.MENUBAR;
	}
});

})();