+ * @class Ext.form.CompositeField
+ * @extends Ext.form.Field
+ * Composite field allowing a number of form Fields to be rendered on the same row. The fields are rendered
+ * using an hbox layout internally, so all of the normal HBox layout config items are available. Example usage:
+ * <pre>
+{
+ xtype: 'compositefield',
+ labelWidth: 120
+ items: [
+ {
+ xtype : 'textfield',
+ fieldLabel: 'Title',
+ width : 20
+ },
+ {
+ xtype : 'textfield',
+ fieldLabel: 'First',
+ flex : 1
+ },
+ {
+ xtype : 'textfield',
+ fieldLabel: 'Last',
+ flex : 1
+ }
+ ]
+}
+ * </pre>
+ * In the example above the composite's fieldLabel will be set to 'Title, First, Last' as it groups the fieldLabels
+ * of each of its children. This can be overridden by setting a fieldLabel on the compositefield itself:
+ * <pre>
+{
+ xtype: 'compositefield',
+ fieldLabel: 'Custom label',
+ items: [...]
+}
+ * </pre>
+ * Any Ext.form.* component can be placed inside a composite field.
+ */
+Ext.form.CompositeField = Ext.extend(Ext.form.Field, {
+
+ /**
+ * @property defaultMargins
+ * @type String
+ * The margins to apply by default to each field in the composite
+ */
+ defaultMargins: '0 5 0 0',
+
+ /**
+ * @property skipLastItemMargin
+ * @type Boolean
+ * If true, the defaultMargins are not applied to the last item in the composite field set (defaults to true)
+ */
+ skipLastItemMargin: true,
+
+ /**
+ * @property isComposite
+ * @type Boolean
+ * Signifies that this is a Composite field
+ */
+ isComposite: true,
+
+ /**
+ * @property combineErrors
+ * @type Boolean
+ * True to combine errors from the individual fields into a single error message at the CompositeField level (defaults to true)
+ */
+ combineErrors: true,
+
+ //inherit docs
+ //Builds the composite field label
+ initComponent: function() {
+ var labels = [],
+ items = this.items,
+ item;
+
+ for (var i=0, j = items.length; i < j; i++) {
+ item = items[i];
+
+ labels.push(item.fieldLabel);
+
+ //apply any defaults
+ Ext.apply(item, this.defaults);
+
+ //apply default margins to each item except the last
+ if (!(i == j - 1 && this.skipLastItemMargin)) {
+ Ext.applyIf(item, {margins: this.defaultMargins});
+ }
+ }
+
+ this.fieldLabel = this.fieldLabel || this.buildLabel(labels);
+
+ /**
+ * @property fieldErrors
+ * @type Ext.util.MixedCollection
+ * MixedCollection of current errors on the Composite's subfields. This is used internally to track when
+ * to show and hide error messages at the Composite level. Listeners are attached to the MixedCollection's
+ * add, remove and replace events to update the error icon in the UI as errors are added or removed.
+ */
+ this.fieldErrors = new Ext.util.MixedCollection(true, function(item) {
+ return item.field;
+ });
+
+ this.fieldErrors.on({
+ scope : this,
+ add : this.updateInvalidMark,
+ remove : this.updateInvalidMark,
+ replace: this.updateInvalidMark
+ });
+
+ Ext.form.CompositeField.superclass.initComponent.apply(this, arguments);
+ },
+
+ /**
+ * @private
+ * Creates an internal container using hbox and renders the fields to it
+ */
+ onRender: function(ct, position) {
+ if (!this.el) {
+ /**
+ * @property innerCt
+ * @type Ext.Container
+ * A container configured with hbox layout which is responsible for laying out the subfields
+ */
+ var innerCt = this.innerCt = new Ext.Container({
+ layout : 'hbox',
+ renderTo: ct,
+ items : this.items,
+ cls : 'x-form-composite',
+ defaultMargins: '0 3 0 0'
+ });
+
+ this.el = innerCt.getEl();
+
+ var fields = innerCt.findBy(function(c) {
+ return c.isFormField;
+ }, this);
+
+ /**
+ * @property items
+ * @type Ext.util.MixedCollection
+ * Internal collection of all of the subfields in this Composite
+ */
+ this.items = new Ext.util.MixedCollection();
+ this.items.addAll(fields);
+
+ //if we're combining subfield errors into a single message, override the markInvalid and clearInvalid
+ //methods of each subfield and show them at the Composite level instead
+ if (this.combineErrors) {
+ this.eachItem(function(field) {
+ Ext.apply(field, {
+ markInvalid : this.onFieldMarkInvalid.createDelegate(this, [field], 0),
+ clearInvalid: this.onFieldClearInvalid.createDelegate(this, [field], 0)
+ });
+ });
+ }
+
+ //set the label 'for' to the first item
+ var l = this.el.parent().parent().child('label', true);
+ if (l) {
+ l.setAttribute('for', this.items.items[0].id);
+ }
+ }
+
+ Ext.form.CompositeField.superclass.onRender.apply(this, arguments);
+ },
+
+ /**
+ * Called if combineErrors is true and a subfield's markInvalid method is called.
+ * By default this just adds the subfield's error to the internal fieldErrors MixedCollection
+ * @param {Ext.form.Field} field The field that was marked invalid
+ * @param {String} message The error message
+ */
+ onFieldMarkInvalid: function(field, message) {
+ var name = field.getName(),
+ error = {field: name, error: message};
+
+ this.fieldErrors.replace(name, error);
+
+ field.el.addClass(field.invalidClass);
+ },
+
+ /**
+ * Called if combineErrors is true and a subfield's clearInvalid method is called.
+ * By default this just updates the internal fieldErrors MixedCollection.
+ * @param {Ext.form.Field} field The field that was marked invalid
+ */
+ onFieldClearInvalid: function(field) {
+ this.fieldErrors.removeKey(field.getName());
+
+ field.el.removeClass(field.invalidClass);
+ },
+
+ /**
+ * @private
+ * Called after a subfield is marked valid or invalid, this checks to see if any of the subfields are
+ * currently invalid. If any subfields are invalid it builds a combined error message marks the composite
+ * invalid, otherwise clearInvalid is called
+ */
+ updateInvalidMark: function() {
+ var ieStrict = Ext.isIE6 && Ext.isStrict;
+
+ if (this.fieldErrors.length == 0) {
+ this.clearInvalid();
+
+ //IE6 in strict mode has a layout bug when using 'under' as the error message target. This fixes it
+ if (ieStrict) {
+ this.clearInvalid.defer(50, this);
+ }
+ } else {
+ var message = this.buildCombinedErrorMessage(this.fieldErrors.items);
+
+ this.sortErrors();
+ this.markInvalid(message);
+
+ //IE6 in strict mode has a layout bug when using 'under' as the error message target. This fixes it
+ if (ieStrict) {
+ this.markInvalid(message);
+ }
+ }
+ },
+
+ /**
+ * Performs validation checks on each subfield and returns false if any of them fail validation.
+ * @return {Boolean} False if any subfield failed validation
+ */
+ validateValue: function() {
+ var valid = true;
+
+ this.eachItem(function(field) {
+ if (!field.isValid()) valid = false;
+ });
+
+ return valid;
+ },
+
+ /**
+ * Takes an object containing error messages for contained fields, returning a combined error
+ * string (defaults to just placing each item on a new line). This can be overridden to provide
+ * custom combined error message handling.
+ * @param {Array} errors Array of errors in format: [{field: 'title', error: 'some error'}]
+ * @return {String} The combined error message
+ */
+ buildCombinedErrorMessage: function(errors) {
+ var combined = [],
+ error;
+
+ for (var i = 0, j = errors.length; i < j; i++) {
+ error = errors[i];
+
+ combined.push(String.format("{0}: {1}", error.field, error.error));
+ }
+
+ return combined.join("<br />");
+ },
+
+ /**
+ * Sorts the internal fieldErrors MixedCollection by the order in which the fields are defined.
+ * This is called before displaying errors to ensure that the errors are presented in the expected order.
+ * This function can be overridden to provide a custom sorting order if needed.
+ */
+ sortErrors: function() {
+ var fields = this.items;
+
+ this.fieldErrors.sort("ASC", function(a, b) {
+ var findByName = function(key) {
+ return function(field) {
+ return field.getName() == key;
+ };
+ };
+
+ var aIndex = fields.findIndexBy(findByName(a.field)),
+ bIndex = fields.findIndexBy(findByName(b.field));
+
+ return aIndex < bIndex ? -1 : 1;
+ });
+ },
+
+ /**
+ * Resets each field in the composite to their previous value
+ */
+ reset: function() {
+ this.eachItem(function(item) {
+ item.reset();
+ });
+
+ // Defer the clearInvalid so if BaseForm's collection is being iterated it will be called AFTER it is complete.
+ // Important because reset is being called on both the group and the individual items.
+ (function() {
+ this.clearInvalid();
+ }).defer(50, this);
+ },
+
+ /**
+ * Calls clearInvalid on all child fields. This is a convenience function and should not often need to be called
+ * as fields usually take care of clearing themselves
+ */
+ clearInvalidChildren: function() {
+ this.eachItem(function(item) {
+ item.clearInvalid();
+ });
+ },
+
+ /**
+ * Builds a label string from an array of subfield labels.
+ * By default this just joins the labels together with a comma
+ * @param {Array} segments Array of each of the labels in the composite field's subfields
+ * @return {String} The built label
+ */
+ buildLabel: function(segments) {
+ return segments.join(", ");
+ },
+
+ /**
+ * Checks each field in the composite and returns true if any is dirty
+ * @return {Boolean} True if any field is dirty
+ */
+ isDirty: function(){
+ //override the behaviour to check sub items.
+ if (this.disabled || !this.rendered) {
+ return false;
+ }
+
+ var dirty = false;
+ this.eachItem(function(item){
+ if(item.isDirty()){
+ dirty = true;
+ return false;
+ }
+ });
+ return dirty;
+ },
+
+ /**
+ * @private
+ * Convenience function which passes the given function to every item in the composite
+ * @param {Function} fn The function to call
+ * @param {Object} scope Optional scope object
+ */
+ eachItem: function(fn, scope) {
+ if(this.items && this.items.each){
+ this.items.each(fn, scope || this);
+ }
+ },
+
+ /**
+ * @private
+ * Passes the resize call through to the inner panel
+ */
+ onResize: function(adjWidth, adjHeight, rawWidth, rawHeight) {
+ var innerCt = this.innerCt;
+
+ if (this.rendered && innerCt.rendered) {
+ innerCt.setSize(adjWidth, adjHeight);
+ }
+
+ Ext.form.CompositeField.superclass.onResize.apply(this, arguments);
+ },
+
+ /**
+ * @private
+ * Forces the internal container to be laid out again
+ */
+ doLayout: function(shallow, force) {
+ if (this.rendered) {
+ var innerCt = this.innerCt;
+
+ innerCt.forceLayout = this.ownerCt.forceLayout;
+ innerCt.doLayout(shallow, force);
+ }
+ },
+
+ /**
+ * @private
+ */
+ beforeDestroy: function(){
+ Ext.destroy(this.innerCt);
+
+ Ext.form.CompositeField.superclass.beforeDestroy.call(this);
+ },
+
+ //override the behaviour to check sub items.
+ setReadOnly : function(readOnly) {
+ readOnly = readOnly || true;
+
+ if(this.rendered){
+ this.eachItem(function(item){
+ item.setReadOnly(readOnly);
+ });
+ }
+ this.readOnly = readOnly;
+ },
+
+ onShow : function() {
+ Ext.form.CompositeField.superclass.onShow.call(this);
+ this.doLayout();
+ },
+
+ //override the behaviour to check sub items.
+ onDisable : function(){
+ this.eachItem(function(item){
+ item.disable();
+ });
+ },
+
+ //override the behaviour to check sub items.
+ onEnable : function(){
+ this.eachItem(function(item){
+ item.enable();
+ });
+ }
+});
+
+Ext.reg('compositefield', Ext.form.CompositeField);
+/**
+ * @class Ext.form.Radio
+ * @extends Ext.form.Checkbox
+ * Single radio field. Same as Checkbox, but provided as a convenience for automatically setting the input type.