SOURCE CODE: Uize.Widget.FormElement
VIEW REFERENCE

/*______________
|       ______  |   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://www.uize.com
| /____/ /__/_| | COPYRIGHT : (c)2007-2012 UIZE
|          /___ |   LICENSE : Available under MIT License or GNU General Public License
|_______________|             http://www.uize.com/license.html
*/

/* Module Meta Data
  type: Class
  importance: 7
  codeCompleteness: 80
  testCompleteness: 0
  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`
*/

Uize.module ({
  name:'Uize.Widget.FormElement',
  required:[
    'Uize.Node',
    'Uize.Node.Event',
    'Uize.Widget.Collapsy',
    'Uize.Node.Classes'
  ],
  builder:function(_superclass){
    /*** Variables for Scruncher Optimization ***/
      var
        _true = true,
        _false = false,
        _null = null,
        _undefined,

        /*** validation/warning variables ***/
          _never = 'never',
          _tentativeValueChanged = 'tentativeValueChanged',
          _valueChanged = 'valueChanged',
          _validated = 'validated',
          _finished = 'finished',
          _validatedAfterFirstFinish = 'validatedAfterFirstFinish'
      ;

    /*** Class Constructor ***/
      var
        _class = _superclass.subclass (
          _null,
          function () {
            var
              _this = this,
              _warningWidget = _this.addChild('warning', Uize.Widget.Collapsy, _this._warningMessageProperties)
            ;

            function _updateUiState() { _this._updateUiState() }
            _this.wire ({
              'Changed.busyInherited':_updateUiState,
              'Changed.enabledInherited':_updateUiState,
              Blur:function() { _warningWidget.set({collapsed:_true}) },
              Focus:function() { _warningWidget.set({collapsed:_false}) }
            });

            _this._warningWidget = _warningWidget;
            _this._isInitialized = _true;
            _this._lastKeyDown = -1;
          }
        ),
        _classPrototype = _class.prototype
      ;

    /*** Private Instance Methods ***/
      _classPrototype._getInputNode = function() { return this.getNode('input') };

      _classPrototype._getParentForm = function() {
        var
          _parentElementsWidget = this.parent,
          _parentForm
        ;
        if (_parentElementsWidget && _parentElementsWidget.parent) {
          _parentForm = _parentElementsWidget.parent;

          if (!_parentForm.isForm)
            _parentForm = _null;
        }
        return _parentForm;
      };

      _classPrototype._updateUiState = function () {
        var _this = this;
        if (_this.isWired) {
          var _enabled = _this.get ('enabledInherited') && !_this.get ('busyInherited');
          _this.setNodeProperties (_this._getInputNode(), {readOnly:!_enabled,disabled:!_enabled});
        }
      };

      _classPrototype._updateUiValue = function () {
        var _this = this;
        if (_this.isWired) {
          var
            _inputNode = _this._getInputNode(),
            _value = _this._value
          ;
          _value + '' != _this.getNodeValue (_inputNode)
            && _this.setNodeValue (_inputNode, _value)
          ;
        }
      };

      _classPrototype._updateUiWarning = function() {
        var
          _this = this,
          _warningShown = _this._warningShown,
          _warningMessage = _this._warningMessage
        ;

        if (_this.isWired) {
          // Update warning widget
          _this._warningWidget &&
            _this._warningWidget.set(
              Uize.copyInto(
                {shown:_warningShown},
                _warningMessage ? {expandedMessage:_warningMessage} : _null
              )
            );

          // visual indicators of warning state
          Uize.Node.Classes.setState(
            [_this._getInputNode(),_this.getNode('label')],
            _this._errorClassName,
            _warningShown
          );
        }
      };

    /*** Public Methods ***/
      // NOTE: can be overidden by subclasses
      _classPrototype.fireOkOnEnter = Uize.returnTrue;

      _classPrototype.checkIsEmpty = function() { return this._value == _null || this._value == '' };

      _classPrototype.checkWarningShown = _classPrototype._checkWarningShown = function() {
        var
          _this = this,
          _warningShownWhen = _this._warningShownWhen,
          _parentForm = _this._getParentForm(),
          _currentWarningShown = _this._warningShown
        ;

        _this.set({
          _warningShown:_this._warningAllowedInherited
            && _this._isValid == _false
            && (
              _parentForm
                ? _parentForm.get('warningShown')
                : (
                  _this._isDirtyInherited
                    && (
                      _warningShownWhen == _validated
                      // keep the current warningShown value if warningShowWhen is set to a value
                      // but that's not the current state
                      || (_warningShownWhen == _finished && (_this._isFinished || _currentWarningShown))
                      || (_warningShownWhen == _validatedAfterFirstFinish && (_this._finishedAtLeastOnce || _currentWarningShown))
                    )
                )
            )
        });
      };

      // To be overridden as necessary by subclasses (should return an array)
      _classPrototype.getMoreValidators = _undefined;

      _classPrototype.getRootNode = function() {
        return this.getNode() || this.getNode('input')
      };

      _classPrototype.restore = function() {
        this.set({
          _finishedAtLeastOnce:_false,
          _isDirty:'inherit',
          _value:this._initialValue
        })
      };

      _classPrototype.updateUi = function () {
        var _this = this;

        if (_this.isWired) {
          _this._updateUiState();
          _this._updateUiValue();
          _this._updateUiWarning();

          _superclass.prototype.updateUi.call (_this);
        }
      };

      _classPrototype.validate = _classPrototype._validate = function() {
        var _this = this;

        if (_this._isInitialized) {
          var
            _validator = _this._validator,
            _validators =
              (
                Uize.isArray(_validator)
                  ? _validator
                  : (_validator != _null ? [_validator] : _null)
              ),
            _moreValidators = _this.getMoreValidators ? _this.getMoreValidators() : _null
          ;

          if (_moreValidators)
            _validators = _validators ? _validators.concat(_moreValidators) : _moreValidators
          ;

          function _setIsValid(_isValid) { _this.set({_isValid:_isValid}) }

          if (_validators != _null) {
            var
              _value = _this._validateWhen == _tentativeValueChanged
                ? _this._tentativeValue : _this._value,
              _validatorsLength = _validators.length,
              _validatorNo = -1
            ;

            function _processNextValidator() {
              if (++_validatorNo < _validatorsLength) {
                function _handleIsValid(_isValid, _newWarningMessage) {
                  if (_isValid == _false) {
                    _this.set({_warningMessage:_newWarningMessage || _this._initialWarningMessage});
                    _setIsValid(_false);
                  }
                  else _processNextValidator();
                }

                var
                  _validatorToEvaluate = _validators[_validatorNo],
                  _isValid = _validatorToEvaluate instanceof RegExp
                    ? _validatorToEvaluate.test (_value)
                    : (
                      _validatorToEvaluate.func || Uize.isFunction (_validatorToEvaluate)
                        ? (
                          _validatorToEvaluate.func || _validatorToEvaluate
                        ).call(_this, _value, _handleIsValid)
                        : _value == _validatorToEvaluate
                    )
                ;

                _handleIsValid(_isValid, _validatorToEvaluate.msg);
              }
              else _setIsValid(_true);
            }

            _processNextValidator();
          }
          else _setIsValid(_true)
        }
      };

      _classPrototype.wireUi = function() {
        var _this = this;

        if (!_this.isWired) {
          var _inputNode = _this._getInputNode();

          if (_inputNode) {
            /*** Set up the read-only state properties (attributes of the node) ***/
              _this._type = _inputNode.type;
              _this._elementName = _inputNode.name;

            /*** Helper functions ***/
              function _fire (_eventName, _domEvent) { _this.fire ({name:_eventName,domEvent:_domEvent}) }
              function _fireClick (_event) { _fire ('Click', _event) }
              function _fireKeyUp (_event) { _fire ('Key Up', _event) }
              function _setValue (_isInitial) {
                _this.set ({_value:_this.getNodeValue(_inputNode)});
                !_isInitial && _this._isDirty != _true &&
                  _this.set({_isDirty:_true});
              }

            var
              _eventsToWire = {
                blur:function () {
                  _setValue();
                  _this.set({_focused:_false});
                },
                focus:function () { _this.set({_focused:_true}) },
                click:function (_event) {
                  _setValue();
                  _fireClick (_event);
                },
                keydown:function (_event) {
                  _this._lastKeyDown = _event.keyCode;
                  _fire ('Key Down', _event);
                }
              }
            ;

            // Build up events to wire
            switch (_this._type) {
              case 'checkbox':
                break;

              case 'radio':  // operates on a group of like-named radio buttons, but one has to have the implied node id
                _this.set ({
                  nodeMap:Uize.copyInto(
                    _this.get('nodeMap') || {},
                    {
                      input:Uize.Node.find({
                        tagName:'INPUT',
                        type:'radio',
                        name:_this._elementName
                      })
                    }
                  )
                });
                _inputNode = _this._getInputNode();
                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 (_this._type != 'textarea' && _this._lastKeyDown == _event.keyCode && Uize.Node.Event.isKeyEnter (_event)) {
                    _setValue ();
                    _this.fireOkOnEnter()
                      && _fire ('Ok', _event)
                    ;
                  }
                  else if (Uize.Node.Event.isKeyEscape (_event)) {
                    _this._updateUiValue();    // replace with old (saved) value
                    _fire ('Cancel', _event);
                    _inputNode.blur();
                  }
                  else {
                    _this.set({
                      _tentativeValue:_this.getNodeValue(_inputNode),
                      _isFinished:_false
                    });
                  }

                  _fireKeyUp (_event);
                };
                _eventsToWire.click = _fireClick;
                break;
            }

            _this.wireNode(_inputNode, _eventsToWire);

            // if no value was set, then grab the value from the node
            _this._value === _undefined
              ? _setValue(_true)
              : _this._updateUiValue()
            ;
          }

          _this._validate();

          _superclass.prototype.wireUi.call (_this);
        }
      };

    /*** Register Properties ***/
      _class.registerProperties({
        _elementName:'elementName', // read-only
          /*?
            State Properties
              elementName
                The name associated with the input nodes belonging to the form element.

                EXAMPLE
                ........................................................
                <input id="myWidget-input" type='button' name='foobar'/>
                ........................................................

                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
              _this = this,
              _warningWidget = _this.children.warning
            ;

            _warningWidget && _warningWidget.set({collapsed:!_this._focused});

            if (_this.isWired) {
              var _inputNode = _this._getInputNode();

              try {
              _inputNode &&
                (Uize.Node.isNode(_inputNode) ? _inputNode : _inputNode[0])[
                  _this._focused ? 'focus' : 'blur'
                ]()
              ;

                // synch up the value with the UI, in case conformer had changed a UI value to something that already matched the programmatic value
                _this.setNodeValue('input', _this._value);
              }
              catch(_ex) {}
            }
          },
          value:_false
        },
        _isEmpty:{
          name:'isEmpty',
          value:_true
        },
        _isDirty:{
          name:'isDirty',
          onChange:function() {
            var
              _this = this,
              _parentForm = _this._getParentForm(),
              _isDirty = _this._isDirty == 'inherit'
                ? (_parentForm ? _parentForm.get('isDirtyInherited') : _false)
                : _this._isDirty
            ;

            _this.set({_isDirtyInherited:_isDirty});
          },
          value:'inherit'
        },
        _isDirtyInherited:{
          name:'isDirtyInherited',
          onChange:_classPrototype._checkWarningShown,
          value:_false
        },
        _isFinished:{
          name:'isFinished',
          onChange:function() {
            var _this = this;

            if (_this._isFinished && _this._isInitialized) {
              _this._validateWhen == _finished
                && _this._validate();
              _this._finishedAtLeastOnce
                || _this.set({_finishedAtLeastOnce:_true});
            }

            _this._checkWarningShown();
          },
          value:_true
        },
        _isValid:{
          name:'isValid',
          onChange:_classPrototype._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
                ........................................................
                <input id="myWidget-input" type='button' name='foobar'/>
                ........................................................

                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:_classPrototype._validate,
          value:_null
        },
        _value:{
          name:'value',
          conformer:function(_value) {
            var _this = this;

            // conform the value to boolean if type is checkbox
            _value = _this._type == 'checkbox' ? _value == _true : _value;

            return Uize.isFunction (this._valueConformer) ? this._valueConformer(_value) : _value
          },
          onChange:function() {
            var _this = 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 (!_this.isWired)
              _this._initialValue = _this._value
            ;

            _this.set({
              _tentativeValue:_this._value, // sync tentative value
              _isFinished:_true,
              _isEmpty:_this.checkIsEmpty()
            });

            _this._validateWhen == _valueChanged
              && _this._validate();

            _this._updateUiValue();
          }/*,
          value:_null*/
        },
        _valueConformer:'valueConformer',
        _warningAllowed:{
          name:'warningAllowed',
          onChange:function() {
            var
              _this = this,
              _parentForm = _this._getParentForm(),
              _warningAllowed = _this._warningAllowed == 'inherit'
                ? (_parentForm ? _parentForm.get('warningAllowedInherited') : _true)
                : _this._warningAllowed
            ;

            _this.set({_warningAllowedInherited:_warningAllowed});
          },
          value:'inherit'
        },
        _warningAllowedInherited:{
          name:'warningAllowedInherited',
          onChange:_classPrototype._checkWarningShown,
          value:_false
        },
        _warningMessage:{
          name:'warningMessage',
          onChange:[
            function() {
              var _this = this;

              if (!_this.isWired)
                _this._initialWarningMessage = _this._warningMessage;
            },
            _classPrototype._updateUiWarning
          ]
        },
        _warningMessageProperties:{ // for the warning
          name:'warningMessageProperties',
          onChange:function() {  this._warningWidget.set(this._warningMessageProperties) }
        },
        _warningShown:{
          name:'warningShown',
          onChange:_classPrototype._updateUiWarning,
          value:_false
        },
        _warningShownWhen:{
          name:'warningShownWhen',
          onChange:_classPrototype._checkWarningShown,
          value:_validated  // valid values: 'validated', 'finished', validatedAfterFirstFinish'
        },

        /*** Private properties used for managing internal state w/ onChange functionality ***/
          _finishedAtLeastOnce:{
            onChange:_classPrototype._checkWarningShown,
            value:_false
          }
      });

    return _class;
  }
});