/* Copyright (c) 2012, Motorola Mobility LLC. All Rights Reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Motorola Mobility LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** @module montage/ui/text-input @requires montage/ui/component @requires montage/ui/native-control @requires montage/core/core */ var Montage = require("montage").Montage, Component = require("ui/component").Component, NativeControl = require("ui/native-control").NativeControl; /** The base class for all text-based input components. You typically won't create instances of this prototype. @class module:montage/ui/text-input.TextInput @extends module:montage/ui/native-control.NativeControl @see module:"montage/ui/input-date.reel".DateInput @see module:"montage/ui/input-text.reel".InputText @see module:"montage/ui/input-number.reel".InputNumber @see module:"montage/ui/input-range.reel".RangeInput @see module:"montage/ui/textarea.reel".TextArea */ var TextInput = exports.TextInput = Montage.create(NativeControl, /** @lends module:montage/ui/text-input.TextInput# */ { _hasFocus: { enumerable: false, value: false }, _value: { enumerable: false, value: null }, _valueSyncedWithInputField: { enumerable: false, value: false }, /** The "typed" data value associated with the input element. When this property is set, if the component's converter property is non-null then its revert() method is invoked, passing it the newly assigned value. The revert() function is responsible for validating and converting the user-supplied value to its typed format. For example, in the case of a DateInput component (which extends TextInput) a user enters a string for the date (for example, "10-12-2005"). A DateConverter object is assigned to the component's converter property. If the comopnent doesn't specify a converter object then the raw value is assigned to value. @type {string} @default null */ value: { get: function() { return this._value; }, set: function(value, fromInput) { if(value !== this._value) { if(this.converter) { var convertedValue; try { convertedValue = this.converter.revert(value); if (this.error) { this.error = null; } this._value = convertedValue; } catch(e) { // unable to convert - maybe error this.error = e; this._valueSyncedWithInputField = false; } } else { this._value = value; } if(fromInput) { this._valueSyncedWithInputField = true; //this.needsDraw = true; } else { this._valueSyncedWithInputField = false; this.needsDraw = true; } } }, serializable: true }, // set value from user input /** @private */ _setValue: { value: function() { var newValue = this.element.value; Object.getPropertyDescriptor(this, "value").set.call(this, newValue, true); } }, /** A reference to a Converter object whose revert() function is invoked when a new value is assigned to the TextInput object's value property. The revert() function attempts to transform the newly assigned value into a "typed" data property. For instance, a DateInput component could assign a DateConverter object to this property to convert a user-supplied date string into a standard date format. @type {Converter} @default null @see {@link module:montage/core/converter.Converter} */ converter:{ value: null, serializable: true }, _error: { value: false }, /** If an error is thrown by the converter object during a new value assignment, this property is set to true, and schedules a new draw cycle so the the UI can be updated to indicate the error state. the montage-text-invalid CSS class is assigned to the component's DOM element during the next draw cycle. @type {boolean} @default false */ error: { get: function() { return this._error; }, set: function(v) { this._error = v; this.errorMessage = this._error ? this._error.message : null; this.needsDraw = true; } }, _errorMessage: {value: null}, /** The message to display when the component is in an error state. @type {string} @default null */ errorMessage: { get: function() { return this._errorMessage; }, set: function(v) { this._errorMessage = v; } }, _updateOnInput: { value: true }, /** When this property and the converter's allowPartialConversion are both true, as the user enters text in the input element each new character is added to the component's value property, which triggers the conversion. Depending on the type of input element being used, this behavior may not be desirable. For instance, you likely would not want to convert a date string as a user is entering it, only when they've completed their input. Specifies whether @type {boolean} @default true */ updateOnInput: { get: function() { return !!this._updateOnInput; }, set: function(v) { this._updateOnInput = v; }, serializable: true }, // HTMLInputElement methods blur: { value: function() { this._element.blur(); } }, focus: { value: function() { this._element.focus(); } }, // select() defined where it's allowed // click() deliberately omitted, use focus() instead // Callbacks prepareForDraw: { enumerable: false, value: function() { var el = this.element; el.addEventListener("focus", this); el.addEventListener('input', this); el.addEventListener('change', this); el.addEventListener('blur', this); } }, _setElementValue: { value: function(value) { this.element.value = (value == null ? '' : value); } }, draw: { enumerable: false, value: function() { Object.getPrototypeOf(TextInput).draw.call(this); var el = this.element; if (!this._valueSyncedWithInputField) { this._setElementValue(this.converter ? this.converter.convert(this._value) : this._value); } if (this.error) { el.classList.add('montage-text-invalid'); el.title = this.error.message || ''; } else { el.classList.remove("montage-text-invalid"); el.title = ''; } } }, didDraw: { enumerable: false, value: function() { if (this._hasFocus && this._value != null) { var length = this._value.toString().length; this.element.setSelectionRange(length, length); } this._valueSyncedWithInputField = true; } }, // Event handlers handleInput: { enumerable: false, value: function() { if (this.converter) { if (this.converter.allowPartialConversion === true && this.updateOnInput === true) { this._setValue(); } } else { this._setValue(); } } }, /** Description TODO @function @param {Event Handler} event TODO */ handleChange: { enumerable: false, value: function(event) { this._setValue(); this._hasFocus = false; } }, /** Description TODO @function @param {Event Handler} event TODO */ handleBlur: { enumerable: false, value: function(event) { this._hasFocus = false; } }, /** Description TODO @function @param {Event Handler} event TODO */ handleFocus: { enumerable: false, value: function(event) { this._hasFocus = true; } } }); // Standard tag attributes - http://www.w3.org/TR/html5/the-input-element.html#the-input-element TextInput.addAttributes({ accept: null, alt: null, autocomplete: null, autofocus: {dataType: "boolean"}, checked: {dataType: "boolean"}, dirname: null, disabled: {dataType: 'boolean'}, form: null, formaction: null, formenctype: null, formmethod: null, formnovalidate: {dataType: 'boolean'}, formtarget: null, height: null, list: null, maxlength: null, multiple: {dataType: 'boolean'}, name: null, pattern: null, placeholder: null, readonly: {dataType: 'boolean'}, required: {dataType: 'boolean'}, size: null, src: null, width: null // "type" is not bindable and "value" is handled as a special attribute });