/**
* FieldContainer is a derivation of {@link Ext.container.Container Container} that implements the
* {@link Ext.form.Labelable Labelable} mixin. This allows it to be configured so that it is
* rendered with a {@link #fieldLabel field label} and optional {@link #msgTarget error message}
* around its sub-items. This is useful for arranging a group of fields within a single item
* in a form, so that it lines up nicely with other fields. A common use is for grouping a set
* of related fields under a single label in a form.
*
* The container's configured {@link #cfg-items} will be layed out within the field body area
* according to the configured {@link #layout} type. The default layout is `'autocontainer'`.
*
* Like regular fields, FieldContainer can inherit its decoration configuration from the
* {@link Ext.form.Panel#fieldDefaults fieldDefaults} of an enclosing FormPanel. In addition,
* FieldContainer itself can pass {@link #fieldDefaults} to any {@link Ext.form.Labelable fields}
* it may itself contain.
*
* If you are grouping a set of {@link Ext.form.field.Checkbox Checkbox} or
* {@link Ext.form.field.Radio Radio} fields in a single labeled container, consider using
* a {@link Ext.form.CheckboxGroup} or {@link Ext.form.RadioGroup} instead as they are specialized
* for handling those types.
*
* # Example
*
* @example
* Ext.create('Ext.form.Panel', {
* title: 'FieldContainer Example',
* width: 550,
* bodyPadding: 10,
*
* items: [{
* xtype: 'fieldcontainer',
* fieldLabel: 'Last Three Jobs',
* labelWidth: 100,
*
* // The body area will contain three text fields, arranged
* // horizontally, separated by draggable splitters.
* layout: 'hbox',
* items: [{
* xtype: 'textfield',
* flex: 1
* }, {
* xtype: 'splitter'
* }, {
* xtype: 'textfield',
* flex: 1
* }, {
* xtype: 'splitter'
* }, {
* xtype: 'textfield',
* flex: 1
* }]
* }],
* renderTo: Ext.getBody()
* });
*
* # Usage of fieldDefaults
*
* @example
* Ext.create('Ext.form.Panel', {
* title: 'FieldContainer Example',
* width: 350,
* bodyPadding: 10,
*
* items: [{
* xtype: 'fieldcontainer',
* fieldLabel: 'Your Name',
* labelWidth: 75,
* defaultType: 'textfield',
*
* // Arrange fields vertically, stretched to full width
* layout: 'anchor',
* defaults: {
* layout: '100%'
* },
*
* // These config values will be applied to both sub-fields, except
* // for Last Name which will use its own msgTarget.
* fieldDefaults: {
* msgTarget: 'under',
* labelAlign: 'top'
* },
*
* items: [{
* fieldLabel: 'First Name',
* name: 'firstName'
* }, {
* fieldLabel: 'Last Name',
* name: 'lastName',
* msgTarget: 'under'
* }]
* }],
* renderTo: Ext.getBody()
* });
*/
Ext.define('Ext.form.FieldContainer', {
extend: 'Ext.container.Container',
alias: 'widget.fieldcontainer',
requires: [
'Ext.layout.component.field.FieldContainer'
],
mixins: {
labelable: 'Ext.form.Labelable',
fieldAncestor: 'Ext.form.FieldAncestor'
},
componentLayout: 'fieldcontainer',
componentCls: Ext.baseCSSPrefix + 'form-fieldcontainer',
shrinkWrap: true,
autoEl: {
tag: 'div',
role: 'presentation'
},
childEls: [
'containerEl'
],
/**
* @cfg {Boolean} combineLabels
* If set to true, and there is no defined {@link #fieldLabel}, the field container will
* automatically generate its label by combining the labels of all the fields it contains.
* Defaults to false.
*/
combineLabels: false,
/**
* @cfg {String} labelConnector
* The string to use when joining the labels of individual sub-fields, when
* {@link #combineLabels} is set to true. Defaults to ', '.
* @locale
*/
labelConnector: ', ',
/**
* @cfg {Boolean} combineErrors
* If set to true, the field container will automatically combine and display the validation
* errors from all the fields it contains as a single error on the container, according to the
* configured {@link #msgTarget}. Defaults to false.
*/
combineErrors: false,
maskOnDisable: false,
// If we allow this to mark with the invalidCls it will cascade to all
// child fields, let them handle themselves
invalidCls: '',
/* eslint-disable indent */
fieldSubTpl: [
'<div id="{id}-containerEl" data-ref="containerEl" class="{containerElCls}"',
'<tpl if="ariaAttributes">',
'<tpl foreach="ariaAttributes"> {$}="{.}"</tpl>',
'<tpl else>',
' role="presentation"',
'</tpl>',
'>',
'{%this.renderContainer(out,values)%}',
'</div>'
],
/* eslint-enable indent */
initComponent: function() {
var me = this;
// Init mixins
me.initLabelable();
me.initFieldAncestor();
me.callParent();
me.initMonitor();
},
onAdd: function(labelItem) {
var me = this;
// Fix for https://sencha.jira.com/browse/EXTJSIV-6424 Which was *sneakily* fixed
// in version 37
// In FF < 37, positioning absolutely within a TD positions relative to the TR!
// So we must add the width of a visible, left-aligned label cell to the x coordinate.
if (labelItem.isLabelable && Ext.isGecko && Ext.firefoxVersion < 37 &&
me.layout.type === 'absolute' && !me.hideLabel && me.labelAlign !== 'top') {
labelItem.x += (me.labelWidth + me.labelPad);
}
me.callParent(arguments);
if (labelItem.isLabelable && me.combineLabels) {
labelItem.oldHideLabel = labelItem.hideLabel;
labelItem.hideLabel = true;
}
me.updateLabel();
},
onRemove: function(labelItem, isDestroying) {
var me = this;
me.callParent(arguments);
if (!isDestroying) {
if (labelItem.isLabelable && me.combineLabels) {
labelItem.hideLabel = labelItem.oldHideLabel;
}
me.updateLabel();
}
},
initRenderData: function() {
var me = this,
data = me.callParent();
data.containerElCls = me.containerElCls;
data = Ext.applyIf(data, me.getLabelableRenderData());
if (me.labelAlign === 'top' || me.msgTarget === 'under') {
data.extraFieldBodyCls += ' ' + Ext.baseCSSPrefix + 'field-container-body-vertical';
}
data.tipAnchorTarget = me.id + '-containerEl';
return data;
},
/**
* Returns the combined field label if {@link #combineLabels} is set to true and if there is no
* set {@link #fieldLabel}. Otherwise returns the fieldLabel like normal. You can also override
* this method to provide a custom generated label.
* @template
* @return {String} The label, or empty string if none.
*/
getFieldLabel: function() {
var label = this.fieldLabel || '';
if (!label && this.combineLabels) {
label = Ext.Array.map(this.query('[isFieldLabelable]'), function(field) {
return field.getFieldLabel();
}).join(this.labelConnector);
}
return label;
},
getSubTplData: function() {
var ret = this.initRenderData();
Ext.apply(ret, this.subTplData);
return ret;
},
getSubTplMarkup: function(fieldData) {
var me = this,
tpl = me.lookupTpl('fieldSubTpl'),
html;
if (!tpl.renderContent) {
me.setupRenderTpl(tpl);
}
html = tpl.apply(me.getSubTplData(fieldData));
return html;
},
/**
* @private
* Updates the content of the labelEl if it is rendered
*/
updateLabel: function() {
var me = this,
label = me.labelEl;
if (label) {
me.setFieldLabel(me.getFieldLabel());
}
},
/**
* @private
* Fired when the error message of any field within the container changes, and updates the
* combined error message to match.
*/
onFieldErrorChange: function() {
if (this.combineErrors) {
// eslint-disable-next-line vars-on-top
var me = this,
oldError = me.getActiveError(),
invalidFields = Ext.Array.filter(me.query('[isFormField]'), function(field) {
return field.hasActiveError();
}),
newErrors = me.getCombinedErrors(invalidFields);
if (newErrors) {
me.setActiveErrors(newErrors);
}
else {
me.unsetActiveError();
}
if (oldError !== me.getActiveError()) {
me.updateLayout();
}
}
},
/**
* Takes an Array of invalid {@link Ext.form.field.Field} objects and builds a combined list
* of error messages from them. Defaults to prepending each message by the field name
* and a colon. This can be overridden to provide custom combined error message handling,
* for instance changing the format of each message or sorting the array (it is sorted
* in order of appearance by default).
* @param {Ext.form.field.Field[]} invalidFields An Array of the sub-fields which are currently
* invalid.
* @return {String[]} The combined list of error messages
*/
getCombinedErrors: function(invalidFields) {
var errors = [],
fLen = invalidFields.length,
f, field, activeErrors, a, aLen, error, label;
for (f = 0; f < fLen; f++) {
field = invalidFields[f];
activeErrors = field.getActiveErrors();
aLen = activeErrors.length;
for (a = 0; a < aLen; a++) {
error = activeErrors[a];
label = field.getFieldLabel();
errors.push((label ? label + ': ' : '') + error);
}
}
return errors;
},
privates: {
applyTargetCls: function(targetCls) {
var containerElCls = this.containerElCls;
this.containerElCls = containerElCls ? containerElCls + ' ' + targetCls : targetCls;
},
getTargetEl: function() {
return this.containerEl;
},
initRenderTpl: function() {
var me = this;
if (!me.hasOwnProperty('renderTpl')) {
me.renderTpl = me.lookupTpl('labelableRenderTpl');
}
return me.callParent();
}
}
});