/** * The text field is the basis for most of the input fields. It provides a baseline of shared * functionality such as input validation, standard events, state management and look and feel. Typically we create * text fields inside a form, like this: * * @example * Ext.create('Ext.form.Panel', { * fullscreen: true, * items: [ * { * xtype: 'fieldset', * title: 'Enter your name', * items: [ * { * xtype: 'textfield', * label: 'First Name', * name: 'firstName' * }, * { * xtype: 'textfield', * label: 'Last Name', * name: 'lastName' * } * ] * } * ] * }); * * This creates two text fields inside a form. Text Fields can also be created outside of a Form, like this: * * Ext.create('Ext.field.Text', { * label: 'Your Name', * value: 'Ed Spencer' * }); * * ## Configuring * * Text field offers several configuration options, including {@link #placeHolder}, {@link #maxLength}, * {@link #autoComplete}, {@link #autoCapitalize} and {@link #autoCorrect}. For example, here is how we would configure * a text field to have a maximum length of 10 characters, with placeholder text that disappears when the field is * focused: * * Ext.create('Ext.field.Text', { * label: 'Username', * maxLength: 10, * placeHolder: 'Enter your username' * }); * * The autoComplete, autoCapitalize and autoCorrect configs simply set those attributes on the text field and allow the * native browser to provide those capabilities. For example, to enable auto complete and auto correct, simply * configure your text field like this: * * Ext.create('Ext.field.Text', { * label: 'Username', * autoComplete: true, * autoCorrect: true * }); * * These configurations will be picked up by the native browser, which will enable the options at the OS level. * * Text field inherits from {@link Ext.field.Field}, which is the base class for all fields and provides * a lot of shared functionality for all fields, including setting values, clearing and basic validation. See the * {@link Ext.field.Field} documentation to see how to leverage its capabilities. */Ext.define('Ext.field.Text', { extend: 'Ext.field.Field', xtype: 'textfield', alternateClassName: 'Ext.form.Text', requires: [ 'Ext.field.trigger.Clear' ], /** * @event focus * Fires when this field receives input focus * @param {Ext.field.Text} this This field * @param {Ext.event.Event} e */ /** * @event blur * Fires when this field loses input focus * @param {Ext.field.Text} this This field * @param {Ext.event.Event} e */ /** * @event paste * Fires when this field is pasted. * @param {Ext.field.Text} this This field * @param {Ext.event.Event} e */ /** * @event mousedown * Fires when this field receives a mousedown * @param {Ext.field.Text} this This field * @param {Ext.event.Event} e */ /** * @event keyup * @preventable * Fires when a key is released on the input element * @param {Ext.field.Text} this This field * @param {Ext.event.Event} e */ /** * @event clearicontap * @preventable * Fires when the clear icon is tapped * @param {Ext.field.Text} this This field * @param {Ext.field.Input} input The field's input component. * @param {Ext.event.Event} e */ /** * @event change * Fires when the value has changed. * @param {Ext.field.Text} this This field * @param {String} newValue The new value * @param {String} oldValue The original value */ /** * @event action * @preventable * Fires whenever the return key or go is pressed. FormPanel listeners * for this event, and submits itself whenever it fires. Also note * that this event bubbles up to parent containers. * @param {Ext.field.Text} this This field * @param {Mixed} e The key event object */ config: { /** * @cfg * @inheritdoc */ clearIcon: true, /** * @cfg {'top'/'left'/'bottom'/'right'/'placeholder'} labelAlign * When value is `'placeholder'`, the label text will be rendered as placeholder * text inside the empty input and will animated to "top" alignment when the input * is focused or contains text. * @inheritdoc * @accessor */ /** * @cfg {String} placeHolder A string value displayed in the input (if supported) when the control is empty. * @accessor */ placeHolder: null, /** * @cfg {Number} maxLength The maximum number of permitted input characters. * @accessor */ maxLength: null, /** * True to set the field's DOM element autocomplete attribute to "on", false to set to "off". * @cfg {Boolean} autoComplete * @accessor */ autoComplete: null, /** * True to set the field's DOM element autocapitalize attribute to "on", false to set to "off". * @cfg {Boolean} autoCapitalize * @accessor */ autoCapitalize: null, /** * True to set the field DOM element autocorrect attribute to "on", false to set to "off". * @cfg {Boolean} autoCorrect * @accessor */ autoCorrect: null, /** * True to set the field DOM element readonly attribute to true. * @cfg {Boolean} readOnly * @accessor */ readOnly: null, /** * @cfg {Object} component The inner component for this field, which defaults to an input text. * @accessor */ component: { xtype: 'textinput' }, // @cmd-auto-dependency {aliasPrefix: "trigger.", isKeyedObject: true} /** * @cfg {Object} triggers * {@link Ext.field.trigger.Trigger Triggers} to use in this field. The keys in * this object are unique identifiers for the triggers. The values in this object * are {@link Ext.field.trigger.Trigger Trigger} configuration objects. * * Ext.create('Ext.field.Text', { * label: 'My Custom Field', * triggers: { * foo: { * cls: 'my-foo-trigger', * handler: function() { * console.log('foo trigger clicked'); * } * }, * bar: { * cls: 'my-bar-trigger', * handler: function() { * console.log('bar trigger clicked'); * } * } * } * }); * * The weight value may be a negative value in order to position custom triggers * ahead of default triggers like that of a DatePicker field. * * Ext.create('Ext.form.DatePicker', { * label: 'Pick a Date', * triggers: { * foo: { * cls: 'my-foo-trigger', * weight: -2, // negative to place before default triggers * handler: function() { * console.log('foo trigger clicked'); * } * }, * bar: { * cls: 'my-bar-trigger', * weight: -1, * handler: function() { * console.log('bar trigger clicked'); * } * } * } * }); */ triggers: { clear: { type: 'clear' } }, bubbleEvents: ['action'], bodyAlign: 'stretch', /** * @cfg {'left'/'center'/'right'} [textAlign='left']. * The text alignment of this field. */ textAlign: null }, defaultBindProperty: 'value', twoWayBindable: { value: 1 }, publishes: { value: 1 }, classCls: Ext.baseCSSPrefix + 'textfield', focusedCls: Ext.baseCSSPrefix + 'focused', emptyCls: Ext.baseCSSPrefix + 'empty', /** * @private */ initialize: function() { var me = this; me.callParent(); me.getComponent().on({ keyup: 'onKeyUp', input: 'onInput', focus: 'onFocus', blur: 'onBlur', paste: 'onPaste', mousedown: 'onMouseDown', scope: this }); // set the originalValue of the textfield, if one exists me.originalValue = me.getValue() || ""; me.getComponent().originalValue = me.originalValue; me.syncEmptyCls(); }, applyValue: function(value) { return Ext.isEmpty(value) ? '' : value; }, /** * @private */ updateValue: function(value, oldValue) { var me = this, component = me.getComponent(), // allows value to be zero but not undefined or null (other falsey values) valueValid = value !== undefined && value !== null && value !== ''; if (component) { component.setValue(value); } me.toggleClearTrigger(valueValid && me.isDirty()); me.syncEmptyCls(); if (me.initialized) { me.fireEvent('change', me, value, oldValue); } // When configured with labelAlign placeholder, we should animate the label back to the // placeholder or placeholder back to label if the field's value was cleared or set programatically. if (!me.isConfiguring && !me.isFocused && me.getLabelAlign() === 'placeholder') { me[!value ? 'animateLabelToPlaceholder' : 'animatePlaceholderToLabel'](); } }, updateLabel: function (newLabel, oldLabel) { var me = this; me.callParent([newLabel, oldLabel]); if (me.getLabelAlign() === 'placeholder') { if (me.getValue()) { me.labelElement.setStyle('opacity', 1); } else { me.setPlaceHolder(newLabel); } } }, updateLabelAlign: function(labelAlign, oldLabelAlign) { this.callParent([labelAlign, oldLabelAlign]); }, updateTextAlign: function(newAlign, oldAlign) { var element = this.element; if (oldAlign) { element.removeCls(Ext.baseCSSPrefix + 'text-align-' + oldAlign); } if (newAlign) { element.addCls(Ext.baseCSSPrefix + 'text-align-' + newAlign); } }, /** * @private */ updatePlaceHolder: function(newPlaceHolder) { var me = this, label = me.getLabel(); //<debug> if ((me.getLabelAlign() === 'placeholder') && newPlaceHolder !== label) { Ext.log.warn('PlaceHolder should not be set when using "labelAlign: \'placeholder\'"'); } //</debug> me.getComponent().setPlaceHolder(newPlaceHolder); }, /** * @private */ updateMaxLength: function(newMaxLength) { this.getComponent().setMaxLength(newMaxLength); }, /** * @private */ updateAutoComplete: function(newAutoComplete) { this.getComponent().setAutoComplete(newAutoComplete); }, /** * @private */ updateAutoCapitalize: function(newAutoCapitalize) { this.getComponent().setAutoCapitalize(newAutoCapitalize); }, /** * @private */ updateAutoCorrect: function(newAutoCorrect) { this.getComponent().setAutoCorrect(newAutoCorrect); }, /** * @private */ updateReadOnly: function(newReadOnly) { this.toggleClearTrigger(!newReadOnly); this.getComponent().setReadOnly(newReadOnly); }, /** * @private */ updateInputType: function(newInputType) { var component = this.getComponent(); if (component) { component.setType(newInputType); } }, /** * @private */ updateName: function(newName) { var component = this.getComponent(); if (component) { component.setName(newName); } }, /** * @private */ updateTabIndex: function(newTabIndex) { var component = this.getComponent(); if (component) { component.setTabIndex(newTabIndex); } }, /** * Updates the {@link #inputCls} configuration on this fields {@link #component} * @private */ updateInputCls: function(newInputCls, oldInputCls) { var component = this.getComponent(); if (component) { component.replaceCls(oldInputCls, newInputCls); } }, updateDisabled: function(disabled, oldDisabled) { this.callParent([disabled, oldDisabled]); this.toggleClearTrigger(!disabled); }, updateClearIcon: function(clearIcon, oldClearIcon) { var me = this, triggers, clearTrigger; if (!me.isConfiguring) { triggers = me.getTriggers(); clearTrigger = triggers && triggers.clear; if (clearIcon) { if (!clearTrigger) { me.addTrigger('clear', 'clear'); } } else if (clearTrigger) { me.removeTrigger('clear'); } } }, applyTriggers: function(triggers, oldTriggers) { var me = this, instances = oldTriggers || {}, clearable = me.getClearIcon(), name, trigger, oldTrigger; for (name in triggers) { trigger = triggers[name]; oldTrigger = instances[name]; // Any key that exists on the incoming object should cause destruction of // the existing trigger for that key, if one exists. This is true for both // truthy values (triggers and trigger configs) and falsy values. Falsy values // cause destruction of the existing trigger without replacement. if (oldTrigger) { oldTrigger.destroy(); } if (trigger) { if (!clearable && (name === 'clear')) { continue; } instances[name] = me.createTrigger(name, trigger); } } return instances; }, updateTriggers: function(triggers, oldTriggers) { this.syncTriggers(); }, /** * Adds a trigger to this text field. * @param {String} name Unique name (within this field) for the trigger. Cannot be the * same as the name of an existing trigger for this field. * @param {Ext.field.trigger.Trigger/Object} trigger The trigger instance or a config * object for a trigger to add * @return {Ext.field.trigger.Trigger} The trigger that was added */ addTrigger: function(name, trigger) { var me = this, triggers = me.getTriggers(), triggerConfig; //<debug> if (triggers && triggers[name]) { Ext.raise('Trigger with name "' + name + '" already exists.'); } if (typeof name !== 'string') { Ext.raise('Cannot add trigger. Key must be a string.'); } if (typeof trigger !== 'string' && !Ext.isObject(trigger)) { Ext.raise('Cannot add trigger "' + name + '". A trigger config or instance is required.'); } //</debug> trigger = me.createTrigger(name, trigger); if (triggers) { triggers[name] = trigger; me.syncTriggers(); } else { triggerConfig = {}; triggerConfig[name] = trigger; me.setTriggers(triggerConfig); } return trigger; }, /** * Removes a trigger from this text field. * @param {String/Ext.field.trigger.Trigger} trigger The name of the trigger to remove, * or a trigger reference. * @param {Boolean} [destroy=true] False to prevent the trigger from being destroyed * on removal. Only use this option if you want to reuse the trigger instance. * @return {Ext.field.trigger.Trigger} The trigger that was removed */ removeTrigger: function(trigger, destroy) { var me = this, triggers = me.getTriggers(), name = trigger, triggerEl; if (name.isTrigger) { name = trigger.getName(); } else { trigger = triggers[name]; } //<debug> if (!name) { Ext.raise('Trigger not found.'); } else if (!triggers[name]) { Ext.raise('Cannot remove trigger. Trigger with name "' + name + '" not found.'); } //</debug> delete triggers[name]; if (destroy !== false) { trigger.destroy(); } else { triggerEl = trigger.el.dom; triggerEl.parentNode.removeChild(triggerEl); } this.syncTriggers(); return trigger; }, /** * @private */ showClearTrigger: function() { var me = this, value = me.getValue(), // allows value to be zero but not undefined or null (other falsey values) valueValid = value !== undefined && value !== null && value !== "", triggers, clearTrigger; if (me.getClearIcon() && !me.getDisabled() && !me.getReadOnly() && valueValid) { triggers = me.getTriggers(); clearTrigger = triggers && triggers.clear; if (clearTrigger) { clearTrigger.show(); } } return me; }, /** * @private */ hideClearTrigger: function() { var triggers = this.getTriggers(), clearTrigger = triggers && triggers.clear; if (clearTrigger) { clearTrigger.hide(); } }, onKeyUp: function(e) { this.fireAction('keyup', [this, e], 'doKeyUp'); }, /** * Called when a key has been pressed in the `<input>` * @private */ doKeyUp: function(me, e) { // getValue to ensure that we are in sync with the dom var value = me.getValue(), // allows value to be zero but not undefined or null (other falsey values) valueValid = value !== undefined && value !== null && value !== ''; me.toggleClearTrigger(valueValid); if (e.browserEvent.keyCode === 13) { me.fireAction('action', [me, e], 'doAction'); } }, doAction: function() { this.blur(); }, onClearIconTap: function(input, e) { this.fireAction('clearicontap', [this, input, e], 'doClearIconTap'); //focus the field after cleartap happens, but only on android. //this is to stop the keyboard from hiding. TOUCH-2064 if (Ext.os.is.Android) { this.getComponent().focus(); } }, /** * @private */ doClearIconTap: function(me, e) { me.setValue(''); }, onInput: function(component, value) { this.setValue(value); }, onFocus: function(e) { var me = this; me.addCls(me.focusedCls); me.isFocused = true; if (me.getLabelAlign() === 'placeholder' && !me.getValue()) { me.animatePlaceholderToLabel(); } me.fireEvent('focus', me, e); }, onBlur: function(e) { var me = this; me.removeCls(me.focusedCls); me.isFocused = false; if (me.getLabelAlign() === 'placeholder' && !me.getValue()) { me.animateLabelToPlaceholder(); } me.fireEvent('blur', me, e); Ext.defer(function() { me.isFocused = false; }, 50); }, onPaste: function(e) { this.fireEvent('paste', this, e); }, onMouseDown: function(e) { this.fireEvent('mousedown', this, e); }, /** * Attempts to set the field as the active input focus. * @return {Ext.field.Text} This field */ focus: function() { this.getComponent().focus(); return this; }, /** * Attempts to forcefully blur input focus for the field. * @return {Ext.field.Text} This field */ blur: function() { this.getComponent().blur(); return this; }, /** * Attempts to forcefully select all the contents of the input field. * @return {Ext.field.Text} this */ select: function() { this.getComponent().select(); return this; }, resetOriginalValue: function() { var me = this, component; me.callParent(); component = me.getComponent(); if(component && component.hasOwnProperty("originalValue")) { me.getComponent().originalValue = me.originalValue; } me.reset(); }, reset: function() { var me = this; me.getComponent().reset(); //we need to call this to sync the input with this field this.callParent(); me.toggleClearTrigger(me.isDirty()); }, isDirty: function() { var component = this.getComponent(); if (component) { return component.isDirty(); } return false; }, doDestroy: function() { var me = this, triggers = me.getTriggers(), triggerGroups = me.triggerGroups, name; if (triggerGroups) { for (name in triggerGroups) { triggerGroups[name].destroy(); } me.triggerGroups = null; } for (name in triggers) { triggers[name].destroy(); } me.setTriggers(null); me.callParent(); }, privates: { animateLabelToPlaceholder: function() { var me = this, animInfo = me.getPlaceholderAnimInfo(); me.labelElement.animate({ from: { left: 0, top: 0, opacity: 1 }, to: animInfo, preserveEndState: true, duration: 250, easing: 'ease-out', callback: function() { me.setPlaceHolder(me.getLabel()); } }); me.lastPlaceholderAnimInfo = animInfo; }, animatePlaceholderToLabel: function() { var me = this; me.labelElement.animate({ from: me.lastPlaceholderAnimInfo || me.getPlaceholderAnimInfo(), to: { left: 0, top: 0, opacity: 1 }, easing: 'ease-out', preserveEndState: true, duration: 250 }); me.setPlaceHolder(null); me.lastPlaceholderAnimInfo = null; }, /** * @private */ createTrigger: function(name, trigger) { if (!trigger.isTrigger) { if (trigger === true) { trigger = { type: name }; } else if (typeof trigger === 'string') { trigger = { type: trigger }; } trigger = Ext.apply({ name: name, field: this }, trigger); trigger = trigger.xtype ? Ext.create(trigger) : Ext.Factory.trigger(trigger); } return trigger; }, getPlaceholderAnimInfo: function() { var me = this, element = me.element, labelElement = me.labelElement, inputElement = me.getComponent().inputElement, labelOffsets = labelElement.getOffsetsTo(element), inputOffsets = inputElement.getOffsetsTo(element), labelPadding = labelElement.getPadding('l'), inputPadding = inputElement.getPadding('l'), translateX = inputOffsets[0] - labelOffsets[0] + (inputPadding - labelPadding), translateY = inputOffsets[1] - labelOffsets[1]; return { left: translateX, top: translateY, opacity: 0 }; }, syncEmptyCls: function() { this.toggleCls(this.emptyCls, !this.getValue()); }, /** * Synchronizes the DOM to match the triggers' configured weight, side, and grouping * @private */ syncTriggers: function() { var me = this, triggers = me.getTriggers(), input = me.getComponent(), triggerGroups = me.triggerGroups || (me.triggerGroups = {}), beforeTriggers = [], afterTriggers = [], triggersByGroup = {}, Trigger = Ext.field.trigger['Trigger'], name, trigger, groupName, triggerGroup, i, ln; for (name in triggers) { trigger = triggers[name]; groupName = trigger.getGroup(); if (groupName) { (triggersByGroup[groupName] || (triggersByGroup[groupName] = [])).push(trigger); } else if (trigger.getSide() === 'left') { beforeTriggers.push(trigger); } else { afterTriggers.push(trigger); } } for (groupName in triggersByGroup) { triggerGroup = triggerGroups[groupName]; if (!triggerGroup) { triggerGroup = triggers[groupName]; // just in case the user configured a group trigger if (!triggerGroup) { triggerGroup = new Trigger(); } triggerGroups[groupName] = triggerGroup; } triggerGroup.setTriggers(Trigger.sort(triggersByGroup[groupName])); if (triggerGroup.getSide() === 'left') { beforeTriggers.push(triggerGroup); } else { afterTriggers.push(triggerGroup); } } Trigger.sort(beforeTriggers); Trigger.sort(afterTriggers); for (i = 0, ln = beforeTriggers.length; i < ln; i++) { input.beforeElement.appendChild(beforeTriggers[i].element); } for (i = 0, ln = afterTriggers.length; i < ln; i++) { input.afterElement.appendChild(afterTriggers[i].element); } for (groupName in triggerGroups) { if (!(groupName in triggersByGroup)) { // group no longer has any triggers. it can be removed. triggerGroup = triggerGroups[groupName]; triggerGroup.setTriggers(null); triggerGroup.destroy(); delete triggerGroups[groupName]; } } }, toggleClearTrigger: function(state) { if (state) { this.showClearTrigger(); } else { this.hideClearTrigger(); } } }});