/**
* A mixin which allows a component to be configured and decorated with a label and/or error message as is
* common for form fields. This is used by e.g. Ext.form.field.Base and Ext.form.FieldContainer
* to let them be managed by the Field layout.
*
* NOTE: This mixin is mainly for internal library use and most users should not need to use it directly. It
* is more likely you will want to use one of the component classes that import this mixin, such as
* Ext.form.field.Base or Ext.form.FieldContainer.
*
* Use of this mixin does not make a component a field in the logical sense, meaning it does not provide any
* logic or state related to values or validation; that is handled by the related Ext.form.field.Field
* mixin. These two mixins may be used separately (for example Ext.form.FieldContainer is Labelable but not a
* Field), or in combination (for example Ext.form.field.Base implements both and has logic for connecting the
* two.)
*
* Component classes which use this mixin should use the Field layout
* or a derivation thereof to properly size and position the label and message according to the component config.
* They must also call the {@link #initLabelable} method during component initialization to ensure the mixin gets
* set up correctly.
*
* @docauthor Jason Johnston <jason@sencha.com>
*/
Ext.define("Ext.form.Labelable", {
requires: ['Ext.XTemplate'],
/**
* @cfg {String/String[]/Ext.XTemplate} labelableRenderTpl
* The rendering template for the field decorations. Component classes using this mixin should include
* logic to use this as their {@link Ext.AbstractComponent#renderTpl renderTpl}, and implement the
* {@link #getSubTplMarkup} method to generate the field body content.
*/
labelableRenderTpl: [
'<tpl if="!hideLabel && !(!fieldLabel && hideEmptyLabel)">',
'<label id="{id}-labelEl"<tpl if="inputId"> for="{inputId}"</tpl> class="{labelCls}"',
'<tpl if="labelStyle"> style="{labelStyle}"</tpl>>',
'<tpl if="fieldLabel">{fieldLabel}{labelSeparator}</tpl>',
'</label>',
'</tpl>',
'<div class="{baseBodyCls} {fieldBodyCls}" id="{id}-bodyEl" role="presentation">{subTplMarkup}</div>',
'<div id="{id}-errorEl" class="{errorMsgCls}" style="display:none"></div>',
'<div class="{clearCls}" role="presentation"><!-- --></div>',
{
compiled: true,
disableFormats: true
}
],
/**
* @cfg {Ext.XTemplate} activeErrorsTpl
* The template used to format the Array of error messages passed to {@link #setActiveErrors}
* into a single HTML string. By default this renders each message as an item in an unordered list.
*/
activeErrorsTpl: [
'<tpl if="errors && errors.length">',
'<ul><tpl for="errors"><li<tpl if="xindex == xcount"> class="last"</tpl>>{.}</li></tpl></ul>',
'</tpl>'
],
/**
* @property isFieldLabelable
* @type Boolean
* Flag denoting that this object is labelable as a field. Always true.
*/
isFieldLabelable: true,
/**
* @cfg {String} [formItemCls='x-form-item']
* A CSS class to be applied to the outermost element to denote that it is participating in the form
* field layout.
*/
formItemCls: Ext.baseCSSPrefix + 'form-item',
/**
* @cfg {String} [labelCls='x-form-item-label']
* The CSS class to be applied to the label element.
* This (single) CSS class is used to formulate the renderSelector and drives the field
* layout where it is concatenated with a hyphen ('-') and {@link #labelAlign}. To add
* additional classes, use {@link #labelClsExtra}.
*/
labelCls: Ext.baseCSSPrefix + 'form-item-label',
* @cfg {String} labelClsExtra
* An optional string of one or more additional CSS classes to add to the label element.
* Defaults to empty.
*/
/**
* @cfg {String} [errorMsgCls='x-form-error-msg']
* The CSS class to be applied to the error message element.
*/
errorMsgCls: Ext.baseCSSPrefix + 'form-error-msg',
/**
* @cfg {String} [baseBodyCls='x-form-item-body']
* The CSS class to be applied to the body content element.
*/
baseBodyCls: Ext.baseCSSPrefix + 'form-item-body',
/**
* @cfg {String} fieldBodyCls
* An extra CSS class to be applied to the body content element in addition to {@link #fieldBodyCls}.
*/
fieldBodyCls: '',
/**
* @cfg {String} [clearCls='x-clear']
* The CSS class to be applied to the special clearing div rendered directly after the field
* contents wrapper to provide field clearing.
*/
clearCls: Ext.baseCSSPrefix + 'clear',
/**
* @cfg {String} [invalidCls='x-form-invalid']
* The CSS class to use when marking the component invalid.
*/
invalidCls : Ext.baseCSSPrefix + 'form-invalid',
/**
* @cfg {String} fieldLabel
* The label for the field. It gets appended with the {@link #labelSeparator}, and its position
* and sizing is determined by the {@link #labelAlign}, {@link #labelWidth}, and {@link #labelPad}
* configs.
*/
fieldLabel: undefined,
/**
* @cfg {String} labelAlign
* <p>Controls the position and alignment of the {@link #fieldLabel}. Valid values are:</p>
* <ul>
* <li><tt>"left"</tt> (the default) - The label is positioned to the left of the field, with its text
* aligned to the left. Its width is determined by the {@link #labelWidth} config.</li>
* <li><tt>"top"</tt> - The label is positioned above the field.</li>
* <li><tt>"right"</tt> - The label is positioned to the left of the field, with its text aligned
* to the right. Its width is determined by the {@link #labelWidth} config.</li>
* </ul>
*/
labelAlign : 'left',
/**
* @cfg {Number} labelWidth
* The width of the {@link #fieldLabel} in pixels. Only applicable if the {@link #labelAlign} is set
* to "left" or "right".
*/
labelWidth: 100,
/**
* @cfg {Number} labelPad
* The amount of space in pixels between the {@link #fieldLabel} and the input field.
*/
labelPad : 5,
/**
* @cfg {String} labelSeparator
* Character(s) to be inserted at the end of the {@link #fieldLabel label text}.
*/
labelSeparator : ':',
/**
* @cfg {String} labelStyle
* A CSS style specification string to apply directly to this field's label.
*/
/**
* @cfg {Boolean} hideLabel
* Set to true to completely hide the label element ({@link #fieldLabel} and {@link #labelSeparator}).
* Also see {@link #hideEmptyLabel}, which controls whether space will be reserved for an empty fieldLabel.
*/
hideLabel: false,
/**
* @cfg {Boolean} hideEmptyLabel
* <p>When set to <tt>true</tt>, the label element ({@link #fieldLabel} and {@link #labelSeparator}) will be
* automatically hidden if the {@link #fieldLabel} is empty. Setting this to <tt>false</tt> will cause the empty
* label element to be rendered and space to be reserved for it; this is useful if you want a field without a label
* to line up with other labeled fields in the same form.</p>
* <p>If you wish to unconditionall hide the label even if a non-empty fieldLabel is configured, then set
* the {@link #hideLabel} config to <tt>true</tt>.</p>
*/
hideEmptyLabel: true,
/**
* @cfg {Boolean} preventMark
* <tt>true</tt> to disable displaying any {@link #setActiveError error message} set on this object.
*/
preventMark: false,
/**
* @cfg {Boolean} autoFitErrors
* Whether to adjust the component's body area to make room for 'side' or 'under'
* {@link #msgTarget error messages}.
*/
autoFitErrors: true,
/**
* @cfg {String} msgTarget <p>The location where the error message text should display.
* Must be one of the following values:</p>
* <div class="mdetail-params"><ul>
* <li><code>qtip</code> Display a quick tip containing the message when the user hovers over the field. This is the default.
* <div class="subdesc"><b>{@link Ext.tip.QuickTipManager#init Ext.tip.QuickTipManager.init} must have been called for this setting to work.</b></div></li>
* <li><code>title</code> Display the message in a default browser title attribute popup.</li>
* <li><code>under</code> Add a block div beneath the field containing the error message.</li>
* <li><code>side</code> Add an error icon to the right of the field, displaying the message in a popup on hover.</li>
* <li><code>none</code> Don't display any error message. This might be useful if you are implementing custom error display.</li>
* <li><code>[element id]</code> Add the error message directly to the innerHTML of the specified element.</li>
* </ul></div>
*/
msgTarget: 'qtip',
/**
* @cfg {String} activeError
* If specified, then the component will be displayed with this value as its active error when
* first rendered. Use {@link #setActiveError} or {@link #unsetActiveError} to
* change it after component creation.
*/
/**
* Performs initialization of this mixin. Component classes using this mixin should call this method
* during their own initialization.
*/
initLabelable: function() {
this.addCls(this.formItemCls);
this.addEvents(
/**
* @event errorchange
* Fires when the active error message is changed via {@link #setActiveError}.
* @param {Ext.form.Labelable} this
* @param {String} error The active error message
*/
'errorchange'
);
},
/**
* Returns the label for the field. Defaults to simply returning the {@link #fieldLabel} config. Can be
* overridden to provide
* @return {String} The configured field label, or empty string if not defined
*/
getFieldLabel: function() {
return this.fieldLabel || '';
},
/**
* @protected
* Generates the arguments for the field decorations {@link #labelableRenderTpl rendering template}.
* @return {Object} The template arguments
*/
getLabelableRenderData: function() {
var me = this,
labelAlign = me.labelAlign,
labelCls = me.labelCls,
labelClsExtra = me.labelClsExtra,
labelPad = me.labelPad,
labelStyle;
// Calculate label styles up front rather than in the Field layout for speed; this
// is safe because label alignment/width/pad are not expected to change.
if (labelAlign === 'top') {
labelStyle = 'margin-bottom:' + labelPad + 'px;';
} else {
labelStyle = 'margin-right:' + labelPad + 'px;';
// Add the width for border-box browsers; will be set by the Field layout for content-box
if (Ext.isBorderBox) {
labelStyle += 'width:' + me.labelWidth + 'px;';
}
}
return Ext.copyTo(
{
inputId: me.getInputId(),
fieldLabel: me.getFieldLabel(),
labelCls: labelClsExtra ? labelCls + ' ' + labelClsExtra : labelCls,
labelStyle: labelStyle + (me.labelStyle || ''),
subTplMarkup: me.getSubTplMarkup()
},
me,
'hideLabel,hideEmptyLabel,fieldBodyCls,baseBodyCls,errorMsgCls,clearCls,labelSeparator',
true
);
},
onLabelableRender: function () {
this.addChildEls(
/**
* @property labelEl
* @type Ext.Element
* The label Element for this component. Only available after the component has been rendered.
*/
'labelEl',
/**
* @property bodyEl
* @type Ext.Element
* The div Element wrapping the component's contents. Only available after the component has been rendered.
*/
'bodyEl',
/**
* @property errorEl
* @type Ext.Element
* The div Element that will contain the component's error message(s). Note that depending on the
* configured {@link #msgTarget}, this element may be hidden in favor of some other form of
* presentation, but will always be present in the DOM for use by assistive technologies.
*/
'errorEl'
);
},
/**
* @protected
* Gets the markup to be inserted into the outer template's bodyEl. Defaults to empty string, should
* be implemented by classes including this mixin as needed.
* @return {String} The markup to be inserted
*/
getSubTplMarkup: function() {
return '';
},
/**
* Get the input id, if any, for this component. This is used as the "for" attribute on the label element.
* Implementing subclasses may also use this as e.g. the id for their own <tt>input</tt> element.
* @return {String} The input id
*/
getInputId: function() {
return '';
},
/**
* Gets the active error message for this component, if any. This does not trigger
* validation on its own, it merely returns any message that the component may already hold.
* @return {String} The active error message on the component; if there is no error, an empty string is returned.
*/
getActiveError : function() {
return this.activeError || '';
},
/**
* Tells whether the field currently has an active error message. This does not trigger
* validation on its own, it merely looks for any message that the component may already hold.
* @return {Boolean}
*/
hasActiveError: function() {
return !!this.getActiveError();
},
/**
* Sets the active error message to the given string. This replaces the entire error message
* contents with the given string. Also see {@link #setActiveErrors} which accepts an Array of
* messages and formats them according to the {@link #activeErrorsTpl}.
*
* Note that this only updates the error message element's text and attributes, you'll have
* to call doComponentLayout to actually update the field's layout to match. If the field extends
* {@link Ext.form.field.Base} you should call {@link Ext.form.field.Base#markInvalid markInvalid} instead.
*
* @param {String} msg The error message
*/
setActiveError: function(msg) {
this.activeError = msg;
this.activeErrors = [msg];
this.renderActiveError();
},
/**
* Gets an Array of any active error messages currently applied to the field. This does not trigger
* validation on its own, it merely returns any messages that the component may already hold.
* @return {String[]} The active error messages on the component; if there are no errors, an empty Array is returned.
*/
getActiveErrors: function() {
return this.activeErrors || [];
},
/**
* Set the active error message to an Array of error messages. The messages are formatted into
* a single message string using the {@link #activeErrorsTpl}. Also see {@link #setActiveError}
* which allows setting the entire error contents with a single string.
*
* Note that this only updates the error message element's text and attributes, you'll have
* to call doComponentLayout to actually update the field's layout to match. If the field extends
* {@link Ext.form.field.Base} you should call {@link Ext.form.field.Base#markInvalid markInvalid} instead.
*
* @param {String[]} errors The error messages
*/
setActiveErrors: function(errors) {
this.activeErrors = errors;
this.activeError = this.getTpl('activeErrorsTpl').apply({errors: errors});
this.renderActiveError();
},
/**
* Clears the active error message(s).
*
* Note that this only clears the error message element's text and attributes, you'll have
* to call doComponentLayout to actually update the field's layout to match. If the field extends
* {@link Ext.form.field.Base} you should call {@link Ext.form.field.Base#clearInvalid clearInvalid} instead.
*/
unsetActiveError: function() {
delete this.activeError;
delete this.activeErrors;
this.renderActiveError();
},
/**
* @private
* Updates the rendered DOM to match the current activeError. This only updates the content and
* attributes, you'll have to call doComponentLayout to actually update the display.
*/
renderActiveError: function() {
var me = this,
activeError = me.getActiveError(),
hasError = !!activeError;
if (activeError !== me.lastActiveError) {
me.fireEvent('errorchange', me, activeError);
me.lastActiveError = activeError;
}
if (me.rendered && !me.isDestroyed && !me.preventMark) {
// Add/remove invalid class
me.el[hasError ? 'addCls' : 'removeCls'](me.invalidCls);
// Update the aria-invalid attribute
me.getActionEl().dom.setAttribute('aria-invalid', hasError);
// Update the errorEl with the error message text
me.errorEl.dom.innerHTML = activeError;
}
},
/**
* Applies a set of default configuration values to this Labelable instance. For each of the
* properties in the given object, check if this component hasOwnProperty that config; if not
* then it's inheriting a default value from its prototype and we should apply the default value.
* @param {Object} defaults The defaults to apply to the object.
*/
setFieldDefaults: function(defaults) {
var me = this;
Ext.iterate(defaults, function(key, val) {
if (!me.hasOwnProperty(key)) {
me[key] = val;
}
});
},
/**
* @protected Calculate and return the natural width of the bodyEl. Override to provide custom logic.
* Note for implementors: if at all possible this method should be overridden with a custom implementation
* that can avoid anything that would cause the browser to reflow, e.g. querying offsetWidth.
*/
getBodyNaturalWidth: function() {
return this.bodyEl.getWidth();
}
});