/*______________ | ______ | U I Z E J A V A S C R I P T F R A M E W O R K | / / | --------------------------------------------------- | / O / | MODULE : Uize.Widget.FormElement Class | / / / | | / / / /| | ONLINE : http://uize.com | /____/ /__/_| | COPYRIGHT : (c)2007-2014 UIZE | /___ | LICENSE : Available under MIT License or GNU General Public License |_______________| http://uize.com/license.html */ /* Module Meta Data type: Class importance: 7 codeCompleteness: 80 docCompleteness: 50 */ /*? Introduction The =Uize.Widget.FormElement= class serves as a wrapper class in order to provide an interface for any form element (input, select, textarea, etc). *DEVELOPERS:* `Tim Carter`, `Chris van Rensburg`, `Ben Ilegbodu`, `Vinson Chuong`, original code contributed by `Zazzle Inc.` */ Uize.module ({ name:'Uize.Widget.FormElement', required:[ 'Uize.Node', 'Uize.Dom.Event', 'Uize.Dom.Classes', 'Uize.Widget.FormElementWarning' ], builder:function (_superclass) { 'use strict'; var /*** Variables for Scruncher Optimization ***/ _true = true, _false = false, _null = null, _undefined, _Uize = Uize, _Uize_Node = _Uize.Node, _Uize_Dom_Event = _Uize.Dom.Event, _checkWarningShown = 'checkWarningShown', /*** validation/warning variables ***/ _never = 'never', _tentativeValueChanged = 'tentativeValueChanged', _valueChanged = 'valueChanged', _validated = 'validated', _finished = 'finished', _validatedAfterFirstFinish = 'validatedAfterFirstFinish' ; /*** Private Instance Methods ***/ function _getInputNode (m) { return m.getNode('input') } function _getParentForm (m) { var _parentElementsWidget = m.parent, _parentForm ; if (_parentElementsWidget && _parentElementsWidget.parent) { _parentForm = _parentElementsWidget.parent; if (!_parentForm.isForm) _parentForm = _null; } return _parentForm; } function _updateUiState (m) { if (m.isWired) { var _enabled = m.get ('enabledInherited') && !m.get ('busyInherited'); m.setNodeProperties (_getInputNode(m), {disabled:!_enabled}); } } function _updateUiValue (m) { if (m.isWired) { var _inputNode = _getInputNode(m), _value = m._value ; _value != m.getNodeValue (_inputNode) && m.setNodeValue (_inputNode, _value === _undefined ? '' : _value) ; } } function _updateUiWarning () { var m = this, _warningShown = m._warningShown, _warningMessage = m._warningMessage ; if (m.isWired) { // Update warning widget m._warningWidget && m._warningWidget.set( _Uize.copyInto( {shown:_warningShown}, _warningMessage ? {message:_warningMessage} : _null ) ); // visual indicators of warning state Uize.Dom.Classes.setState( [_getInputNode(m),m.getNode('label'),m.getNode ('shell')], m._errorClassName, _warningShown ); } } return _superclass.subclass ({ omegastructor:function () { var m = this, _warningWidget = m._warningWidget = m.addChild( 'warning', m._warningWidgetClass || _Uize.Widget.FormElementWarning ), _boundUpdateUiState = function () {_updateUiState(m)} ; m.wire ({ 'Changed.busyInherited':_boundUpdateUiState, 'Changed.enabledInherited':_boundUpdateUiState }); m._isInitialized = _true; m._lastKeyDown = -1; }, instanceMethods:{ fireOkOnEnter:_Uize.returnTrue, // NOTE: can be overidden by subclasses checkIsEmpty:function () { return this._value == _null || this._value === '' }, checkWarningShown:function () { var m = this, _warningShownWhen = m._warningShownWhen, _parentForm = _getParentForm(m), _currentWarningShown = m._warningShown ; m.set({ _warningShown:m._warningAllowedInherited && m._isValid == _false && ( _parentForm ? _parentForm.get('warningShown') : ( m._isDirtyInherited && ( _warningShownWhen == _validated // keep the current warningShown value if warningShowWhen is set to a value // but that's not the current state || (_warningShownWhen == _finished && (m._isFinished || _currentWarningShown)) || (_warningShownWhen == _validatedAfterFirstFinish && (m._finishedAtLeastOnce || _currentWarningShown)) ) ) ) }); }, getMoreValidators:_undefined, // To be overridden as necessary by subclasses (should return an array) getRootNode:function () { return this.getNode() || this.getNode('input') }, restore:function () { this.set({ _finishedAtLeastOnce:_false, _isDirty:'inherit', _value:this._initialValue }); }, updateUi:function () { var m = this; if (m.isWired) { _updateUiState(m); _updateUiValue(m); _updateUiWarning.call(m); _superclass.doMy (m,'updateUi'); } }, valueConformer:function (_value) { return _Uize.isFunction (this._valueConformer) ? this._valueConformer(_value) : _value; }, validate:function () { var m = this; if (m._isInitialized) { var _validator = m._validator, _validators = ( _Uize.isArray(_validator) ? _validator : (_validator != _null ? [_validator] : _null) ), _moreValidators = m.getMoreValidators ? m.getMoreValidators() : _null ; if (_moreValidators) _validators = _validators ? _validators.concat(_moreValidators) : _moreValidators ; var _setIsValid = function (_isValid) { m.set({_isValid:_isValid}) }; if (_validators != _null) { var _value = m._validateWhen == _tentativeValueChanged ? m._tentativeValue : m._value, _validatorsLength = _validators.length, _validatorNo = -1, _processNextValidator = function () { if (++_validatorNo < _validatorsLength) { var _handleIsValid = function (_isValid, _newWarningMessage) { if (_isValid == _false) { m.set({_warningMessage:_newWarningMessage || m._initialWarningMessage}); _setIsValid(_false); } else _processNextValidator(); }, _validatorToEvaluate = _validators[_validatorNo], _validatorFunction = _validatorToEvaluate.func || (_Uize.isFunction (_validatorToEvaluate) ? _validatorToEvaluate : _null), _isValid = _validatorFunction ? _validatorFunction.call(m, _value, _handleIsValid) : ( _validatorToEvaluate instanceof RegExp ? _validatorToEvaluate.test (_value) : _value == _validatorToEvaluate ) ; if (_isValid != _null) // sign that the validation is asynchronous _handleIsValid(_isValid, _validatorToEvaluate.msg); } else _setIsValid(_true); } ; _processNextValidator(); } else _setIsValid(_true); } }, wireUi:function () { var m = this; if (!m.isWired) { var _inputNode = _getInputNode(m); if (_inputNode) { /*** Set up the read-only state properties (attributes of the node) ***/ m._type = _inputNode.type; m._elementName = _inputNode.name; var _fire = function (_eventName, _domEvent) { m.fire ({name:_eventName,domEvent:_domEvent}) }, _fireClick = function (_event) { _fire ('Click', _event) }, _fireKeyUp = function (_event) { _fire ('Key Up', _event) }, _setValue = function (_isInitial) { m.set ({_value:m.getNodeValue(_inputNode)}); !_isInitial && m._isDirty != _true && m.set({_isDirty:_true}); }, _eventsToWire = { blur:function () { _setValue(); m.set({_focused:_false}); }, focus:function () { m.set({_focused:_true}) }, click:function (_event) { _setValue(); _fireClick (_event); }, keydown:function (_event) { m._lastKeyDown = _event.keyCode; _fire ('Key Down', _event); _Uize_Dom_Event.isKeyEnter(_event) && m._type != 'textarea' && _Uize_Dom_Event.abort(_event) ; } } ; // Build up events to wire switch (m._type) { case 'checkbox': break; case 'radio': // operates on a group of like-named radio buttons, but one has to have the implied node id m.set ({ nodeMap:_Uize.copyInto( m.get('nodeMap') || {}, { input:_Uize_Node.find({ tagName:'INPUT', type:'radio', name:m._elementName }) } ) }); _inputNode = _getInputNode(m); break; case 'select-one': case 'select-multiple': _eventsToWire.change = _setValue; _eventsToWire.keyup = function (_event) { _setValue (); _fireKeyUp (_event); }; _eventsToWire.click = _fireClick; break; default: // text, password, HTML5 text input, textarea, etc... _eventsToWire.keyup = function (_event) { // NOTE: When inputting Kanji, it's standard to use the enter button to choose kanji characters. // So everytime a user wants to type a multi-kanji word, 'Ok' would fire (which could ultimately // cause a form submission, which would be bad). So now the check to see if we should fire 'Ok' // checks to see if both keydown AND keyup are ENTER (since when you type kanji, you keydown on // a non-enter key, do stuff, then keyup to continue). if (m._type != 'textarea' && m._lastKeyDown == _event.keyCode && _Uize_Dom_Event.isKeyEnter (_event)) { _setValue (); m.fireOkOnEnter() && _fire ('Ok', _event) ; } else if (_Uize_Dom_Event.isKeyEscape (_event)) { _updateUiValue(m); // replace with old (saved) value _fire ('Cancel', _event); _inputNode.blur(); } else { m.set({ _tentativeValue:m.getNodeValue(_inputNode), _isFinished:_false }); } _fireKeyUp (_event); }; _eventsToWire.click = _fireClick; break; } m.wireNode(_inputNode, _eventsToWire); // if no value was set, then grab the value from the node m._value === _undefined ? _setValue(_true) : _updateUiValue(m) ; } m.validate(); _superclass.doMy (m,'wireUi'); } } }, stateProperties:{ _elementName:'elementName', // read-only /*? State Properties elementName The name associated with the input nodes belonging to the form element. EXAMPLE ........................................................ ........................................................ For a =Uize.Widget.FormElement= instance with the =idPrefix= of ='myWidget'= and the above HTML for its =input= implied node, the value of the =elementName= state property will be ='foobar'=. */ _errorClassName:{ name:'errorClassName', value:'error' }, _focused:{ name:'focused', onChange:function () { var m = this, _warningWidget = m.children.warning, _focused = m._focused ; _warningWidget && _warningWidget.set({focused:_focused}); if (m.isWired) { var _inputNode = _getInputNode(m); // If focused state property is out of sync with 'focused' state of the DOM node (via document.activeElement) // then force a focus. We don't force a blur because blurring isn't as important (an apparently it causes // issues with Samsung Galaxy Tab 10.1) if (_focused && _inputNode && document.activeElement != _inputNode) { setTimeout( function() { try { (_Uize_Node.isNode(_inputNode) ? _inputNode : _inputNode[0]).focus(); } catch(_ex) {} }, 0 ); // sync up the value with the UI, in case conformer had changed a UI value to something that already matched the programmatic value m.setNodeValue('input', m._value); } } }, value:_false }, _isEmpty:{ name:'isEmpty', value:_true }, _isDirty:{ name:'isDirty', onChange:function () { var m = this, _parentForm = _getParentForm(m), _isDirty = m._isDirty == 'inherit' ? (_parentForm ? _parentForm.get('isDirtyInherited') : _false) : m._isDirty ; m.set({_isDirtyInherited:_isDirty}); }, value:'inherit' }, _isDirtyInherited:{ name:'isDirtyInherited', onChange:_checkWarningShown, value:_false }, _isFinished:{ name:'isFinished', onChange:function () { var m = this; if (m._isFinished && m._isInitialized) { m._validateWhen == _finished && m.validate(); m._finishedAtLeastOnce || m.set({_finishedAtLeastOnce:_true}); } m.checkWarningShown(); }, value:_true }, _isValid:{ name:'isValid', onChange:_checkWarningShown, value:_false }, _tentativeValue:{ // readonly name:'tentativeValue', onChange:function () { this._validateWhen == _tentativeValueChanged && this.validate() ; }, value:_null }, _type:'type', // read-only /*? State Properties type The type associated with the input node belonging to the form element. EXAMPLE ........................................................ ........................................................ For a =Uize.Widget.FormElement= instance with the =idPrefix= of ='myWidget'= and the above HTML for its =input= implied node, the value of the =type= state property will be ='button'=. */ _validateWhen:{ name:'validateWhen', value:_tentativeValueChanged // valid values: 'never', 'tentativeValueChanged', 'valueChanged' & 'finished' }, _validator:{ name:'validator', onChange:'validate', value:_null }, _value:{ name:'value', conformer:function (_value) { var m = this; // conform the value to boolean if type is checkbox _value = m._type == 'checkbox' ? _value == _true : _value; return m.valueConformer(_value); }, onChange:function () { var m = this; // As long as the widget is not wired & the value changes, update the initial value // so that when the widget is wired we have something restore to. We need to do it here // so that if value is set after construction but before wiring, we'll still capture // it as an initial value if (!m.isWired) m._initialValue = m._value ; m.set({ _tentativeValue:m._value, // sync tentative value _isFinished:_true, _isEmpty:m.checkIsEmpty() }); m._validateWhen == _valueChanged && m.validate(); _updateUiValue(m); }/*, value:_null*/ }, _valueConformer:'valueConformer', _warningAllowed:{ name:'warningAllowed', onChange:function () { var m = this, _parentForm = _getParentForm(m), _warningAllowed = m._warningAllowed == 'inherit' ? (_parentForm ? _parentForm.get('warningAllowedInherited') : _true) : m._warningAllowed ; m.set({_warningAllowedInherited:_warningAllowed}); }, value:'inherit' }, _warningAllowedInherited:{ name:'warningAllowedInherited', onChange:_checkWarningShown, value:_false }, _warningMessage:{ name:'warningMessage', onChange:[ function () { var m = this; if (!m.isWired) m._initialWarningMessage = m._warningMessage; }, _updateUiWarning ] }, _warningShown:{ name:'warningShown', onChange:_updateUiWarning, value:_false }, _warningShownWhen:{ name:'warningShownWhen', onChange:_checkWarningShown, value:_validated // valid values: 'validated', 'finished', validatedAfterFirstFinish' }, _warningWidgetClass:'warningWidgetClass', /*** Private properties used for managing internal state w/ onChange functionality ***/ _finishedAtLeastOnce:{ onChange:_checkWarningShown, value:_false } } }); } });