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

/**
 * @fileoverview
 * Implementation of textbox widgets.
 * @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.buttons.js
 */

(function(){

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

/** @ignore */
Kekule.Widget.HtmlClassNames = Object.extend(Kekule.Widget.HtmlClassNames, {
	OVERLAP: 'K-Overlap',
	FORMCONTROL: 'K-FormControl',
	CHECKBOX: 'K-CheckBox',
	TEXTBOX: 'K-TextBox',
	COMBOTEXTBOX: 'K-ComboTextBox',
	COMBOTEXTBOX_ASSOC_WIDGET: 'K-ComboTextBox-Assoc-Widget',
	BUTTONTEXTBOX: 'K-ButtonTextBox',
	TEXTAREA: 'K-TextArea',
	SELECTBOX: 'K-SelectBox',
	COMBOBOX: 'K-ComboBox',
	COMBOBOX_TEXTWRAPPER: 'K-ComboBox-TextWrapper'
});

/**
 * Widget based on form controls.
 * @class
 * @augments Kekule.Widget.BaseWidget
 *
 * @property {name} Name of form control (name attribute of HTML element).
 * @property {Variant} value Value of form control.
 * @property {Bool} readOnly
 * @property {Bool} isDirty Whether the widget value has been changed by user.
 * @property {Hash} propertyAssocInfo This property stores the association information of widget and an object property.
 */
/**
 * Invoked when the value of form control element is changed by user.
 * This event will actually be fired when "change" event occurs on form element.
 * Instead of simply "change", the event name is "valueChange" to avoid conflict with
 * change event of ObjectEx.
 *   event param of it has field: {widget}
 * @name Kekule.Widget.FormWidget#valueChange
 * @event
 */
Kekule.Widget.FormWidget = Class.create(Kekule.Widget.BaseWidget,
/** @lends Kekule.Widget.FormWidget# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Widget.FormWidget',
	/** @constructs */
	initialize: function($super, parentOrElementOrDocument, isDumb)
	{
		this.reactValueChangeBind = this.reactValueChange.bind(this);  // important, this method must be set before bind to element
		this.reactInputBind = this.reactInput.bind(this);
		this.setPropStoreFieldValue('isDirty', false);
		$super(parentOrElementOrDocument, isDumb);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('name', {'dataType': DataType.STRING,
			'getter': function() { return this.getCoreElement().name; },
			'setter': function(value) { this.getCoreElement().name = value; }
		});
		this.defineProp('value', {'dataType': DataType.VARIANT, 'serializable': false,
			'getter': function() { return this.getCoreElement().value; },
			'setter': function(value) { this.getCoreElement().value = value; }
		});
		this.defineProp('isDirty', {'dataType': DataType.BOOL, 'serializable': false});
		this.defineProp('readOnly', {'dataType': DataType.BOOL, 'serializable': false,
			'getter': function() { return this.getCoreElement().readOnly; },
			'setter': function(value) { this.getCoreElement().readOnly = value; }
		});
	},
	/** @private */
	getTextSelectable: function()
	{
		return true;
	},
	/** @ignore */
	doGetWidgetClassName: function($super)
	{
		return $super() + ' ' + CNS.FORMCONTROL;
	},

	/** @ignore */
	doBindElement: function($super, element)
	{
		$super(element);
		var coreElem = this.getCoreElement();
		if (coreElem)
		{
			var ie8Fix = Kekule.Browser.IE && (Kekule.Browser.IEVersion === 8);

			Kekule.X.Event.addListener(coreElem, 'change', this.reactValueChangeBind);
			Kekule.X.Event.addListener(coreElem, 'input', this.reactInputBind);

			if (ie8Fix)  // for IE 8, change or input event can not evoked on input, so we use keyup to detect
			{
				this._bindKeyupEvent = true;
				Kekule.X.Event.addListener(coreElem, 'keyup', this.reactInputBind);
			}
			else
				this._bindKeyupEvent = false;
		}
	},
	/** @ignore */
	doUnbindElement: function($super, element)
	{
		var coreElem = this.getCoreElement();
		if (coreElem)
		{
			Kekule.X.Event.removeListener(coreElem, 'change', this.reactValueChangeBind);
			Kekule.X.Event.removeListener(coreElem, 'input', this.reactInputBind);
			if (this._bindKeyupEvent)
				Kekule.X.Event.removeListener(coreElem, 'keyup', this.reactInputBind);  // for IE6-8
		}
		$super(element);
	},
	notifyValueChanged: function()
	{
		//console.log('value change', this.getClassName());
		this.setIsDirty(true);
		this.invokeEvent('valueChange', {'widget': this});
	},
	/**
	 * Select all content in widget.
	 */
	selectAll: function()
	{
		var elem = this.getCoreElement();
		if (elem && elem.select)
			elem.select();
		return this;
	},
	/** @private */
	reactValueChange: function()
	{
		this.notifyValueChanged();
	},
	/** @private */
	reactInput: function(e)
	{
		this.setIsDirty(true);
	}
});

/**
 * An general check box box widget.
 * @class
 * @augments Kekule.Widget.FormWidget
 *
 * @property {Bool} checked Whether the box is checked.
 * @property {String} text Caption after check box.
 */
Kekule.Widget.CheckBox = Class.create(Kekule.Widget.FormWidget,
/** @lends Kekule.Widget.CheckBox# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Widget.CheckBox',
	/** @private */
	BINDABLE_TAG_NAMES: ['div', 'span'],
	/** @constructs */
	initialize: function($super, parentOrElementOrDocument, checked)
	{
		$super(parentOrElementOrDocument);
		if (Kekule.ObjUtils.notUnset(checked))
			this.setChecked(!!checked);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('checked', {'dataType': DataType.BOOL,
			'getter': function()
			{
				var core = this.getCoreElement();
				return core? core.checked: false;
			},
			'setter': function(value)
			{
				var core = this.getCoreElement();
				if (core)
					core.checked = !!value;
			}
		});
		this.defineProp('text', {'dataType': DataType.STRING,
			'getter': function()
			{
				return Kekule.DomUtils.getElementText(this.getLabelElem());
			},
			'setter': function(value)
			{
				Kekule.DomUtils.setElementText(this.getLabelElem(), value);
			}
		});
	},
	/** @ignore */
	getCoreElement: function()
	{
		return this.getInputElem();
	},

	/** @ignore */
	doGetWidgetClassName: function($super)
	{
		return $super() + ' ' + CNS.CHECKBOX;
	},

	/**
	 * Returns <input type="checkbox"> element inside widget.
	 * @private
	 */
	getInputElem: function()
	{
		var elem = this.getElement();
		if (elem)
		{
			var inputs = elem.getElementsByTagName('input');
			if (inputs && inputs.length)
				return inputs[0];
		}
		return null;
	},
	/**
	 * Returns <label> element inside widget.
	 * @private
	 */
	getLabelElem: function()
	{
		var elem = this.getElement();
		if (elem)
		{
			if (elem.tagName.toLowerCase() === 'label')
				return elem;
			else
			{
				var labels = elem.getElementsByTagName('label');
				if (labels && labels.length)
					return labels[0];
			}
		}
		return null;
	},

	/** @ignore */
	doCreateRootElement: function(doc)
	{
		var result = doc.createElement('span');
		return result;
	},
	/** @ignore */
	doCreateSubElements: function(doc, rootElem)
	{
		/*
		var rootTag = rootElem.tagName.toLowerCase();
		var parentElem = null;
		if (rootTag === 'label')
		{
			parentElem = rootElem;
		}
		else
		{
			parentElem = this.doCreateRootElement(doc);
			rootElem.appendChild(parentElem);
		}
		*/
		var labelElem = doc.createElement('label');
		rootElem.appendChild(labelElem);
		var inputElem = doc.createElement('input');
		inputElem.setAttribute('type', 'checkbox');
		labelElem.appendChild(inputElem);
		return [labelElem, inputElem];
	}
});

/**
 * An general text box widget.
 * @class
 * @augments Kekule.Widget.FormWidget
 *
 * @property {String} text Text in textbox.
 * @property {String} placeholder Placeholder text of text box.
 */
Kekule.Widget.TextBox = Class.create(Kekule.Widget.FormWidget,
/** @lends Kekule.Widget.TextBox# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Widget.TextBox',
	/** @private */
	BINDABLE_TAG_NAMES: ['input'],
	/** @constructs */
	initialize: function($super, parentOrElementOrDocument, text)
	{
		$super(parentOrElementOrDocument);
		if (text)
			this.setText(text);
		//this.setUseCornerDecoration(true);
		//this._manualPlaceholder = !Kekule.BrowserFeature.html5Form.placeholder;  // indicates old browser that not support placeholder
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('text', {'dataType': DataType.STRING,
			'getter': function() { return this.getElement().value; },
			'setter': function(value)
			{
				if (value)
					this.getElement().value = value;
				else
					this.getElement().value = '';
			}
		});
		this.defineProp('placeholder', {'dataType': DataType.STRING,
			'getter': function() { return this.getElement().placeholder; },
			'setter': function(value) { this.getElement().placeholder = value; }
		});
	},
	/** @ignore */
	doGetWidgetClassName: function($super)
	{
		return $super() + ' ' + CNS.TEXTBOX;
	},
	/** @ignore */
	doCreateRootElement: function(doc)
	{
		var result = doc.createElement('input');
		result.setAttribute('type', 'text');
		return result;
	},

	/** @ignore */
	doBindElement: function($super, element)
	{
		$super(element);
	}
});

/**
 * An text box with additional widget at heading (left) and tailing (right).
 * @class
 * @augments Kekule.Widget.FormWidget
 *
 * @property {String} text Text in textbox.
 * @property {String} placeholder Placeholder text of text box.
 * @property {Bool} overlapOnTextBox If true, heading/tailing widget will be put directly on textbox
 *   and the text box will use CSS padding to avoid text overlap with widgets. Otherwise Widgets will
 *   be put at left/right of text box.
 */
Kekule.Widget.ComboTextBox = Class.create(Kekule.Widget.FormWidget,
/** @lends Kekule.Widget.ComboTextBox# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Widget.ComboTextBox',
	/** @private */
	BINDABLE_TAG_NAMES: ['div', 'span'],
	/** @constructs */
	initialize: function($super, parentOrElementOrDocument, text)
	{
		$super(parentOrElementOrDocument);
		if (text)
			this.setText(text);
		//this.setBubbleUiEvents(true);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('text', {'dataType': DataType.STRING, 'serializable': false,
			'getter': function()
			{
				var textBox = this.getTextBox();
				return textBox? textBox.getText(): null;
			},
			'setter': function(value)
			{
				var textBox = this.getTextBox();
				if (textBox)
					textBox.setText(value);
			}
		});
		this.defineProp('placeholder', {'dataType': DataType.STRING,
			'getter': function() { return this.getTextBox().getPlaceholder(); },
			'setter': function(value) { this.getTextBox().setPlaceholder(value); }
		});

		this.defineProp('overlapOnTextBox', {'dataType': DataType.BOOL,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('overlapOnTextBox', value);
				if (value)
					this.addClassName(CNS.OVERLAP);
				else
					this.removeClassName(CNS.OVERLAP);
				this.adjustWidgetsSize();
			}
		});
		// private
		this.defineProp('textBox', {'dataType': 'Kekule.Widget.TextBox', 'serializable': false, 'setter': null});
		this.defineProp('headingWidget', {'dataType': 'Kekule.Widget.BaseWidget', 'serializable': false,
			'setter': function(value)
			{
				var old = this.getHeadingWidget();
				if (value !== old)
				{
					this.setPropStoreFieldValue('headingWidget', value);
					if (old)
					{
						old.setParent(null);  // remove old
						old.removeClassName(CNS.COMBOTEXTBOX_ASSOC_WIDGET);
					}
					if (value)
					{
						var refElem = this.getTextBox()? this.getTextBox().getElement():
							this.getTailingWidget()? this.getTailingWidget().getElement():
							null;
						value.setParent(this);
						value.addClassName(CNS.COMBOTEXTBOX_ASSOC_WIDGET);
						value.insertToElem(this.getElement(), refElem);
					}
				}
			}
		});
		this.defineProp('tailingWidget', {'dataType': 'Kekule.Widget.BaseWidget', 'serializable': false,
			'setter': function(value)
			{
				var old = this.getTailingWidget();
				if (value !== old)
				{
					this.setPropStoreFieldValue('tailingWidget', value);
					if (old)
					{
						old.setParent(null);  // remove old
						old.removeClassName(CNS.COMBOTEXTBOX_ASSOC_WIDGET);
					}
					if (value)
					{
						value.setParent(this);
						value.addClassName(CNS.COMBOTEXTBOX_ASSOC_WIDGET);
						value.appendToElem(this.getElement());
					}
				}
			}
		});
	},
	/** @ignore */
	finalize: function($super)
	{
		this._finalizeSubWidgets();
		$super();
	},
	/** @private */
	_finalizeSubWidgets: function()
	{
		var textBox = this.getTextBox();
		if (textBox)
		{
			textBox.finalize();
		}
		var widget = this.getHeadingWidget();
		if (widget)
		{
			widget.finalize();
			//this.setHeadingWidget(null);
		}
		var widget = this.getTailingWidget();
		if (widget)
		{
			widget.finalize();
			//this.setTailingWidget(null);
		}
	},

	/** @ignore */
	getCoreElement: function($super)
	{
		var textBox = this.getTextBox();
		if (textBox)
			return textBox.getElement();
		else
			return $super();
	},

	/** @ignore */
	doGetWidgetClassName: function($super)
	{
		return $super() + ' ' + CNS.COMBOTEXTBOX;
	},

	/** @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;
		this._finalizeSubWidgets();

		/*
		// important, use span to put a width 100% input box,
		// otherwise width of input box is hard to set.
		var wrapper = doc.createElement('span');
		wrapper.className = CNS.COMBOBOX_TEXTWRAPPER;
		rootElem.appendChild(wrapper);
		*/
		var textBox = new Kekule.Widget.TextBox(this);
		textBox.addClassName(CNS.TEXTBOX);
		//textBox.appendToElem(wrapper);
		textBox.appendToElem(this.getElement());
		this.setPropStoreFieldValue('textBox', textBox);

		result.push(textBox.getElement());
		return result;
	},

	/** @ignore */
	relayEvent: function($super, eventName, event)
	{
		var invokerWidget = event.widget;
		if ((invokerWidget === this.getTextBox()) || (invokerWidget === this.getHeadingWidget()) || (invokerWidget === this.getTailingWidget()))
			event.widget = this;
		return $super(eventName, event);
	},

	/** @ignore */
	doSetEnabled: function(value)
	{
		var textBox = this.getTextBox();
		if (textBox)
			textBox.setEnabled(value);
		var widget = this.getHeadingWidget();
		if (widget)
			widget.setEnabled(value);
		widget = this.getTailingWidget();
		if (widget)
			widget.setEnabled(value);
	},

	/** @ignore */
	doResize: function()
	{
		this.adjustWidgetsSize();
	},

	/** @ignore */
	widgetShowStateChanged: function($super, isShown, byDomChange)
	{
		$super(isShown, byDomChange);
		if (isShown)
			this.adjustWidgetsSize();
	},
	/** @ignore */
	doInsertedToDom: function()
	{
		if (this.isShown())
			this.adjustWidgetsSize();
	},
	/**
	 * Called after new heading or tailing widget set.
	 * @private
	 */
	assocWidgetChanged: function()
	{
		this.adjustWidgetsSize();
	},
	/** @private */
	adjustWidgetsSize: function()
	{
		var SU = Kekule.StyleUtils;
		var overlap = this.getOverlapOnTextBox();
		var position = overlap? 'absolute': 'relative';

		var textElem = this.getTextBox().getElement();
		if (!textElem)  // textbox disposed, may be in finalize phase, no need to adjust
			return;
		//var textRect = Kekule.HtmlElementUtils.getElemBoundingClientRect(textElem);
		var textRect = Kekule.HtmlElementUtils.getElemPageRect(textElem);

		// heading
		var widget = this.getHeadingWidget();
		var elem = widget? widget.getElement(): null;
		if (elem && widget.isShown())
		{
			var style = elem.style;
			style.position = position;
			if (overlap)
			{
				//var rect = Kekule.HtmlElementUtils.getElemBoundingClientRect(elem);
				var rect = Kekule.HtmlElementUtils.getElemPageRect(elem);
				style.left = 0;
				style.top = //(rect.height - textRect.height) / 2;
					((textRect.height - rect.height) / 2) + 'px';

				textElem.style.paddingLeft = rect.width + 'px';
			}
			else
			{
				SU.removeStyleProperty(style, 'left');
				SU.removeStyleProperty(style, 'top');
				SU.removeStyleProperty(textElem.style, 'paddingLeft');
			}
		}
		else
		{
			SU.removeStyleProperty(textElem.style, 'paddingLeft');
		}

		// tailing
		var widget = this.getTailingWidget();
		var elem = widget? widget.getElement(): null;
		if (elem && widget.isShown())
		{
			var style = elem.style;
			style.position = position;
			if (overlap)
			{
				//var rect = Kekule.HtmlElementUtils.getElemBoundingClientRect(elem);
				var rect = Kekule.HtmlElementUtils.getElemPageRect(elem);
				style.right = 0;
				style.top = //(rect.height - textRect.height) / 2;
					((textRect.height - rect.height) / 2) + 'px';

				textElem.style.paddingRight = rect.width + 'px';
			}
			else
			{
				SU.removeStyleProperty(style, 'right');
				SU.removeStyleProperty(style, 'top');
				SU.removeStyleProperty(textElem.style, 'paddingRight');
			}
		}
		else
		{
			SU.removeStyleProperty(textElem.style, 'paddingRight');
		}
	}
});

/**
 * An text box with additional button at tailing (right).
 * @class
 * @augments Kekule.Widget.ComboTextBox
 *
 * @property {Kekule.Widget.Button} button Button at the tailing of text box.
 * @property {String} buttonText Text of button.
 * @property {String} buttonKind Predefined kind of button, value from {@link Kekule.Widget.Button.Kinds}.
 */
/**
 * Invoked when button in widget is executed.
 *   Event param of it has field: {widget: this (not button)}
 * @name Kekule.Widget.FormWidget#buttonExecute
 * @event
 */
Kekule.Widget.ButtonTextBox = Class.create(Kekule.Widget.ComboTextBox,
/** @lends Kekule.Widget.ButtonTextBox# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Widget.ButtonTextBox',
	/** @constructs */
	initialize: function($super, parentOrElementOrDocument, text)
	{
		$super(parentOrElementOrDocument, text);
		this.setOverlapOnTextBox(true);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('button', {'dataType': 'Kekule.Widget.Button', 'serializable': false, 'setter': null,
			'getter': function() { return this.getTailingWidget(); }
		});
		this.defineProp('buttonKind', {'dataType': DataType.STRING,
			'getter': function() { return this.getButton().getButtonKind(); },
			'setter': function(value) { this.getButton().setButtonKind(value); }
		});
		this.defineProp('buttonText', {'dataType': DataType.STRING,
			'getter': function() { return this.getButton().getText(); },
			'setter': function(value) { this.getButton().setText(value).setShowText(!!value); }
		});
	},
	/** @ignore */
	doGetWidgetClassName: function($super)
	{
		return $super() + ' ' + CNS.BUTTONTEXTBOX;
	},

	/** @ignore */
	doCreateSubElements: function($super, doc, rootElem)
	{
		var result = $super(doc, rootElem);
		var btn = this.createAssocButton();
		if (btn)
			result.push(btn.getElement());
		return result;
	},

	/** @private */
	createAssocButton: function()
	{
		var btn = new Kekule.Widget.Button(this);
		btn.setShowText(false);
		this.setTailingWidget(btn);
		btn.addEventListener('change', this.adjustWidgetsSize, this);
		btn.addEventListener('execute', function(e)
			{
				this.invokeEvent('buttonExecute', {'widget': this});
			}, this);
		return btn;
	},

	/** @private */
	execBtn: function(e)
	{
		var btn = this.getButton();
		if (btn)
		{
			btn.execute(e);
			return true;
		}
	},

	// ui event reactor
	/** @ignore */
	react_keypress: function(e)
	{
		if (e.getKeyCode() === 13)  // enter
		{
			this.execBtn(e);
		}
	},
	/** @ignore */
	react_keydown: function(e)
	{
		if (this.getButtonKind() === Kekule.Widget.Button.Kinds.DROPDOWN)
		{
			var KC = Kekule.X.Event.KeyCode;
			if (e.getKeyCode() === KC.DOWN)
			{
				this.execBtn(e);
			}
		}
	}
});


/**
 * An general text area widget.
 * @class
 * @augments Kekule.Widget.FormWidget
 *
 * @property {String} text Text in textarea.
 * @property {String} placeholder Placeholder text of text box.
 * //@property {Int} rows Rows in textarea.
 * //@property {Int} cols Cols in textarea.
 * @property {String} wrap Wrap mode of textarea, value between "physical", "virtual" and "off".
 * @property {Bool} autoSizeX
 * @property {Bool} autoSizeY
 */
Kekule.Widget.TextArea = Class.create(Kekule.Widget.FormWidget,
/** @lends Kekule.Widget.TextArea# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Widget.TextArea',
	/** @private */
	BINDABLE_TAG_NAMES: ['textarea'],
	/** @constructs */
	initialize: function($super, parentOrElementOrDocument, text)
	{
		$super(parentOrElementOrDocument);
		if (text)
			this.setText(text);
		//this.setUseCornerDecoration(true);
		//this._manualPlaceholder = !Kekule.BrowserFeature.html5Form.placeholder;  // indicates old browser that not support placeholder
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('text', {'dataType': DataType.STRING,
			'getter': function() { return this.getValue(); },
			'setter': function(value)
			{
				this.setValue(value);
				this.adjustAutoSize();
				/*
				if (value)
					this.getElement().value = value;
				else
					this.getElement().value = '';
				*/
			}
		});
		this.defineProp('placeholder', {'dataType': DataType.STRING,
			'getter': function() { return this.getElement().placeholder; },
			'setter': function(value) { this.getElement().placeholder = value; }
		});
		this.defineProp('autoSizeX', {'dataType': DataType.BOOL,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('autoSizeX', value);
				this._autoSizeChanged();
			}
		});
		this.defineProp('autoSizeY', {'dataType': DataType.BOOL,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('autoSizeY', value);
				this._autoSizeChanged();
			}
		});
		//this.defineElemAttribMappingProp('cols', 'cols', {'dataType': DataType.INT});
		//this.defineElemAttribMappingProp('rows', 'rows', {'dataType': DataType.INT});
		//this.defineElemAttribMappingProp('wrap', 'wrap', {'dataType': DataType.STRING});
		this.defineProp('wrap', {'dataType': DataType.STRING,
			'getter': function()
			{
				return this.getElement().wrap;
			},
			'setter': function(value)
			{
				this.getElement().wrap = value;
			}
		});
	},
	/** @ignore */
	doGetWidgetClassName: function($super)
	{
		return $super() + ' ' + CNS.TEXTAREA;
	},
	/** @ignore */
	doCreateRootElement: function(doc)
	{
		var result = doc.createElement('textarea');
		return result;
	},

	/** @ignore */
	doBindElement: function($super, element)
	{
		$super(element);
		this.adjustAutoSize();
	},

	/** @ignore */
	setValue: function($super, value)
	{
		$super(value);
		this.adjustAutoSize();
	},

	/** @ignore */
	widgetShowStateChanged: function($super, isShown, byDomChange)
	{
		$super(isShown, byDomChange);
		if (isShown)
			this.adjustAutoSize();
	},

	/** @ignore */
	reactValueChange: function($super)
	{
		this.adjustAutoSize();
		$super();
	},
	/** @ignore */
	react_keyup: function()
	{
		this.adjustAutoSize();
	},
	react_keypress: function()
	{
		this.adjustAutoSize();
	},

	/** @ignore */
	doWidgetShowStateChanged: function(isShown)
	{
		if (isShown)
		{
			this.adjustAutoSize();
		}
	},

	/** @private */
	_autoSizeChanged: function()
	{
		var style = this.getCoreElement().style;
		if (this.getAutoSizeX())
		{
			style.overflowX = 'hidden';
			this.setWrap('off');
		}
		else
		{
			style.overflowX = 'auto';
			this.setWrap('');
		}
		if (this.getAutoSizeY())
			style.overflowY = 'hidden';
		else
			style.overflowY = 'auto';
		this.adjustAutoSize();
	},

	/** @private */
	adjustAutoSize: function()
	{
		if (this.getAutoSizeX() || this.getAutoSizeY())
			this.adjustSizeByContent();
	},

	/**
	 * Change rows and cols property of textarea to fit the content inside it.
	 * @private
	 */
	adjustSizeByContent: function()
	{
		/*
		var text = this.getText();
		var lineCount = Kekule.StrUtils.getLineCount(text);
		var colCount = Kekule.StrUtils.getMaxLineCharCount(text);
		this.setRows(lineCount);
		this.setCols(colCount);
		return this;
		*/
		var elem = this.getCoreElement();
		var text = this.getText();
		var isEmpty = !text;

		var style = elem.style;
		if (this.getAutoSizeX())
			style.width = '1px';
		if (this.getAutoSizeY())
			style.height = '1px';

		var scrollDim = Kekule.HtmlElementUtils.getElemScrollDimension(elem);
		var clientDim = Kekule.HtmlElementUtils.getElemClientDimension(elem);
		if (this.getAutoSizeX())
		{
			if (isEmpty)  // use min width
				style.width = '1em';
			else if (scrollDim.width > clientDim.width)
				style.width = scrollDim.width + 'px';
		}
		if (this.getAutoSizeY())
		{
			if (isEmpty)  // use min height
				style.height = '1em';
			if (scrollDim.height > clientDim.height)
				style.height = scrollDim.height + 'px';
		}
	}
});

/**
 * An general select box widget.
 * @class
 * @augments Kekule.Widget.FormWidget
 *
 * @property {Array} items An array of hash objects that contains value and title info of select box item.
 *   Each item of array may have the following fields: {text, value, title, data}.
 * @property {Int} index Selected index of box items.
 * @property {Variant} value Selected value of box.
 */
Kekule.Widget.SelectBox = Class.create(Kekule.Widget.FormWidget,
/** @lends Kekule.Widget.SelectBox# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Widget.SelectBox',
	/** @private */
	BINDABLE_TAG_NAMES: ['select'],
	/** @private */
	ITEM_DATA_FIELD: '__$item_data__',
	/** @private */
	ITEM_VALUE_FIELD: '__$item_value__',
	/** @constructs */
	initialize: function($super, parentOrElementOrDocument, items)
	{
		$super(parentOrElementOrDocument);
		if (items)
			this.setItems(items);
		//this.setUseCornerDecoration(true);
		//this._manualPlaceholder = !Kekule.BrowserFeature.html5Form.placeholder;  // indicates old browser that not support placeholder
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('items', {'dataType': DataType.ARRAY, 'serializable': false,
			'getter': function()
			{
				var elems = this.getAllItemElems();
				var result = [];
				for (var i = 0, l = elems.length; i < l; ++i)
				{
					var elem = elems[i];
					var info = this._getBoxItemInfo(elem);
					result.push(info);
				}
				return result;
			},
			'setter': function(value)
			{
				var root = this.getElement();
				this.clear();
				if (value)
				{
					var items = Kekule.ArrayUtils.toArray(value);
					for (var i = 0, l = items.length; i < l; ++i)
					{
						var info = items[i];
						if (info)
						{
							var elem = this._createBoxItemElem(root);
							this._setBoxItemInfo(elem, info);
						}
					}
				}
			}
		});
		this.defineProp('index', {'dataType': DataType.INT,
			'getter': function()
			{
				/*
				var elems = this.getAllItemElems();
				for (var i = 0, l = elems.length; i < l; ++i)
				{
					var elem = elems[i];
					if (elem.selected)
						return i;
				}
				return -1;
				*/
				return this.getElement().selectedIndex;
			},
			'setter': function(value)
			{
				/*
				//var old = this.getIndex();
				var elems = this.getAllItemElems();
				var newIndex = parseInt(value);
				if (newIndex >= 0)
				{
					var newElem = elems[newIndex];
					if (newElem)
						newElem.selected = true;
				}
				else
				{

				}
				*/
				//console.log('set selectedIndex', value);
				this.getElement().selectedIndex = value;
			}
		});
		/*
		this.defineProp('text', {'dataType': DataType.STRING, 'serializable': false,
			'getter': function()
			{
				var elem = this.getSelectedItemElem();
				if (elem)
				{
					var info = this._getBoxItemInfo(elem);
					if (info)
						return info.text || info.value;
				}
				return undefined;
			},
			'setter': function(value)
			{
				var elems = this.getAllItemElems();
				for (var i = 0, l = elems.length; i < l; ++i)
				{
					var elem = elems[i];
					var info = this._getBoxItemInfo(elem);
					//if (info.value === value)
					if (info)
					{
						elem.selected = (info.text === value);
					}
					else
						elem.selected = false;
				}
			}
		});
		*/
	},
	/** @ignore */
	doGetWidgetClassName: function($super)
	{
		return $super() + ' ' + CNS.SELECTBOX;
	},
	/** @ignore */
	doCreateRootElement: function(doc)
	{
		var result = doc.createElement('select');
		return result;
	},
	/** @private */
	_createBoxItemElem: function(parentElem, refElem)
	{
		var doc = parentElem.ownerDocument;
		var result = doc.createElement('option');
		if (refElem)
			parentElem.insertBefore(result, refElem);
		else
			parentElem.appendChild(result);
		return result;
	},

	/** @ignore */
	doBindElement: function($super, element)
	{
		$super(element);
	},
	/** @private */
	doGetValue: function()
	{
		var elem = this.getSelectedItemElem();
		if (elem)
		{
			var info = this._getBoxItemInfo(elem);
			return info.value;
		}
		else
			return undefined;
	},
	/** @private */
	doSetValue: function(value)
	{
		var elems = this.getAllItemElems();
		var index;
		for (var i = 0, l = elems.length; i < l; ++i)
		{
			var elem = elems[i];
			var info = this._getBoxItemInfo(elem);
			//console.log(info, info.value === value);
			if (info.value === value)
			{
				/*
				elem.selected = (info.value === value);
				if (elem.selected)
					index = elem.selected;
				*/
				//console.log('set value at index', i);
				this.setIndex(i);
				return;
			}
		}
		this.setIndex(-1);
	},

	/** @private */
	getAllItemElems: function()
	{
		return DU.getDirectChildElems(this.getElement(), 'option');
	},
	/** @private */
	getSelectedItemElem: function()
	{
		var elems = this.getAllItemElems();
		for (var i = 0, l = elems.length; i < l; ++i)
		{
			var elem = elems[i];
			if (elem.selected)
				return elem;
		}
		return null;
	},

	/**
	 * Clear all items in box.
	 */
	clear: function()
	{
		this.getElement().innerHTML = '';
	},

	/**
	 * Drop down the selection list of box.
	 * NOTE: Can only work in Webkit.
	 */
	dropDown: function()
	{
		var doc = this.getDocument();
		var elem = this.getElement();
		var event = this.getDocument().createEvent('MouseEvents');
		// typeArg,canBubbleArg,cancelableArg,viewArg,detailArg,screenXArg,screenYArg,clientXArg,clientYArg,ctrlKeyArg,altKeyArg,shiftKeyArg,metaKeyArg,buttonArg,relatedTargetArg
		//event.initMouseEvent('mousedown', true, true, doc.defaultView, null, null, null, null, null, null, null, null, null, null, Kekule.X.Event.MouseButton.LEFT, elem);
		event.initEvent('mousedown', true, true);
		this.getElement().dispatchEvent(event);
	},

	/** @private */
	_getBoxItemInfo: function(itemElem)
	{
		var result = {};
		result.text = EU.getInnerText(itemElem);
		//if (OU.notUnset(itemElem[this.ITEM_VALUE_FIELD]))  // value may be null or undefined
			result.value = itemElem[this.ITEM_VALUE_FIELD];  // as itemElem.value is always a string, we need another field to store variant value
		if (OU.notUnset(itemElem.title))
			result.title = itemElem.title;
		if (OU.notUnset(itemElem[this.ITEM_DATA_FIELD]))
			result.data = itemElem[this.ITEM_DATA_FIELD];
		return result;
	},
	/** @private */
	_setBoxItemInfo: function(itemElem, info)
	{
		if (DataType.isSimpleValue(info))  // info is direct text
		{
			info = {'value': info};
		}
		var text = info.text || info.value;
		if (OU.notUnset(text))
			Kekule.DomUtils.setElementText(itemElem, text);
		//if (OU.notUnset(info.value))  // value may be null or undefined
		{
			itemElem.value = '' + info.value;
			// as itemElem.value is always a string, we need another field to store variant value
			itemElem[this.ITEM_VALUE_FIELD] = info.value;
		}
		if (OU.notUnset(info.title))
			itemElem.title = info.title;
		if (OU.notUnset(info.data))
			itemElem[this.ITEM_DATA_FIELD] = info.data;
		if (info.selected)
		{
			itemElem.setAttribute('selected', 'selected');
		}
		else
			itemElem.selected = false;
		return this;
	}
});

/**
 * An general combo box widget. A combination of text box and select box.
 * @class
 * @augments Kekule.Widget.FormWidget
 *
 * @property {String} text Text in edit box.
 * @property {Array} items An array of hash objects that contains value and title info of select box item.
 *   Each item of array may have the following fields: {text, value, title, data}.
 */
/**
 * Invoked when user select a value from select box.
 *   event param of it has field: {widget, value}
 * @name Kekule.Widget.ComboBox#valueSelect
 * @event
 */
Kekule.Widget.ComboBox = Class.create(Kekule.Widget.FormWidget,
/** @lends Kekule.Widget.ComboBox# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Widget.ComboBox',
	/** @private */
	BINDABLE_TAG_NAMES: ['div', 'span'],
	/** @constructs */
	initialize: function($super, parentOrElementOrDocument)
	{
		$super(parentOrElementOrDocument);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('text', {'dataType': DataType.STRING, 'serializable': false,
			'getter': function()
			{
				var textBox = this.getTextBox();
				return textBox? textBox.getText(): null;
			},
			'setter': function(value)
			{
				var textBox = this.getTextBox();
				if (textBox)
				{
					textBox.setText(value);
					this.textChanged();
				}
			}
		});
		this.defineProp('items', {'dataType': DataType.ARRAY, 'serializable': false,
			'getter': function()
			{
				var selectBox = this.getSelectBox();
				return selectBox? selectBox.getItems(): null;
			},
			'setter': function(value)
			{
				var selectBox = this.getSelectBox();
				if (selectBox)
					selectBox.setItems(value);
			}
		});
		// private
		this.defineProp('textBox', {'dataType': 'Kekule.Widget.TextBox', 'serializable': false, 'setter': null, 'scope': Class.PropertyScope.PRIVATE});
		this.defineProp('selectBox', {'dataType': 'Kekule.Widget.SelectBox', 'serializable': false, 'setter': null, 'scope': Class.PropertyScope.PRIVATE});
	},
	finalize: function($super)
	{
		this._finalizeSubElements();
		$super();
	},
	_finalizeSubElements: function()
	{
		var textBox = this.getTextBox();
		if (textBox)
		{
			textBox.finalize();
			//this.setTextBox(null);
		}
		var selectBox = this.getSelectBox();
		if (selectBox)
		{
			selectBox.finalize();
			//this.setSelectBox(null);
		}
	},

	/** @ignore */
	getCoreElement: function($super)
	{
		var textBox = this.getTextBox();
		if (textBox)
			return textBox.getElement();
		else
			return $super();
	},

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

		// important, use span to put a width 100% input box,
		// otherwise width of input box is hard to set.
		var wrapper = doc.createElement('span');
		wrapper.className = CNS.COMBOBOX_TEXTWRAPPER;
		rootElem.appendChild(wrapper);
		var textBox = new Kekule.Widget.TextBox(this);
		//textBox.addClassName(CNS.COMBOBOX_TEXTBOX);
		textBox.appendToElem(wrapper);
		this.setPropStoreFieldValue('textBox', textBox);

		var selectBox = new Kekule.Widget.SelectBox(this);
		//selectBox.addClassName(CNS.COMBOBOX_SELECTBOX);
		selectBox.appendToElem(rootElem);
		this.setPropStoreFieldValue('selectBox', selectBox);

		// add event listener
		selectBox.addEventListener('valueChange', function(e)
			{
				var itemElem = selectBox.getSelectedItemElem();
				if (itemElem)
				{
					//var value = itemElem.value || itemElem.text;
					var value = itemElem.value;
					var text = itemElem.text || itemElem.value;
					//var value = selectBox.getValue();
					//textBox.setValue(value);
					if (text !== textBox.getText())
					{
						textBox.setText(text);
						self.notifyValueChanged();
					}
					textBox.selectAll();
					textBox.focus();
					self.invokeEvent('valueSelect', {'widget': self, 'value': value});
				}
			}
		);
		textBox.addEventListener('valueChange', function(e)
			{
				//selectBox.setValue(textBox.getValue());
				self.textChanged();
			}
		);
		Kekule.X.Event.addListener(textBox.getElement(), 'keydown', function(e)
			{
				var keyCode = e.getKeyCode();
				if (keyCode === Kekule.X.Event.KeyCode.DOWN)
				{
					selectBox.dropDown();
				}
			}
		);

		return [wrapper];
	},

	/** @ignore */
	relayEvent: function($super, eventName, event)
	{
		var invokerWidget = event.widget;
		if ((invokerWidget === this.getTextBox()) || (invokerWidget === this.getSelectBox()))
		{
			event.widget = this;
			if (eventName === 'valueChange')  // avoid call value change twice
				return;
		}
		return $super(eventName, event);
	},

	/**
	 * Called when text in text box changes.
	 * @private
	 */
	textChanged: function()
	{
		var text = this.getText();
		var value = this._getValueOfText(text);
		this.getSelectBox().setValue(value);
	},
	/**
	 * Get corresponding value in select box.
	 * @private
	 */
	_getValueOfText: function(text)
	{
		var items = this.getSelectBox().getItems();
		for (var i = 0, l = items.length; i < l; ++i)
		{
			var item = items[i];
			if (item.text && (item.text === text))
				return item.value;
		}
		return text;
	},

	/** @ignore */
	doSetEnabled: function(value)
	{
		var textBox = this.getTextBox();
		if (textBox)
			textBox.setEnabled(value);
		var selectBox = this.getSelectBox();
		if (selectBox)
			selectBox.setEnabled(value);
	},

	/** @ignore */
	doGetValue: function($super)
	{
		var text = $super();
		// check if text is in select box, if so, return corresponding select box value
		var result = this._getValueOfText(text);
		return result;
	},
	/** @ignore */
	doSetValue: function($super, value)
	{
		var text = value;
		// check if value is in select box, if so, set corresponding text to text box
		var items = this.getSelectBox().getItems();
		this.getSelectBox().setIndex(-1);
		for (var i = 0, l = items.length; i < l; ++i)
		{
			var item = items[i];
			if (item.value === value)
			{
				text = item.text || item.value;
				this.getSelectBox().setIndex(i);
				//console.log('set value', i, text, item.value, item.text);
				break;
			}
		}
		$super(text);  // set value of core element(text box)
	}
});

})();