Source: widgets/chem/editor/kekule.chemEditor.utilWidgets.js

/**
 * @fileoverview
 * Small widgets used by chem editor.
 * @author Partridge Jiang
 */

/*
 * requires /lan/classes.js
 * requires /core/kekule.common.js
 * requires /widgets/commonCtrls/kekule.widget.containers.js
 * requires /widgets/commonCtrls/kekule.widget.formControls.js
 * requires /widgets/commonCtrls/kekule.widget.dialogs.js
 * requires /widgets/commonCtrls/kekule.widget.tabViews.js
 * requires /widgets/chem/kekule.chemWidget.base.js
 * requires /widgets/chem/editor/kekule.chemEditor.configs.js
 * requires /widgets/chem/editor/kekule.chemEditor.editorUtils.js
 */

(function(){
"use strict";

var AU = Kekule.ArrayUtils;
var CNS = Kekule.Widget.HtmlClassNames;
var CCNS = Kekule.ChemWidget.HtmlClassNames;

/** @ignore */
Kekule.ChemWidget.HtmlClassNames = Object.extend(Kekule.ChemWidget.HtmlClassNames, {
	STRUCTURE_NODE_SELECT_PANEL: 'K-Chem-StructureNodeSelectPanel',
	STRUCTURE_NODE_SELECT_PANEL_SET_BUTTON: 'K-Chem-StructureNodeSelectPanel-SetButton',
	STRUCTURE_NODE_SETTER: 'K-Chem-StructureNodeSetter',
	STRUCTURE_NODE_SETTER_INPUTBOX: 'K-Chem-StructureNodeSetter-InputBox',
	STRUCTURE_CONNECTOR_SELECT_PANEL: 'K-Chem-StructureConnectorSelectPanel',
	STRUCTURE_CONNECTOR_SELECT_PANEL_SET_BUTTON: 'K-Chem-StructureConnectorSelectPanel-SetButton',
	CHARGE_SELECT_PANEL: 'K-Chem-Charge-SelectPanel',
	CHARGE_SELECT_PANEL_BTNGROUP: 'K-Chem-Charge-SelectPanel-BtnGroup',
	CHARGE_SELECT_PANEL_CHARGE_BTN: 'K-Chem-Charge-SelectPanel-ChargeBtn'
});

/**
 * An panel to set the atom of a chem structure node.
 * @class
 * @augments Kekule.Widget.Panel
 *
 * @property {Array} elementSymbols Array of atomic symbols.
 *   Elements that can be directly selected by button.
 *   Note: Isotope id (e.g., D, 13C) can be used here.
 *   Note: a special symbol '...' can be used to create a periodic table button.
 * @property {Array} nonElementInfos Array of info of displayed non-elements.
 *   Non element node (e.g. pseudoatom) types that can be directly selected by button.
 *   Each item is a hash {nodeClass, props, text, hint, description, isVarList, isNotVarList}.
 *   The field isVarList and isVarNotList is a special flag to indicate whether this item
 *   is a variable atom list.
 * @property {Array} subGroupInfos Array of info of displayed subgroups.
 *   Each item is a hash {text, hint, inputText, formulaText, description}.
 * @property {Array} subGroupRepItems Displayed subgroup repository items.
 *   Change this value will update property subGroupInfos.
 */
/**
 * Invoked when the new atom value has been setted.
 *   event param of it has field: {nodeClass, props, repositoryItem}
 * @name Kekule.ChemWidget.StructureNodeSelectPanel#valueChange
 * @event
 */
Kekule.ChemWidget.StructureNodeSelectPanel = Class.create(Kekule.Widget.Panel,
/** @lends Kekule.ChemWidget.StructureNodeSelectPanel# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.StructureNodeSelectPanel',
	/** @private */
	BTN_DATA_FIELD: '__$btn_data__',
	/** @construct */
	initialize: function($super, parentOrElementOrDocument)
	{
		this.setPropStoreFieldValue('displayElements', true);
		this.setPropStoreFieldValue('displayNonElements', true);
		this.setPropStoreFieldValue('displaySubgroups', true);
		$super(parentOrElementOrDocument);
		this.addEventListener('execute', this.reactSelButtonExec.bind(this));
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('elementSymbols', {
			'dataType': DataType.ARRAY,
			'scope': Class.PropertyScope.PUBLIC
		});
		this.defineProp('nonElementInfos', {
			'dataType': DataType.ARRAY,
			'scope': Class.PropertyScope.PUBLIC
		});
		this.defineProp('subGroupInfos', {
			'dataType': DataType.ARRAY,
			'scope': Class.PropertyScope.PUBLIC
		});
		this.defineProp('subGroupRepItems', {
			'dataType': DataType.ARRAY,
			'scope': Class.PropertyScope.PUBLIC,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('subGroupRepItems', value);
				this._updateSubGroupInfosFromRepItems(value);
			}
		});
		this.defineProp('displayElements', {
			'dataType': DataType.BOOL,
			'scope': Class.PropertyScope.PUBLISHED
		});
		this.defineProp('displayNonElements', {
			'dataType': DataType.BOOL,
			'scope': Class.PropertyScope.PUBLISHED
		});
		this.defineProp('displaySubgroups', {
			'dataType': DataType.BOOL,
			'scope': Class.PropertyScope.PUBLISHED
		});

		this.defineProp('activeNode', {'dataType': DataType.ARRAY, 'serializable': false,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('activeNode', value);
				this.activeNodeChanged(value);
			}
		});
		// private
		this.defineProp('periodicTableDialog', {
			'dataType': DataType.OBJECT,
			'scope': Class.PropertyScope.PRIVATE,
			'serializable': false,
			'setter': null,
			'getter': function(canCreate)
			{
				var result = this.getPropStoreFieldValue('periodicTableDialog');
				if (!result && canCreate)  // create new one
				{
					var parentElem = this.getCoreElement();
					var doc = this.getDocument();
					result = this._createPeriodicTableDialogWidget(doc, parentElem);
					this.setPropStoreFieldValue('periodicTableDialog', result);
				}
				return result;
			}
		});
	},
	/** @private */
	initPropValues: function($super)
	{
		return $super();
	},
	/** @ignore */
	doFinalize: function($super)
	{
		var dialog = this.getPeriodicTableDialog();
		if (dialog)
			dialog.finalize();
		$super();
	},

	/**
	 * A helper method to set elementSymbols, nonElementInfos, subGroupInfos/subGroupRepItems properties at the same time.
	 * @param {Hash} data A hash object contains fields {elementSymbols, nonElementInfos, subGroupInfos, subGroupRepItems}
	 */
	setSelectableInfos: function(data)
	{
		this.beginUpdate();
		try
		{
			this.setPropValues(data);
		}
		finally
		{
			this.endUpdate();
		}
	},

	/**
	 * Event handler to react on selector button clicked.
	 * @private
	 */
	reactSelButtonExec: function(e)
	{
		var self = this;
		//var currNode = this.getNode();
		var btn = e.target;
		if (this._selButtons.indexOf(btn) >= 0)  // is a selector button
		{
			var data = btn[this.BTN_DATA_FIELD];
			if (data)
			{
				if (data.isPeriodicTable)  // special periodic table button to select single element
				{
					this._openPeriodicTableDialog(btn, function(result){
						if (result === Kekule.Widget.DialogButtons.OK)
						{
							var symbol = self._periodicTable.getSelectedSymbol();
							self.notifyValueChange(self.generateSelectableDataFromElementSymbol(symbol));
						}
					}, {'isSimpleAtom': true});
				}
				else if (data.isVarList || data.isVarNotList)  // atom list, use periodic table to select elements
				{
					this._openPeriodicTableDialog(btn, function(result){
						if (result === Kekule.Widget.DialogButtons.OK)
						{
							var symbols = self._periodicTable.getSelectedSymbols();
							var nodeClass = Kekule.VariableAtom;
							var props = data.isVarList?
								{'allowedIsotopeIds': symbols, 'disallowedIsotopeIds': null}:
								{'allowedIsotopeIds': null, 'disallowedIsotopeIds': symbols};
							self.notifyValueChange({'nodeClass': nodeClass, 'props': props});
						}
					}, {'isVarList': data.isVarList, 'isVarNotList': data.isVarNotList});
				}
				else  // normal button
				{
					this.notifyValueChange(data);
				}
			}
		}
	},
	/**
	 * Notify the new atom value has been setted.
	 * @private
	 */
	notifyValueChange: function(newData)
	{
		this.invokeEvent('valueChange', {
			'value': {
				'nodeClass': newData.nodeClass,
				'props': newData.props,
				//'node': newData.node,
				'repositoryItem': newData.repositoryItem
			}
		});
	},
	/** @private */
	activeNodeChanged: function(newNode)
	{
		// TODO: process to switch active node
	},

	/** @ignore */
	doObjectChange: function(modifiedPropNames)
	{
		var affectedProps = [
			'elementSymbols', 'nonElementInfos', 'subGroupInfos',
			'displayElements', 'displayNonElements', 'displaySubGroups'
		];
		if (Kekule.ArrayUtils.intersect(modifiedPropNames, affectedProps).length)
			this.updatePanelContent(this.getDocument(), this.getCoreElement());
	},

	/** @ignore */
	doGetWidgetClassName: function($super)
	{
		return $super() + ' ' + CCNS.STRUCTURE_NODE_SELECT_PANEL;
	},
	/** @ignore */
	doCreateRootElement: function(doc)
	{
		var result = doc.createElement('span');
		return result;
	},
	/** @ignore */
	doCreateSubElements: function($super, doc, rootElem)
	{
		var result = $super(doc, rootElem);

		this.updatePanelContent(doc, rootElem);
		// Custom input

		return result;
	},

	/** @private */
	_updateSubGroupInfosFromRepItems: function(repItems)
	{
		var infos = [];
		if (repItems && repItems.length)
		{
			for (var i = 0, l = repItems.length; i < l; ++i)
			{
				var repItem = repItems[i];
				var structFragment = repItem.getStructureFragment();
				var text = structFragment.getAbbr();
				if (!text)
				{
					var formulaText = structFragment.getFormulaText();
					var formula = Kekule.FormulaUtils.textToFormula(formulaText);
					var richText = formula.getDisplayRichText();
					text = Kekule.Render.RichTextUtils._toDebugHtml(richText);
				}
				/*
				if (!text)
				{
					text = repItem.getInputTexts()[0];
				}
				*/
				var info = {
					'text': text,
					'repositoryItem': repItem,
					'nodeClass': structFragment.getClass(),
					'node': structFragment,
					'hint': repItem.getName()
				};
				infos.push(info);
			}
			this.setSubGroupInfos(infos);
		}
	},

	/** @private */
	updatePanelContent: function(doc, rootElem)
	{
		// empty old buttons and sections
		if (this._selButtons)
		{
			for (var i = this._selButtons.length - 1; i >= 0; --i)
			{
				var btn = this._selButtons[i];
				this.removeWidget(btn);
			}
		}
		this._selButtons = [];
		Kekule.DomUtils.clearChildContent(rootElem);

		var btnData = this.generateSelectableData();
		var tabNames = this._getDisplayedTabPageNames();
		var tab = new Kekule.Widget.TabView(this);

		if (tabNames.indexOf('atom') >= 0)
		{
			var tabPageAtom = tab.createNewTabPage(Kekule.$L('ChemWidgetTexts.CAPTION_ATOM'));
			if (this.getDisplayElements())  // normal elements
				var section = this.doCreateSelButtonSection(doc, tabPageAtom.getCoreElement(), btnData.elementData);
			if (this.getDisplayNonElements())// non-element
				var section = this.doCreateSelButtonSection(doc, tabPageAtom.getCoreElement(), btnData.nonElementData, true);
		}
		if (tabNames.indexOf('subgroup') >= 0)
		{
			// subgroups
			var tabPageSubgroup = tab.createNewTabPage(Kekule.$L('ChemWidgetTexts.CAPTION_SUBGROUP'));
			var section = this.doCreateSelButtonSection(doc, tabPageSubgroup.getCoreElement(), btnData.subGroupData, true);
		}

		tab.setShowTabButtons(tabNames.length > 1);
	},
	/** @private */
	_getDisplayedTabPageNames: function()
	{
		var result = [];
		if (this.getDisplayElements() || this.getDisplayNonElements())
			result.push('atom');
		if (this.getDisplaySubgroups())
			result.push('subgroup');
		return result;
	},

	// generate button data
	/** @private */
	generateSelectableDataFromElementSymbol: function(symbol)
	{
		var caption = symbol;
		var isotopeInfo = Kekule.IsotopesDataUtil.getIsotopeInfoById(symbol);
		if (isotopeInfo)
		{
			if (isotopeInfo.isotopeAlias)
				caption = isotopeInfo.isotopeAlias;
			else
			{
				caption = '<sup>' + isotopeInfo.massNumber + '</sup>' + isotopeInfo.elementSymbol;
			}
		}
		return {
			'text': caption,
			'nodeClass': Kekule.Atom,
			'props': {'isotopeId': symbol}
		};
	},
	/** @private */
	generateSelectableData: function()
	{
		var elementSymbols = this.getElementSymbols() || [];
		var elementData = [];
		for (var i = 0, l = elementSymbols.length; i < l; ++i)
		{
			var data;
			if (elementSymbols[i] === '...')  // a special periodic table symbol
				data = {
					'text': '\u2026',  //elementSymbols[i],
					'nodeClass': null,
					'hint': Kekule.$L('ChemWidgetTexts.CAPTION_ATOMLIST_PERIODIC_TABLE'),
					'isPeriodicTable': true
				};
			else
				data = this.generateSelectableDataFromElementSymbol(elementSymbols[i]);
			elementData.push(data);
		}

		var nonElementData = this.getNonElementInfos();

		var subGroupData = this.getSubGroupInfos();

		return {
			'elementData': elementData,
			'nonElementData': nonElementData,
			'subGroupData': subGroupData
		};
	},

	// methods about sub widget creation
	/** @private */
	doCreateSelButton: function(doc, parentElem, buttonData, showDescription)
	{
		var caption = buttonData.text;
		if (showDescription && buttonData.description)
			caption += '<span style="font-size:80%"> ' + buttonData.description + '</span>';
		//var btnClass = buttonData.isPeriodicTable? Kekule.Widget.Button: Kekule.Widget.RadioButton;
		var btnClass = Kekule.Widget.Button;
		var result = new btnClass(doc, caption);
		result.addClassName(CCNS.STRUCTURE_NODE_SELECT_PANEL_SET_BUTTON);
		if (result.setGroup)  // radio button
			result.setGroup(this.getClassName());
		if (buttonData.hint)
			result.setHint(buttonData.hint);
		result[this.BTN_DATA_FIELD] = buttonData;
		this._selButtons.push(result);
		return result;
	},
	/** @private */
	doCreateSectionSelButtons: function(doc, sectionElem, data, showDescription)
	{
		if (!data)
			return;
		for (var i = 0, l = data.length; i < l; ++i)
		{
			var btn = this.doCreateSelButton(doc, sectionElem, data[i], showDescription);
			btn.setParent(this);
			btn.appendToElem(sectionElem);
		}
	},
	/** @private */
	doCreateSection: function(doc, parentElem)
	{
		var result = doc.createElement('div');
		result.className = CNS.SECTION;
		if (parentElem)
			parentElem.appendChild(result);
		return result;
	},
	/** @private */
	doCreateSelButtonSection: function(doc, parentElem, elementData, showDescription)
	{
		var section = this.doCreateSection(doc, parentElem);
		this.doCreateSectionSelButtons(doc, section, elementData, showDescription);
		return section;
	},

	/** @private */
	_createPeriodicTableDialogWidget: function(doc, parentElem)
	{
		var dialog = new Kekule.Widget.Dialog(doc, Kekule.$L('ChemWidgetTexts.CAPTION_PERIODIC_TABLE_DIALOG'),
				[Kekule.Widget.DialogButtons.OK, Kekule.Widget.DialogButtons.CANCEL]
		);
		var table = new Kekule.ChemWidget.PeriodicTable(doc);
		table.setUseMiniMode(true).setEnableSelect(true);
		table.setParent(dialog);
		table.appendToElem(dialog.getClientElem());
		this._periodicTable = table;
		return dialog;
	},
	/** @private */
	_openPeriodicTableDialog: function(caller, callback, extraInfo)
	{
		var dialog = this.getPeriodicTableDialog(true);
		var enableMultiSelect = extraInfo.isVarList || extraInfo.isVarNotList;
		this._periodicTable.setEnableMultiSelect(enableMultiSelect).setSelectedSymbol(null);

		var node = this.getActiveNode();

		// var list
		if ((extraInfo.isVarList || extraInfo.isVarNotList) && node instanceof Kekule.VariableAtom)
		{
			var allowedIds = node.getAllowedIsotopeIds();
			var disallowedIds = node.getDisallowedIsotopeIds();
			this._periodicTable.setSelectedSymbols(extraInfo.isVarList? allowedIds: disallowedIds);
		}

		// simple atom
		if (!enableMultiSelect && node instanceof Kekule.Atom)
		{
			this._periodicTable.setSelectedSymbol(node.getSymbol());
		}

		dialog.openPopup(callback, this || caller);
	}
});

/**
 * A widget used by AtomIaController to set node in chem structures in chem editor.
 * @class
 * @augments Kekule.Widget.BaseWidget
 *
 * @property {Kekule.Widget.ButtonTextBox} nodeInputBox A text box to input atom or subgroup directly.
 * @property {Kekule.ChemWidget.StructureNodeSelectPanel} nodeSelectPanel The child node selector panel inside this widget.
 * @property {Bool} showInputBox
 * @property {Bool} showSelectPanel
 * @property {Bool} useDropDownSelectPanel If true, the select panel will be a drop-down child widget of input box.
 * @property {Array} selectableElementSymbols Array of atomic symbols.
 *   Elements that can be directly selected by button.
 *   Note: Isotope id (e.g., D, 13C) can be used here.
 *   Note: a special symbol '...' can be used to create a periodic table button.
 * @property {Array} selectableNonElementInfos Array of info of displayed non-elements.
 *   Non element node (e.g. pseudoatom) types that can be directly selected by button.
 *   Each item is a hash {nodeClass, props, text, hint, description}.
 * @property {Array} selectableSubGroupInfos Array of info of displayed subgroups.
 *   Each item is a hash {text, hint, inputText, formulaText, description}.
 * @property {Array} selectableSubGroupRepItems Displayed subgroup repository items.
 *   Change this value will update property subGroupInfos.
 *
 * @property {Object} labelConfigs Label configs object of render configs.
 *
 * @property {Array} nodes Structure nodes that currently be edited in node setter.
 *   Note: When done editting, the changes will not directly applied to nodes, editor should handle them insteadly.
 * @property {Hash} value Node new properties setted by setter. Include fields: {nodeClass, props, repositoryItem}
 * @property {String} nodeLabel
 */
/**
 * Invoked when the new atom value has been setted.
 *   event param of it has field: {nodeClass, props, repositoryItem}
 * @name Kekule.ChemWidget.StructureNodeSetter#valueChange
 * @event
 */
/**
 * Invoked when the new atom value has been selected from selection panel.
 *   event param of it has field: {nodeClass, props, repositoryItem}
 * @name Kekule.ChemWidget.StructureNodeSetter#valueSelect
 * @event
 */
Kekule.ChemWidget.StructureNodeSetter = Class.create(Kekule.Widget.BaseWidget,
/** @lends Kekule.ChemWidget.StructureNodeSetter# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.StructureNodeSetter',
	/** @construct */
	initialize: function($super, parentOrElementOrDocument)
	{
		$super(parentOrElementOrDocument);
		this._valueSetBySelectPanel = false;  // an internal flag, whether the value of node is set by click on select panel
		/*
		var self = this;
		this.addEventListener('keyup', function(e){
			console.log('key event', e);
			if (self.getUseDropDownSelectPanel())
			{
				var ev = e.htmlEvent;
				var keyCode = ev.getKeyCode();
				if (keyCode === Kekule.X.Event.KeyCode.DOWN)
				{
					self.showNodeSelectPanel();
				}
				else if (keyCode === Kekule.X.Event.KeyCode.UP)
				{
					self.hideNodeSelectPanel();
				}
			}
		});
		*/
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('nodes', {'dataType': DataType.ARRAY, 'serializable': false,
			'setter': function(value)
			{
				var nodes = Kekule.ArrayUtils.toArray(value);
				this.setPropStoreFieldValue('nodes', nodes);
				this.nodesChanged(nodes);
			}
		});
		this.defineProp('value', {'dataType': DataType.HASH, 'serializable': false, 'setter': null,
			'getter': function()
			{
				var result = this.getPropStoreFieldValue('value');
				if (!result && !this._valueSetBySelectPanel)
				{
					var text = this.getNodeInputBox().getValue();
					result = this._getValueFromDirectInputText(text);
				}
				return result;
			}
		});
		this.defineProp('nodeLabel', {'dataType': DataType.STRING, 'serializable': false, 'setter': null,
			'getter': function() { return this.getNodeInputBox().getValue(); }
		});

		this.defineProp('nodeInputBox', {
			'dataType': 'Kekule.Widget.ButtonTextBox',
			'serializable': false,
			'setter': false
		});
		this.defineProp('nodeSelectPanel', {
			'dataType': 'Kekule.ChemWidget.StructureNodeSelectPanel',
			'serializable': false,
			'setter': false
		});
		this.defineProp('showInputBox', {
			'dataType': DataType.BOOL,
			'getter': function() { return this.getNodeInputBox().getDisplayed(); },
			'setter': function(value) { this.getNodeInputBox().setDisplayed(value); }
		});
		this.defineProp('showSelectPanel', {
			'dataType': DataType.BOOL,
			'getter': function() { return this.getNodeSelectPanel().getDisplayed(); },
			'setter': function(value) { this.getNodeSelectPanel().setDisplayed(value); }
		});
		this.defineProp('useDropDownSelectPanel', {
			'dataType': DataType.BOOL,
			'setter': function(value) {
				if (this.getUseDropDownSelectPanel() !== value)
				{
					this.setPropStoreFieldValue('useDropDownSelectPanel', value);
					this._updateDropDownSettings(value);
				}
			}
		});

		this.defineProp('labelConfigs', {'dataType': DataType.OBJECT, 'serializable': false});
		//this.defineProp('editor', {'dataType': DataType.OBJECT, 'serializable': false});

		this._defineSelectPanelDelegatedProp('selectableElementSymbols', 'elementSymbols');
		this._defineSelectPanelDelegatedProp('selectableNonElementInfos', 'nonElementInfos');
		this._defineSelectPanelDelegatedProp('selectableSubGroupInfos', 'subGroupInfos');
		this._defineSelectPanelDelegatedProp('selectableSubGroupRepItems', 'subGroupRepItems');
		this._defineSelectPanelDelegatedProp('displaySelectableElements', 'displayElements');
		this._defineSelectPanelDelegatedProp('displaySelectableNonElements', 'displayNonElements');
		this._defineSelectPanelDelegatedProp('displaySelectableSubgroups', 'displaySubgroups');
	},
	/** @private */
	initPropValues: function($super)
	{
		$super();
	},
	/** @ignore */
	doFinalize: function($super)
	{
		var panel = this.getNodeSelectPanel();
		panel.finalize();
		$super();
	},
	/**
	 * Define property that directly mapped to select panel's property.
	 * @private
	 */
	_defineSelectPanelDelegatedProp: function(propName, selectPanelPropName)
	{
		if (!selectPanelPropName)
			selectPanelPropName = propName;
		var originalPropInfo = ClassEx.getPropInfo(Kekule.ChemWidget.StructureNodeSelectPanel, selectPanelPropName);
		var propOptions = Object.create(originalPropInfo);
		propOptions.getter = null;
		propOptions.setter = null;
		if (originalPropInfo.getter)
		{
			propOptions.getter = function()
			{
				return this.getNodeSelectPanel().getPropValue(selectPanelPropName);
			};
		}
		if (originalPropInfo.setter)
		{
			propOptions.setter = function(value)
			{
				this.getNodeSelectPanel().setPropValue(selectPanelPropName, value);
			}
		}
		return this.defineProp(propName, propOptions);
	},

	/** @ignore */
	doGetWidgetClassName: function($super)
	{
		return $super() + ' ' + CCNS.STRUCTURE_NODE_SETTER;
	},
	/** @ignore */
	doCreateRootElement: function(doc)
	{
		var result = doc.createElement('span');
		return result;
	},
	/** @ignore */
	doCreateSubElements: function($super, doc, rootElem)
	{
		var result = $super(doc, rootElem);
		var self = this;

		// input box
		var inputter = new Kekule.Widget.ButtonTextBox(this);
		this.setPropStoreFieldValue('nodeInputBox', inputter);
		inputter.addClassName(CCNS.STRUCTURE_NODE_SETTER_INPUTBOX);
		inputter.appendToElem(this.getCoreElement());
		inputter.addEventListener('valueChange', function(e){
			self._valueSetBySelectPanel = false;
			e.stopPropagation();  // elimiate valueChange event of inputbox, avoid bubble to parent
		});
		inputter.addEventListener('keyup', function(e){  // response to enter keypress in input box
			var ev = e.htmlEvent;
			var keyCode = ev.getKeyCode();
			if (keyCode === Kekule.X.Event.KeyCode.ENTER)
			{
				self._applyDirectInput();
			}
			if (self.getUseDropDownSelectPanel())
			{
				if (keyCode === Kekule.X.Event.KeyCode.DOWN)
				{
					self.showNodeSelectPanel();
				}
				else if (keyCode === Kekule.X.Event.KeyCode.UP)
				{
					self.hideNodeSelectPanel();
				}
			}
		});
		inputter.addEventListener('blur', function(e){
			if (e.target === inputter.getTextBox() && inputter.getIsDirty())
			{
				self._applyDirectInput();
			}
		});
		inputter.getButton().addEventListener('execute', function(e){
			if (self.getUseDropDownSelectPanel())
			{
				//self.getNodeSelectPanel().show(self.getNodeInputBox(), null, Kekule.Widget.ShowHideType.DROPDOWN);
				self.toggleNodeSelectPanel();
			}
			else
			{
				self._applyDirectInput();
			}
		});

		// select panel
		var panel = new Kekule.ChemWidget.StructureNodeSelectPanel(this);
		this.setPropStoreFieldValue('nodeSelectPanel', panel);
		panel.appendToElem(this.getCoreElement());
		panel.addEventListener('valueChange', function(e){
			var newEventArg = Object.extend({}, e);
			//self.invokeEvent('valueChange', {'value': e.value});
			e.stopPropagation();

			self._valueSetBySelectPanel = true;
			self.notifyValueChange(e.value, true);
			// when value is set by button in selector panel, auto hide it in drop down mode
			if (self.getUseDropDownSelectPanel())
				panel.hide();
		});

		this._updateDropDownSettings(this.getUseDropDownSelectPanel());

		return result;
	},
	/** @private */
	_updateDropDownSettings: function(useDropDownPanel)
	{
		var inputBox = this.getNodeInputBox();
		var selectPanel = this.getNodeSelectPanel();
		if (useDropDownPanel)
		{
			inputBox.setButtonKind(Kekule.Widget.Button.Kinds.DROPDOWN);
			selectPanel.removeFromDom();
		}
		else
		{
			inputBox.setButtonKind(Kekule.Widget.Button.Kinds.ENTER);
			selectPanel.appendToElem(this.getCoreElement());
			selectPanel.setStyleProperty('position', '');  // clear position:absolute value from previous dropdown show
			selectPanel.show(null, null,  Kekule.Widget.ShowHideType.DEFAULT);
		}
	},

	/** @private */
	_indexOfNonElementLabel: function(nodeLabel)
	{
		var infos = this.getNonAtomLabelInfos();
		for (var i = 0, l = infos.length; i < l; ++i)
		{
			var info = infos[i];
			if (info.nodeLabel === nodeLabel)
				return i;
		}
		return -1;
	},
	/** @private */
	_getNonAtomInfo: function(nodeLabel)
	{
		var infos = this.getSelectableNonElementInfos();
		if (infos)
		{
			for (var i = 0, l = infos.length; i <l; ++i)
			{
				var info = infos[i];
				if (info.text === nodeLabel)
					return info;
			}
		}
		return null;
	},
	/** @private */
	_getValueFromDirectInputText: function(text)
	{
		var nodeClass, modifiedProps, newNode, repItem, isUnknownPAtom;

		var nonAtomInfo = this._getNonAtomInfo(text);
		if (nonAtomInfo)  // is not an atom
		{
			nodeClass = nonAtomInfo.nodeClass;
			modifiedProps = nonAtomInfo.props;
			//isNonAtom = true;
		}
		else
		{
			//var editor = this.getEditor();
			// check if it is predefined subgroups first
			var subGroupRepositoryItem = Kekule.Editor.StoredSubgroupRepositoryItem2D.getRepItemOfInputText(text);
			repItem = subGroupRepositoryItem;
			if (subGroupRepositoryItem)  // add subgroup
			{
				/*
				 var baseAtom = Kekule.ArrayUtils.toArray(this.getNodes())[0];
				 var repResult = subGroupRepositoryItem.createObjects(baseAtom) || {};
				 var repObjects = repResult.objects;
				 if (editor)
				 {
				 var transformParams = Kekule.Editor.RepositoryStructureUtils.calcRepObjInitialTransformParams(editor, subGroupRepositoryItem, repResult, baseAtom, null);
				 editor.transformCoordAndSizeOfObjects(repObjects, transformParams);
				 }
				 */
				newNode = subGroupRepositoryItem.getStructureFragment(); //repObjects[0];
				nodeClass = newNode.getClass();
			}
			else if (text) // add normal node
			{
				nodeClass = Kekule.ChemStructureNodeFactory.getClassByLabel(text, null); // explicit set defaultClass parameter to null
				if (!nodeClass)
				{
					nodeClass = Kekule.Pseudoatom;
					isUnknownPAtom = true;
				}
				modifiedProps = (nodeClass === Kekule.Atom) ? {'isotopeId': text} :
						(nodeClass === Kekule.Pseudoatom) ? {'symbol': text} :
						{};
			}
		}
		var data = {
			'nodeClass': nodeClass, 'props': modifiedProps, /*'node': newNode*/ 'repositoryItem': repItem, 'isUnknownPseudoatom': isUnknownPAtom
		};

		return data;
	},
	/**
	 * Create new node modification information based on string in input text box directly.
	 * @private
	 */
	_applyDirectInput: function()
	{
		this._valueSetBySelectPanel = false;
		var inputBox = this.getNodeInputBox();
		var text = inputBox.getValue();
		var data = this._getValueFromDirectInputText(text);

		this.notifyValueChange(data);
		inputBox.setIsDirty(false);
	},

	/** @private */
	_getVarAtomListLabel: function()
	{
		var labelConfigs = this.getLabelConfigs();
		return labelConfigs? labelConfigs.getVariableAtom(): Kekule.ChemStructureNodeLabels.VARIABLE_ATOM;
	},
	_getVarAtomNotListLabel: function()
	{
		var labelConfigs = this.getLabelConfigs();
		return '~' + (labelConfigs? labelConfigs.getVariableAtom(): Kekule.ChemStructureNodeLabels.VARIABLE_ATOM);
	},
	/*
	 * Returns label that shows in node edit.
	 * @param {Kekule.ChemStructureNode} node
	 * @returns {String}
	 * @private
	 */
	/*
	_getNodeLabel: function(node)
	{
		var labelConfigs = this.getLabelConfigs();
		if (node.getIsotopeId)  // atom
			return node.getIsotopeId();
		else if (node instanceof Kekule.SubGroup)
		{
			var groupLabel = node.getAbbr() || node.getFormulaText();
			return groupLabel || labelConfigs.getRgroup();
		}
		else
		{
			var ri = node.getCoreDisplayRichTextItem(null, null, labelConfigs);
			return Kekule.Render.RichTextUtils.toText(ri);
		}
	},
	*/
	/** @private */
	_getAllNodeLabels: function(nodes)
	{
		return Kekule.Editor.StructureUtils.getAllChemStructureNodesLabel(nodes, this.getLabelConfigs());
		/*
		var nodeLabel;
		for (var i = 0, l = nodes.length; i < l; ++i)
		{
			var node = nodes[i];
			var currLabel = this._getNodeLabel(node);
			if (!nodeLabel)
				nodeLabel = currLabel;
			else
			{
				if (nodeLabel !== currLabel)  // different label, currently has different nodes
				{
					return null;
				}
			}
		}
		return nodeLabel;
		*/
	},

	/**
	 * Called when nodes property has been changed.
	 * @private
	 */
	nodesChanged: function(newNodes)
	{
		// update node label in edit
		var currLabel = this._getAllNodeLabels(newNodes) || '';
		var activeNode = currLabel? newNodes[0]: null;  // not empty currLabel means all nodes are in the same type
		this.getNodeSelectPanel().setActiveNode(activeNode);
		this.getNodeInputBox().setValue(currLabel);
		this.getNodeInputBox().setIsDirty(false);
		this.setPropStoreFieldValue('value', null);
		this._valueSetBySelectPanel = false;
	},

	/**
	 * Notify the new atom value has been setted.
	 * @private
	 */
	notifyValueChange: function(newData, isSelectedFromPanel)
	{
		//console.log('value changed', newData);
		this.setPropStoreFieldValue('value', newData);
		var eventData = {
			'nodeClass': newData.nodeClass,
			'props': newData.props,
			//'node': newData.node,
			'repositoryItem': newData.repositoryItem,
			'isUnknownPseudoatom': newData.isUnknownPseudoatom
		};
		if (isSelectedFromPanel)
			this.invokeEvent('valueSelect', {'value': eventData});
		this.invokeEvent('valueChange', {'value': eventData});
	},

	/**
	 * A helper method to change elementSymbols, nonElementInfos, subGroupInfos/subGroupRepItems properties of select panel at the same time.
	 * @param {Hash} data A hash object contains fields {elementSymbols, nonElementInfos, subGroupInfos, subGroupRepItems}
	 */
	setSelectableInfos: function(data)
	{
		this.getNodeSelectPanel().setSelectableInfos(data);
	},

	/**
	 * Show node select panel.
	 * Note this method should be called in drop down mode.
	 */
	showNodeSelectPanel: function()
	{
		var panel = this.getNodeSelectPanel();
		if (!panel.isShown())
		{
			panel.show(this.getNodeInputBox(), null, Kekule.Widget.ShowHideType.DROPDOWN);
		}
	},
	/**
	 * Hide node select panel.
	 * Note this method should be called in drop down mode.
	 */
	hideNodeSelectPanel: function()
	{
		var panel = this.getNodeSelectPanel();
		if (panel.isShown())
		{
			panel.hide();
		}
	},
	/**
	 * Toggle the show/hide state of node select panel.
	 * Note this method should be called in drop down mode.
	 */
	toggleNodeSelectPanel: function()
	{
		var panel = this.getNodeSelectPanel();
		if (panel.isShown())
			panel.hide();
		else
			panel.show(this.getNodeInputBox(), null, Kekule.Widget.ShowHideType.DROPDOWN);
	}
});

/**
 * An panel to set the bond type/order of a chem structure connector.
 * @class
 * @augments Kekule.Widget.Panel
 *
 * @property {Array} bondData Array of available bond properties.
 *   Each item is a hash, containing the properties of this bond item.
 *   e.g. {
 *     'bondProps': {'bondType': Kekule.BondType.COVALENT, 'bondOrder': Kekule.BondOrder.SINGLE, 'stereo': Kekule.BondStereo.NONE},
 *     'text': 'Single Bond', 'description': 'Single bond'
 *   }
 * @property {Hash} activeBondPropValues Bond property-value hash object of current selected bond.
 * @property {Array} bondPropNames Property names used in bondData.bondProps. Used to compare bond property values.
 *  ReadOnly.
 */
/**
 * Invoked when the new bond property has been setted.
 *   event param of it has field: {props}
 * @name Kekule.ChemWidget.StructureConnectorSelectPanel#valueChange
 * @event
 */
Kekule.ChemWidget.StructureConnectorSelectPanel = Class.create(Kekule.Widget.Panel,
/** @lends Kekule.ChemWidget.StructureConnectorSelectPanel# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.StructureConnectorSelectPanel',
	/** @private */
	BTN_DATA_FIELD: '__$btn_data__',
	/** @private */
	BTN_GROUP: '__$bond_btn_group__',
	/** @construct */
	initialize: function($super, parentOrElementOrDocument)
	{
		$super(parentOrElementOrDocument);
		this._selButtons = [];
		this._activeBtn = null;
		this.addEventListener('execute', this.reactSelButtonExec.bind(this));
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('bondData', {
			'dataType': DataType.ARRAY,
			'scope': Class.PropertyScope.PUBLIC
		});
		this.defineProp('activeBondPropValues', {
			'dataType': DataType.HASH,
			'scope': Class.PropertyScope.PUBLIC,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('activeBondPropValues', value);
				this.activeBondPropsChanged(value);
			}
		});
		this.defineProp('activeBondHtmlClass', {
			'dataType': DataType.STRING,
			'scope': Class.PropertyScope.PRIVATE,
			'setter': null,
			'getter': function()
			{
				var btn = this._activeBtn;
				if (btn)
				{
					var data = btn[this.BTN_DATA_FIELD];
					return data && data.htmlClass;
				}
				return null;
			}
		});
		this.defineProp('bondPropNames', {'dataType': DataType.ARRAY, 'serializable': false})
	},

	/** @ignore */
	doObjectChange: function(modifiedPropNames)
	{
		var affectedProps = [
			'bondData'
		];
		if (Kekule.ArrayUtils.intersect(modifiedPropNames, affectedProps).length)
			this.updatePanelContent(this.getDocument(), this.getCoreElement());
	},

	/** @ignore */
	doGetWidgetClassName: function($super)
	{
		return $super() + ' ' + CCNS.STRUCTURE_CONNECTOR_SELECT_PANEL;
	},
	/** @ignore */
	doCreateRootElement: function(doc)
	{
		var result = doc.createElement('span');
		return result;
	},
	/** @ignore */
	doCreateSubElements: function($super, doc, rootElem)
	{
		var result = $super(doc, rootElem);

		this.updatePanelContent(doc, rootElem);
		// Custom input

		return result;
	},

	/** @private */
	activeBondPropsChanged: function(value)
	{
		// deselect all buttons first
		if (this._activeBtn)
		{
			this._activeBtn.setChecked(false);
			this._activeBtn = null;
		}
		// then check the proper button
		if (value)
		{
			var btns = this._selButtons;
			for (var i = 0, l = btns.length; i < l; ++i)
			{
				var data = btns[i][this.BTN_DATA_FIELD];
				if (data && this._isBondPropsMatch(value, data.bondProps, this.getBondPropNames()))
				{
					btns[i].setChecked(true);
					this._activeBtn = btns[i];
					break;
				}
			}
		}
	},
	/** @private */
	_isBondPropsMatch: function(src, target, propNames)
	{
		return Kekule.Editor.StructureUtils.isBondPropsMatch(src, target, propNames);
	},

	/**
	 * Event handler to react on selector button clicked.
	 * @private
	 */
	reactSelButtonExec: function(e)
	{
		var self = this;
		//var currNode = this.getNode();
		var btn = e.target;
		if (this._selButtons.indexOf(btn) >= 0)  // is a selector button
		{
			this._activeBtn = btn;
			var data = btn[this.BTN_DATA_FIELD];
			if (data)
			{
				this.setPropStoreFieldValue('activeBondPropValues', data.bondProps);
				this.notifyValueChange(data.bondProps);
			}
		}
	},
	/**
	 * Notify the new bond props value has been setted.
	 * @private
	 */
	notifyValueChange: function(newData)
	{
		this.invokeEvent('valueChange', {
			'value': newData
		});
	},

	/** @private */
	updatePanelContent: function(doc, rootElem)
	{
		if (this._selButtons)
		{
			for (var i = this._selButtons.length - 1; i >= 0; --i)
			{
				var btn = this._selButtons[i];
				this.removeWidget(btn);
			}
		}
		var propNames = [];
		this._selButtons = [];
		//Kekule.DomUtils.clearChildContent(rootElem);
		var bondData = this.getBondData();
		if (bondData)
		{
			for (var i = 0, l = bondData.length; i < l; ++i)
			{
				var data = bondData[i];
				var propData = data.bondProps;
				var btn = this._createBondSelButton(doc, data);
				btn.appendToElem(rootElem);
				this._selButtons.push(btn);
				if (propData)
				  AU.pushUnique(propNames, Kekule.ObjUtils.getOwnedFieldNames(propData));
			}
		}
		if (!this.getBondPropNames())
			this.setPropStoreFieldValue('bondPropNames', propNames);
	},
	/** @private */
	_createBondSelButton: function(doc, data)
	{
		var result = new Kekule.Widget.RadioButton(this);
		result.addClassName(CCNS.STRUCTURE_CONNECTOR_SELECT_PANEL_SET_BUTTON);
		result.setGroup(this.BTN_GROUP).setShowGlyph(true).setShowText(false);
		result.setText(data.text || null).setHint(data.hint || data.description || null);
		if (data.htmlClass)
			result.addClassName(data.htmlClass);
		result[this.BTN_DATA_FIELD] = data;
		return result;
	}
});

/**
 * An panel to set the charge chem structure atom.
 * @class
 * @augments Kekule.Widget.Panel
 *
 * @property {Number} value Charge value of selected objects or set by panel.
 * @property {Number} minCharge
 * @property {Number} maxCharge
 */
/**
 * Invoked when the new bond property has been setted.
 *   event param of it has field: {props}
 * @name Kekule.ChemWidget.ChargeSelectPanel#valueChange
 * @event
 */
Kekule.ChemWidget.ChargeSelectPanel = Class.create(Kekule.Widget.Panel,
/** @lends Kekule.ChemWidget.ChargeSelectPanel# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.ChargeSelectPanel',
	/** @private */
	CHARGE_FIELD: '__$charge__',
	/** @construct */
	initialize: function($super, parentOrElementOrDocument)
	{
		this._chargeBtnGroups = {};  // private
		this._selectedButton = null; // private
		this._chargeButtonMap = [];  // private
		$super(parentOrElementOrDocument);
		this.addEventListener('execute', this.reactSelButtonExec.bind(this));
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('value', {
			'dataType': DataType.NUMBER,
			'scope': Class.PropertyScope.PUBLIC,
			'setter': function(value)
			{
				var oldValue = this.getValue();
				if (value !== oldValue)
				{
					var oldSelBtn = this._chargeButtonMap[oldValue];
					if (oldSelBtn)
					{
						oldSelBtn.setChecked(false);
						oldSelBtn.blur();
					}
					this.setPropStoreFieldValue('value', value);
					var newSelBtn = this._chargeButtonMap[value];
					if (newSelBtn)
						newSelBtn.setChecked(true);
					this._selectedButton = newSelBtn;
				}
			}
		});
		this.defineProp('minCharge', {
			'dataType': DataType.NUMBER,
			'scope': Class.PropertyScope.PUBLIC
		});
		this.defineProp('maxCharge', {
			'dataType': DataType.NUMBER,
			'scope': Class.PropertyScope.PUBLIC
		});
	},
	/** @ignore */
	initPropValues: function($super)
	{
		$super();
		this.setMaxCharge(4).setMinCharge(-4);
	},
	/** @ignore */
	doGetWidgetClassName: function($super)
	{
		return $super() + ' ' + CCNS.CHARGE_SELECT_PANEL;
	},
	/** @ignore */
	doCreateSubElements: function($super, doc, rootElem)
	{
		var result = $super(doc, rootElem);
		this.updateChargeButtons(doc, rootElem);
		return result;
	},
	/** @private */
	updateChargeButtons: function(doc, rootElem)
	{
		// clear old btn groups
		var groups = this._chargeBtnGroups;
		var groupNames = ['zero', 'positive', 'negative'];
		for (var i = 0, l = groupNames.length; i <l; ++i)
		{
			var group = groups[groupNames[i]];
			if (group)
				group.clearWidgets();
			else
			{
				group = new Kekule.Widget.ButtonGroup(this);
				group.addClassName(CCNS.CHARGE_SELECT_PANEL_BTNGROUP);
				group.appendToElem(rootElem);
				groups[groupNames[i]] = group;
			}
		}

		// recreate charge buttons
		var chargeMax = Math.floor(this.getMaxCharge());
		var chargeMin = Math.floor(this.getMinCharge());
		var btn;
		var sPositive = Kekule.$L('ChemWidgetTexts.TEXT_CHARGE_POSITIVE');
		var sNegative = Kekule.$L('ChemWidgetTexts.TEXT_CHARGE_NEGATIVE');
		// positive
		if (chargeMax >= 1)
		{
			var group = this._chargeBtnGroups.positive;
			for (var i = Math.max(chargeMin, 1); i <= chargeMax; ++i)
			{
				btn = this._createChargeButton(group, i, i + sPositive);
				//btn.appendToWidget(group);
			}
		}
		// zero
		if (chargeMax >= 0 && chargeMin <= 0)
		{
			var group = this._chargeBtnGroups.zero;
			btn = this._createChargeButton(group, 0, Kekule.$L('ChemWidgetTexts.TEXT_CHARGE_NONE'), Kekule.$L('ChemWidgetTexts.HINT_CHARGE_NONE'));
			//btn.appendToWidget(group);
		}
		// negative
		if (chargeMin <= -1)
		{
			var group = this._chargeBtnGroups.negative;
			for (var i = Math.min(chargeMax, -1); i >= chargeMin; --i)
			{
				btn = this._createChargeButton(group, i, Math.abs(i) + sNegative);
				//btn.appendToWidget(group);
			}
		}
	},
	/** @private */
	_createChargeButton: function(btnGroup, charge, text, hint)
	{
		var result = new Kekule.Widget.RadioButton(btnGroup, text);
		result[this.CHARGE_FIELD] = charge;
		this._chargeButtonMap[charge] = result;
		if (hint)
			result.setHint(hint);
		result.addClassName(CCNS.CHARGE_SELECT_PANEL_CHARGE_BTN);
		//result.setGroup(this.getClassName());  // group as one
		result.appendToWidget(btnGroup);
		if (this.getValue() === charge)
		{
			result.setChecked(true);
			this._selectedButton = result;
		}
		return result;
	},

	/** @private */
	reactSelButtonExec: function(e)
	{
		var target = e.target;
		var charge = target[this.CHARGE_FIELD];
		if (Kekule.ObjUtils.notUnset(charge))
		{
			this.setValue(charge);
			this.notifyValueChange(charge);
		}
	},

	/**
	 * Notify the new bond props value has been setted.
	 * @private
	 */
	notifyValueChange: function(newValue)
	{
		this.invokeEvent('valueChange', {
			'value': newValue
		});
	}
});

})();