/**
* Base class for form fields that provides default event handling, rendering, and other common
* functionality needed by all form field types. Utilizes the {@link Ext.form.field.Field} mixin
* for value handling and validation, and the {@link Ext.form.Labelable} mixin to provide label
* and error message display.
*
* In most cases you will want to use a subclass, such as {@link Ext.form.field.Text} or
* {@link Ext.form.field.Checkbox}, rather than creating instances of this class directly. However
* if you are implementing a custom form field, using this as the parent class is recommended.
*
* # Values and Conversions
*
* Because Base implements the Field mixin, it has a main value that can be initialized with the
* {@link #value} config and manipulated via the {@link #getValue} and {@link #setValue} methods.
* This main value can be one of many data types appropriate to the current field, for instance a
* {@link Ext.form.field.Date Date} field would use a JavaScript Date object as its value type.
* However, because the field is rendered as a HTML input, this value data type can not always
* be directly used in the rendered field.
*
* Therefore Base introduces the concept of a "raw value". This is the value of the rendered HTML
* input field, and is normally a String. The {@link #getRawValue} and {@link #setRawValue} methods
* can be used to directly work with the raw value, though it is recommended to use getValue
* and setValue in most cases.
*
* Conversion back and forth between the main value and the raw value is handled by the
* {@link #valueToRaw} and {@link #rawToValue} methods. If you are implementing a subclass
* that uses a non-String value data type, you should override these methods to handle
* the conversion.
*
* # Rendering
*
* The content of the field body is defined by the {@link #fieldSubTpl} XTemplate, with its argument
* data created by the {@link #getSubTplData} method. Override this template and/or method to create
* custom field renderings.
*/
Ext.define('Ext.form.field.Base', {
extend: 'Ext.Component',
alternateClassName: ['Ext.form.Field', 'Ext.form.BaseField'],
xtype: 'field',
requires: [
'Ext.util.DelayedTask',
'Ext.XTemplate'
],
mixins: [
'Ext.form.Labelable',
'Ext.form.field.Field'
],
/**
* @property focusable
* @inheritdoc
*/
focusable: true,
/**
* @cfg shrinkWrap
* @inheritdoc
*/
shrinkWrap: true,
/* eslint-disable indent, max-len */
/**
* @cfg {Ext.XTemplate} fieldSubTpl
* The content of the field body is defined by this config option.
* @private
*/
fieldSubTpl: [ // note: {id} here is really {inputId}, but {cmpId} is available
'<input id="{id}" data-ref="inputEl" type="{type}" {inputAttrTpl}',
' size="1"', // allows inputs to fully respect CSS widths across all browsers
'<tpl if="name"> name="{name}"</tpl>',
'<tpl if="value"> value="{[Ext.util.Format.htmlEncode(values.value)]}"</tpl>',
'<tpl if="placeholder"> placeholder="{placeholder}"</tpl>',
'{%if (values.maxLength !== undefined){%} maxlength="{maxLength}"{%}%}',
'<tpl if="readOnly"> readonly="readonly"</tpl>',
'<tpl if="disabled"> disabled="disabled"</tpl>',
'<tpl if="tabIdx != null"> tabindex="{tabIdx}"</tpl>',
'<tpl if="fieldStyle"> style="{fieldStyle}"</tpl>',
'<tpl if="ariaEl == \'inputEl\'">',
'<tpl foreach="ariaElAttributes"> {$}="{.}"</tpl>',
'</tpl>',
'<tpl foreach="inputElAriaAttributes"> {$}="{.}"</tpl>',
' class="{fieldCls} {typeCls} {typeCls}-{ui} {editableCls} {inputCls} {fixCls}" autocomplete="off"/>',
{
disableFormats: true
}
],
/* eslint-enable indent, max-len */
/**
* @property defaultBindProperty
* @inheritdoc
*/
defaultBindProperty: 'value',
/**
* @cfg autoEl
* @inheritdoc
*/
autoEl: {
role: 'presentation'
},
subTplInsertions: [
/**
* @cfg {String/Array/Ext.XTemplate} inputAttrTpl
* An optional string or `XTemplate` configuration to insert in the field markup
* inside the input element (as attributes). If an `XTemplate` is used, the component's
* {@link #getSubTplData subTpl data} serves as the context.
*/
'inputAttrTpl'
],
/**
* @cfg childEls
* @inheritdoc
*/
childEls: [
/**
* @property {Ext.dom.Element} inputEl
* The input Element for this Field. Only available after the field has been rendered.
*/
'inputEl'
],
/**
* @cfg {String} name
* The name of the field. This is used as the parameter name when including the field value
* in a {@link Ext.form.Basic#submit form submit()}. If no name is configured, it falls back
* to the {@link #inputId}. To prevent the field from being included in the form submit,
* set {@link #submitValue} to false.
*/
/**
* @cfg {String} inputType
* The type attribute for input fields -- e.g. radio, text, password, file. The extended types
* supported by HTML5 inputs (url, email, etc.) may also be used, though using them will cause
* older browsers to fall back to 'text'.
*
* The type 'password' must be used to render that field type currently -- there is no separate
* Ext component for that. You can use {@link Ext.form.field.File} which creates
* a custom-rendered file upload field, but if you want a plain unstyled file input you can use
* a Base with inputType:'file'.
*/
inputType: 'text',
/**
* @cfg {Boolean} isTextInput
* `true` if this field renders as a text input.
*
* @private
* @since 5.0.1
*/
isTextInput: true,
/**
* @cfg {Number} tabIndex
*
* Sets a DOM tabIndex for this field. tabIndex may be set to `-1` in order to remove
* the field from the tab rotation.
*
* **Note:** tabIndex only applies to fields that are rendered. It does not effect
* fields built via applyTo
*/
/**
* @cfg {String} invalidText
* The error text to use when marking a field invalid and no message is provided
* @locale
*/
invalidText: 'The value in this field is invalid',
/**
* @cfg {String} fieldCls
* The default CSS class for the field input
*/
fieldCls: Ext.baseCSSPrefix + 'form-field',
/**
* @cfg {String} fieldStyle
* Optional CSS style(s) to be applied to the {@link #inputEl field input element}.
* Should be a valid argument to {@link Ext.dom.Element#applyStyles}. Defaults to undefined.
* See also the {@link #setFieldStyle} method for changing the style after initialization.
*/
/**
* @cfg [publishes=['rawValue', 'value', 'dirty']]
* @inheritdoc
*/
/**
* @cfg {String} focusCls
* The CSS class to use when the field receives focus
*/
focusCls: 'form-focus',
/**
* @cfg {String} dirtyCls
* The CSS class to use when the field value {@link #isDirty is dirty}.
*/
dirtyCls: Ext.baseCSSPrefix + 'form-dirty',
/**
* @cfg {String[]} checkChangeEvents
* A list of event names that will be listened for on the field's
* {@link #inputEl input element}, which will cause the field's value to be checked for changes.
* If a change is detected, the {@link #change change event} will be fired, followed by
* validation if the {@link #validateOnChange} option is enabled.
*
* Defaults to ['change', 'propertychange', 'keyup'] in Internet Explorer, and
* ['change', 'input', 'textInput', 'keyup', 'dragdrop'] in other browsers.
* This catches all the ways that field values can be changed in most supported browsers;
* the only known exceptions at the time of writing are:
*
* - Safari 3.2 and older: cut/paste in textareas via the context menu, and dragging text
* into textareas
* - Opera 10 and 11: dragging text into text fields and textareas, and cut via the context
* menu in text fields and textareas
* - Opera 9: Same as Opera 10 and 11, plus paste from context menu in text fields
* and textareas
*
* If you need to guarantee on-the-fly change notifications including these edge cases, you can
* call the {@link #checkChange} method on a repeating interval, e.g. using
* {@link Ext.TaskManager}, or if the field is within a {@link Ext.form.Panel}, you can use
* the FormPanel's {@link Ext.form.Panel#pollForChanges} configuration to set up
* such a task automatically.
*/
checkChangeEvents: Ext.isIE && (!document.documentMode || document.documentMode <= 9)
? ['change', 'propertychange', 'keyup']
: ['change', 'input', 'textInput', 'keyup', 'dragdrop'],
// While input is supported in IE9, we use attachEvent for events, so we need to fall back here
ignoreChangeRe: /data-errorqtip|style\.|className/,
/**
* @cfg {Number} checkChangeBuffer
* Defines a timeout in milliseconds for buffering {@link #cfg!checkChangeEvents} that fire
* in rapid succession.
* Defaults to 50 milliseconds.
*/
checkChangeBuffer: 50,
/**
* @cfg liquidLayout
* @inheritdoc
*/
liquidLayout: true,
/**
* @cfg {Boolean} readOnly
* true to mark the field as readOnly in HTML.
*/
readOnly: false,
/**
* @cfg {String} readOnlyCls
* The CSS class applied to the component's main element when it is {@link #readOnly}.
*/
readOnlyCls: Ext.baseCSSPrefix + 'form-readonly',
/**
* @cfg {String} inputId
* The id that will be given to the generated input DOM element. Defaults to an automatically
* generated id. If you configure this manually, you must make sure it is unique
* in the document.
*/
/**
* @cfg {Boolean} [validateOnBlur=true]
* Whether the field should validate when it loses focus. This will cause fields to be validated
* as the user steps through the fields in the form regardless of whether they are making
* changes to those fields along the way. See also {@link #validateOnChange}.
*/
validateOnBlur: true,
/**
* @cfg {Boolean} [validateOnFocusLeave=false] Set to `true` to validate the field
* when focus leaves the field's component hierarchy entirely.
*
* The difference between {@link #validateOnBlur} and this option is that the former
* will happen when field's _input element_ blurs. In complex fields such as ComboBox
* or Date focus may leave the input element to the drop-down picker, which will cause
* {@link #validateOnBlur} to happen prematurely.
*
* Using this option is recommended for accessible applications. The default value
* is `false` for backwards compatibility; this option and {@link #validateOnBlur}
* are mutually exclusive.
*
* @since 6.5.3
*/
validateOnFocusLeave: false,
/**
* @cfg {String} formatText
* Helpful text describing acceptable format for field values. This text will be
* announced by Assistive Technologies such as screen readers when the field is
* focused.
*
* This option is superseded by {@link #ariaHelp}.
*
* @deprecated 6.2.0 This config is deprecated.
* @locale
*/
/**
* @private
*/
hasFocus: false,
/**
* @cfg baseCls
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'field',
/**
* @cfg fieldBodyCls
* @inheritdoc
*/
fieldBodyCls: Ext.baseCSSPrefix + 'field-body',
webkitBorderBoxBugCls: Ext.baseCSSPrefix + 'webkit-border-box-bug',
/**
* @property maskOnDisable
* @inheritdoc
*/
maskOnDisable: false,
// Instructs the layout to stretch the inputEl to 100% width when laying
// out under fixed conditions. Defaults to true for all fields except check/radio
// Doesn't seem worth it to introduce a whole new layout class just for this flag
stretchInputElFixed: true,
// Form fields render their ARIA attributes to the inputEl
/**
* @property ariaEl
* @inheritdoc
*/
ariaEl: 'inputEl',
/**
* @property focusEl
* @inheritdoc
*/
focusEl: 'inputEl',
renderAriaElements: true,
/**
* @event specialkey
* Fires when any key related to navigation (arrows, tab, enter, esc, etc.) is pressed.
* To handle other keys see {@link Ext.util.KeyMap}. You can check
* {@link Ext.event.Event#getKey} to determine which key was pressed.
* For example:
*
* var form = new Ext.form.Panel({
* ...
* items: [{
* fieldLabel: 'Field 1',
* name: 'field1',
* allowBlank: false
* },{
* fieldLabel: 'Field 2',
* name: 'field2',
* listeners: {
* specialkey: function(field, e){
* // e.HOME, e.END, e.PAGE_UP, e.PAGE_DOWN,
* // e.TAB, e.ESC, arrow keys: e.LEFT, e.RIGHT, e.UP, e.DOWN
* if (e.getKey() == e.ENTER) {
* var form = field.up('form').getForm();
* form.submit();
* }
* }
* }
* }
* ],
* ...
* });
*
* @param {Ext.form.field.Base} this
* @param {Ext.event.Event} e The event object
*/
/**
* @event writeablechange
* Fires when this field changes its read-only status.
* @param {Ext.form.field.Base} this
* @param {Boolean} Read only flag
*/
initComponent: function() {
var me = this;
me.callParent();
me.subTplData = me.subTplData || {};
// Init mixins
me.initLabelable();
me.initField();
me.initDefaultName();
// validateOnBlur and validateOnFocusLeave are mutually exclusive,
// with latter taking precedence
if (me.validateOnFocusLeave) {
me.validateOnBlur = false;
}
// Add to protoEl before render
if (me.readOnly) {
me.addCls(me.readOnlyCls);
}
me.addCls(Ext.baseCSSPrefix + 'form-type-' + me.inputType);
// formatText is superseded by ariaHelp but we still apply it for compatibility
if (me.format && me.formatText && !me.ariaHelp) {
me.ariaHelp = Ext.String.format(me.formatText, me.format);
}
},
/**
* @private
*/
initDefaultName: function() {
var me = this;
// Default name to inputId
if (!me.name) {
me.name = me.getInputId();
}
},
/**
* Returns the input id for this field. If none was specified via the {@link #inputId} config,
* then an id will be automatically generated.
*/
getInputId: function() {
return this.inputId || (this.inputId = this.id + '-inputEl');
},
/**
* Creates and returns the data object to be used when rendering the {@link #fieldSubTpl}.
* @return {Object} The template data
* @template
*/
getSubTplData: function(fieldData) {
var me = this,
id = me.id,
type = me.inputType,
inputId = me.getInputId(),
inputCls = me.inputCls || '',
fixCls = '',
data, ariaAttr, inputElAttr;
if (Ext.supports.WebKitInputTableBoxModelBug) {
// workaround for https://bugs.webkit.org/show_bug.cgi?id=137693
// Can't use inputCls or typeCls here since they will be appended
// with ui in different subclasses which breaks things.
fixCls += me.webkitBorderBoxBugCls;
}
data = Ext.apply({
ui: me.ui,
id: inputId,
cmpId: id,
name: me.name || inputId,
disabled: me.disabled,
readOnly: me.readOnly,
value: me.getRawValue(),
type: type,
fieldCls: me.fieldCls,
fieldStyle: me.getFieldStyle(),
childElCls: fieldData.childElCls,
tabIdx: me.tabIndex,
inputCls: inputCls,
typeCls: Ext.baseCSSPrefix + 'form-' + (me.isTextInput ? 'text' : type),
fixCls: fixCls,
ariaEl: me.ariaEl
}, me.subTplData);
if (me.ariaRole) {
ariaAttr = {};
if (!me.ariaStaticRoles[me.ariaRole]) {
// When ARIA attributes are rendered they should always reflect
// component's state. This contrasts with the standard HTML attributes
// like disabled and readonly, which are only present when enabled.
ariaAttr['aria-hidden'] = !!me.hidden;
ariaAttr['aria-disabled'] = !!me.disabled;
// For most of the fields ariaEl === inputEl but certain types like Combo boxes
// and their descendants are compound widgets and need to have ARIA attributes
// on different elements.
inputElAttr = {
// Input fields start out as valid
'aria-invalid': false,
'aria-readonly': !!me.readOnly
};
// aria-label is not present by default, and aria-labelledby
// generally should not be used for fields' inputEls since usually
// they are referenced by their respective <label> elements.
if (me.ariaLabel) {
ariaAttr['aria-label'] = Ext.String.htmlEncode(me.ariaLabel);
}
ariaAttr = Ext.apply(ariaAttr, me.getAriaAttributes());
// If aria-describedby was set explicitly, don't override. Note that
// describedby applies to inputEl since most often that's the focusable
// element.
if (!ariaAttr['aria-describedby']) {
if (me.ariaHelp) {
inputElAttr['aria-describedby'] =
id + '-ariaStatusEl ' + id + '-ariaHelpEl';
}
else {
inputElAttr['aria-describedby'] = id + '-ariaStatusEl';
}
}
data.inputElAriaAttributes = inputElAttr;
}
if (me.ariaRole !== 'native') {
ariaAttr.role = me.ariaRole;
}
// aria-label is not present by default, and aria-labelledby
// generally should not be used for fields' inputEls since usually
// they are referenced by their respective <label> elements.
if (me.ariaLabel) {
ariaAttr['aria-label'] = me.ariaLabel;
}
if (me.format && me.formatText && !data.title) {
ariaAttr.title = Ext.String.formatEncode(me.formatText, me.format);
}
data.ariaElAttributes = ariaAttr;
}
me.getInsertionRenderData(data, me.subTplInsertions);
return data;
},
/**
* Gets the markup to be inserted into the outer template's bodyEl.
* For fields this is the actual input element.
* @protected
*/
getSubTplMarkup: function(fieldData) {
var me = this,
data = me.getSubTplData(fieldData),
preSubTpl = me.lookupTpl('preSubTpl'),
postSubTpl = me.lookupTpl('postSubTpl'),
markup = '';
if (preSubTpl) {
markup += preSubTpl.apply(data);
}
markup += me.lookupTpl('fieldSubTpl').apply(data);
if (postSubTpl) {
markup += postSubTpl.apply(data);
}
return markup;
},
initRenderData: function() {
return Ext.applyIf(this.callParent(), this.getLabelableRenderData());
},
/**
* Set the {@link #fieldStyle CSS style} of the {@link #inputEl field input element}.
* @param {String/Object/Function} style The style(s) to apply. Should be a valid argument
* to {@link Ext.dom.Element#applyStyles}.
*/
setFieldStyle: function(style) {
var me = this,
inputEl = me.inputEl;
if (inputEl) {
inputEl.applyStyles(style);
}
me.fieldStyle = style;
},
getFieldStyle: function() {
var style = this.fieldStyle;
return Ext.isObject(style) ? Ext.DomHelper.generateStyles(style, null, true) : style || '';
},
onRender: function() {
// This noOptimize can be removed after SDKTOOLS-946 is fixed
// @noOptimize.callParent
this.callParent(arguments);
this.mixins.labelable.self.initTip();
this.renderActiveError();
},
beforeBlur: function(e) {
if (this.validateOnBlur) {
this.validate();
}
},
onFocusLeave: function(e) {
if (this.validateOnFocusLeave) {
this.validate();
}
this.callParent([e]);
this.completeEdit();
},
/**
* @method
* @protected
* Called when focus leaves this input field.
* Used to postprocess raw values and perform conversion and validation.
*/
completeEdit: Ext.emptyFn,
isFileUpload: function() {
return this.inputType === 'file';
},
/**
* @private
* Private override to use getSubmitValue() as a convenience
*/
getSubmitData: function() {
var me = this,
data = null,
val;
if (!me.disabled && me.submitValue) {
val = me.getSubmitValue();
if (val !== null) {
data = {};
data[me.getName()] = val;
}
}
return data;
},
/**
* Returns the value that would be included in a standard form submit for this field.
* This will be combined with the field's name to form a name=value pair in the
* {@link #getSubmitData submitted parameters}. If an empty string is returned then just
* the name= will be submitted; if null is returned then nothing will be submitted.
*
* Note that the value returned will have been {@link #processRawValue processed}
* but may or may not have been successfully {@link #validate validated}.
*
* @return {String} The value to be submitted, or null.
*/
getSubmitValue: function() {
return this.processRawValue(this.getRawValue());
},
/**
* Returns the raw value of the field, without performing any normalization, conversion,
* or validation. To get a normalized and converted value see {@link #getValue}.
* @return {String} value The raw String value of the field
*/
getRawValue: function() {
var me = this,
v = (me.inputEl ? me.inputEl.getValue() : Ext.valueFrom(me.rawValue, ''));
me.rawValue = v;
return v;
},
/**
* Sets the field's raw value directly, bypassing {@link #valueToRaw value conversion},
* change detection, and validation. To set the value with these additional inspections
* see {@link #setValue}.
* @param {Object} value The value to set
* @return {Object} value The field value that is set
*/
setRawValue: function(value) {
var me = this,
rawValue = me.rawValue;
if (!me.transformRawValue.$nullFn) {
value = me.transformRawValue(value);
}
value = Ext.valueFrom(value, '');
if (rawValue === undefined || rawValue !== value) {
me.rawValue = value;
// Some Field subclasses may not render an inputEl
if (me.inputEl) {
me.bindChangeEvents(false);
me.inputEl.dom.value = value;
me.bindChangeEvents(true);
}
}
if (me.rendered && me.reference) {
me.publishState('rawValue', value);
}
return value;
},
/**
* @method
* Transform the raw value before it is set
* @protected
* @param {Object} value The value
* @return {Object} The value to set
*/
transformRawValue: Ext.identityFn,
/**
* Converts a mixed-type value to a raw representation suitable for displaying in the field.
* This allows controlling how value objects passed to {@link #setValue} are shown to the user,
* including localization. For instance, for a {@link Ext.form.field.Date}, this would control
* how a Date object passed to {@link #setValue} would be converted to a String for display
* in the field.
*
* See {@link #rawToValue} for the opposite conversion.
*
* The base implementation simply does a standard toString conversion, and converts
* {@link Ext#isEmpty empty values} to an empty string.
*
* @param {Object} value The mixed-type value to convert to the raw representation.
* @return {Object} The converted raw value.
*/
valueToRaw: function(value) {
return '' + Ext.valueFrom(value, '');
},
/**
* Converts a raw input field value into a mixed-type value that is suitable for this particular
* field type. This allows controlling the normalization and conversion of user-entered values
* into field-type-appropriate values, e.g. a Date object for {@link Ext.form.field.Date},
* and is invoked by {@link #getValue}.
*
* It is up to individual implementations to decide how to handle raw values that cannot be
* successfully converted to the desired object type.
*
* See {@link #valueToRaw} for the opposite conversion.
*
* The base implementation does no conversion, returning the raw value untouched.
*
* @param {Object} rawValue
* @return {Object} The converted value.
* @method
*/
rawToValue: Ext.identityFn,
/**
* Performs any necessary manipulation of a raw field value to prepare it for
* {@link #rawToValue conversion} and/or {@link #validate validation}, for instance
* stripping out ignored characters. In the base implementation it does nothing;
* individual subclasses may override this as needed.
*
* @param {Object} value The unprocessed string value
* @return {Object} The processed string value
* @method
*/
processRawValue: Ext.identityFn,
/**
* Returns the current data value of the field. The type of value returned is particular
* to the type of the particular field (e.g. a Date object for {@link Ext.form.field.Date}),
* as the result of calling {@link #rawToValue} on the field's
* {@link #processRawValue processed} String value. To return the raw String value,
* see {@link #getRawValue}.
* @return {Object} value The field value
*/
getValue: function() {
var me = this,
val = me.rawToValue(me.processRawValue(me.getRawValue()));
me.value = val;
return val;
},
/**
* Sets a data value into the field and runs the change detection and validation.
* To set the value directly without these inspections see {@link #setRawValue}.
* @param {Object} value The value to set
* @return {Ext.form.field.Field} this
*/
setValue: function(value) {
var me = this;
me.setRawValue(me.valueToRaw(value));
return me.mixins.field.setValue.call(me, value);
},
onBoxReady: function() {
var me = this;
me.callParent(arguments);
if (me.setReadOnlyOnBoxReady) {
me.setReadOnly(me.readOnly);
}
},
onDisable: function() {
var me = this,
inputEl = me.inputEl;
me.callParent();
if (inputEl) {
inputEl.dom.disabled = true;
if (me.hasActiveError()) {
// clear invalid state since the field is now disabled
me.clearInvalid();
me.hadErrorOnDisable = true;
}
}
// Disabled fields always validate to true, so if
// wasValid is false, the state will have changed, but
// we only want to do so if we have a previous wasValid value
if (me.wasValid === false) {
me.checkValidityChange(true);
}
},
onEnable: function() {
var me = this,
inputEl = me.inputEl,
mark = me.preventMark,
valid;
me.callParent();
if (inputEl) {
inputEl.dom.disabled = false;
}
if (me.wasValid !== undefined) {
me.forceValidation = true;
me.preventMark = !me.hadErrorOnDisable;
valid = me.isValid();
me.forceValidation = false;
me.preventMark = mark;
me.checkValidityChange(valid);
}
delete me.hadErrorOnDisable;
},
/**
* Sets the read only state of this field.
* @param {Boolean} readOnly Whether the field should be read only.
*/
setReadOnly: function(readOnly) {
var me = this,
inputEl = me.inputEl,
old = me.readOnly;
readOnly = !!readOnly;
me[readOnly ? 'addCls' : 'removeCls'](me.readOnlyCls);
me.readOnly = readOnly;
if (inputEl) {
inputEl.dom.readOnly = readOnly;
inputEl.dom.setAttribute('aria-readonly', readOnly);
}
else if (me.rendering) {
me.setReadOnlyOnBoxReady = true;
}
if (readOnly !== old) {
me.fireEvent('writeablechange', me, readOnly);
}
},
/**
* @private
*/
fireKey: function(e, eOpts) {
if (e.isSpecialKey()) {
this.fireEvent('specialkey', this, e, eOpts);
}
},
initEvents: function() {
var me = this,
inputEl = me.inputEl,
onFieldMutation = me.onFieldMutation,
events = me.checkChangeEvents,
len = events.length,
i, event;
if (inputEl) {
me.mon(
inputEl, Ext.supports.SpecialKeyDownRepeat ? 'keydown' : 'keypress', me.fireKey, me
);
for (i = 0; i < len; ++i) {
event = events[i];
if (event === 'propertychange') {
me.usesPropertychange = true;
}
if (event === 'textInput') {
me.usesTextInput = true;
}
me.mon(inputEl, event, onFieldMutation, me);
}
}
me.callParent();
},
/**
* @private
* Called when some event (See the checkChangeEvents property) mutates the input field.
* We react to changes.
*
* Subclasses may provide an inplementation which may perform other tasks (eg ComboBox value
* matching) before calling the checkChange method.
*/
onFieldMutation: function(e) {
// When using propertychange, we want to skip out on various values, since they won't cause
// the underlying value to change.
if (!this.readOnly && !(e.type === 'propertychange' &&
this.ignoreChangeRe.test(e.browserEvent.propertyName))) {
this.startCheckChangeTask();
}
},
startCheckChangeTask: function() {
var me = this,
task = me.checkChangeTask;
if (!task) {
me.checkChangeTask = task = new Ext.util.DelayedTask(me.doCheckChangeTask, me);
}
if (!me.bindNotifyListener) {
// We continually create/destroy the listener as needed (see doCheckChangeTask)
// because we're listening to a global event, so we don't want the event
// to be triggered unless absolutely necessary. In this case,
// we only need to fix the value when we have a pending change to check.
me.bindNotifyListener =
Ext.on('beforebindnotify', me.onBeforeNotify, me, { destroyable: true });
}
task.delay(me.checkChangeBuffer);
},
doCheckChangeTask: function() {
var bindNotifyListener = this.bindNotifyListener;
if (bindNotifyListener) {
bindNotifyListener.destroy();
this.bindNotifyListener = null;
}
this.checkChange();
},
publishValue: function() {
var me = this;
if (me.rendered && !me.getErrors().length) {
me.publishState('value', me.getValue());
}
},
/**
* @private
* Called when the field's dirty state changes. Adds/removes the {@link #dirtyCls}
* on the main element.
* @param {Boolean} isDirty
*/
onDirtyChange: function(isDirty) {
var me = this;
me[isDirty ? 'addCls' : 'removeCls'](me.dirtyCls);
if (me.rendered && me.reference) {
me.publishState('dirty', isDirty);
}
},
/**
* Returns whether or not the field value is currently valid by {@link #getErrors validating}
* the {@link #processRawValue processed raw value} of the field. **Note**: {@link #disabled}
* fields are always treated as valid.
*
* @return {Boolean} True if the value is valid, else false
*/
isValid: function() {
var me = this,
disabled = me.disabled,
validate = me.forceValidation || !disabled;
return validate ? me.validateValue(me.processRawValue(me.getRawValue())) : disabled;
},
/**
* Uses {@link #getErrors} to build an array of validation errors. If any errors are found,
* they are passed to {@link #markInvalid} and false is returned, otherwise true is returned.
*
* Previously, subclasses were invited to provide an implementation of this to process
* validations - from 3.2 onwards {@link #getErrors} should be overridden instead.
*
* @param {Object} value The value to validate
* @return {Boolean} True if all validations passed, false if one or more failed
*/
validateValue: function(value) {
var me = this,
errors = me.getErrors(value),
isValid = Ext.isEmpty(errors);
if (!me.preventMark) {
if (isValid) {
me.clearInvalid();
}
else {
me.markInvalid(errors);
}
}
return isValid;
},
/**
* @method markInvalid
* @inheritdoc Ext.form.field.Field#method-markInvalid
*/
markInvalid: function(errors) {
// Save the message and fire the 'invalid' event
var me = this,
oldMsg = me.getActiveError(),
active;
me.setActiveErrors(Ext.Array.from(errors));
active = me.getActiveError();
if (oldMsg !== active) {
me.setError(active);
if (!me.ariaStaticRoles[me.ariaRole] && me.inputEl) {
me.inputEl.dom.setAttribute('aria-invalid', true);
}
}
},
/**
* Clear any invalid styles/messages for this field.
*
* **Note**: this method does not cause the Field's {@link #validate} or {@link #isValid}
* methods to return `true` if the value does not _pass_ validation. So simply clearing
* a field's errors will not necessarily allow submission of forms submitted with the
* {@link Ext.form.action.Submit#clientValidation} option set.
*/
clearInvalid: function() {
// Clear the message and fire the 'valid' event
var me = this,
hadError = me.hasActiveError();
delete me.hadErrorOnDisable;
me.unsetActiveError();
if (hadError) {
me.setError('');
if (!me.ariaStaticRoles[me.ariaRole] && me.inputEl) {
me.inputEl.dom.setAttribute('aria-invalid', false);
}
}
},
/**
* Set the current error state
* @private
* @param {String} error The error message to set
*/
setError: function(error) {
var me = this,
msgTarget = me.msgTarget,
prop;
if (me.rendered) {
if (msgTarget === 'title' || msgTarget === 'qtip') {
prop = msgTarget === 'qtip' ? 'data-errorqtip' : 'title';
me.getActionEl().dom.setAttribute(prop, error || '');
}
else {
me.updateLayout();
}
}
},
/**
* @private
* Overrides the method from the Ext.form.Labelable mixin to also add the invalidCls
* to the inputEl, as that is required for proper styling in IE with nested fields
* (due to lack of child selector)
*/
renderActiveError: function() {
var me = this,
hasError = me.hasActiveError(),
invalidCls = me.invalidCls + '-field';
if (me.inputEl) {
// Add/remove invalid class
me.inputEl[hasError ? 'addCls' : 'removeCls']([
invalidCls, invalidCls + '-' + me.ui
]);
}
me.mixins.labelable.renderActiveError.call(me);
},
doDestroy: function() {
var me = this,
task = me.checkChangeTask;
if (task) {
task.cancel();
}
Ext.destroy(me.bindNotifyListener);
me.cleanupField();
me.callParent();
},
privates: {
applyBind: function(bind, currentBindings) {
var me = this,
valueBinding = currentBindings && currentBindings.value,
bindings, newValueBind;
bindings = me.callParent([ bind, currentBindings ]);
if (bindings) {
newValueBind = bindings.value;
me.hasBindingValue = !!newValueBind;
if (newValueBind !== valueBinding && me.getInherited().modelValidation) {
me.updateValueBinding(bindings);
}
}
return bindings;
},
applyRenderSelectors: function() {
var me = this;
me.callParent();
// If the inputId config is not specified then normal childEls will pick up
// our inputEl. Otherwise we need to get it now.
if (!me.inputEl) {
me.inputEl = me.el.getById(me.getInputId());
}
},
// These 2 events trigger when setting the value programmatically in IE.
// propertychange for older browsers, textInput for IE10+. Here we
bindChangeEvents: function(active) {
var method = active ? 'resumeEvent' : 'suspendEvent',
inputEl = this.inputEl;
if (this.usesPropertychange) {
inputEl[method]('propertychange');
}
if (this.usesTextInput) {
inputEl[method]('textInput');
}
},
getActionEl: function() {
return this.inputEl || this.el;
},
getFocusEl: function() {
return this.inputEl;
},
initRenderTpl: function() {
var me = this;
if (!me.hasOwnProperty('renderTpl')) {
me.renderTpl = me.lookupTpl('labelableRenderTpl');
}
return me.callParent();
},
onBeforeNotify: function() {
// This event is fired before the scheduler fires off any bindings.
// If we happen to be in the state where we are pending a state change check,
// force it to flush here so that we have the correct state in the viewmodel before
// the bindings trigger, otherwise we may get an old value pushed into the field before
// it runs the check.
this.checkChangeTask.cancel();
this.checkChange();
},
updateValueBinding: function(bindings) {
var me = this,
newBinding = bindings.value,
fieldBinding = bindings.$fieldBinding;
if (fieldBinding) {
fieldBinding.destroy();
bindings.$fieldBinding = null;
}
if (newBinding && newBinding.bindValidationField) {
me.fieldBinding = newBinding.bindValidationField('setValidationField', me);
}
}
},
deprecated: {
"5": {
methods: {
doComponentLayout: function() {
// In IE if propertychange is one of the checkChangeEvents, we need to remove
// the listener prior to layout and re-add it after, to prevent it from firing
// needlessly for attribute and style changes applied to the inputEl.
this.bindChangeEvents(false);
this.callParent(arguments);
this.bindChangeEvents(true);
}
}
}
}
});