/** * 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. */Ext.define("Ext.form.Labelable", { extend: 'Ext.Mixin', requires: [ 'Ext.XTemplate', 'Ext.overrides.dom.Element' ], isLabelable: true, mixinConfig: { id: 'labelable', on: { beforeRender: 'beforeLabelRender', onRender: 'onLabelRender' } }, config: { childEls: [ /** * @property {Ext.dom.Element} labelEl * The label Element for this component. Only available after the component * has been rendered. */ 'labelEl', /** * @property {Ext.dom.Element} bodyEl * The div Element wrapping the component's contents. Only available after * the component has been rendered. */ 'bodyEl', /** * @property {Ext.dom.Element} errorEl * 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', 'errorWrapEl', 'ariaErrorEl', 'ariaStatusEl', 'ariaHelpEl', 'labelTextEl' ] }, /* eslint-disable indent, max-len */ /** * @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.Component#renderTpl renderTpl}, * and implement the {@link #getSubTplMarkup} method to generate the field body content. * @private */ labelableRenderTpl: [ '{beforeLabelTpl}', '<label id="{id}-labelEl" data-ref="labelEl" class="{labelCls} {labelCls}-{ui} {labelClsExtra} ', '{childElCls} {unselectableCls}" style="{labelStyle}"', '<tpl if="inputId && !skipLabelForAttribute"> for="{inputId}"</tpl>', ' {labelAttrTpl}>', '<span class="{labelInnerCls} {labelInnerCls}-{ui}" style="{labelInnerStyle}">', '{beforeLabelTextTpl}', '<span id="{id}-labelTextEl" data-ref="labelTextEl" class="{labelTextCls}">', '<tpl if="fieldLabel">{fieldLabel}', '<tpl if="labelSeparator">{labelSeparator}</tpl>', '</tpl>', '</span>', '{afterLabelTextTpl}', '</span>', '</label>', '{afterLabelTpl}', '<div id="{id}-bodyEl" data-ref="bodyEl" role="presentation"', ' class="{baseBodyCls} {baseBodyCls}-{ui}<tpl if="fieldBodyCls">', ' {fieldBodyCls} {fieldBodyCls}-{ui}</tpl> {growCls} {extraFieldBodyCls}"', '<tpl if="bodyStyle"> style="{bodyStyle}"</tpl>>', '{beforeBodyEl}', '{beforeSubTpl}', '{[values.$comp.getSubTplMarkup(values)]}', '{afterSubTpl}', '{afterBodyEl}', // ARIA elements serve different purposes: // - ariaHelpEl may contain optional hints about the field, such as // expected format. This text is static and usually does not change // once rendered. It is also optional. // - ariaStatusEl is used to convey status of the field. Validation errors // are rendered here, as well as other information that might be helpful // to Assistive Technology users exploring the app in browse mode. // - ariaErrorEl is used for announcing dynamic changes in the field state, // so that AT users receive updates while in forms mode. // // Both ariaHelpEl and ariaStatusEl are referenced by the field's input element // via aria-describedby. '<tpl if="renderAriaElements">', '<tpl if="ariaHelp">', '<span id="{id}-ariaHelpEl" data-ref="ariaHelpEl"', ' class="' + Ext.baseCSSPrefix + 'hidden-offsets">', '{ariaHelp}', '</span>', '</tpl>', '<span id="{id}-ariaStatusEl" data-ref="ariaStatusEl" aria-hidden="true"', ' class="' + Ext.baseCSSPrefix + 'hidden-offsets">', '{ariaStatus}', '</span>', '<span id="{id}-ariaErrorEl" data-ref="ariaErrorEl" aria-hidden="true" aria-live="assertive"', ' class="' + Ext.baseCSSPrefix + 'hidden-clip">', '</span>', '</tpl>', '</div>', '<tpl if="renderError">', '<div id="{id}-errorWrapEl" data-ref="errorWrapEl" class="{errorWrapCls} {errorWrapCls}-{ui}', ' {errorWrapExtraCls}" style="{errorWrapStyle}">', '<div role="presentation" id="{id}-errorEl" data-ref="errorEl" ', 'class="{errorMsgCls} {invalidMsgCls} {invalidMsgCls}-{ui}" ', 'data-anchorTarget="{tipAnchorTarget}">', '</div>', '</div>', '</tpl>', { disableFormats: true } ], /** * @cfg {String/String[]/Ext.XTemplate} activeErrorsTpl * The template used to format the Array of error messages passed to {@link #setActiveErrors} * into a single HTML string. if the {@link #msgTarget} is title, it defaults to a list * separated by new lines. Otherwise, it renders each message as an item in an unordered list. */ activeErrorsTpl: undefined, htmlActiveErrorsTpl: [ '<tpl if="errors && errors.length">', '<ul class="{listCls}">', '<tpl for="errors"><li>{.}</li></tpl>', '</ul>', '</tpl>' ], plaintextActiveErrorsTpl: [ '<tpl if="errors && errors.length">', '<tpl for="errors"><tpl if="xindex > 1">\n</tpl>{.}</tpl>', '</tpl>' ], ariaActiveErrorsTpl: [ '<tpl if="errors && errors.length">', '<tpl for="errors" between=". ">{.}</tpl>', '</tpl>' ], /* eslint-enable indent, max-len */ /** * @property {Boolean} isFieldLabelable * Flag denoting that this object is labelable as a field. Always true. */ isFieldLabelable: true, /** * @cfg {String} formItemCls * 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 * 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', /** * @private */ topLabelCls: Ext.baseCSSPrefix + 'form-item-label-top', rightLabelCls: Ext.baseCSSPrefix + 'form-item-label-right', labelInnerCls: Ext.baseCSSPrefix + 'form-item-label-inner', labelTextCls: Ext.baseCSSPrefix + 'form-item-label-text', topLabelSideErrorCls: Ext.baseCSSPrefix + 'form-item-label-top-side-error', /** * @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 * The CSS class to be applied to the error message element. */ errorMsgCls: Ext.baseCSSPrefix + 'form-error-msg', errorWrapCls: Ext.baseCSSPrefix + 'form-error-wrap', errorWrapSideCls: Ext.baseCSSPrefix + 'form-error-wrap-side', errorWrapUnderCls: Ext.baseCSSPrefix + 'form-error-wrap-under', errorWrapUnderSideLabelCls: Ext.baseCSSPrefix + 'form-error-wrap-under-side-label', /** * @cfg {String} baseBodyCls * The CSS class to be applied to the body content element. */ baseBodyCls: Ext.baseCSSPrefix + 'form-item-body', invalidIconCls: Ext.baseCSSPrefix + 'form-invalid-icon', invalidUnderCls: Ext.baseCSSPrefix + 'form-invalid-under', noLabelCls: Ext.baseCSSPrefix + 'form-item-no-label', /** * @cfg {String} fieldBodyCls * An extra CSS class to be applied to the body content element in addition to * {@link #baseBodyCls}. */ fieldBodyCls: '', extraFieldBodyCls: '', /** * @cfg {String} invalidCls * 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} and {@link #labelWidth} configs. */ fieldLabel: undefined, /** * @cfg {String} labelAlign * Controls the position and alignment of the {@link #fieldLabel}. Valid values are: * * - "left" (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. * - "top" - The label is positioned above the field. * - "right" - 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. */ labelAlign: 'left', /** * @cfg {Number} labelWidth * The width of the {@link #fieldLabel} in pixels. Only applicable if {@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 field body. * This defaults to `5` for compatibility with Ext JS 4, however, as of Ext JS 5 * the space between the label and the body can optionally be determined by the theme * using the {@link #$form-label-horizontal-spacing} (for side-aligned labels) and * {@link #$form-label-vertical-spacing} (for top-aligned labels) SASS variables. * In order for the stylesheet values as to take effect, you must use a labelPad value * of `null`. */ labelPad: 5, /** * @cfg {String} labelSeparator * Character(s) to be inserted at the end of the {@link #fieldLabel label text}. * * Set to empty string to hide the separator completely. * @locale */ 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 * When set to true, the label element ({@link #fieldLabel} and {@link #labelSeparator}) * will be automatically hidden if the {@link #fieldLabel} is empty. Setting this to false * 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. * * If you wish to unconditionall hide the label even if a non-empty fieldLabel is configured, * then set the {@link #hideLabel} config to true. */ hideEmptyLabel: true, /** * @cfg {Boolean} preventMark * true to disable displaying any {@link #setActiveError error message} set on this object. */ preventMark: false, /** * @cfg {Boolean} autoFitErrors * Whether to adjust the component's body width to make room for 'side' * {@link #msgTarget error messages}. */ autoFitErrors: true, /** * @cfg {String} msgTarget * The location where the error message text should display. Must be one of the following * values: * * - `qtip` Display a quick tip containing the message when the user hovers over the field. * This is the default. * * **{@link Ext.tip.QuickTipManager#init} must have been called for this setting to work.** * * - `title` Display the message in a default browser title attribute popup. * - `under` Add a block div beneath the field containing the error message. * - `side` Add an error icon to the right of the field, displaying the message in a popup * on hover. * - `none` Don't display any error message. This might be useful if you are implementing * custom error display. * - `[element id]` Add the error message directly to the innerHTML of the specified element. */ msgTarget: 'qtip', /** * @private * Map for msg target lookup, if target is not in this map it is assumed * to be an element id */ msgTargets: { qtip: 1, title: 1, under: 1, side: 1, none: 1 }, /** * @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. */ /** * @private * Tells the layout system that the height can be measured immediately because the width * does not need setting. */ noWrap: true, /** * @cfg {String} [ariaHelp] Optional text description for this object. This text will be * announced to Assistive Technology users when the object is focused. * @locale */ ariaHelp: undefined, /** * @cfg {String} ariaErrorText Localized announcement text for validation errors. This text * will be used by Assistive Technologies such as screen readers to alert the users when * field validation fails. * * This config is used with {@link Ext.String#format}. '{0}' will be replaced with the actual * error message(s), '{1}' will be replaced with field label. * @locale */ ariaErrorText: 'Input error. {0}.', labelableInsertions: [ /** * @cfg {String/Array/Ext.XTemplate} beforeBodyEl * An optional string or `XTemplate` configuration to insert in the field markup * at the beginning of the input containing element. If an `XTemplate` is used, * the component's {@link Ext.Component#renderData render data} serves as the context. */ 'beforeBodyEl', /** * @cfg {String/Array/Ext.XTemplate} afterBodyEl * An optional string or `XTemplate` configuration to insert in the field markup * at the end of the input containing element. If an `XTemplate` is used, the component's * {@link Ext.Component#renderData render data} serves as the context. */ 'afterBodyEl', /** * @cfg {String/Array/Ext.XTemplate} beforeLabelTpl * An optional string or `XTemplate` configuration to insert in the field markup * before the label element. If an `XTemplate` is used, the component's * {@link Ext.Component#renderData render data} serves as the context. */ 'beforeLabelTpl', /** * @cfg {String/Array/Ext.XTemplate} afterLabelTpl * An optional string or `XTemplate` configuration to insert in the field markup * after the label element. If an `XTemplate` is used, the component's * {@link Ext.Component#renderData render data} serves as the context. */ 'afterLabelTpl', /** * @cfg {String/Array/Ext.XTemplate} beforeSubTpl * An optional string or `XTemplate` configuration to insert in the field markup * before the {@link #getSubTplMarkup subTpl markup}. If an `XTemplate` is used, the * component's {@link Ext.Component#renderData render data} serves as the context. */ 'beforeSubTpl', /** * @cfg {String/Array/Ext.XTemplate} afterSubTpl * An optional string or `XTemplate` configuration to insert in the field markup * after the {@link #getSubTplMarkup subTpl markup}. If an `XTemplate` is used, the * component's {@link Ext.Component#renderData render data} serves as the context. */ 'afterSubTpl', /** * @cfg {String/Array/Ext.XTemplate} beforeLabelTextTpl * An optional string or `XTemplate` configuration to insert in the field markup * before the label text. If an `XTemplate` is used, the component's * {@link Ext.Component#renderData render data} serves as the context. */ 'beforeLabelTextTpl', /** * @cfg {String/Array/Ext.XTemplate} afterLabelTextTpl * An optional string or `XTemplate` configuration to insert in the field markup * after the label text. If an `XTemplate` is used, the component's * {@link Ext.Component#renderData render data} serves as the context. */ 'afterLabelTextTpl', /** * @cfg {String/Array/Ext.XTemplate} labelAttrTpl * An optional string or `XTemplate` configuration to insert in the field markup * inside the label element (as attributes). If an `XTemplate` is used, the component's * {@link Ext.Component#renderData render data} serves as the context. */ 'labelAttrTpl' ], statics: { /** * Use a custom QuickTip instance separate from the main QuickTips singleton, so that we * can give it a custom frame style. Responds to errorqtip rather than the qtip property. * @static * @private */ initTip: function() { var tip = this.tip, cfg, copy; if (tip) { return; } cfg = { id: 'ext-form-error-tip', //<debug> // tell the spec runner to ignore this element when checking if the dom is clean sticky: true, //</debug> ui: 'form-invalid' }; // On Touch devices, tapping the target shows the qtip if (Ext.supports.Touch) { cfg.dismissDelay = 0; cfg.anchor = 'top'; cfg.showDelay = 0; cfg.showOnTap = true; cfg.listeners = { beforeshow: function() { this.minWidth = Ext.fly(this.activeTarget.el).getWidth(); } }; } tip = this.tip = Ext.create('Ext.tip.QuickTip', cfg); copy = Ext.apply({}, tip.tagConfig); copy.attribute = 'errorqtip'; tip.setTagConfig(copy); }, /** * Destroy the error tip instance. * @static */ destroyTip: function() { this.tip = Ext.destroy(this.tip); } }, /** * @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 */ /** * Performs initialization of this mixin. Component classes using this mixin should call * this method during their own initialization. */ initLabelable: function() { var me = this, padding = me.padding; // This Component is rendered as a table. Padding doesn't work on tables // Before padding can be applied to the encapsulating table element, copy the padding into // an extraMargins property which is to be added to all computed margins post render :( if (padding) { me.padding = undefined; me.extraMargins = Ext.Element.parseBox(padding); } // IE8 hack for https://sencha.jira.com/browse/EXTJS-17536. // Need to force a relayout of the display:table form item. // TODO: Remove when IE8 retires. if (Ext.isIE8) { me.restoreDisplay = Ext.Function.createDelayed(me.doRestoreDisplay, 0, me); } if (!me.activeErrorsTpl) { if (me.msgTarget === 'title') { me.activeErrorsTpl = me.plaintextActiveErrorsTpl; } else { me.activeErrorsTpl = me.htmlActiveErrorsTpl; } } me.addCls([me.formItemCls, me.formItemCls + '-' + me.ui]); // Prevent first render of active error, at Field render time from signalling a change // from undefined to " me.lastActiveError = ''; // bubbleEvents on the prototype of a mixin won't work, so call enableBubble me.enableBubble('errorchange'); }, /** * Returns the trimmed label by slicing off the label separator character. Can be overridden. * @return {String} The trimmed field label, or empty string if not defined */ trimLabelSeparator: function() { var me = this, separator = me.labelSeparator, label = me.fieldLabel || '', lastChar = label.substr(label.length - 1); // if the last char is the same as the label separator then slice it off // otherwise just return label value return lastChar === separator ? label.slice(0, -1) : label; }, /** * Returns the label for the field. Defaults to simply returning the {@link #fieldLabel} config. * Can be overridden to provide a custom generated label. * @template * @return {String} The configured field label, or empty string if not defined */ getFieldLabel: function() { return this.trimLabelSeparator(); }, /** * Set the label of this field. * @param {String} label The new label. The {@link #labelSeparator} will be automatically * appended to the label string. */ setFieldLabel: function(label) { var me = this, separator = me.labelSeparator, errorWrapEl = me.errorWrapEl, sideLabel = (me.labelAlign !== 'top'), noLabelCls = me.noLabelCls, errorWrapUnderSideLabelCls = me.errorWrapUnderSideLabelCls; label = label || ''; me.fieldLabel = label; if (me.rendered) { if (Ext.isEmpty(label) && me.hideEmptyLabel) { me.addCls(noLabelCls); if (sideLabel && errorWrapEl) { errorWrapEl.removeCls(errorWrapUnderSideLabelCls); } } else { if (separator) { label = me.trimLabelSeparator() + separator; } me.labelTextEl.dom.innerHTML = label; me.removeCls(noLabelCls); if (sideLabel && errorWrapEl) { errorWrapEl.addCls(errorWrapUnderSideLabelCls); } } me.updateLayout(); } }, setHideLabel: function(hideLabel) { var me = this; if (hideLabel !== me.hideLabel) { me.hideLabel = hideLabel; if (me.rendered) { me[hideLabel ? 'addCls' : 'removeCls'](me.noLabelCls); me.updateLayout(); } } }, setHideEmptyLabel: function(hideEmptyLabel) { var me = this, hide; if (hideEmptyLabel !== me.hideEmptyLabel) { me.hideEmptyLabel = hideEmptyLabel; if (me.rendered && !me.hideLabel) { hide = hideEmptyLabel && !me.getFieldLabel(); me[hide ? 'addCls' : 'removeCls'](me.noLabelCls); me.updateLayout(); } } }, getInsertionRenderData: function(data, names) { var i = names.length, name, value; while (i--) { name = names[i]; value = this[name]; if (value) { if (typeof value !== 'string') { if (!value.isTemplate) { value = Ext.XTemplate.getTpl(this, name); } value = value.apply(data); } } data[name] = value || ''; } return data; }, /** * Generates the arguments for the field decorations {@link #labelableRenderTpl * rendering template}. * @param {Object} data optional object to use as the base data object. If provided, * this method will add properties to the base object instead of creating a new one. * @return {Object} The template arguments * @protected */ getLabelableRenderData: function() { var me = this, labelAlign = me.labelAlign, topLabel = (labelAlign === 'top'), rightLabel = (labelAlign === 'right'), sideError = (me.msgTarget === 'side'), underError = (me.msgTarget === 'under'), errorMsgCls = me.errorMsgCls, labelPad = me.labelPad, labelWidth = me.labelWidth, labelClsExtra = me.labelClsExtra || '', errorWrapExtraCls = sideError ? me.errorWrapSideCls : me.errorWrapUnderCls, labelStyle = '', labelInnerStyle = '', labelVisible = me.hasVisibleLabel(), autoFitErrors = me.autoFitErrors, defaultBodyWidth = me.defaultBodyWidth, bodyStyle, data; if (topLabel) { labelClsExtra += ' ' + me.topLabelCls; if (labelPad) { labelInnerStyle = 'padding-bottom:' + labelPad + 'px;'; } if (sideError && !autoFitErrors) { labelClsExtra += ' ' + me.topLabelSideErrorCls; } } else { if (rightLabel) { labelClsExtra += ' ' + me.rightLabelCls; } if (labelPad) { labelStyle += me.getHorizontalPaddingStyle() + labelPad + 'px;'; } labelStyle += 'width:' + (labelWidth + (labelPad ? labelPad : 0)) + 'px;'; // inner label needs width as well so that setting width on the outside // that is smaller than the natural width, will be ensured to take width // away from the body, and not the label. labelInnerStyle = 'width:' + labelWidth + 'px'; } if (labelVisible) { if (!topLabel && underError) { errorWrapExtraCls += ' ' + me.errorWrapUnderSideLabelCls; } } if (defaultBodyWidth) { // This is here to support textfield's deprecated "size" config bodyStyle = 'min-width:' + defaultBodyWidth + 'px;max-width:' + defaultBodyWidth + 'px;'; } data = { id: me.id, inputId: me.getInputId(), labelCls: me.labelCls, labelClsExtra: labelClsExtra, labelStyle: labelStyle + (me.labelStyle || ''), labelInnerStyle: labelInnerStyle, labelInnerCls: me.labelInnerCls, labelTextCls: me.labelTextCls, skipLabelForAttribute: !!me.skipLabelForAttribute, unselectableCls: Ext.Element.unselectableCls, bodyStyle: bodyStyle, baseBodyCls: me.baseBodyCls, fieldBodyCls: me.fieldBodyCls, extraFieldBodyCls: me.extraFieldBodyCls, errorWrapCls: me.errorWrapCls, errorWrapExtraCls: errorWrapExtraCls, renderError: sideError || underError, invalidMsgCls: sideError ? me.invalidIconCls : underError ? me.invalidUnderCls : '', errorMsgCls: errorMsgCls, growCls: me.grow ? me.growCls : '', tipAnchorTarget: me.id + '-inputEl', errorWrapStyle: (sideError && !autoFitErrors) ? 'visibility:hidden' : 'display:none', fieldLabel: me.getFieldLabel(), labelSeparator: me.labelSeparator, renderAriaElements: !!me.renderAriaElements, ariaStatus: '' }; if (me.ariaHelp) { data.ariaHelp = Ext.String.htmlEncode(me.ariaHelp); } me.getInsertionRenderData(data, me.labelableInsertions); return data; }, // hook for rtl getHorizontalPaddingStyle: function() { return 'padding-right:'; }, beforeLabelRender: function() { var me = this; me.setFieldDefaults(me.getInherited().fieldDefaults); if (me.ownerLayout) { me.addCls(Ext.baseCSSPrefix + me.ownerLayout.type + '-form-item'); } if (!me.hasVisibleLabel()) { me.addCls(me.noLabelCls); } }, onLabelRender: function() { var me = this, style = {}, ExtElement = Ext.Element, errorWrapEl = me.errorWrapEl, margins, side; if (errorWrapEl) { errorWrapEl.setVisibilityMode( (me.msgTarget === 'side' && !me.autoFitErrors) ? ExtElement.VISIBILITY : ExtElement.DISPLAY ); } if (me.extraMargins) { margins = me.el.getMargin(); for (side in margins) { if (margins.hasOwnProperty(side)) { style['margin-' + side] = (margins[side] + me.extraMargins[side]) + 'px'; } } me.el.setStyle(style); } }, /** * Checks if the field has a visible label * @return {Boolean} True if the field has a visible label */ hasVisibleLabel: function() { if (this.hideLabel) { return false; } return !(this.hideEmptyLabel && !this.getFieldLabel()); }, /** * 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 * @protected */ 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 * input 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.setActiveErrors(msg); }, /** * 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) { var me = this, errorWrapEl = me.errorWrapEl, msgTarget = me.msgTarget, isSide = msgTarget === 'side', isQtip = msgTarget === 'qtip', ariaErrorEl = me.ariaErrorEl, actionEl, activeError, tpl, targetEl, ariaTpl, errStr, errText; errors = Ext.Array.from(errors); tpl = me.lookupTpl('activeErrorsTpl'); me.activeErrors = errors; activeError = me.activeError = tpl.apply({ fieldLabel: me.fieldLabel, errors: errors, listCls: Ext.baseCSSPrefix + 'list-plain' }); me.renderActiveError(); if (me.rendered) { actionEl = me.getActionEl(); if (isSide) { me.errorEl.dom.setAttribute('data-errorqtip', activeError); } else if (isQtip) { actionEl.dom.setAttribute('data-errorqtip', activeError); } else if (msgTarget === 'title') { actionEl.dom.setAttribute('title', activeError); } // If msgTarget is title, setting an alert is redundant for ARIA purposes if (msgTarget !== 'title' && ariaErrorEl) { ariaTpl = me.lookupTpl('ariaActiveErrorsTpl'); errStr = ariaTpl.apply({ errors: errors }); // Setting innerHTML on aria-live element will replace inner text node, // and the browser will fire DOM change event even if the text is the same. // We don't want the announcement to repeat if the text hasn't changed. errText = Ext.String.formatEncode(me.ariaErrorText, errStr, me.fieldLabel); if (ariaErrorEl.dom.innerHTML !== errText) { ariaErrorEl.dom.innerHTML = errText; } // ariaStatusEl is not aria-live so it's OK to change it every time. // Contents will be announced only upon focusing the field. me.ariaStatusEl.dom.innerHTML = Ext.String.htmlEncode(errStr); } if (isSide || isQtip) { Ext.form.Labelable.initTip(); } if (!me.msgTargets[msgTarget]) { targetEl = Ext.get(msgTarget); if (targetEl) { targetEl.dom.innerHTML = activeError; } } } if (errorWrapEl) { errorWrapEl.setVisible(errors.length > 0); if (isSide && me.autoFitErrors) { me.labelEl.addCls(me.topLabelSideErrorCls); } me.updateLayout(); } }, /** * 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() { var me = this, errorWrapEl = me.errorWrapEl, ariaErrorEl = me.ariaErrorEl, msgTarget = me.msgTarget, restoreDisplay = me.restoreDisplay, actionEl, targetEl; if (me.hasActiveError()) { delete me.activeError; delete me.activeErrors; me.renderActiveError(); if (me.rendered) { actionEl = me.getActionEl(); if (msgTarget === 'qtip') { actionEl.dom.removeAttribute('data-errorqtip'); } else if (msgTarget === 'title') { actionEl.dom.removeAttribute('title'); } if (msgTarget !== 'title' && ariaErrorEl) { ariaErrorEl.dom.innerHTML = me.ariaStatusEl.dom.innerHTML = ''; } if (!me.msgTargets[msgTarget]) { targetEl = Ext.get(msgTarget); if (targetEl) { targetEl.dom.innerHTML = ''; } } if (errorWrapEl) { errorWrapEl.hide(); if (msgTarget === 'side' && me.autoFitErrors) { me.labelEl.removeCls(me.topLabelSideErrorCls); } me.updateLayout(); // IE8 hack for https://sencha.jira.com/browse/EXTJS-17536. // Need to force a relayout of the display:table form item. // TODO: Remove when IE8 retires. if (restoreDisplay) { me.el.dom.style.display = 'block'; me.restoreDisplay(); } } } } }, doRestoreDisplay: function() { // IE8 hack for https://sencha.jira.com/browse/EXTJS-17536. // Need to force a relayout of the display:table form item. // TODO: Remove this method when IE8 retires. var el = this.el; if (el && el.dom) { el.dom.style.display = ''; } }, /** * @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.lastActiveError = activeError; me.fireEvent('errorchange', me, activeError); } if (me.rendered && !me.destroyed && !me.preventMark) { me.toggleInvalidCls(hasError); // Update the errorEl (There will only be one if msgTarget is 'side' or 'under') // with the error message text if (me.errorEl) { me.errorEl.dom.innerHTML = activeError; } } }, /** * @private * Add/remove invalid class(es) * @param {Boolean} hasError */ toggleInvalidCls: function(hasError) { this.el[hasError ? 'addCls' : 'removeCls'](this.invalidCls); }, /** * 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 key; for (key in defaults) { if (!this.hasOwnProperty(key)) { this[key] = defaults[key]; } } }}, function() { if (Ext.supports.Touch) { this.prototype.msgTarget = 'side'; }});