]> git.proxmox.com Git - extjs.git/blob - extjs/classic/classic/src/form/Labelable.js
add extjs 6.0.1 sources
[extjs.git] / extjs / classic / classic / src / form / Labelable.js
1 /**
2 * A mixin which allows a component to be configured and decorated with a label and/or error message as is
3 * common for form fields. This is used by e.g. Ext.form.field.Base and Ext.form.FieldContainer
4 * to let them be managed by the Field layout.
5 *
6 * NOTE: This mixin is mainly for internal library use and most users should not need to use it directly. It
7 * is more likely you will want to use one of the component classes that import this mixin, such as
8 * Ext.form.field.Base or Ext.form.FieldContainer.
9 *
10 * Use of this mixin does not make a component a field in the logical sense, meaning it does not provide any
11 * logic or state related to values or validation; that is handled by the related Ext.form.field.Field
12 * mixin. These two mixins may be used separately (for example Ext.form.FieldContainer is Labelable but not a
13 * Field), or in combination (for example Ext.form.field.Base implements both and has logic for connecting the
14 * two.)
15 *
16 * Component classes which use this mixin should use the Field layout
17 * or a derivation thereof to properly size and position the label and message according to the component config.
18 * They must also call the {@link #initLabelable} method during component initialization to ensure the mixin gets
19 * set up correctly.
20 */
21 Ext.define("Ext.form.Labelable", {
22 extend: 'Ext.Mixin',
23
24 requires: [
25 'Ext.XTemplate',
26 'Ext.overrides.dom.Element'
27 ],
28
29 isLabelable: true,
30
31 mixinConfig: {
32 id: 'labelable',
33
34 on: {
35 beforeRender: 'beforeLabelRender',
36 onRender: 'onLabelRender'
37 }
38 },
39
40 config: {
41 childEls: [
42 /**
43 * @property {Ext.dom.Element} labelEl
44 * The label Element for this component. Only available after the component has been rendered.
45 */
46 'labelEl',
47
48 /**
49 * @property {Ext.dom.Element} bodyEl
50 * The div Element wrapping the component's contents. Only available after the component has been rendered.
51 */
52 'bodyEl',
53
54 /**
55 * @property {Ext.dom.Element} errorEl
56 * The div Element that will contain the component's error message(s). Note that depending on the configured
57 * {@link #msgTarget}, this element may be hidden in favor of some other form of presentation, but will always
58 * be present in the DOM for use by assistive technologies.
59 */
60 'errorEl',
61
62 'errorWrapEl',
63 'ariaErrorEl'
64 ]
65 },
66
67 /**
68 * @cfg {String/String[]/Ext.XTemplate} labelableRenderTpl
69 * The rendering template for the field decorations. Component classes using this mixin
70 * should include logic to use this as their {@link Ext.Component#renderTpl renderTpl},
71 * and implement the {@link #getSubTplMarkup} method to generate the field body content.
72 * @private
73 */
74 labelableRenderTpl: [
75 '{beforeLabelTpl}',
76 '<label id="{id}-labelEl" data-ref="labelEl" class="{labelCls} {labelCls}-{ui} {labelClsExtra} ',
77 '{childElCls} {unselectableCls}" style="{labelStyle}"<tpl if="inputId">',
78 ' for="{inputId}"</tpl> {labelAttrTpl}>',
79 '<span class="{labelInnerCls} {labelInnerCls}-{ui}" style="{labelInnerStyle}">',
80 '{beforeLabelTextTpl}',
81 '<tpl if="fieldLabel">{fieldLabel}',
82 '<tpl if="labelSeparator">{labelSeparator}</tpl>',
83 '</tpl>',
84 '{afterLabelTextTpl}',
85 '</span>',
86 '</label>',
87 '{afterLabelTpl}',
88 '<div id="{id}-bodyEl" data-ref="bodyEl" role="presentation"',
89 ' class="{baseBodyCls} {baseBodyCls}-{ui}<tpl if="fieldBodyCls">',
90 ' {fieldBodyCls} {fieldBodyCls}-{ui}</tpl> {growCls} {extraFieldBodyCls}"',
91 '<tpl if="bodyStyle"> style="{bodyStyle}"</tpl>>',
92 '{beforeBodyEl}',
93 '{beforeSubTpl}',
94 '{[values.$comp.getSubTplMarkup(values)]}',
95 '{afterSubTpl}',
96 '{afterBodyEl}',
97 // Unlike errorEl below ariaErrorEl is always rendered but is clipped out of existence
98 '<div id="{id}-ariaErrorEl" data-ref="ariaErrorEl" role="alert" aria-live="polite"',
99 ' class="' + Ext.baseCSSPrefix + 'hidden-clip">',
100 '</div>',
101 '</div>',
102 '<tpl if="renderError">',
103 '<div id="{id}-errorWrapEl" data-ref="errorWrapEl" class="{errorWrapCls} {errorWrapCls}-{ui}',
104 ' {errorWrapExtraCls}" style="{errorWrapStyle}">',
105 '<div role="presentation" id="{id}-errorEl" data-ref="errorEl" ',
106 'class="{errorMsgCls} {invalidMsgCls} {invalidMsgCls}-{ui}" ',
107 'data-anchorTarget="{tipAnchorTarget}">',
108 '</div>',
109 '</div>',
110 '</tpl>',
111 {
112 disableFormats: true
113 }
114 ],
115
116 /**
117 * @cfg {String/String[]/Ext.XTemplate} activeErrorsTpl
118 * The template used to format the Array of error messages passed to {@link #setActiveErrors} into a single HTML
119 * string. if the {@link #msgTarget} is title, it defaults to a list separated by new lines. Otherwise, it
120 * renders each message as an item in an unordered list.
121 */
122 activeErrorsTpl: undefined,
123
124 htmlActiveErrorsTpl: [
125 '<tpl if="errors && errors.length">',
126 '<ul class="{listCls}">',
127 '<tpl for="errors"><li>{.}</li></tpl>',
128 '</ul>',
129 '</tpl>'
130 ],
131
132 plaintextActiveErrorsTpl: [
133 '<tpl if="errors && errors.length">',
134 '<tpl for="errors"><tpl if="xindex &gt; 1">\n</tpl>{.}</tpl>',
135 '</tpl>'
136 ],
137
138 /**
139 * @property {Boolean} isFieldLabelable
140 * Flag denoting that this object is labelable as a field. Always true.
141 */
142 isFieldLabelable: true,
143
144 /**
145 * @cfg {String} formItemCls
146 * A CSS class to be applied to the outermost element to denote that it is participating in the form field layout.
147 */
148 formItemCls: Ext.baseCSSPrefix + 'form-item',
149
150 /**
151 * @cfg {String} labelCls
152 * The CSS class to be applied to the label element. This (single) CSS class is used to formulate the renderSelector
153 * and drives the field layout where it is concatenated with a hyphen ('-') and {@link #labelAlign}. To add
154 * additional classes, use {@link #labelClsExtra}.
155 */
156 labelCls: Ext.baseCSSPrefix + 'form-item-label',
157
158 /**
159 * @private
160 */
161 topLabelCls: Ext.baseCSSPrefix + 'form-item-label-top',
162 rightLabelCls: Ext.baseCSSPrefix + 'form-item-label-right',
163 labelInnerCls: Ext.baseCSSPrefix + 'form-item-label-inner',
164 topLabelSideErrorCls: Ext.baseCSSPrefix + 'form-item-label-top-side-error',
165
166 /**
167 * @cfg {String} labelClsExtra
168 * An optional string of one or more additional CSS classes to add to the label element. Defaults to empty.
169 */
170
171 /**
172 * @cfg {String} errorMsgCls
173 * The CSS class to be applied to the error message element.
174 */
175 errorMsgCls: Ext.baseCSSPrefix + 'form-error-msg',
176
177 errorWrapCls: Ext.baseCSSPrefix + 'form-error-wrap',
178 errorWrapSideCls: Ext.baseCSSPrefix + 'form-error-wrap-side',
179 errorWrapUnderCls: Ext.baseCSSPrefix + 'form-error-wrap-under',
180 errorWrapUnderSideLabelCls: Ext.baseCSSPrefix + 'form-error-wrap-under-side-label',
181
182 /**
183 * @cfg {String} baseBodyCls
184 * The CSS class to be applied to the body content element.
185 */
186 baseBodyCls: Ext.baseCSSPrefix + 'form-item-body',
187
188 invalidIconCls: Ext.baseCSSPrefix + 'form-invalid-icon',
189
190 invalidUnderCls: Ext.baseCSSPrefix + 'form-invalid-under',
191
192 noLabelCls: Ext.baseCSSPrefix + 'form-item-no-label',
193
194 /**
195 * @cfg {String} fieldBodyCls
196 * An extra CSS class to be applied to the body content element in addition to {@link #baseBodyCls}.
197 */
198 fieldBodyCls: '',
199
200 /**
201 * @cfg {String} invalidCls
202 * The CSS class to use when marking the component invalid.
203 */
204 invalidCls : Ext.baseCSSPrefix + 'form-invalid',
205
206 /**
207 * @cfg {String} fieldLabel
208 * The label for the field. It gets appended with the {@link #labelSeparator}, and its position and sizing is
209 * determined by the {@link #labelAlign} and {@link #labelWidth} configs.
210 */
211 fieldLabel: undefined,
212
213 /**
214 * @cfg {String} labelAlign
215 * Controls the position and alignment of the {@link #fieldLabel}. Valid values are:
216 *
217 * - "left" (the default) - The label is positioned to the left of the field, with its text aligned to the left.
218 * Its width is determined by the {@link #labelWidth} config.
219 * - "top" - The label is positioned above the field.
220 * - "right" - The label is positioned to the left of the field, with its text aligned to the right.
221 * Its width is determined by the {@link #labelWidth} config.
222 */
223 labelAlign : 'left',
224
225 /**
226 * @cfg {Number} labelWidth
227 * The width of the {@link #fieldLabel} in pixels. Only applicable if {@link #labelAlign}
228 * is set to "left" or "right".
229 */
230 labelWidth: 100,
231
232 /**
233 * @cfg {Number} labelPad
234 * The amount of space in pixels between the {@link #fieldLabel} and the field body.
235 * This defaults to `5` for compatibility with Ext JS 4, however, as of Ext JS 5
236 * the space between the label and the body can optionally be determined by the theme
237 * using the {@link #$form-label-horizontal-spacing} (for side-aligned labels) and
238 * {@link #$form-label-vertical-spacing} (for top-aligned labels) SASS variables.
239 * In order for the stylesheet values as to take effect, you must use a labelPad value
240 * of `null`.
241 */
242 labelPad: 5,
243
244 //<locale>
245 /**
246 * @cfg {String} labelSeparator
247 * Character(s) to be inserted at the end of the {@link #fieldLabel label text}.
248 *
249 * Set to empty string to hide the separator completely.
250 */
251 labelSeparator : ':',
252 //</locale>
253
254 /**
255 * @cfg {String} labelStyle
256 * A CSS style specification string to apply directly to this field's label.
257 */
258
259 /**
260 * @cfg {Boolean} hideLabel
261 * Set to true to completely hide the label element ({@link #fieldLabel} and {@link #labelSeparator}). Also see
262 * {@link #hideEmptyLabel}, which controls whether space will be reserved for an empty fieldLabel.
263 */
264 hideLabel: false,
265
266 /**
267 * @cfg {Boolean} hideEmptyLabel
268 * When set to true, the label element ({@link #fieldLabel} and {@link #labelSeparator}) will be automatically
269 * hidden if the {@link #fieldLabel} is empty. Setting this to false will cause the empty label element to be
270 * rendered and space to be reserved for it; this is useful if you want a field without a label to line up with
271 * other labeled fields in the same form.
272 *
273 * If you wish to unconditionall hide the label even if a non-empty fieldLabel is configured, then set the
274 * {@link #hideLabel} config to true.
275 */
276 hideEmptyLabel: true,
277
278 /**
279 * @cfg {Boolean} preventMark
280 * true to disable displaying any {@link #setActiveError error message} set on this object.
281 */
282 preventMark: false,
283
284 /**
285 * @cfg {Boolean} autoFitErrors
286 * Whether to adjust the component's body width to make room for 'side'
287 * {@link #msgTarget error messages}.
288 */
289 autoFitErrors: true,
290
291 /**
292 * @cfg {String} msgTarget
293 * The location where the error message text should display. Must be one of the following values:
294 *
295 * - `qtip` Display a quick tip containing the message when the user hovers over the field.
296 * This is the default.
297 *
298 * **{@link Ext.tip.QuickTipManager#init} must have been called for this setting to work.**
299 *
300 * - `title` Display the message in a default browser title attribute popup.
301 * - `under` Add a block div beneath the field containing the error message.
302 * - `side` Add an error icon to the right of the field, displaying the message in a popup on hover.
303 * - `none` Don't display any error message. This might be useful if you are implementing custom error display.
304 * - `[element id]` Add the error message directly to the innerHTML of the specified element.
305 */
306 msgTarget: 'qtip',
307
308 /**
309 * @private
310 * Map for msg target lookup, if target is not in this map it is assumed
311 * to be an element id
312 */
313 msgTargets: {
314 qtip: 1,
315 title: 1,
316 under: 1,
317 side: 1,
318 none: 1
319 },
320
321 /**
322 * @cfg {String} activeError
323 * If specified, then the component will be displayed with this value as its active error when first rendered. Use
324 * {@link #setActiveError} or {@link #unsetActiveError} to change it after component creation.
325 */
326
327 /**
328 * @private
329 * Tells the layout system that the height can be measured immediately because the width does not need setting.
330 */
331 noWrap: true,
332
333 labelableInsertions: [
334
335 /**
336 * @cfg {String/Array/Ext.XTemplate} beforeBodyEl
337 * An optional string or `XTemplate` configuration to insert in the field markup
338 * at the beginning of the input containing element. If an `XTemplate` is used, the component's {@link Ext.Component#renderData render data}
339 * serves as the context.
340 */
341 'beforeBodyEl',
342
343 /**
344 * @cfg {String/Array/Ext.XTemplate} afterBodyEl
345 * An optional string or `XTemplate` configuration to insert in the field markup
346 * at the end of the input containing element. If an `XTemplate` is used, the component's {@link Ext.Component#renderData render data}
347 * serves as the context.
348 */
349 'afterBodyEl',
350
351 /**
352 * @cfg {String/Array/Ext.XTemplate} beforeLabelTpl
353 * An optional string or `XTemplate` configuration to insert in the field markup
354 * before the label element. If an `XTemplate` is used, the component's {@link Ext.Component#renderData render data}
355 * serves as the context.
356 */
357 'beforeLabelTpl',
358
359 /**
360 * @cfg {String/Array/Ext.XTemplate} afterLabelTpl
361 * An optional string or `XTemplate` configuration to insert in the field markup
362 * after the label element. If an `XTemplate` is used, the component's {@link Ext.Component#renderData render data}
363 * serves as the context.
364 */
365 'afterLabelTpl',
366
367 /**
368 * @cfg {String/Array/Ext.XTemplate} beforeSubTpl
369 * An optional string or `XTemplate` configuration to insert in the field markup
370 * before the {@link #getSubTplMarkup subTpl markup}. If an `XTemplate` is used, the
371 * component's {@link Ext.Component#renderData render data} serves as the context.
372 */
373 'beforeSubTpl',
374
375 /**
376 * @cfg {String/Array/Ext.XTemplate} afterSubTpl
377 * An optional string or `XTemplate` configuration to insert in the field markup
378 * after the {@link #getSubTplMarkup subTpl markup}. If an `XTemplate` is used, the
379 * component's {@link Ext.Component#renderData render data} serves as the context.
380 */
381 'afterSubTpl',
382
383 /**
384 * @cfg {String/Array/Ext.XTemplate} beforeLabelTextTpl
385 * An optional string or `XTemplate` configuration to insert in the field markup
386 * before the label text. If an `XTemplate` is used, the component's {@link Ext.Component#renderData render data}
387 * serves as the context.
388 */
389 'beforeLabelTextTpl',
390
391 /**
392 * @cfg {String/Array/Ext.XTemplate} afterLabelTextTpl
393 * An optional string or `XTemplate` configuration to insert in the field markup
394 * after the label text. If an `XTemplate` is used, the component's {@link Ext.Component#renderData render data}
395 * serves as the context.
396 */
397 'afterLabelTextTpl',
398
399 /**
400 * @cfg {String/Array/Ext.XTemplate} labelAttrTpl
401 * An optional string or `XTemplate` configuration to insert in the field markup
402 * inside the label element (as attributes). If an `XTemplate` is used, the component's
403 * {@link Ext.Component#renderData render data} serves as the context.
404 */
405 'labelAttrTpl'
406 ],
407
408 statics: {
409 /**
410 * Use a custom QuickTip instance separate from the main QuickTips singleton, so that we
411 * can give it a custom frame style. Responds to errorqtip rather than the qtip property.
412 * @static
413 * @private
414 */
415 initTip: function() {
416 var tip = this.tip,
417 cfg, copy;
418
419 if (tip) {
420 return;
421 }
422
423 cfg = {
424 id: 'ext-form-error-tip',
425 //<debug>
426 // tell the spec runner to ignore this element when checking if the dom is clean
427 sticky: true,
428 //</debug>
429 ui: 'form-invalid'
430 };
431
432 // On Touch devices, tapping the target shows the qtip
433 if (Ext.supports.Touch) {
434 cfg.dismissDelay = 0;
435 cfg.anchor = 'top';
436 cfg.showDelay = 0;
437 cfg.listeners = {
438 beforeshow: function() {
439 this.minWidth = Ext.fly(this.anchorTarget).getWidth();
440 }
441 };
442 }
443 tip = this.tip = Ext.create('Ext.tip.QuickTip', cfg);
444 copy = Ext.apply({}, tip.tagConfig);
445 copy.attribute = 'errorqtip';
446 tip.setTagConfig(copy);
447 },
448
449 /**
450 * Destroy the error tip instance.
451 * @static
452 */
453 destroyTip: function() {
454 this.tip = Ext.destroy(this.tip);
455 }
456 },
457
458 /**
459 * @event errorchange
460 * Fires when the active error message is changed via {@link #setActiveError}.
461 * @param {Ext.form.Labelable} this
462 * @param {String} error The active error message
463 */
464
465 /**
466 * Performs initialization of this mixin. Component classes using this mixin should call this method during their
467 * own initialization.
468 */
469 initLabelable: function() {
470 var me = this,
471 padding = me.padding;
472
473 // This Component is rendered as a table. Padding doesn't work on tables
474 // Before padding can be applied to the encapsulating table element, copy the padding into
475 // an extraMargins property which is to be added to all computed margins post render :(
476 if (padding) {
477 me.padding = undefined;
478 me.extraMargins = Ext.Element.parseBox(padding);
479 }
480
481 // IE8 hack for https://sencha.jira.com/browse/EXTJS-17536.
482 // Need to force a relayout of the display:table form item.
483 // TODO: Remove when IE8 retires.
484 if (Ext.isIE8) {
485 me.restoreDisplay = Ext.Function.createDelayed(me.doRestoreDisplay, 0, me);
486 }
487
488 if (!me.activeErrorsTpl) {
489 if (me.msgTarget === 'title') {
490 me.activeErrorsTpl = me.plaintextActiveErrorsTpl;
491 } else {
492 me.activeErrorsTpl = me.htmlActiveErrorsTpl;
493 }
494 }
495
496 me.addCls([me.formItemCls, me.formItemCls + '-' + me.ui]);
497
498 // Prevent first render of active error, at Field render time from signalling a change from undefined to "
499 me.lastActiveError = '';
500
501 // bubbleEvents on the prototype of a mixin won't work, so call enableBubble
502 me.enableBubble('errorchange');
503 },
504
505 /**
506 * Returns the trimmed label by slicing off the label separator character. Can be overridden.
507 * @return {String} The trimmed field label, or empty string if not defined
508 */
509 trimLabelSeparator: function() {
510 var me = this,
511 separator = me.labelSeparator,
512 label = me.fieldLabel || '',
513 lastChar = label.substr(label.length - 1);
514
515 // if the last char is the same as the label separator then slice it off otherwise just return label value
516 return lastChar === separator ? label.slice(0, -1) : label;
517 },
518
519 /**
520 * Returns the label for the field. Defaults to simply returning the {@link #fieldLabel} config. Can be overridden
521 * to provide a custom generated label.
522 * @template
523 * @return {String} The configured field label, or empty string if not defined
524 */
525 getFieldLabel: function() {
526 return this.trimLabelSeparator();
527 },
528
529 /**
530 * Set the label of this field.
531 * @param {String} label The new label. The {@link #labelSeparator} will be automatically appended to the label
532 * string.
533 */
534 setFieldLabel: function(label){
535 label = label || '';
536
537 var me = this,
538 separator = me.labelSeparator,
539 labelEl = me.labelEl,
540 errorWrapEl = me.errorWrapEl,
541 sideLabel = (me.labelAlign !== 'top'),
542 noLabelCls = me.noLabelCls,
543 errorWrapUnderSideLabelCls = me.errorWrapUnderSideLabelCls;
544
545 me.fieldLabel = label;
546 if (me.rendered) {
547 if (Ext.isEmpty(label) && me.hideEmptyLabel) {
548 me.addCls(noLabelCls);
549 if (sideLabel && errorWrapEl) {
550 errorWrapEl.removeCls(errorWrapUnderSideLabelCls);
551 }
552 } else {
553 if (separator) {
554 label = me.trimLabelSeparator() + separator;
555 }
556 labelEl.dom.firstChild.innerHTML = label;
557 me.removeCls(noLabelCls);
558 if (sideLabel && errorWrapEl) {
559 errorWrapEl.addCls(errorWrapUnderSideLabelCls);
560 }
561 }
562 me.updateLayout();
563 }
564 },
565
566 setHideLabel: function(hideLabel) {
567 var me = this;
568
569 if (hideLabel !== me.hideLabel) {
570 me.hideLabel = hideLabel;
571 if (me.rendered) {
572 me[hideLabel ? 'addCls' : 'removeCls'](me.noLabelCls);
573 me.updateLayout();
574 }
575 }
576 },
577
578 setHideEmptyLabel: function(hideEmptyLabel) {
579 var me = this,
580 hide;
581
582 if (hideEmptyLabel !== me.hideEmptyLabel) {
583 me.hideEmptyLabel = hideEmptyLabel;
584 if (me.rendered && !me.hideLabel) {
585 hide = hideEmptyLabel && !me.getFieldLabel();
586 me[hide ? 'addCls' : 'removeCls'](me.noLabelCls);
587 me.updateLayout();
588 }
589 }
590 },
591
592 getInsertionRenderData: function (data, names) {
593 var i = names.length,
594 name, value;
595
596 while (i--) {
597 name = names[i];
598 value = this[name];
599
600 if (value) {
601 if (typeof value !== 'string') {
602 if (!value.isTemplate) {
603 value = Ext.XTemplate.getTpl(this, name);
604 }
605 value = value.apply(data);
606 }
607 }
608
609 data[name] = value || '';
610 }
611
612 return data;
613 },
614
615 /**
616 * Generates the arguments for the field decorations {@link #labelableRenderTpl
617 * rendering template}.
618 * @param {Object} data optional object to use as the base data object. If provided,
619 * this method will add properties to the base object instead of creating a new one.
620 * @return {Object} The template arguments
621 * @protected
622 */
623 getLabelableRenderData: function() {
624 var me = this,
625 labelAlign = me.labelAlign,
626 topLabel = (labelAlign === 'top'),
627 rightLabel = (labelAlign === 'right'),
628 sideError = (me.msgTarget === 'side'),
629 underError = (me.msgTarget === 'under'),
630 errorMsgCls = me.errorMsgCls,
631 labelPad = me.labelPad,
632 labelWidth = me.labelWidth,
633 labelClsExtra = me.labelClsExtra || '',
634 errorWrapExtraCls = sideError ? me.errorWrapSideCls : me.errorWrapUnderCls,
635 labelStyle = '',
636 labelInnerStyle = '',
637 labelVisible = me.hasVisibleLabel(),
638 autoFitErrors = me.autoFitErrors,
639 defaultBodyWidth = me.defaultBodyWidth,
640 bodyStyle, data;
641
642 if (topLabel) {
643 labelClsExtra += ' ' + me.topLabelCls;
644 if (labelPad) {
645 labelInnerStyle = 'padding-bottom:' + labelPad + 'px;';
646 }
647 if (sideError && !autoFitErrors) {
648 labelClsExtra += ' ' + me.topLabelSideErrorCls;
649 }
650 } else {
651 if (rightLabel) {
652 labelClsExtra += ' ' + me.rightLabelCls;
653 }
654 if (labelPad) {
655 labelStyle += me.getHorizontalPaddingStyle() + labelPad + 'px;';
656 }
657 labelStyle += 'width:' + (labelWidth + (labelPad ? labelPad : 0)) + 'px;';
658 // inner label needs width as well so that setting width on the outside
659 // that is smaller than the natural width, will be ensured to take width
660 // away from the body, and not the label.
661 labelInnerStyle = 'width:' + labelWidth + 'px';
662 }
663
664 if (labelVisible) {
665 if (!topLabel && underError) {
666 errorWrapExtraCls += ' ' + me.errorWrapUnderSideLabelCls;
667 }
668 }
669
670 if (defaultBodyWidth) {
671 // This is here to support textfield's deprecated "size" config
672 bodyStyle = 'min-width:' + defaultBodyWidth + 'px;max-width:' +
673 defaultBodyWidth + 'px;';
674 }
675
676 data = {
677 id: me.id,
678 inputId: me.getInputId(),
679 labelCls: me.labelCls,
680 labelClsExtra: labelClsExtra,
681 labelStyle: labelStyle + (me.labelStyle || ''),
682 labelInnerStyle: labelInnerStyle,
683 labelInnerCls: me.labelInnerCls,
684 unselectableCls: Ext.Element.unselectableCls,
685 bodyStyle: bodyStyle,
686 baseBodyCls: me.baseBodyCls,
687 fieldBodyCls: me.fieldBodyCls,
688 extraFieldBodyCls: me.extraFieldBodyCls,
689 errorWrapCls: me.errorWrapCls,
690 errorWrapExtraCls: errorWrapExtraCls,
691 renderError: sideError || underError,
692 invalidMsgCls: sideError ? me.invalidIconCls : underError ? me.invalidUnderCls : '',
693 errorMsgCls: errorMsgCls,
694 growCls: me.grow ? me.growCls : '',
695 tipAnchorTarget: me.id + '-inputEl',
696 errorWrapStyle: (sideError && !autoFitErrors) ?
697 'visibility:hidden' : 'display:none',
698 fieldLabel: me.getFieldLabel(),
699 labelSeparator: me.labelSeparator
700 };
701
702 me.getInsertionRenderData(data, me.labelableInsertions);
703
704 return data;
705 },
706
707 // hook for rtl
708 getHorizontalPaddingStyle: function() {
709 return 'padding-right:';
710 },
711
712 beforeLabelRender: function() {
713 var me = this;
714 me.setFieldDefaults(me.getInherited().fieldDefaults);
715 if (me.ownerLayout) {
716 me.addCls(Ext.baseCSSPrefix + me.ownerLayout.type + '-form-item');
717 }
718 if (!me.hasVisibleLabel()) {
719 me.addCls(me.noLabelCls);
720 }
721 },
722
723 onLabelRender: function() {
724 var me = this,
725 style = {},
726 ExtElement = Ext.Element,
727 errorWrapEl = me.errorWrapEl,
728 margins, side;
729
730 if (errorWrapEl) {
731 errorWrapEl.setVisibilityMode((me.msgTarget === 'side' && !me.autoFitErrors) ?
732 ExtElement.VISIBILITY : ExtElement.DISPLAY);
733 }
734
735 if (me.extraMargins) {
736 margins = me.el.getMargin();
737 for (side in margins) {
738 if (margins.hasOwnProperty(side)) {
739 style['margin-' + side] = (margins[side] + me.extraMargins[side]) + 'px';
740 }
741 }
742 me.el.setStyle(style);
743 }
744 },
745
746 /**
747 * Checks if the field has a visible label
748 * @return {Boolean} True if the field has a visible label
749 */
750 hasVisibleLabel: function(){
751 if (this.hideLabel) {
752 return false;
753 }
754 return !(this.hideEmptyLabel && !this.getFieldLabel());
755 },
756
757 /**
758 * Gets the markup to be inserted into the outer template's bodyEl. Defaults to empty string, should be implemented
759 * by classes including this mixin as needed.
760 * @return {String} The markup to be inserted
761 * @protected
762 */
763 getSubTplMarkup: function() {
764 return '';
765 },
766
767 /**
768 * Get the input id, if any, for this component. This is used as the "for" attribute on the label element.
769 * Implementing subclasses may also use this as e.g. the id for their own input element.
770 * @return {String} The input id
771 */
772 getInputId: function() {
773 return '';
774 },
775
776 /**
777 * Gets the active error message for this component, if any. This does not trigger validation on its own, it merely
778 * returns any message that the component may already hold.
779 * @return {String} The active error message on the component; if there is no error, an empty string is returned.
780 */
781 getActiveError : function() {
782 return this.activeError || '';
783 },
784
785 /**
786 * Tells whether the field currently has an active error message. This does not trigger validation on its own, it
787 * merely looks for any message that the component may already hold.
788 * @return {Boolean}
789 */
790 hasActiveError: function() {
791 return !!this.getActiveError();
792 },
793
794 /**
795 * Sets the active error message to the given string. This replaces the entire error message contents with the given
796 * string. Also see {@link #setActiveErrors} which accepts an Array of messages and formats them according to the
797 * {@link #activeErrorsTpl}. Note that this only updates the error message element's text and attributes, you'll
798 * have to call doComponentLayout to actually update the field's layout to match. If the field extends {@link
799 * Ext.form.field.Base} you should call {@link Ext.form.field.Base#markInvalid markInvalid} instead.
800 * @param {String} msg The error message
801 */
802 setActiveError: function(msg) {
803 this.setActiveErrors(msg);
804 },
805
806 /**
807 * Gets an Array of any active error messages currently applied to the field. This does not trigger validation on
808 * its own, it merely returns any messages that the component may already hold.
809 * @return {String[]} The active error messages on the component; if there are no errors, an empty Array is
810 * returned.
811 */
812 getActiveErrors: function() {
813 return this.activeErrors || [];
814 },
815
816 /**
817 * Set the active error message to an Array of error messages. The messages are formatted into a single message
818 * string using the {@link #activeErrorsTpl}. Also see {@link #setActiveError} which allows setting the entire error
819 * contents with a single string. Note that this only updates the error message element's text and attributes,
820 * you'll have to call doComponentLayout to actually update the field's layout to match. If the field extends
821 * {@link Ext.form.field.Base} you should call {@link Ext.form.field.Base#markInvalid markInvalid} instead.
822 * @param {String[]} errors The error messages
823 */
824 setActiveErrors: function(errors) {
825 var me = this,
826 errorWrapEl = me.errorWrapEl,
827 msgTarget = me.msgTarget,
828 isSide = msgTarget === 'side',
829 isQtip = msgTarget === 'qtip',
830 actionEl, activeError, tpl, targetEl;
831
832 errors = Ext.Array.from(errors);
833 tpl = me.getTpl('activeErrorsTpl');
834
835 me.activeErrors = errors;
836 activeError = me.activeError = tpl.apply({
837 fieldLabel: me.fieldLabel,
838 errors: errors,
839 listCls: Ext.baseCSSPrefix + 'list-plain'
840 });
841
842 me.renderActiveError();
843
844 if (me.rendered) {
845 actionEl = me.getActionEl();
846
847 if (isSide) {
848 me.errorEl.dom.setAttribute('data-errorqtip', activeError);
849 }
850 else if (isQtip) {
851 actionEl.dom.setAttribute('data-errorqtip', activeError);
852 }
853 else if (msgTarget === 'title') {
854 actionEl.dom.setAttribute('title', activeError);
855 }
856
857 // If msgTarget is title, setting an alert is redundant for ARIA purposes
858 if (msgTarget !== 'title') {
859 me.ariaErrorEl.dom.innerHTML = errors.join('. ');
860 actionEl.dom.setAttribute('aria-describedby', me.ariaErrorEl.id);
861 }
862
863 if (isSide || isQtip) {
864 Ext.form.Labelable.initTip();
865 }
866
867 if (!me.msgTargets[msgTarget]) {
868 targetEl = Ext.get(msgTarget);
869
870 if (targetEl) {
871 targetEl.dom.innerHTML = activeError;
872 }
873 }
874 }
875
876 if (errorWrapEl) {
877 errorWrapEl.setVisible(errors.length > 0);
878 if (isSide && me.autoFitErrors) {
879 me.labelEl.addCls(me.topLabelSideErrorCls);
880 }
881 me.updateLayout();
882 }
883 },
884
885 /**
886 * Clears the active error message(s). Note that this only clears the error message element's text and attributes,
887 * you'll have to call doComponentLayout to actually update the field's layout to match. If the field extends {@link
888 * Ext.form.field.Base} you should call {@link Ext.form.field.Base#clearInvalid clearInvalid} instead.
889 */
890 unsetActiveError: function() {
891 var me = this,
892 errorWrapEl = me.errorWrapEl,
893 msgTarget = me.msgTarget,
894 restoreDisplay = me.restoreDisplay,
895 actionEl, targetEl;
896
897 if (me.hasActiveError()) {
898 delete me.activeError;
899 delete me.activeErrors;
900 me.renderActiveError();
901
902 if (me.rendered) {
903 actionEl = me.getActionEl();
904
905 if (msgTarget === 'qtip') {
906 actionEl.dom.removeAttribute('data-errorqtip');
907 }
908 else if (msgTarget === 'title') {
909 actionEl.dom.removeAttribute('title');
910 }
911
912 if (msgTarget !== 'title') {
913 me.ariaErrorEl.dom.innerHTML = '';
914 actionEl.dom.removeAttribute('aria-describedby');
915 }
916
917 if (!me.msgTargets[msgTarget]) {
918 targetEl = Ext.get(msgTarget);
919
920 if (targetEl) {
921 targetEl.dom.innerHTML = '';
922 }
923 }
924
925 if (errorWrapEl) {
926 errorWrapEl.hide();
927 if (msgTarget === 'side' && me.autoFitErrors) {
928 me.labelEl.removeCls(me.topLabelSideErrorCls);
929 }
930 me.updateLayout();
931
932 // IE8 hack for https://sencha.jira.com/browse/EXTJS-17536.
933 // Need to force a relayout of the display:table form item.
934 // TODO: Remove when IE8 retires.
935 if (restoreDisplay) {
936 me.el.dom.style.display = 'block';
937 me.restoreDisplay();
938 }
939 }
940 }
941 }
942 },
943
944 doRestoreDisplay: function() {
945 // IE8 hack for https://sencha.jira.com/browse/EXTJS-17536.
946 // Need to force a relayout of the display:table form item.
947 // TODO: Remove this method when IE8 retires.
948 var el = this.el;
949 if (el && el.dom) {
950 el.dom.style.display = '';
951 }
952 },
953
954 /**
955 * @private
956 * Updates the rendered DOM to match the current activeError. This only updates the content and
957 * attributes, you'll have to call doComponentLayout to actually update the display.
958 */
959 renderActiveError: function() {
960 var me = this,
961 activeError = me.getActiveError(),
962 hasError = !!activeError;
963
964 if (activeError !== me.lastActiveError) {
965 me.lastActiveError = activeError;
966 me.fireEvent('errorchange', me, activeError);
967 }
968
969 if (me.rendered && !me.destroyed && !me.preventMark) {
970 me.toggleInvalidCls(hasError);
971 // Update the errorEl (There will only be one if msgTarget is 'side' or 'under') with the error message text
972 if (me.errorEl) {
973 me.errorEl.dom.innerHTML = activeError;
974 }
975 }
976 },
977
978 /**
979 * @private
980 * Add/remove invalid class(es)
981 * @param {Boolean} hasError
982 */
983 toggleInvalidCls: function(hasError) {
984 this.el[hasError ? 'addCls' : 'removeCls'](this.invalidCls);
985 },
986
987 /**
988 * Applies a set of default configuration values to this Labelable instance. For each of the properties in the given
989 * object, check if this component hasOwnProperty that config; if not then it's inheriting a default value from its
990 * prototype and we should apply the default value.
991 * @param {Object} defaults The defaults to apply to the object.
992 */
993 setFieldDefaults: function(defaults) {
994 var key;
995
996 for (key in defaults) {
997 if (!this.hasOwnProperty(key)) {
998 this[key] = defaults[key];
999 }
1000 }
1001 }
1002 }, function() {
1003 if (Ext.supports.Touch) {
1004 this.prototype.msgTarget = 'side';
1005 }
1006 });