/*
* Copyright 2016 Anyware Services
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This class is a configurable form panel that can contains tabs, fieldsets, repeaters and widgets. Configuration is made through XML or JSON requests.
* The configuration format can be in JSON or XML.
* The 2 steps to use this components are to call once and only once:
*
* 1) create the form (#configure)
* 2) fill the values (#setValues)
*/
Ext.define('Ametys.form.ConfigurableFormPanel', {
extend: "Ext.form.Panel",
statics: {
/**
* @property {Number} HORIZONTAL_PADDING_FIELDSET The left and right padding for fieldset
* @private
* @readonly
*/
HORIZONTAL_PADDING_FIELDSET: 5,
/**
* @property {Number} VERTICAL_PADDING_FIELDSET The top and bottom padding for fieldset
* @private
* @readonly
*/
VERTICAL_PADDING_FIELDSET: 5,
/**
* @property {Number} OFFSET_FIELDSET The offset for fieldset
* @private
* @readonly
*/
OFFSET_FIELDSET: 20,
/**
* @property {Number} PADDING_TAB The padding for tabs
* @private
* @readonly
*/
PADDING_TAB: 5,
/**
* @property {Number} LABEL_WIDTH The width for labels (at root nesting level)
* @private
* @readonly
*/
LABEL_WIDTH: 200,
/**
* @property {Number} FIELD_MINWIDTH The minimum width for fields
* @private
* @readonly
*/
FIELD_MINWIDTH: 130,
/**
* @private
* @property {String} OUTOFTAB_FIELDSET_ID The id referencing the fields that do not belong to any fieldset
* @readonly
*/
OUTOFTAB_FIELDSET_ID: 'out-of-tab-fieldset',
/**
* @private
* @property {Number} FLEX_TABLE_OF_CONTENT The flex weight of the table of content when visible
*/
FLEX_TABLE_OF_CONTENT: 0.2,
/**
* @private
* @property {Number} FLEX_FORM The flex weight of the form
*/
FLEX_FORM: 0.8
},
/**
* @private
* @property {Number} [maxNestedLevel=0] For layout purposes, this value is the number of nested repeaters/fieldsets.
*/
maxNestedLevel: 0,
/**
* @cfg {String/String[]/Ext.XTemplate} tabErrorFieldsTpl
* The template used to format the Array of warnings and errors fields passed to tab ToolTip into a single HTML
* string. It renders each message as an item in an unordered list.
*/
tabErrorFieldsTpl: [
'<div class="a-configurable-form-panel-tooltip-status">',
'<tpl if="errors && errors.length">',
'<tpl if="errors.length == 1">',
"<span class=\"a-configurable-form-panel-tooltip-status-error-label\">{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_TAB_TPL_ERROR_FIELD}}</span>",
'</tpl>',
'<tpl if="errors.length != 1">',
"<span class=\"a-configurable-form-panel-tooltip-status-error-label\">{errors.length}{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_TAB_TPL_ERROR_FIELDS}}</span>",
'</tpl>',
'<ul class="error"><tpl for="errors"><li>{.}</li></tpl></ul>',
'</tpl>',
'<tpl if="warns && warns.length">',
'<tpl if="warns.length == 1">',
"<span class=\"a-configurable-form-panel-tooltip-status-warn-label\">{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_TAB_TPL_WARN_FIELD}}</span>",
'</tpl>',
'<tpl if="warns.length != 1">',
"<span class=\"a-configurable-form-panel-tooltip-status-warn-label\">{warns.length}{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_TAB_TPL_WARN_FIELDS}}</span>",
'</tpl>',
'<ul class="warn"><tpl for="warns"><li>{.}</li></tpl></ul>',
'</tpl>',
'</div>'
],
/**
* @cfg {Boolean} [withTitleOnLabels=false] True to wrap field's labels within a span with title as such <span title="My label">My label</span>. Useful if labels could be cut by CSS style.
*/
/**
* @cfg {Boolean} [tableOfContents=false]
* True to display a table of contents panel at the left of the form summarizing the first level fieldsets.
* This can only be applied with a form in linear mode. See #cfg-tab-policy-mode.
*/
tableOfContents: false,
/**
* @property {Boolean} _showTableOfContents True if a table of contents is displayed. See #cfg-tableOfContents.
*/
/**
* @cfg {Object} defaultFieldConfig Default config to apply to all form fields
*/
/**
* @cfg {String} [tab-policy-mode] The display tab policy name. Currently accepted values are 'default' or 'inline'.
*/
/**
* @cfg {String} testURL The url to use to run the verifications. see {@link Ametys.form.ConfigurableFormPanel.FieldCheckersManager#check}
*/
/**
* @private
* @property {String} _testURL See #cfg-testURL.
*/
/**
* @cfg {Function} testHandler An optionnal function called each time a test is done, that's allow to transform sent parameters
* @cfg {Ametys.form.ConfigurableFormPanel.FieldChecker[]} testHandler.fieldCheckers The field checkers executed
* @cfg {Object} testHandler.fieldCheckersInfo A map which key is the testHandler.fieldCheckers ids
* @cfg {String[]} testHandler.fieldCheckersInfo.testParamsNames The array of param names
* @cfg {Object[]} testHandler.fieldCheckersInfo.rawTestValues The array of param values
* @cfg {Object} testHandler.returnedValue The modified fieldCheckersInfo
*/
/**
* @private
* @property {Function} _testHandler See #cfg-testHandler.
*/
/**
* @property {Ametys.form.ConfigurableFormPanel.TableOfContents} _tableOfContents The table of contents instance attached to this form
* @private
*/
/**
* @property {Ext.form.Field[]} _fields The configuration fields
* @private
*/
/**
* @property {String[]} _initiallyNotNullFieldNames the names of the fields that are initially not null
* @private
*/
/**
* @property {Ext.panel.Panel/Ext.tab.Panel} _tabPanel The main panel or tabpanel depending on policy.
* @private
*/
/**
* @private
* @property {String[]} _notInFirstEditionPanels the ids list of the root panels (tabs or panel) that have been edited at least once.
* We consider a tab edited once when the focus has switched from one field of one of the tabs fieldsets to a different fieldset, of this tab or of another tab.
*/
/**
* @private
* @property {Ametys.form.ConfigurableFormPanel.FieldCheckersManager} _fieldCheckersManager The field checkers manager instance of this form
*/
/**
* @private
* @property {Ext.container.Container} _formContainer The container of this form
*/
/**
* @private
* @property {Boolean} _hasFieldsBeforeTabs True if there are some fields before the {@link Ext.tab.Panel}
*/
/** @cfg {Object} itemsLayout The layout to use in the container. Default to { type: 'anchor' }. */
/** @cfg {Object} [tabItemsLayout] The layout to use for the tab items if {@link #cfg-tab-policy-mode} is 'default'. Default to {@link #cfg-itemsLayout}, then { type: 'anchor' }. */
/** @cfg {Object} fieldsetLayout The layout to use in the nested fieldsets. Default to { type: 'anchor' }. */
/** @cfg {Object} tabsLayout The layout to use in the tabs. Default to { type: 'anchor' }. */
/** @cfg {String} [tab-position='top'] If {@link #cfg-tab-policy-mode} is 'default', the position where the tab strip should be rendered. Currently accepted values are 'top'/'bottom'/'left'/'right'.
/** @cfg {Object} [tabBar] If {@link #cfg-tab-policy-mode} is 'default', the optional configuration object passed to the internal {@link Ext.tab.Bar}. */
/** @cfg {Boolean} [spacingCls=true] True to add the 'a-panel-spacing' CSS class */
/** @cfg {Object} additionalWidgetsConfFromParams Additional configuration for every widget that will be created by this form. Each key of this object is the widget configuration name to add, and each corresponding value is the configuration name that will be read from the input configuration stream. */
/**
* @property {Object} _additionalWidgetsConfFromParams See #cfg-additionalWidgetsConfFromParams.
*/
/** @cfg {Object} additionalWidgetsConf Additional configuration for every widget that will be created by this form. Each key of this object is the widget configuration name to add, and each corresponding value is the value of widget configuration. */
/**
* @property {Object} _additionalWidgetsConf See #cfg-additionalWidgetsConf.
*/
/**
* @cfg {String} [labelAlign="right"] The label position. See Ext.form.Labelable#cfg-labelAlign
*/
labelAlign: 'right',
scrollable: true,
layout: {
type: 'anchor',
reserveScrollbar: true
},
border: false,
focusable: true,
/**
* @cfg {Number} [tabIndex=0] DOM tabIndex attribute for the focused element
*/
tabIndex: 0,
/**
* @cfg {String} [defaultPathSeparator="/"] The default separator for fields
*/
defaultPathSeparator: '/',
/**
* @cfg {String} [fieldNamePrefix=""] The prefix to all submitted fields (should end with '.' if non empty)
*/
fieldNamePrefix: '',
/**
* @cfg {Boolean} [hideDisabledFields=false] Set to true to hide the disabled fields
*/
hideDisabledFields: false,
/**
* @cfg {Boolean} [displayGroupsDescriptions=true] Set to false to not display the descriptions of grouping elements (tabs / filedsets / composites)
*/
/**
* @private
* @property {Boolean} _displayGroupsDescriptions indicates if the descriptions of grouping elements have to be displayed. See {@link #displayGroupsDescriptions}
*/
/**
* @private
* @property {Boolean} _formReady indicates if the form is ready. The form is ready when all fields are rendered and have a value set.
*/
_formReady: false,
/**
* @private
* @property {Boolean} _addingRepeaterEntry indicates if a new repeater entry is currently being added.
*/
_addingRepeaterEntry: false,
/**
* @private
* @property {String} _focusFieldId The identifier of the lastly selected field that is still focused
*/
_focusFieldId: null,
/**
* @private
* @property {String} _lastSelectedFieldId The identifier of the lastly selected field, regardless if it is focused or not
*/
_lastSelectedFieldId: null,
/**
* @private
* @property {Object} _externalDisableConditionsValues the values of external disable conditions
*/
_externalDisableConditionsValues: {},
/**
* @event inputblur
* Fires when a field loses the focus
* @param {Ext.form.Field} field The field
*/
/**
* @event inputfocus
* Fires when a field received the focus
* @param {Ext.form.Field} field The field
*/
/**
* @event htmlnodeselected
* Fires when a HTML element is selected
* @param {Ext.form.Field} field The field
* @param {HTMLElement} node The selected HTML element
*/
/**
* @event formready
* Fired after all fields have been drawn and values have been set.
* This event should be also fired each time new fields are inserted (from a new repeater instance for example).
* @param {Ext.form.Panel} form The form containing the fields
*/
/**
* @event repeaterEntryReady
* Fired after all fields a new repeater entry has been inserted, which means that all its fields have been drawn and the repeater entry is initialized.
* @param {Ametys.form.ConfigurableFormPanel.Repeater} repeater The repeater containing the entry.
*/
/**
* @event fieldchange
* Fired when one of the fields changes after the form is ready
* @param {Ext.form.Field} field The field that changed
*/
/**
* @event testresultschange
* Fired when the results of the tests change
* @param {Object} testResults the tests results
*/
/**
* @property {Boolean} isConfigurableFormPanel=true Sepcifiy this component is a configurableformpanel
* @readonly
*/
isConfigurableFormPanel: true,
constructor: function (config)
{
var me = this;
config = config || {};
config.cls = Ext.Array.from(config.cls);
config.cls.push("a-configurable-form-panel");
if (config.spacingCls !== false)
{
config.cls.push("a-panel-spacing");
}
this._tabPolicy = config['tab-policy-mode'] || 'default';
this._showTableOfContents = config.tableOfContents === true && this._tabPolicy == 'inline';
config.dockedItems = Ext.Array.from(config.dockedItems);
var items = [];
if (this._showTableOfContents)
{
items.push({
xtype: 'component'
})
}
items.push({
xtype: 'toolbar',
style: {
paddingTop: '10px',
paddingBottom: 0
},
items:[
{
cls: 'a-btn-light',
iconCls: 'ametysicon-minus-sign4',
tooltip: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_INLINETAB_COLLAPSE_ALL}}",
handler: function (btn) {
me._expandOrCollapseAllInlineTab(me._tabPanel, btn, true)
}
},
{
cls: 'a-btn-light',
iconCls: 'ametysicon-expand16',
tooltip: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_INLINETAB_EXPAND_ALL}}",
handler: function (btn) {
me._expandOrCollapseAllInlineTab(me._tabPanel, btn, false)
}
}
]
});
config.dockedItems.push(Ext.create('Ext.container.Container', {
dock: 'top',
hidden: true,
itemId: 'inline-toolbar',
layout: this._showTableOfContents ? 'hbox' : 'fit',
items: items
}));
this.defaultFieldConfig = config.defaultFieldConfig || {};
if (this._showTableOfContents)
{
config = Ext.apply (config, {
layout: {
type: 'hbox',
align: 'stretch'
},
scrollable: true // with a table of contents, the child items are scrollable vertically. at minwidth this scroll is horizontal
})
}
this._additionalWidgetsConf = config.additionalWidgetsConf || {};
this._additionalWidgetsConfFromParams = config.additionalWidgetsConfFromParams || {};
this._testURL = config.testURL;
this._testHandler = config.testHandler;
config.items = this._getFormItems(config);
this.callParent(arguments);
this._fields = [];
this._repeaters = [];
this._notInFirstEditionPanels = [];
this._initiallyNotNullFieldNames = [];
this.hideDisabledFields = config.hideDisabledFields === true;
this._fieldCheckersManager = Ext.create('Ametys.form.ConfigurableFormPanel.FieldCheckersManager', {
form: this
});
this._displayGroupsDescriptions = config.displayGroupsDescriptions !== false;
this._fieldNamePrefix = config["fieldNamePrefix"] || '';
this.on('afterrender', this._setupFieldListeners, this);
this.on('resize', this._onResize, this);
if (config.defaultPathSeparator)
{
this.defaultPathSeparator = config.defaultPathSeparator;
}
this._protectMethods();
},
/**
* This method will protect many tools methods from beeing called at bad time
* @private
*/
_protectMethods: function()
{
this.configure = Ext.Function.createInterceptor(this.configure, this._isNotDestroyed, this);
this.getFormContainer = Ext.Function.createInterceptor(this.getFormContainer, this._isNotDestroyed, this);
this.setValues = Ext.Function.createInterceptor(this.setValues, this._isNotDestroyed, this);
this._getFormContainerCfg = Ext.Function.createInterceptor(this._getFormContainerCfg, this._isNotDestroyed, this);
this._getFormItems = Ext.Function.createInterceptor(this._getFormItems, this._isNotDestroyed, this);
this._getTableOfContents = Ext.Function.createInterceptor(this._getTableOfContents, this._isNotDestroyed, this);
this._getTableOfContentsCfg = Ext.Function.createInterceptor(this._getTableOfContentsCfg, this._isNotDestroyed, this);
this.destroyComponents = Ext.Function.createInterceptor(this.destroyComponents, this._isNotDestroyed, this);
this.executeFormReady = Ext.Function.createInterceptor(this.executeFormReady, this._isNotDestroyed, this);
this.getChildrenFields = Ext.Function.createInterceptor(this.getChildrenFields, this._isNotDestroyed, this);
this.getField = Ext.Function.createInterceptor(this.getField, this._isNotDestroyed, this);
this.getFieldNamePrefix = Ext.Function.createInterceptor(this.getFieldNamePrefix, this._isNotDestroyed, this);
this.getFieldNames = Ext.Function.createInterceptor(this.getFieldNames, this._isNotDestroyed, this);
this.getInvalidFieldValues = Ext.Function.createInterceptor(this.getInvalidFieldValues, this._isNotDestroyed, this);
this.getInvalidFields = Ext.Function.createInterceptor(this.getInvalidFields, this._isNotDestroyed, this);
this.getInvalidRepeaters = Ext.Function.createInterceptor(this.getInvalidRepeaters, this._isNotDestroyed, this);
this.getJsonValues = Ext.Function.createInterceptor(this.getJsonValues, this._isNotDestroyed, this);
this.getMessageTargetConf = Ext.Function.createInterceptor(this.getMessageTargetConf, this._isNotDestroyed, this);
this.getRelativeField = Ext.Function.createInterceptor(this.getRelativeField, this._isNotDestroyed, this);
this.getRelativeFields = Ext.Function.createInterceptor(this.getRelativeFields, this._isNotDestroyed, this);
this.getRepeaters = Ext.Function.createInterceptor(this.getRepeaters, this._isNotDestroyed, this);
this.getTestURL = Ext.Function.createInterceptor(this.getTestURL, this._isNotDestroyed, this);
this.getWarnedFields = Ext.Function.createInterceptor(this.getWarnedFields, this._isNotDestroyed, this);
this.isFormReady = Ext.Function.createInterceptor(this.isFormReady, this._isNotDestroyed, this);
this.getExternalDisableConditionsValues = Ext.Function.createInterceptor(this.getExternalDisableConditionsValues, this._isNotDestroyed, this);
this.markFieldsInvalid = Ext.Function.createInterceptor(this.markFieldsInvalid, this._isNotDestroyed, this);
this.markFieldsWarning = Ext.Function.createInterceptor(this.markFieldsWarning, this._isNotDestroyed, this);
this.notifyAddRepeaterEntry = Ext.Function.createInterceptor(this.notifyAddRepeaterEntry, this._isNotDestroyed, this);
this.onRelativeFieldsChange = Ext.Function.createInterceptor(this.onRelativeFieldsChange, this._isNotDestroyed, this);
this.setAdditionalWidgetsConf = Ext.Function.createInterceptor(this.setAdditionalWidgetsConf, this._isNotDestroyed, this);
},
/**
* Test if the form is ready, i.e. when all fields are rendered and have a value set.
* @return {Boolean} `true` when the form is ready.
*/
isFormReady: function()
{
return this._formReady;
},
/**
* Retrieves the values of external disable conditions
* @return {Object} the values of external disable conditions
*/
getExternalDisableConditionsValues: function()
{
return this._externalDisableConditionsValues;
},
/**
* Set an additional configuration for every widget that will be created by the form.
* @param {String} name The name of additional configuration parameter
* @param {Object} value The value of additional configuration parameter
*/
setAdditionalWidgetsConf: function (name, value)
{
this._additionalWidgetsConf[name] = value;
},
/**
* @private
* When a field is selected
* @param {Ext.form.Field} field The field that has been selected (focused) or null if the last selected field blurred
*/
_onFieldSelectedOrBlurred: function(field)
{
this._focusFieldId = field != null ? field.getId() : null;
if (field)
{
this._handlePanelsEdition(field);
this._lastSelectedFieldId = field.getId();
}
},
/**
* @private
* When a richtext field is selected (a different html node is selected in it), focused or blurred
* @param {Ext.form.Field} field The field that contains the HTML node
* @param {HTMLElement} node The selected HTML node or null on focus/blur
*/
_onRichTextFieldHTMLNodeSelected: function(field, node)
{
this._onFieldSelectedOrBlurred(field);
},
/**
* @private
* Compare the selected field to the previously selected field in order to determine whether the previous tab (thumbnails mode) or previous
* panel (linearized mode) is in first edition or not, and validate the fields/update the tabs status accordingly
* @param {Ext.form.Field} field the newly focused field
*/
_handlePanelsEdition: function(field)
{
if (this._lastSelectedFieldId && field.getId() != this._lastSelectedFieldId)
{
// The focus has switched
var previouslyFocusedField = this.getField(this._lastSelectedFieldId);
// It might have been destroyed already (if contained in a deleted repeater item)
if (previouslyFocusedField)
{
var previousPanel = previouslyFocusedField.up('panel[cls~=ametys-form-tab-item], panel[cls~=ametys-form-tab-inline-item]');
var currentPanel = field.up('panel[cls~=ametys-form-tab-item], panel[cls~=ametys-form-tab-inline-item]');
// The previous panel and/or the current panel can be null if it is outside of the thumbnails
if (!previousPanel && currentPanel)
{
this._notInFirstEditionPanels.push(Ametys.form.ConfigurableFormPanel.OUTOFTAB_FIELDSET_ID);
// The previous panel is outside of the thumbnails
this._validateTabOrPanelFields(null);
}
else if (previousPanel && (!currentPanel || (previousPanel.id != currentPanel.id)))
{
// The focus has switched from one panel or from outside of the thumbnails to another panel
// => the previously selected panel is not in first edition mode anymore
if (!Ext.Array.contains(this._notInFirstEditionPanels, previousPanel.id))
{
this._notInFirstEditionPanels.push(previousPanel.id);
}
this._validateTabOrPanelFields(previousPanel);
}
}
}
},
// Inherited to unregister from the message bus
destroy: function()
{
this.destroyComponents();
Ametys.message.MessageBus.unAll(this);
this.callParent(arguments);
},
/**
* Retrieves the fields in the form as a set of key/value pairs, using their getJsonValue() method to collect the values
*/
getJsonValues: function()
{
var values = {},
fields = this.form.getFields().items,
fLen = fields.length,
field;
for (var f = 0; f < fLen; f++)
{
field = fields[f];
values[field.getName()] = field.getJsonValue();
}
return values;
},
reset: function()
{
// reset the fields
this.callParent(arguments);
// Reset the warnings and the status of the field checkers
this._fieldCheckersManager.reset();
// Reset the repeaters
Ext.Array.each(this._repeaters, function(repeaterId)
{
var repeaterPanel = Ext.getCmp(repeaterId);
// repeater panel can already have been deleted if they were within a parent repeater item
if (repeaterPanel)
{
repeaterPanel.reset();
}
});
},
isValid: function()
{
var isFormValid = this.callParent();
var areAllRepeatersValid = true;
// Are the repeaters of the form valid ?
Ext.Array.each(this._repeaters, function(repeaterId)
{
var repeaterCt = Ext.getCmp(repeaterId);
// repeater panel can already have been deleted if they were within a parent repeater item
if (repeaterCt && !repeaterCt.isValid())
{
areAllRepeatersValid = false;
}
});
// Force the update of the tabs status
this._updateTabsStatus(true);
return isFormValid && areAllRepeatersValid;
},
/**
* Get the form container config to be used during its creation.
* @protected
*/
_getFormContainerCfg: function(config)
{
return {
xtype: 'container',
scrollable: false,
border: false,
layout: config.itemsLayout || { type: 'anchor' },
items: config.items,
// minWidth is a minWidth of a field + a number of repeaters margins
minWidth: this._getFieldMinWidth(config.labelAlign)
+ (Ametys.form.ConfigurableFormPanel.Repeater.NESTED_OFFSET+1) * 3 // 3 level of repeaters
};
},
/**
* @private
*/
_getFieldMinWidth(labelAlign)
{
return (labelAlign != 'top' ? (this.defaultFieldConfig.labelWidth || Ametys.form.ConfigurableFormPanel.LABEL_WIDTH) : 0)
+ Ametys.form.ConfigurableFormPanel.FIELD_MINWIDTH
+ (labelAlign != 'top' ? 20 : 0) // ametysDescription
},
/**
* Get the table of contents config to be used during its creation.
* @protected
*/
_getTableOfContentsCfg: function ()
{
return {
xtype: 'configurable-form-panel.toc',
form: this,
scrollable: 'vertical',
flex: Ametys.form.ConfigurableFormPanel.FLEX_TABLE_OF_CONTENT
}
},
/**
* @private
* Listener on general resize
* @param {Ametys.form.ConfigurableFormPanel} panel The configurable form panel
* @param {Number} width The panel width
* @param {Number} height The panel height
*/
_onResize: function(panel, width, height)
{
var toc = this._getTableOfContents();
if (toc)
{
toc.setVisible(width >= this.getFormContainer().getInitialConfig().minWidth * 1.2);
var tocSpacer = this.getDockedComponent('inline-toolbar').items.get(0);
tocSpacer.setWidth(toc.getWidth() + 5);
tocSpacer.setVisible(toc.isVisible());
}
},
/**
* @protected
* Get the items of the form to be used during its creation.
* @return {Object[]} the array of objects items of the form container
*/
_getFormItems: function(config)
{
if (this._showTableOfContents)
{
var hItems = [];
hItems.push(this._getTableOfContentsCfg());
hItems.push(Ext.apply(this._getFormContainerCfg(config), {
flex: Ametys.form.ConfigurableFormPanel.FLEX_FORM,
scrollable: true
}));
return hItems;
}
else
{
return this._getFormContainerCfg(config);
}
},
/**
* Get the names of fields handle by the form panel
* @return {String[]} The fields' names
*/
getFieldNames: function ()
{
return this._fields;
},
/**
* Get the prefix for field name
* @return {String} The prefix
*/
getFieldNamePrefix: function ()
{
return this._fieldNamePrefix;
},
/**
* Get the form container in which the edition form must be drawn.
* By default it is the first child item of this panel.
* @return {Ext.container.Container} The form draw zone
* @protected
*/
getFormContainer: function ()
{
return this._showTableOfContents ? this.items.get(1) : this.items.get(0);
},
/**
* Get the table of contents
* @return {Ametys.form.ConfigurableFormPanel.TableOfContents} The table of contents or null
*/
_getTableOfContents: function ()
{
return this._showTableOfContents ? this.items.get(0) : null;
},
/**
* Get the url used to run the checks on parameters
* @return {String} The url
*/
getTestURL: function ()
{
return this._testURL;
},
/**
* Execute the function used to change test parameters
* @cfg {Object} fieldCheckersInfo A map which key is the testHandler.fieldCheckers ids
* @cfg {String[]} fieldCheckersInfo.testParamsNames The array of param names
* @cfg {Object[]} fieldCheckersInfo.rawTestValues The array of param values
* @return {Object} The modified fieldCheckersInfo
*/
testHandler: function (fieldCheckers, fieldCheckersInfo)
{
return this._testHandler ? Ext.bind(this._testHandler, this)(fieldCheckers, fieldCheckersInfo) : fieldCheckersInfo;
},
/**
* Get a field in this form by id or name
* @param {String} name The name (or id) of the searched field
* @return {Ext.form.field.Field} The first matching field, or null if none was found.
*/
getField: function (name)
{
return this.getForm().findField(name);
},
/**
* Call Ext.form.field.Field.markInvalid on all fields of the form that are in error
* @param {Object} fieldsInError The fields in error: the key is the name and the value is the error message.
*/
markFieldsInvalid: function (fieldsInError)
{
for (var name in fieldsInError)
{
var fd = this.getForm().findField(name);
if (fd)
{
fd.markInvalid(fieldsInError[name]);
}
}
this._updateTabsStatus(true);
},
/**
* Call Ext.form.field.Base.markWarning on all fields of the form that have warnings
* @param {Object} fieldsWithWarning The fields with warning: the key is the name and the value is the warning message.
*/
markFieldsWarning: function (fieldsWithWarning)
{
for (var name in fieldsWithWarning)
{
var fd = this.getForm().findField(name);
if (fd && !this._hasActiveServerWarning(fd))
{
var warnings = [];
for (var message of (fieldsWithWarning[name]))
{
warnings.push({
type: 'server',
fieldName: name,
message: message
});
}
fd.addActiveWarnings(warnings);
}
}
this._updateTabsStatus(true);
},
_hasActiveServerWarning: function(field)
{
for (var activeWarning of field.getActiveWarnings())
{
if (Ext.isObject(activeWarning) && activeWarning.type === 'server')
{
return true;
}
}
return false;
},
/**
* Function to call when renaming a field of the form. Called by Ametys.form.ConfigurableFormPanel.Repeater
* @param {Number/String} field The position or the name of the renamed field
* @param {String} newName The new name
* @private
*/
_onRenameField : function (field, newName)
{
var pos = -1;
if (Ext.isNumber(field))
{
pos = field;
}
else if (typeof field == 'string')
{
var pos = this._fields.indexOf(field);
}
if (pos >= 0 && pos < this._fields.length)
{
this._fields[pos] = newName;
}
},
/**
* Function to call when removing a field of the form. Called by Ametys.form.ConfigurableFormPanel.Repeater
* @param {String} name The name of the removed field
* @private
*/
_onRemoveField: function (name)
{
var pos = this._fields.indexOf(name);
if (pos >= 0)
{
this._fields.splice(pos, 1);
}
},
/**
* Destroy the all form items.
* Call this if the form is not destroyed but you want to free its underlying children.
*/
destroyComponents: function()
{
this._formReady = false;
this._fields = [];
this._repeaters = [];
this._tabPanel = null;
this._externalDisableConditionsValues = {};
if (this.items && this.items.length > 0)
{
this.getFormContainer().removeAll();
if (this._showTablesOfContents)
{
this.items.get(0).removeAll();
}
}
},
/**
* @private
* Expand or collapse child items. Show a load mask.
* @param {Ext.panel.Panel} tabpanel The panel containing items to collapse or expand
* @param {Ext.Button} btn the clicked button to collapse or expand all
* @param {Boolean} collapse true to collapse all tab' items or false to expand
*/
_expandOrCollapseAllInlineTab: function (tabpanel, btn, collapse)
{
this.mask("{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_INLINETAB_WAIT_MSG}}");
Ext.Function.defer(this._doExpandOrCollapseAllInlineTab, 100, this, [tabpanel, btn, collapse]);
},
/**
* @private
* Expand or collapse child items
* @param {Ext.panel.Panel} tabpanel The panel containing items to collapse or expand
* @param {Ext.Button} btn the clicked button to collapse or expand all
* @param {Boolean} collapse true to collapse all tab' items or false to expand
*/
_doExpandOrCollapseAllInlineTab : function (tabpanel, btn, collapse)
{
this.suspendLayouts();
try
{
tabpanel.items.each (function (panel) {
if (collapse)
{
panel.collapse();
}
else
{
panel.expand();
}
});
}
finally
{
this.resumeLayouts(true);
this.unmask();
}
},
/**
* @private
* Creates a tab container
* @return {Ext.tab.Panel} the tab container
*/
_addTab: function ()
{
var tabPanel;
var me = this;
if (this._tabPolicy === 'inline')
{
tabPanel = Ext.create('Ext.panel.Panel', {
cls: 'ametys-form-tab-inline',
layout: this.initialConfig.tabsLayout || { type: 'anchor' },
border: false
});
this.getDockedComponent('inline-toolbar').show();
}
else
{
tabPanel = Ext.create('Ext.tab.Panel', {
cls: 'ametys-form-tab',
//plain: true,
//bodyStyle: "border-width: 0 1px 1px 1px !important",
margin: (this._hasFieldsBeforeTabs ? '15' : '0') + ' 0 0 0',
tabPosition: this.initialConfig.tabPosition || 'top',
tabRotation: 0,
tabBar: this.initialConfig.tabBar,
deferredRender: false,
listeners: {
'add': function(tabpanel, panel, index, eOpts) {
if (tabpanel.getActiveTab() == null)
{
tabpanel.setActiveTab(panel);
}
},
'tabchange': {fn: this._onTabChange, scope: this}
}
});
this.getDockedComponent('inline-toolbar').hide();
}
this.getFormContainer().add(tabPanel);
this._tabPanel = tabPanel;
return tabPanel;
},
getFocusEl: function()
{
var focusEl = this.element || this.el;
Ext.Array.each(this._getFields(), function(field) {
if (field != null && field.focusable && Ext.isFunction(field.focus)
&& (!Ext.isFunction(field.isVisible) || field.isVisible())
&& (!Ext.isFunction(field.isDisabled) || !field.isDisabled()))
{
focusEl = field;
return false;
}
});
return focusEl;
},
/**
* @private
* Focus the first field when the form is ready and rendered
*/
_setupFieldListeners: function()
{
if (this.rendered && this._formReady)
{
// Set a high priority on listeners to be sure the focused field will be updated before invoking other listeners
this.on({
'inputfocus': {fn: this._onFieldSelectedOrBlurred, scope: this, priority: 100},
'inputblur': {fn: this._onFieldSelectedOrBlurred, scope: this, priority: 100},
'htmlnodeselected': {fn: this._onRichTextFieldHTMLNodeSelected, scope: this, priority: 100}
});
}
},
/**
* @private
* Function invoked when a new tab is selected
* @param {Ext.tab.Panel} tabPanel the tab panel
* @param {Ext.Component} newCard the newly activated item
* @param {Ext.Component} oldCard the previously active item
*/
_onTabChange: function(tabPanel, newCard, oldCard)
{
if (oldCard != null)
{
// Focus the first visible field of the newly selected tab
// Doing so will trigger the validation of the previous tab if needed
var fields = this._getFields(newCard != null ? newCard.getId() : Ametys.form.ConfigurableFormPanel.OUTOFTAB_FIELDSET_ID);
fields = fields.filter(f => f.isVisible() && f.isFocusable())
if (fields.length > 0)
{
fields[0].focus();
}
// There is no field to focus so we force the validation of the previous tab to be safe
// For exemple, a tab with only an empty repeater
else
{
// The focus has switched from one panel or from outside of the thumbnails to another panel
// => the previously selected panel is not in first edition mode anymore
if (!Ext.Array.contains(this._notInFirstEditionPanels, oldCard.id))
{
this._notInFirstEditionPanels.push(oldCard.id);
}
this._validateTabOrPanelFields(oldCard);
}
}
},
/**
* Creates a tab item with its label
* @param {Ext.container.Container} ct The container where to add the tab item
* @param {String} label The label of the tab item
* @param {String} headerCls Custom CSS class name to apply to header
* @return {Ext.panel.Panel} the tab item
* @private
*/
_addTabItem: function (ct, label, headerCls)
{
if (this._tabPolicy === 'inline')
{
var fieldset = Ext.create('Ext.panel.Panel', {
title: label,
bodyPadding: Ametys.form.ConfigurableFormPanel.PADDING_TAB + ' ' + Ametys.form.ConfigurableFormPanel.PADDING_TAB + ' 0 ' + Ametys.form.ConfigurableFormPanel.PADDING_TAB,
margin: '0 0 5 0',
layout: this.initialConfig.tabsLayout || { type: 'anchor' },
collapsible: true,
titleCollapse: false,
header: {
titlePosition: 1,
cls: headerCls
},
border: true,
shadow: false,
cls: 'ametys-form-tab-inline-item'
});
ct.add(fieldset);
if (this._showTableOfContents)
{
// Add the tab item in the table of contents
this._getTableOfContents().addNavigationItem(label, fieldset.getId());
}
return fieldset;
}
else
{
var tabitem = Ext.create('Ext.panel.Panel', {
title: label,
cls: 'ametys-form-tab-item',
layout: this.initialConfig.tabItemsLayout || this.initialConfig.itemsLayout || { type: 'anchor' },
padding: Ametys.form.ConfigurableFormPanel.PADDING_TAB + ' ' + Ametys.form.ConfigurableFormPanel.PADDING_TAB + ' 0 ' + Ametys.form.ConfigurableFormPanel.PADDING_TAB,
header: {
cls: headerCls
},
border: false,
scrollable: true
});
ct.add(tabitem);
return tabitem;
}
},
/**
* @private
* Validate tab when changing the focus to a different fieldset
* @param {Ext.panel.Panel} previousPanel The panel just left, can be null if the panel if outside of the thumbnails
*/
_validateTabOrPanelFields: function(previousPanel)
{
// Validate the fields
var panelId = previousPanel != null ? previousPanel.getId() : Ametys.form.ConfigurableFormPanel.OUTOFTAB_FIELDSET_ID;
var fields = this._getFields(panelId);
Ext.suspendLayouts();
Ext.Array.each(fields, function(field)
{
if (previousPanel != null || field.up('panel[cls~=ametys-form-tab-item], panel[cls~=ametys-form-tab-inline-item]') == null)
{
// Trigger internal validation without firing validity change.
field.isValid();
}
});
// Validate the repeaters
var repeaters = this.getRepeaters(panelId);
Ext.Array.each(repeaters, function(repeater)
{
if (previousPanel != null || repeater.up('panel[cls~=ametys-form-tab-item], panel[cls~=ametys-form-tab-inline-item]') == null)
{
// Trigger internal validation without firing validity change.
repeater.isValid();
}
});
// No tab status when outside of the thumbnails
this._updateTabStatus(previousPanel);
Ext.resumeLayouts(true);
},
/**
* @private
* Get the list of fields in a container (any level) or all the fields
* @param {String} componentId The id of the component to get the field from, can be null to get all the fields
* @return {Ext.Component[]} An array of components which have the field mixin.
*/
_getFields: function(componentId)
{
var fields = [];
// Function walking the component tree and adding fields to the array.
var fieldWalker = function(component)
{
if (component.isFormField)
{
fields.push(component);
}
else if (component.isXType('container') && component.isVisible())
{
component.items.each(fieldWalker);
}
}
if (componentId != Ametys.form.ConfigurableFormPanel.OUTOFTAB_FIELDSET_ID)
{
var component = componentId != null ? Ext.getCmp(componentId) : this;
if (component.isFormField)
{
fields.push(component);
}
else if (component.items)
{
component.items.each(fieldWalker);
}
}
else
{
this.getFormContainer().items.each(function(formItem) {
// Do not consider the fields within tabs
if (!formItem.hasCls('ametys-form-tab-inline') && !formItem.hasCls('ametys-form-tab'))
{
fields = Ext.Array.union(fields, this._getFields(formItem.getId()));
}
}, this);
}
return fields;
},
/**
* @private
* Get the list of field checkers contained in the given tab or outside of the tabs (will return the global field checkers)
* @param {String} componentId the id of the component to get the field checkers from, can be null to get all field checkers
* @return {Ametys.form.ConfigurableFormPanel.FieldChecker[]} The array of field checkers contained in the panel, in the whole form, or outside the tabs
*/
_getFieldCheckers: function(componentId)
{
var fieldCheckers = [];
if (componentId != Ametys.form.ConfigurableFormPanel.OUTOFTAB_FIELDSET_ID)
{
var parentCmp = componentId != null ? Ext.getCmp(componentId) : this.getFormContainer();
if (parentCmp.items)
{
var nestedContainers = parentCmp.query('container');
Ext.Array.each(nestedContainers, function(container){
if (container.hasCls('param-checker-container'))
{
fieldCheckers.push(container.down('button').fieldChecker);
}
});
}
}
else
{
this.getFormContainer().items.each(function(formItem) {
// Do not consider the field checkers within tabs
if (!formItem.hasCls('ametys-form-tab-inline') && !formItem.hasCls('ametys-form-tab'))
{
fieldCheckers = Ext.Array.union(fieldCheckers, this._getFieldCheckers(formItem.getId()));
}
}, this);
}
return fieldCheckers;
},
/**
* Get the list of repeaters in a container (any level) or the first level repeaters exclusively
* @param {String} componentId The id of the component to get the repeaters from. Can be null to get all the repeaters of the form
* @return {Ext.Component[]} An array of components which have the field mixin.
*/
getRepeaters: function (componentId)
{
var repeaters = [];
if (componentId != Ametys.form.ConfigurableFormPanel.OUTOFTAB_FIELDSET_ID)
{
var component = componentId != null ? Ext.getCmp(componentId) : this.getFormContainer();
if (component != null && !component .isFormField)
{
repeaters = component.query('panel[isRepeater]');
}
}
else
{
this.getFormContainer().items.each(function(formItem) {
// Do not consider the repeaters within tabs
if (!formItem.hasCls('ametys-form-tab-inline') && !formItem.hasCls('ametys-form-tab'))
{
repeaters = Ext.Array.union(repeaters, this.getRepeaters(formItem.getId()));
}
}, this);
}
return repeaters;
},
/**
* Get the list of composites in a container (any level) or the first level repeaters exclusively
* @param {String} componentId The id of the component to get the composites from. Can be null to get all the composites of the form
* @return {Ext.Component[]} An array of components which have the field mixin.
*/
getComposites: function (componentId)
{
var composites = [];
if (componentId != Ametys.form.ConfigurableFormPanel.OUTOFTAB_FIELDSET_ID)
{
var component = componentId != null ? Ext.getCmp(componentId) : this.getFormContainer();
if (component != null && !component .isFormField)
{
composites = component.query('panel[isComposite]');
}
}
else
{
this.getFormContainer().items.each(function(formItem) {
// Do not consider the composites within tabs
if (!formItem.hasCls('ametys-form-tab-inline') && !formItem.hasCls('ametys-form-tab'))
{
composites = Ext.Array.union(composites, this.getComposites(formItem.getId()));
}
}, this);
}
return composites;
},
/**
* @private
* Initialize the status of all tabs
*/
_initializeTabsStatus: function()
{
if (this._tabPanel)
{
this.suspendLayouts();
this._tabPanel.items.each (function (item) {
var header = item.tab ? item.tab : item.getHeader();
if (header != null && header.isHeader)
{
header.addCls(['empty']);
}
});
this.resumeLayouts(true);
}
},
/**
* @private
* Update the status of all tabs
* @param {Boolean} force True to force the rendering of warning and errors
*/
_updateTabsStatus: function(force)
{
if (this._tabPanel)
{
this._tabPanel.items.each (function (item) {
this._updateTabStatus (item, force);
}, this);
}
},
/**
* @private
* Update the tab status. Possibly the table of contents status as well
* @param {Ext.panel.Panel} panel The panel (tab card or fieldset panel).
* @param {Boolean} force True to force the rendering of warning and errors even if the panel is in first display
* @return true if the tab status has changed, false otherwise
*/
_updateTabStatus: function(panel, force)
{
// The header is the tab when in tab mode or the header in linear mode.
var header = null;
if (panel)
{
header = panel.tab ? panel.tab : (panel.getHeader().isHeader ? panel.getHeader() : null);
}
var hasHeaderChanged = false,
hasNavigationItemChanged = false;
if (header != null || this._showTableOfContents)
{
this.suspendLayouts();
var panelId = panel != null ? panel.getId() : Ametys.form.ConfigurableFormPanel.OUTOFTAB_FIELDSET_ID;
// Let's get all errors and warnings from the field checkers
var fieldCheckers = this._getFieldCheckers(panelId),
testsErrorMessages = [],
testsWarnMessages = [];
if (!Ext.isEmpty(fieldCheckers))
{
Ext.Array.each(fieldCheckers, function(fieldChecker){
var status = fieldChecker.getStatus();
if (status != Ametys.form.ConfigurableFormPanel.FieldChecker.STATUS_HIDDEN)
{
var label = fieldChecker.label;
if (status == Ametys.form.ConfigurableFormPanel.FieldChecker.STATUS_FAILURE)
{
testsErrorMessages.push("{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_FIELD_CHECKER}}" + " '" + label + "': " + fieldChecker.getErrorMsg());
}
else if (status == Ametys.form.ConfigurableFormPanel.FieldChecker.STATUS_WARNING)
{
testsWarnMessages.push("{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_FIELD_CHECKER}}" + " '" + label + "': " + "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_FIELD_CHECKER_STATUS_WARNING}}");
}
}
});
}
var fields = this._getFields(panelId);
var errorFields = [];
var warnFields = [];
for (var i = 0; i < fields.length; i++)
{
var field = fields[i];
if (field.getActiveErrors().length > 0)
{
errorFields.push("{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_FIELD}}" + " " + this._getFieldLabel(field, panel));
}
if (Ext.isFunction(field.hasActiveWarning) && field.hasActiveWarning())
{
warnFields.push("{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_FIELD}}" + " " + this._getFieldLabel(field, panel));
}
}
// Invalidate repeaters
var repeaters = this.getRepeaters(panel);
Ext.Array.each(repeaters, function(repeater){
if (repeater.getErrors().length > 0)
{
errorFields.push(this._getRepeaterLabel(repeater, panel));
}
});
// var isActive = panel.tab ? panel.ownerCt.getActiveItem() == panel : true;
// header[isActive ? 'addCls' : 'removeCls']('active');
var firstEdition = !Ext.Array.contains(this._notInFirstEditionPanels, panelId);
var navigationItem = null;
if (this._showTableOfContents)
{
navigationItem = this._getTableOfContents().getNavigationItem(panelId);
}
if (header && !firstEdition)
{
// When not in first edition mode, remove the startup class.
header.removeCls('startup');
header.addCls('not-startup');
}
if (navigationItem && !firstEdition)
{
// When not in first edition mode, remove the startup class.
navigationItem.removeCls('startup');
navigationItem.addCls('not-startup');
}
var errors = Ext.Array.union(errorFields, testsErrorMessages);
var warnings = Ext.Array.union(warnFields, testsWarnMessages);
var hasError = errors.length > 0;
var hasWarn = warnings.length > 0;
if (force && (hasError || hasWarn))
{
this._notInFirstEditionPanels.push(panelId);
// Remove the startup class as well if there is something to report at startup
if (header)
{
header.addCls('not-startup');
header.removeCls('startup');
}
if (navigationItem)
{
navigationItem.addCls('not-startup');
navigationItem.removeCls('startup');
}
}
if (header && header.rendered)
{
var oldHeaderClassName = header.el.dom.className;
header.removeCls(['error', 'warning']);
}
if (navigationItem && navigationItem.el && navigationItem.el.dom)
{
var oldNavigationItemClassName = navigationItem.el.dom.className;
navigationItem.removeCls(['error', 'warning']);
}
if (hasError)
{
if (header)
{
header.addCls('error');
}
if (navigationItem)
{
navigationItem.addCls('error');
}
}
else if (hasWarn)
{
if (header)
{
header.addCls('warning');
}
if (navigationItem)
{
navigationItem.addCls('warning');
}
}
if (panel && header && header.rendered)
{
hasHeaderChanged = header.el.dom.className != oldHeaderClassName;
if (hasHeaderChanged)
{
if (header.rendered)
{
// As we change width with CSS we have to prevent tabs from overlapping one another
header.updateLayout();
this._createStatusTooltip (header.getEl(), panel, errors, warnings);
}
else
{
header.on ('afterrender', Ext.bind (this._createStatusTooltip, this, [header.getEl(), panel, errorFields, warnFields], false), this, {single: true});
}
}
}
if (navigationItem && navigationItem.el && navigationItem.el.dom)
{
hasNavigationItemChanged = navigationItem.el.dom.className != oldNavigationItemClassName;
if (hasNavigationItemChanged)
{
this._createStatusTooltip (navigationItem.getEl(), navigationItem, errors, warnings);
}
}
this.resumeLayouts(hasHeaderChanged || hasNavigationItemChanged);
}
},
/**
* @private
* Create tab tooltip
* @param {Ext.dom.Element} el the element to whom bound the tooltip
* @param {Ext.Component} cmp The component
* @param {String[]} errors the list of errors
* @param {String[]} warnings the list of warnings
*/
_createStatusTooltip : function (el, cmp, errors, warnings)
{
Ext.tip.QuickTipManager.unregister(el);
if (errors.length > 0 || warnings.length > 0)
{
// Set the tooltip.
var title = cmp.title;
var text = this._getStatusTooltipText(errors, warnings);
Ext.tip.QuickTipManager.register({
target: el.id,
title: title,
text: text,
cls: ['x-fluent-tooltip', 'a-configurable-form-panel-tooltip'],
width: 350,
dismissDelay: 0
});
}
},
/**
* @private
* Get the tooltip message.
* @param {String[]} errorFields The error fields' labels.
* @param {String[]} warnFields The warning fields' labels.
* @return {String} the tooltip message markup.
*/
_getStatusTooltipText: function(errorFields, warnFields)
{
var html = '';
if (errorFields.length > 0 || warnFields.length > 0)
{
html += Ext.XTemplate.getTpl(this, 'tabErrorFieldsTpl').apply({
errors: errorFields,
warns: warnFields,
});
}
return html;
},
/**
* @private
* Get a field's label.
* @param {Ext.form.Labelable} field A component with the Labelable mixin.
* @param {Ext.Panel} tabpanel The panel containing the field.
* @return {String} the tooltip message markup.
*/
_getFieldLabel: function(field, tabpanel)
{
var label = '';
if (tabpanel)
{
var ownerCt = field.ownerCt;
while (ownerCt != null && ownerCt.title && ownerCt.id != tabpanel.id)
{
label = ownerCt.title + " > " + label;
ownerCt = ownerCt.ownerCt;
}
}
// Remove the starting or trailing '*' character
var fieldLabel = field.getFieldLabel() || field.boxLabel || "";
if (Ext.String.startsWith(fieldLabel, '*'))
{
fieldLabel = fieldLabel.substr(1).trim();
}
else if (Ext.String.endsWith(fieldLabel, '*'))
{
fieldLabel = fieldLabel.substr(0, fieldLabel.length - 1).trim();
}
return label + fieldLabel;
},
/**
* @private
* Get the full label of a component of the form
* @param {Ext.Component} cmp the component
* @return the full label of the given component
*/
_getFullLabel: function(cmp)
{
var label = '';
if (cmp.isFieldLabelable)
{
// Remove the starting or trailing '*' character
label = cmp.getFieldLabel() || cmp.boxLabel || "";
if (Ext.String.startsWith(label, '*'))
{
label = label.substr(1).trim();
}
else if (Ext.String.endsWith(label, '*'))
{
label = label.substr(0, label.length - 1).trim();
}
}
else if (cmp.isRepeater)
{
label = cmp.getLabel();
}
else
{
label = cmp.title;
}
var ownerCt = cmp.ownerCt;
while (ownerCt != null && (ownerCt.title || ownerCt.isRepeater))
{
var ownerLabel = ownerCt.isRepeater ? ownerCt.getLabel() : ownerCt.title;
label = ownerLabel + " > " + label;
ownerCt = ownerCt.ownerCt;
}
return label;
},
/**
* @private
* Get a repeater's label.
* @param {Ametys.form.ConfigurableFormPanel.Repeater} repeater A repeater.
* @param {Ext.Panel} tabpanel The panel containing the repeater.
* @return {String} the label
*/
_getRepeaterLabel: function (repeater, tabpanel)
{
var label = '';
var ownerCt = repeater.ownerCt;
while (ownerCt != null && ownerCt.title && ownerCt.id != tabpanel.id)
{
label = ownerCt.title + " > " + label;
ownerCt = ownerCt.ownerCt;
}
var repeaterLabel = repeater.getLabel();
return label + repeaterLabel;
},
/**
* @private
* Listens when a repeater validity changes.
* @param {Ametys.form.ConfigurableFormPanel.Repeater} repeater The repeater.
* @param {Boolean} isValid Whether or not the repeater is now valid.
*/
_onRepeaterValidityChange: function (repeater, isValid)
{
if (this._formReady)
{
// Find the tab card (panel) to which belongs the field.
var card = repeater.up('panel[cls~=ametys-form-tab-item], panel[cls~=ametys-form-tab-inline-item]');
if (card == null)
{
return;
}
// Update the tab status and tooltip.
this._updateTabStatus(card);
}
},
/**
* @private
* Listens when a field validity changes.
* @param {Ext.form.field.Field} field The field.
* @param {Boolean} isValid Whether or not the field is now valid.
*/
_onFieldValidityChange: function(field, isValid)
{
if (this._formReady)
{
// Find the tab card (panel) to which belongs the field.
var panel = field.up('panel[cls~=ametys-form-tab-item], panel[cls~=ametys-form-tab-inline-item]');
// Do not update the card status if the tab is still in first edition mode
if (panel && !Ext.Array.contains(this._notInFirstEditionPanels, panel.id))
{
return;
}
// Update the tab status and tooltip.
this._updateTabStatus(panel);
}
},
/**
* @private
* Listens when the value of a field is changed
* @param {Ext.form.field.Field} field The field.
* @param {Boolean} newValue The new value
*/
_onFieldChange: function(field, newValue)
{
if (this._formReady && !field.isHidden() && !field.isDisabled())
{
this.fireEvent('fieldchange', field);
}
if (!Ext.isEmpty(newValue))
{
// Find the tab card (panel) to which belongs the field.
var card = field.up('panel[cls~=ametys-form-tab-item], panel[cls~=ametys-form-tab-inline-item]');
if (card)
{
var header = card.tab ? card.tab : card.getHeader();
if (header != null && header.isHeader)
{
header.removeCls(['empty']);
}
}
}
// Remove server warnings
var activeWarnings = field.getActiveWarnings();
var oldActiveWarningsLength = activeWarnings.length;
var warningsToRemove = [];
for (var activeWarning of activeWarnings)
{
if (Ext.isObject(activeWarning) && activeWarning.type == 'server')
{
warningsToRemove.push(activeWarning);
}
}
for (var warningToRemove of warningsToRemove)
{
activeWarnings = Ext.Array.remove(activeWarnings, warningToRemove);
}
if (activeWarnings.length < oldActiveWarningsLength)
{
field.markWarning(activeWarnings);
}
},
/**
* @private
* Show or hide the elements of a fieldset, including the field checkers
* @param {Ext.form.field.Checkbox} checkbox the checkbox
* @param {Ext.panel.Panel} fieldset the fieldset the group switch belongs to
* @param {Boolean} startup true if this is the first call, false otherwise
*/
_showHideFieldset: function(checkbox, fieldset, startup)
{
Ext.suspendLayouts();
var checked = checkbox.getValue();
if (fieldset.rendered)
{
if (checked)
{
// always expand when the box is checked
fieldset.expand();
}
else
{
fieldset.collapse();
}
}
else
{
fieldset.collapsed = !checked
}
fieldset.items.eachKey(function(key){
var fieldsetElement = Ext.getCmp(key);
// do not show/hide the checkbox itself
if (fieldsetElement.getId() == checkbox.getId())
{
return;
}
if (fieldsetElement.hasCls('param-checker-container'))
{
// field checker
fieldsetElement.down('button').fieldChecker.setStatus(checked ? Ametys.form.ConfigurableFormPanel.FieldChecker.STATUS_NOT_TESTED
: Ametys.form.ConfigurableFormPanel.FieldChecker.STATUS_HIDDEN);
}
else if (!startup)
{
// validate fields on expanding/collapsing
// validation on hidden fields always returns true
fieldsetElement.validate();
}
});
if (!startup)
{
this._fieldCheckersManager.updateTestResults();
}
Ext.resumeLayouts(true);
},
/**
* @private
* Prevent click propagation to avoid collapsing/expanding the fieldset
* @param {Ext.form.field.Checkbox} checkbox the checkbox
*/
_preventClickPropagation: function(checkbox)
{
checkbox.getEl().on('click', function(event) {event.stopPropagation();})
},
/**
* Add a decription to the form
* @param {Ext.Element} ct The container where to add the decription
* @param {Object} fieldset The fieldset
* @param {String} [fieldset.description] The description of the fieldset. If null, nothing will be added
* @param {String[]} [fieldset.descriptionCls] The additional css classes name
* @param {Number} offset The field offset.
* @param {Number} roffset The field right offset.
* @private
*/
_addDescription: function(ct, fieldset, offset, roffset)
{
if (this._displayGroupsDescriptions && fieldset.description)
{
this._addText(ct, {
offset: offset,
roffset: roffset,
value: fieldset.description,
cls: fieldset.descriptionCls
});
}
},
/**
* Add a text to the form
* @param {Ext.Element} ct The container where to add the text
* @param {Object} config The text configuration object:
* @param {String} config.value The text value
* @param {String[]} [config.cls] The additional css classes name
* @param {String/Object} [config.style] Replace the default style with this one
* @return {Ext.Component} The created component for this text
* @private
*/
_addText: function(ct, config)
{
var roffset = config.roffset || 0;
var textCfg = {
html: config.value || "",
cls: Ext.Array.merge(['a-form-text'], config['cls'] || []),
style: config.style || 'margin-right:' + Math.max(this.maxNestedLevel * Ametys.form.ConfigurableFormPanel.OFFSET_FIELDSET - roffset, 0) + 'px',
anchor: '100%'
};
var text = Ext.create('Ext.Component', textCfg);
ct.add(text);
return text;
},
/**
* Creates a fieldset with this label
* @param {Ext.Element} ct The container where to add the fieldset
* @param {String} label The label of the fieldset
* @param {Number} nestingLevel The nesting level of the fieldset.
* @param {Object} switcher If the group can be switched on/off, the configuration object corresponding to the group-switch parameter. A config for #_createInputField.
* @return {Ext.form.FieldSet} The created fieldset
* @private
*/
_addFieldSet: function (ct, label, nestingLevel, switcher)
{
var me = this;
var fdCfg = {
style: '',
nestingLevel: nestingLevel,
ametysFieldSet: true,
layout: this.initialConfig.fieldsetLayout || { type: 'anchor' },
cls: 'ametys-fieldset',
bodyPadding: Ametys.form.ConfigurableFormPanel.VERTICAL_PADDING_FIELDSET + ' ' + Ametys.form.ConfigurableFormPanel.HORIZONTAL_PADDING_FIELDSET + ' ' + Ametys.form.ConfigurableFormPanel.VERTICAL_PADDING_FIELDSET + ' ' + Ametys.form.ConfigurableFormPanel.HORIZONTAL_PADDING_FIELDSET,
margin: '0 0 5 ' + (nestingLevel > 1 ? Ametys.form.ConfigurableFormPanel.OFFSET_FIELDSET : '0'),
border: false,
shadow: false
};
if (switcher != null)
{
var switcherCfg = Ext.apply ({minWidth: 20, style: ' '}, switcher);
var switcherField = this._createInputField(switcherCfg);
switcherField.on('render', Ext.bind(this._preventClickPropagation, this));
Ext.apply(fdCfg, {
title: switcherCfg.label,
ui: 'light',
collapsible: true,
collapsed: true,
titleCollapse: false,
hideCollapseTool: true,
border: true,
isSwitcher: true,
header: {
titlePosition: 2
},
listeners: {
'add': {
fn: function(){
// every time something is added to this panel we want to hide it depending on the switch value
// hiding such element will make isValid work as we want
me._showHideFieldset(switcherField, this, true);
}
}
}
});
}
else if (label)
{
Ext.apply(fdCfg, {
title : label,
ui: 'light',
collapsible: true,
titleCollapse: false,
hideCollapseTool: false,
header: {
titlePosition: 1
},
border: true
});
}
var fieldset = new Ext.panel.Panel(fdCfg);
if (switcherField != null)
{
// we need to add the switchfield here as a regular field in order for the form to consider it as a field immediately
// otherwise tools of the header are only added during rendering
// The field will be moved from the fieldset to the head automatically since a component cannot be used twice
fieldset.add(switcherField);
fieldset.header.items = fieldset.header.items || [];
fieldset.header.items.push(switcherField);
switcherField.on('change', Ext.bind(this._showHideFieldset, this, [fieldset, false], 1));
}
ct.add(fieldset);
fieldset.on('add', function(fs, comp) {
comp.on('resize', function(elt, w, h, oldW, oldH) {
if (oldW && oldH) // non-empty oldW && oldH means that is not the first size
{
// When a subcomponent of this fieldset is resized we need to enlarge (not scroll)
fs.updateLayout();
}
})
})
return fieldset;
},
/**
* Add an input field to the form
* @param {Ext.Element} ct The container where to add the input
* @param {Object} config The input configuration object:
* @param {String} config.type The type of the field to create
* @param {String} config.name The name of the field (the one used to submit the request)
* @param {Object} config.value The value of the field at the creating time
* @param {String} config.fieldLabel The label of the field
* @param {String} config.ametysDescription The associated description
* @param {String[]} config.enumeration The list of values if applyable (only for type text)
* @param {Object} config.enumerationConfig The configuration object of the enumerator if applyable
* @param {String} config.widget The widget to use for edition. Can be null
* @param {Boolean} config.mandatory True if the field can not be empty
* @param {String} config.regexp The regexp to use to validate the field value
* @param {String} config.invalidText The text to display when the field value is not valid
* @param {String} config.disabled If true the field will be disabled
* @param {Number/String} config.width Replace the default width with this one
* @param {String} startVisible Optionally, if 'false' this field will be hidden
* @return {Ext.form.field.Field} The created field
* @private
*/
_addInputField: function (ct, config, startVisible)
{
var field = this._createInputField(config);
if (startVisible == 'false')
{
field.hide();
}
if (field != null)
{
ct.add(field);
this._fields.push(field.getName());
field.on('validitychange', this._onFieldValidityChange, this);
field.on('errorchange', this._onFieldValidityChange, this);
field.on('warningchange', this._onFieldValidityChange, this);
field.on('change', this._onFieldChange, this);
}
return field;
},
/**
* Creates and returns an input field depending on the given configuration
* @param {Object} config this object have the following keys:
* @param {String} config.type The type of the field to create
* @param {String} config.name The name of the field (the one used to submit the request)
* @param {Object} config.value The value of the field at the creating time
* @param {String} config.fieldLabel The label of the field
* @param {String} config.ametysDescription The associated description
* @param {String[]} config.enumeration The list of values if applyable (only for type text)
* @param {String} config.widget The widget to use for edition. Can be null
* @param {Boolean} config.mandatory True if the field can not be empty
* @param {String} config.regexp The regexp to use to validate the field value
* @param {String} [config.invalidText] The text to display when the field value is not valid
* @param {String} config.disabled If true the field will be disabled
* @param {Number/String} config.width Replace the default width with this one
* @param {Number/String} config.minWidth Replace the default min width with this one
* @param {String/Object} config.style Replace the default style with this one
* @return {Ext.form.field.Field} The created field
* @private
*/
_createInputField: function (config)
{
var me = this;
var offset = config.offset || 0;
var roffset = config.roffset || 0;
if (Ext.isString(config.flex))
{
config.flex = parseFloat(config.flex);
}
if (Ext.isString(config.width))
{
config.width = parseInt(config.width);
config.flex = undefined;
}
if (Ext.isString(config.maxHeight))
{
config.maxHeight = parseInt(config.maxHeight);
}
var labelWidth = this.labelAlign == 'top' ? Ametys.form.ConfigurableFormPanel.LABEL_WIDTH : (Ametys.form.ConfigurableFormPanel.LABEL_WIDTH - offset);
var fieldCfg = Ext.clone(this.defaultFieldConfig);
Ext.applyIf (fieldCfg, {
cls: 'ametys',
style: config.style || 'margin-right:' + Math.max(this.maxNestedLevel * Ametys.form.ConfigurableFormPanel.OFFSET_FIELDSET - roffset, 0) + 'px'
+ (this.labelAlign == 'top' ? '; margin-left: ' + (this.maxNestedLevel * Ametys.form.ConfigurableFormPanel.OFFSET_FIELDSET - offset) + 'px' : ''),
labelAlign: this.labelAlign,
labelWidth: labelWidth,
labelSeparator: '',
// the field will be submitted even when it is disabled
submitDisabledValue: true,
minWidth: config.minWidth || this._getFieldMinWidth(this.labelAlign),
anchor: '100%',
allowBlank: !config.mandatory,
regex: config.regexp ? new RegExp (config.regexp) : null,
regexText: config.regexText || config.invalidText || "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_INVALID_REGEXP}}" + config.regexp,
disabled: config.disabled,
msgTarget: 'side'
});
var widgetCfg = Ext.apply(config, fieldCfg);
var field = Ametys.form.WidgetManager.getWidget (config.widget, config.type.toLowerCase(), widgetCfg);
field.on({
'focus': { fn: function (fd, e) { this.fireEvent ('inputfocus', fd, e)}, scope: this},
'blur': { fn: function (fd, e) { this.fireEvent ('inputblur', fd, e)}, scope: this},
});
if (field.isRichText)
{
field.on({
'editorhtmlnodeselected': { fn: function (field, node) { this.fireEvent ('htmlnodeselected', field, node)}, scope: this}
});
}
// if field is disabled or not visible (group switch off) we return no errors
field.getErrors = Ext.Function.createInterceptor(field.getErrors, function() { return this.isVisible() && !this.isDisabled() && (!this.ownerCt.isSwitcher || this.ownerCt.getCollapsed() === false); }, null, []);
field.getActiveWarnings = Ext.Function.createInterceptor(field.getActiveWarnings, function() { return this.isVisible() && !this.isDisabled() && (!this.ownerCt.isSwitcher || this.ownerCt.getCollapsed() === false); }, null, []);
field.getActiveWarning = Ext.Function.createInterceptor(field.getActiveWarning, function() { return this.isVisible() && !this.isDisabled() && (!this.ownerCt.isSwitcher || this.ownerCt.getCollapsed() === false); }, null, null);
return field;
},
/**
* Add a repeater to the form
* @param {Ext.Element} ct The container where to add the repeater
* @param {Object} config The repeater configuration object. See Ametys.form.ConfigurableFormPanel.Repeater configuration.
* @param {Number} initialSize The initial size
* @return {Ametys.form.ConfigurableFormPanel.Repeater} The created repeater panel
* @private
*/
_addRepeater: function (ct, config, initialSize)
{
var repeater = this._createRepeater(config);
this._repeaters.push(repeater.getId());
// First instances
var initialSize = initialSize || 0;
if (repeater.getMinSize() > initialSize)
{
initialSize = repeater.getMinSize();
}
ct.add(repeater);
// Add the initial items, expand the last one.
for (var i = 0; i < initialSize; i++)
{
var collapsed = i < (initialSize-1);
repeater.addRepeaterItem({animate: false, collapsed: collapsed});
}
repeater.on('validitychange', this._onRepeaterValidityChange, this);
return repeater;
},
/**
* Creates and returns a repeater panel from the configuration object
* @param {Object} config The repeater configuration object. See {@link Ametys.form.ConfigurableFormPanel.Repeater} configuration.
* @return {Ametys.form.ConfigurableFormPanel.Repeater} The created repeater panel
* @private
*/
_createRepeater: function (config)
{
var repeaterCfg = Ext.applyIf(config, {
minSize: 0,
maxSize: Number.MAX_VALUE,
defaultPathSeparator: this.defaultPathSeparator,
form: this
});
return Ext.create('Ametys.form.ConfigurableFormPanel.Repeater', repeaterCfg);
},
/**
* Notify the form that a new repeater entry is being added
* @param {Boolean} start True indicates that the process started , false indicates the it ended.
*/
notifyAddRepeaterEntry: function(start)
{
this._addingRepeaterEntry = start;
},
/**
* @private
* Test if an element is a HTMLElement or not
* @param {Object} o The object to test
* @return {Boolean} true is o is an instance of HTMLElement
*/
_isElement: function isElement(o) {
return o instanceof Node ||
(o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName==="string");
},
/**
* @method configure
*
* This function creates and add form elements from a definition
*
* The JSON configuration format is
*
* {
* "<fieldName>": {
* "label": "My field"
* "description": "This describes what my field is made for"
* "type": "STRING",
* "validation": {
* "mandatory": true
* },
* "multiple": false
* },
* // ...
* "<tabName>": {
* "role": "tab"
* "label": "My first tab",
* "description": "Lorem ipsum dolor sit amet",
* "descriptionCls": ['my-custom-css-class'],
* "elements": {
* "<fieldName>": {...},
* "<fieldsetName>": {
* "role": "fieldset",
* "label": "My fieldset",
* "description": "This describes what my composite is made for",
* "elements": {...}
* },
* "<fieldName>": {
* "type": "composite",
* "label": "My composite",
* "description": "This describes what my composite is made for",
* "elements": {...}
* },
* "<fieldName>": {
* "type": "repeater",
* "label": "My repeater",
* "description": "This describes what my repeater is made for",
* "add-label": "Add",
* "del-label": "Delete",
* "header-label": "Some header",
* "min-size": 0,
* "min-size": 5,
* "initial-size": 1,
* "elements": {...}
* },
* // ...
* }
* },
* "<tabName>": {
* "role": "tab",
* // ...
* },
* "field-checker": [{
* "id": "my-checker-id",
* "label": "My field checker",
* "description": "This describes what my field checker is made for",
* "icon-glyph": "ametysicon-data110",
* "linked-field": ['linked.field.1.path', 'linked.field.2.path'], // you can begin a linked field with the default separator (#cfg-defaultPathSeparator),
* // in order to use an absolute path instead of a relative path, see the
* // second field checker below where we have defaultPathSeparator = "/"
* "order": "1" // used to sort the field checkers amongst themselves, the smaller the vertically higher
* },
* {
* "id": "my-other-checker-id",
* "label": "My second field checker",
* "description": "This describes what my second field checker is made for",
* "small-icon-path": "path for the small icon representing the test",
* "medium-icon-path": "path for the medium icon representing the test",
* "large-icon-path": "path for the large icon representing the test",
* "linked-field": ['/linked.field.1.path', '/linked.field.2.path'],
* "order": "10", // will be displayed below the first checker
* }]
* }
*
* The **<fieldName>** is the form name of the field. (Note that you can prefix all field names using #cfg-fieldNamePrefix). See under for the reserved fieldName "groups"
*
* The String **label** is the readable name of your field that is visible to the user.
*
* The String **description** is a sentence to help the user understand the field. It will appear in a tooltip on the right help mark.
*
* The String **type** is the kind of value handled by the field. The supported types depend on the configuration of your Ametys.runtime.form.WidgetManager. Kernel provides widgets for the following types (case is not important):
* STRING, LONG, DOUBLE, BOOLEAN, DATE, DATETIME, BINARY, FILE, GEOCODE, REFERENCE, RICH-TEXT, USER.
*
* The boolean **labelWithField** can be set to true for BOOLEAN types using edition.checkbox widget in order to display the label on the right of the checkbox instead of on the left. It can also be put in the object **widget-params**.
*
* The object **validation** field is a field validator.
* Can be an object with the optional properties
*
* - boolean **mandatory** to true, to check the field is not empty AND add a '*' at its label.
* - String **invalidText** : a general text error if the field is not valid
* - String **regexp** : a regular expression that will be checked
* - String **regexText** : the text error if the regexp is not checked
*
* The Object array **enumeration** lists available values. Note that types and widgets that can be used with enumeration is quite limited.
* Each item of the array is an object with **value** and **label**.
* Example: enumeration: [{value: 1, label: "One"}, {value: 2, label: "Two"}]
*
* The Object **default-value** is the default value for the field if not set with #setValues.
*
* The boolean **multiple** specifies if the user can enter several values in the field. Types and widgets that support multiple data is quite limited.
*
* The String **widget** specifies the widget to use. This is optional to use the default widget for the given type, multiple and enumeration values.
* The widgets are selected using the js class {@link Ametys.form.WidgetManager} and the extension point org.ametys.runtime.ui.widgets.WidgetsManager.
* Note that you can transmit additional configuration to all widgets using #cfg-additionalWidgetsConfFromParams
*
* The optional Object **widget-params** will be transmitted to the widget configuration : values depends on the widget you did select.
*
* The boolean **hidden** will hide this field.
*
* The boolean **can-not-write** makes the field in read only mode.
*
* The Object array **annotations** describes available XML annotations on a richtext.
* Each item is an object with properties : **name** (the XML tagname), **label** (the label of the button to set this annotation, defaults to name) and **description** (the help text associated to the button).
* Exemple: annotations: [ { name: "JUSTICE", label: "Justice term", description: "Use this button to annotate the selected text as a justice term" } ]
*
* The object **disableCondition** can be defined in order to disable/enable the current parameter. It has the following configuration that must be written in JSON:
*
* - Object **conditions** conditions that can contain several condition objects or other conditions
* - Object **conditions** recursively describe sub conditions groups.
* - Object[] **condition** Object describing a unit condition (see under).
* - String **type** the type of the underlying conditions. Can be set to "and" (default value) or "or".
*
* The Object **condition** has the following attributes:
*
* - String **id** the id of the field that will be evaluated
* - String **operator** the operator used to evaluated the field. Can be **eq**, **neq**, **gt**, **geq**, **leq** or **lt**
* - String **value** the value with which the field will be compared to
*
* The object **field-checker** can be used in order to check the value of certain fields. It can contain the following attributes:
*
* - String **id** The id of the parameter checker.
* - String **large-icon-path** The relative path to the 48x48 icon representing the test
* - String **medium-icon-path** The relative path to the 32x32 icon representing the test
* - String **small-icon-path** The relative path to the 16x16 icon representing the test
* - String **icon-glyph** the glyph used for the icon representing the test
* - String **icon-decorator** the decorator to use on the icon
* - String[] **linked-fields** the absolute or relative paths of the linked field (fields used to run the check). Always JSON-encoded even for XML configurations.
* - String **label** The label of the parameter checker
* - String **description** The description of the parameter checker
* - String **order** The displaying order (from top to bottom). Indeed, several parameter checkers can be defined on a given tab, fieldset or parameter.
*
* You can define a **field-checker** directly under the data node/object, in which case the checker will be global and displayed at the bottom of
* the main form container. See the example at the top of this documentation.
*
* A field with type composite can have a **switcher** attribute. The switcher is a boolean field that is used to enable/disable the other fields of its group. The switcher object must have a **label** and an **id** (the name of the boolean field) and optionally a **default-value**.
*
* A field with type repeater needs the following attributes:
* - String **add-label**: The label to display on the add button
* - String **del-label**: The label to display on the delete button
* - String **header-label**: The label to display on the repeater itselft
* - Number **min-size**: The optional minimum size of the repeater. For example 2 means it will at least be repeated twice. 0 if not specified.
* - Number **max-size**: The optional maximum size of the repeater. Default value is infinite.
* - Number **initial-size**: The optional size when loading the form (must be between minSize and maxSize). minSize is the default value.
*
* Composites and repeaters also contain the attribute **elements**, with the recursive child elements
*
* Graphical grouping of fields must have the **role** attribute. It can be "tab" or "fieldset" to create a tab grouping or a fieldset grouping.
* It can contain the following attributes:
* - String **label** The label of the grouping.
* - String **description** The description of the grouping (as HTML). It is inserted at the beginning of the elements of the fieldset.
* - String[] **descriptionCls** An optional array of extra CSS classes to add to the text component's element of the description.
* - Object **field-checker** (see above) Groups can also have a parameter checker for both "tab" and "fieldset" role
* - Object **elements** The child elements of the group: this is a recursive data object.
* Note that tab grouping can be replaced by simple panels according to a user preference.
*
* @param {Object} data The data to create the form structure.
*/
configure: function(data)
{
this._formReady = false;
this.maxNestedLevel = countLevels(data);
this._configureJSON(data);
// Add disable conditions listeners.
// It is not possible to activate disable conditions yet, because values of external conditions are not available
this._executeOnFields(this._addDisableConditionsListeners);
this._fieldCheckersManager.initializeFieldCheckers();
function countLevels(d)
{
let level = 0;
for (let it in d)
{
if (d[it].elements)
{
level = Math.max(level, 1 + countLevels(d[it].elements));
}
}
return level;
}
},
/**
* @private
* Is not destroyed?
* @return {Boolean} true if the configurable form panel is destroyed, false otherwise
*/
_isNotDestroyed: function()
{
return !this.destroyed;
},
/**
* @private
* This function creates and add form elements from a JSON definition
* @param {Object} data The JSON definition of the form fields.
* @param {String} prefix The input prefix to concatenate to input name
* @param {Ext.container.Container} [ct=this] The container where to add the form element
* @param {Number} [offset=0] The field offset.
* @param {Number} [roffset=0] The field right offset.
*/
_configureJSON: function (data, prefix, ct, offset, roffset)
{
prefix = prefix || this.getFieldNamePrefix();
ct = ct || this.getFormContainer();
offset = offset || 0;
roffset = roffset || 0;
var tabs = [];
this._hasFieldsBeforeTabs = false;
for (var name in data)
{
if (!data[name])
{
continue;
}
var nestingLevel = this._getNestingLevel(ct);
var item = data[name];
if (!item.type)
{
if (name == 'field-checker' && nestingLevel == 1)
{
// Global field checker
if (!Ext.isEmpty(item))
{
this._fieldCheckersManager.addFieldCheckers(ct, item, prefix, offset, roffset);
}
}
else if (!this._isGroupEmpty(item)) // ignore empty groups
{
if (item.role == 'tab' && nestingLevel == 1) // tab
{
tabs.push(item);
}
else // fieldset
{
this._configureFieldsetJSON(item, prefix, ct, offset, roffset, nestingLevel);
}
}
}
else
{
if (item.type == 'composite')
{
if (item.role == 'tab' && nestingLevel == 1)
{
if (!this._isGroupEmpty(item)) // ignore empty tab
{
tabs.push(item);
}
}
else
{
this._configureCompositeJSON(item, prefix, ct, offset, roffset, nestingLevel);
}
}
else if (item.type == 'repeater')
{
this._configureRepeaterJSON(item, prefix, ct, offset, roffset, nestingLevel);
}
else
{
this._configureFieldJSON(item, name, prefix, ct, offset, roffset);
}
}
}
this._configureTabsJSON(tabs, prefix, offset, roffset);
},
/**
* @private
*/
_isGroupEmpty: function (group)
{
if (Ext.Object.isEmpty(group.elements))
{
return true;
}
else
{
// Check that child elements are not empty
var elmts = group.elements;
for (var name in elmts)
{
if (name != 'groups')
{
// This group contains at least one element that is not a fieldset
return false;
}
else
{
var groups = elmts[name];
for (var i=0; i < groups.length; i++)
{
if (!this._isGroupEmpty(groups[i]))
{
// This group contains at least one fieldset that is not empty
return false;
}
}
}
}
return true;
}
},
/**
* @private
* This function compute the nesting level of the elements in the given container
* @param {Ext.container.Container} ct The container
*/
_getNestingLevel: function(ct)
{
var parentLevelPanel = ct.up('panel[nestingLevel]');
var nestingLevel = 1;
if (ct.nestingLevel)
{
nestingLevel = ct.nestingLevel + 1;
}
else if (parentLevelPanel)
{
nestingLevel = parentLevelPanel.nestingLevel + 1;
}
return nestingLevel;
},
/**
* @private
* This function creates and add a fieldset from a JSON definition
* @param {Object} fieldsetCfg The JSON definition of the fieldset.
* @param {String} prefix The input prefix to concatenate to input name
* @param {Ext.container.Container} ct The container where to add the fieldset
* @param {Number} offset The fieldset offset.
* @param {Number} roffset The fieldset right offset.
* @param {Number} nestingLevel The fieldset nesting level
* @param {Object} switcherCfg The configuration object of the group switcher
*/
_configureFieldsetJSON: function(fieldsetCfg, prefix, ct, offset, roffset, nestingLevel, switcherCfg)
{
var fieldset = this._addFieldSet(ct, fieldsetCfg.label, nestingLevel, switcherCfg);
if (fieldsetCfg.name) // Composite fieldset
{
fieldset.name = prefix.substring(0, prefix.length - 1);
}
fieldset.isComposite = fieldsetCfg.isComposite;
fieldset.disableCondition = fieldsetCfg.disableCondition;
fieldset.disabledItemRendering = fieldsetCfg.disabledItemRendering;
// Transmit offset + 5 (padding) + 1 (border) + 11 (margin + border) if we are in a nested composite.
var finalOffset = offset
+ Ametys.form.ConfigurableFormPanel.HORIZONTAL_PADDING_FIELDSET
+ (nestingLevel > 1 ? Ametys.form.ConfigurableFormPanel.OFFSET_FIELDSET : 0)
+ 1;
var finalROffset = roffset
+ Ametys.form.ConfigurableFormPanel.HORIZONTAL_PADDING_FIELDSET
+ 1;
this._addDescription(fieldset, fieldsetCfg, finalOffset, finalROffset);
this._configureJSON(fieldsetCfg.elements, prefix, fieldset, finalOffset, finalROffset);
var fieldCheckers = fieldsetCfg['field-checker'];
if (!Ext.isEmpty(fieldCheckers))
{
this._fieldCheckersManager.addFieldCheckers(fieldset, fieldCheckers, prefix, finalOffset, finalROffset);
}
},
/**
* @private
* This function creates and add a composite from a JSON definition
* @param {Object} composite The JSON definition of the composite.
* @param {String} prefix The input prefix to concatenate to input name
* @param {Ext.container.Container} ct The container where to add the composite
* @param {Number} offset The composite offset.
* @param {Number} roffset The composite right offset.
* @param {Number} nestingLevel the composite nesting level
*/
_configureCompositeJSON: function(composite, prefix, ct, offset, roffset, nestingLevel)
{
var switcherCfg = composite.switcher;
if (switcherCfg != null)
{
switcherCfg.name = switcherCfg.id;
delete switcherCfg.id;
switcherCfg.type = 'boolean';
switcherCfg.value = switcherCfg.value || switcherCfg['default-value'] || false;
delete switcherCfg['default-value'];
}
if (!composite['unnamed-group'])
{
prefix = prefix + composite.name + this.defaultPathSeparator;
}
composite.isComposite = true;
this._configureFieldsetJSON(composite, prefix, ct, offset, roffset, nestingLevel, switcherCfg);
},
/**
* @private
* This function creates and add a repeater from a JSON definition
* @param {Object} repeater The JSON definition of the repeater.
* @param {String} prefix The input prefix to concatenate to input name
* @param {Ext.container.Container} ct The container where to add the repeater
* @param {Number} offset The repeater offset.
* @param {Number} roffset The repeater right offset.
* @param {Number} nestingLevel the repeater nesting level
*/
_configureRepeaterJSON: function(repeater, prefix, ct, offset, roffset, nestingLevel)
{
var repeaterCfg = {
prefix: prefix,
name: repeater.name,
label: repeater.label,
description: repeater.description,
disableCondition: repeater.disableCondition,
disabledItemRendering: repeater.disabledItemRendering,
addLabel: repeater['add-label'],
delLabel: repeater['del-label'],
headerLabel: repeater['header-label'],
minSize: repeater['min-size'] || 0,
maxSize: repeater['max-size'] || Number.MAX_VALUE,
readOnly: repeater['can-not-write'] === true,
composition: repeater.elements,
fieldCheckers: repeater['field-checker'],
nestingLevel: nestingLevel,
offset: offset,
roffset: roffset
}
if (repeater['widget-params'])
{
repeaterCfg = Ext.merge (repeaterCfg, repeater['widget-params']);
}
if (repeater['__externalDisableConditionsValues'])
{
repeaterCfg.newItemExternalDisableConditionsValues = repeater['__externalDisableConditionsValues'];
}
this._addRepeater(ct, repeaterCfg, repeater['initial-size'] || 0);
},
/**
* @private
* This function creates and add an input field from a JSON definition
* @param {Object} fieldData The JSON definition of the input field.
* @param {String} name The input field's name
* @param {String} prefix The input prefix to concatenate to input name
* @param {Ext.container.Container} ct The container where to add the input field
* @param {Number} offset The input field offset.
* @param {Number} roffset The input field right offset.
*/
_configureFieldJSON: function(fieldData, name, prefix, ct, offset, roffset)
{
var label = fieldData.label;
var isMandatory = fieldData.validation ? (fieldData.validation.mandatory) || false : false;
var type = fieldData.type.toLowerCase();
var labelWithMandatory = this.labelAlign == 'top' ? label + (label && isMandatory ? ' *' : '') : (label && isMandatory ? '* ' : '') + label;
var fieldLabel = this.withTitleOnLabels ? '<span title="' + label + '">' + labelWithMandatory + '</span>' : labelWithMandatory; // FIXME Runtime-1465
var widgetCfg = {
name: prefix + name,
shortName: name,
type: type,
hideLabel: fieldData.hideLabel,
mandatory: isMandatory,
value: fieldData['default-value'],
multiple: fieldData.multiple === true || fieldData.multiple === 'true', // some components may send a string instead of the awaited boolean
widget: fieldData.widget,
disabled: fieldData['can-not-write'] === true, // should be readOnly, but the visual is not greyed at all...
disableCondition: fieldData.disableCondition,
disabledItemRendering: fieldData.disabledItemRendering,
style: fieldData.style,
minWidth: fieldData.minWidth,
maxHeight: fieldData.maxHeight,
repeaterMode: fieldData.repeaterMode,
form: this,
offset: offset,
roffset: roffset
};
if (!fieldData.hideDescription)
{
var description = fieldData.description;
var help = fieldData.help;
widgetCfg.ametysDescription = description || '';
widgetCfg.ametysDescriptionUrl = help || '';
}
var labelWithField = fieldData['labelWithField'] || (fieldData['widget-params'] || {})['labelWithField'];
labelWithField = Ext.isBoolean(labelWithField) ? labelWithField : labelWithField === "true";
if (labelWithField && type == 'boolean')
{
widgetCfg.boxLabel = fieldLabel;
}
else
{
widgetCfg.fieldLabel = fieldLabel;
}
if (fieldData.hidden)
{
// Do not override the hidden property for Ext.form.field.Hidden fields
widgetCfg.hidden = fieldData.hidden;
}
// Add configured configuration
Ext.Object.each(this._additionalWidgetsConfFromParams, function(key, value, object) {
widgetCfg[key] = fieldData[value];
});
Ext.Object.each(this._additionalWidgetsConf, function(key, value, object) {
widgetCfg[key] = value;
});
if (fieldData.validation)
{
var validation = fieldData.validation;
widgetCfg.validationConfig = validation;
widgetCfg.regexp = validation.regexp || null;
if (validation.invalidText)
{
widgetCfg.invalidText = validation.invalidText;
}
if (validation.regexText)
{
widgetCfg.regexText = validation.regexText;
}
}
if (fieldData.enumeration !== undefined || fieldData.enumerationConfig !== undefined)
{
var enumeration = null; // for some widgets enumeration can be null null for lazy loading entries from enumerationConfig
if (fieldData.enumeration)
{
enumeration = [];
var entries = fieldData.enumeration;
for (var j=0; j < entries.length; j++)
{
enumeration.push([entries[j].value, entries[j].label]);
}
}
widgetCfg.enumeration = enumeration;
widgetCfg.enumerationConfig = fieldData.enumerationConfig || {};
}
let changeListeners = null;
if (fieldData['widget-params'])
{
widgetCfg = Ext.merge (widgetCfg, fieldData['widget-params']);
if (fieldData['widget-params'].changeListeners)
{
try
{
changeListeners = JSON.parse(fieldData['widget-params'].changeListeners);
widgetCfg.changeListeners = null;
}
catch (e)
{
this.getLogger().error("The value of 'widget-params' for '" + name + "' is not a valid json. It will be ignored.", e);
}
}
}
if (fieldData.annotations)
{
var annotations = [];
var entries = fieldData.annotations;
for (var j=0; j < entries.length; j++)
{
annotations.push({
name: entries[j].name,
label: entries[j].label || entries[j].name,
description: entries[j].description || entries[j].label || entries[j].name
});
}
widgetCfg.annotations = annotations;
}
var field = this._addInputField(ct, widgetCfg);
if (changeListeners)
{
let $class = eval(field.$className);
if (!$class.onRelativeValueChange)
{
this.getLogger().error("The widget " + field.$className + " does not support onRelativeValueChange set in its widget-params 'changeListeners'. Listeners will be ignored.");
}
else
{
for (let event of Object.keys(changeListeners))
{
let paths = Ext.Array.from(changeListeners[event]);
for (let path of paths)
{
function proxyHandler(relativePath, data, newValue, oldValue)
{
let handled = $class.onRelativeValueChange(event, relativePath, data, newValue, oldValue);
if (handled === false)
{
this.getLogger().error("The method " + field.$className + "#onRelativeValueChange does not support the event '" + event + " set in the widget-params")
}
}
Ametys.form.Widget.onRelativeValueChange(path, field, proxyHandler, this);
}
}
}
}
this._hasFieldsBeforeTabs = true;
var fieldCheckers = fieldData['field-checker'];
if (!Ext.isEmpty(fieldCheckers))
{
this._fieldCheckersManager.addFieldCheckers(ct, fieldCheckers, prefix, offset, roffset, field);
}
},
/**
* @private
* This function creates and add the tabs from a JSON definition
* @param {Object} tabs The JSON definition of the tabs.
* @param {String} prefix The input prefix to concatenate to input name
* @param {Number} offset The tabs offset.
* @param {Number} roffset The tabs right offset.
*/
_configureTabsJSON: function(tabs, prefix, offset, roffset)
{
var me = this;
if (tabs.length > 0)
{
var tabPanel = this._addTab();
for (var i=0; i < tabs.length; i++)
{
var tab = this._addTabItem (tabPanel, tabs[i].label || "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_TAB_UNTITLED}}");
tab.on('add', function(tab, component) {
component.on('resize', function(elt, w, h, oldW, oldH) {
if (oldH && oldH != h) // non-empty oldH means that is not the first size ; moreover the issue happens only in vertical resize
{
// When a subcomponent of this tab is resized we need to enlarge (not scroll)
tab.updateLayout();
}
})
});
// Transmit offset + 5 (padding) + 1 (border) + 11 (margin + border) if we are in a nested composite.
var finalOffset = offset
+ Ametys.form.ConfigurableFormPanel.PADDING_TAB
+ 1;
var finalROffset = roffset
+ Ametys.form.ConfigurableFormPanel.PADDING_TAB
+ 1;
this._addDescription(tab, tabs[i], finalOffset, finalROffset);
this._configureJSON (tabs[i].elements, prefix, tab, finalOffset, finalROffset);
// Add the field checker at the end of the current tab
var fieldCheckers = tabs[i]['field-checker'];
if (!Ext.isEmpty(fieldCheckers))
{
this._fieldCheckersManager.addFieldCheckers(tab, fieldCheckers, prefix, finalOffset, finalROffset);
}
}
}
},
/**
* @method setValues
*
* Fill the configured form with values. #configure must have been called previously with data matching the configured data.
*
* The data can be a JSON or an XML object.
*
* The JSON format
* ===============
* See the following structure:
*
* {
* "values": {
* "<fieldPath>": "a string value",
* // ...
* },
* "invalid": {
* "otherfield": "rawvalue"
* // ...
* },
*
* "repeaters": [{
* "prefix": "",
* "name": "repeater1",
* "count": 2
* },
* {
* "prefix": "repeater1[1]/",
* "name": "repeater2",
* "count": 2
* },
* {
* "prefix": "repeater1[1]/repeater2[1]/",
* "name": "repeater3",
* "count": 2
* },
* {
* "prefix": "repeater1[1]/repeater2[2]/",
* "name": "repeater3",
* "count": 1
* },
* {
* "prefix": "repeater1[2]/",
* "name": "repeater2",
* "count": 1
* },
* {
* "prefix": "repeater1[2]/repeater2[1]/",
* "name": "repeater3",
* "count": 2
* }]
* }
*
* The **values** and **invalid** keywords are configurable via the **valuesTagName** and **invalidFieldsTagName** parameter (see below).
*
* The Object **values** will fill the fields. The **<fieldPath>** is the path of the field to fill.
* Example: {"rootFieldName": "a value", "repeater1[1]/repeater2[2]/otherFieldName": "another value", ...}
*
* The Object **invalid** will pre-fill fields with raw values. For exemple, you can pre-fill a date field with a non-date string.
* The **invalid** values should not set the same values already brought by **values**, but they will replace them in such a case.
* *
* The Object array **repeaters** allows to know the size of every repeater. Each entry has this attributes:
*
* - String **name** The name of the repeater
* - String **prefix** The path to this repeater
* - Number **count** The size of the repeater
*
*
* The XML format
* ==============
* See the following structure:
*
* <myrootnode>
* <values>
* <fieldname json="false" value="3"/>
* <!-- ... -->
* </values>
* </myrootnode>
*
* About the **values**:
*
* - the tag name is the name of the concerned field (without prefix).
* - those tags are recursive for sub-field (child of composites).
* - for repeaters, an attribute **entryCount** is set on the tag, its value is the size of the repeater. Each entry is encapsulated in an **entry** tag with an attribute **name** equals to the position (1 based) of the entry.
* - the attribute **json** set to true means the value will be interpreted as JSON before being set on the field
* - the value itself can be either the value of the attribute **value**, or the text of the tag
* - multiple values are set by repeating the tag.
*
* There is no **invalid** tag available.
*
* Here is a full example:
*
* <myrootnode>
* <values>
* <!-- A simple text value -->
* <title>My title</title>
* <!-- A composite -->
* <illustration>
* <alt-text>My alternative text</alt-text>
* </illustration>
* <!-- A richtext value -->
* <content><p>my rich text value</p></content>
* <!-- A repeater -->
* <attachments entryCount="1">
* <entry name="1">
* <!-- A file metadata. The widget waits for an object value according to its documentation {@link Ametys.form.widget.File#setValue} -->
* <attachment json="true">
* {
* "type": "metadata",
* "mimeType": "application/unknown",
* "path": "attachments/1/attachment",
* "filename": "ametysv4.ep",
* "size": "188249",
* "lastModified": "2015-06-03T14:15:22.232+02:00",
* "viewUrl": "/cms/plugins/cms/binaryMetadata/attachments/1/attachment?objectId=content://ec7ef7a1-139a-4863-a866-76196ed556cb",
* "downloadUrl": "/cms/plugins/cms/binaryMetadata/attachments/1/attachment?objectId=content://ec7ef7a1-139a-4863-a866-76196ed556cb&&download=true"
* }
* </attachment>
* <attachment-text>fichier</attachment-text>
* </entry>
* </attachments>
* </values>
* </myrootnode>
*
* @param {Object/HTMLElement} data The object that will fill the form.
* @param {String} [valuesTagName=values] the tag name for the values
* @param {String} [invalidFieldsTagName=invalid] the tag name for the invalid fields
* @param {String} [warnIfNullMessage] the warning message to display if there is no value
*/
setValues: function(data, valuesTagName, invalidFieldsTagName, warnIfNullMessage)
{
valuesTagName = valuesTagName || "values";
invalidFieldsTagName = invalidFieldsTagName || "invalid";
// Extract values of external disable conditions
this._externalDisableConditionsValues = this._extractExternalDisableConditionsValues(data, valuesTagName);
if (this._isElement(data))
{
this._setValuesXML(data, valuesTagName);
}
else if (Ext.isObject(data))
{
this._setValuesJSON(data, valuesTagName, invalidFieldsTagName);
}
// Activate disable conditions on fields
this._executeOnFields(this._activateDisableConditions);
if (warnIfNullMessage)
{
Ext.Array.each(Ext.Array.difference(this.getFieldNames(), this._initiallyNotNullFieldNames), function(fieldName){
var field = this.getField(fieldName);
if (field.type != "password" && !field.isDisabled())
{
field.on('render', function() {field.markWarning(warnIfNullMessage)});
}
}, this);
}
if (this._showTableOfContents)
{
// Add the "Out of tabs" navigation item if necessary
var outOfTabFields = this._getFields(Ametys.form.ConfigurableFormPanel.OUTOFTAB_FIELDSET_ID),
outOfTabRepeaters = this.getRepeaters(Ametys.form.ConfigurableFormPanel.OUTOFTAB_FIELDSET_ID);
if (!Ext.isEmpty(Ext.Array.union(outOfTabFields, outOfTabRepeaters)))
{
this._getTableOfContents().addNavigationItem("{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_OUTOFTAB_FIELDSET}}", Ametys.form.ConfigurableFormPanel.OUTOFTAB_FIELDSET_ID);
}
}
this._formReady = true;
this.fireEvent('formready', this);
this._setupFieldListeners();
// Possible warning/errors at startup
this.getFormContainer().on('afterlayout', Ext.bind(this._updateTabsStatus, this, [true], false), this, {single: 'true'});
},
/**
* @private
* Extracts the external disable conditions values from the given data.
* @param {Object/HTMLElement} data The object containing the external disable conditions' values.
* @param {String} valuesTagName the tag name for the values
*/
_extractExternalDisableConditionsValues(data, valuesTagName)
{
if (this._isElement(data))
{
var valuesNode = Ext.dom.Query.select(valuesTagName, data);
return this._extractExternalDisableConditionsValuesFromXML(valuesNode);
}
else if (Ext.isObject(data))
{
var values = data[valuesTagName];
if (values && values.hasOwnProperty("__externalDisableConditionsValues"))
{
return values["__externalDisableConditionsValues"];
}
}
// No external conditions has been found in given data
return {};
},
/**
* @private
* Extracts the external disable conditions values from a XML dom.
* @param {HTMLElement} externalDCValuesNode The XML dom containing the conditions values
*/
_extractExternalDisableConditionsValuesFromXML(valuesNode)
{
if (valuesNode != undefined)
{
var externalDCValuesNode = Ext.dom.Query.selectNode("/__externalDisableConditionsValues", valuesNode);
let conditionsValues = {};
let conditionsNodes = Ext.dom.Query.selectDirectElements("condition", externalDCValuesNode);
for (let i=0; i < conditionsNodes.length; i++)
{
let conditionNode = conditionsNodes[i];
let conditionId = conditionNode.getAttribute("id");
let conditionValueAsStr = Ext.dom.Query.selectValue('', conditionNode, '');
let conditionValue = conditionValueAsStr.trim() === "true";
conditionsValues[conditionId] = conditionValue;
}
return conditionsValues;
}
else
{
return {};
}
},
/**
* @private
* This function set the values of form fields from a XML dom.
* @param {HTMLElement} xml The XML dom
* @param {String} valuesTagName the tag name for the values
*/
_setValuesXML: function (xml, valuesTagName)
{
this._initializeTabsStatus();
var valuesNode = Ext.dom.Query.select(valuesTagName, xml);
if (valuesNode != undefined)
{
var metadataNodes = Ext.dom.Query.selectDirectElements("*", valuesNode);
for (let i=0; i < metadataNodes.length; i++)
{
var valueTagName = metadataNodes[i].tagName;
if (valueTagName !== "__externalDisableConditionsValues")
{
this._setValuesXMLMetadata(metadataNodes[i], this.getFieldNamePrefix() + valueTagName);
}
}
}
},
/**
* @private
* Set the values of form fields from an automatic backup.
* @param {Object} data The backup data object.
* @param {Object[]} data.repeaters The repeater item counts.
* @param {String} valuesTagName the tag name for the values
* @param {String} invalidFieldsTagName the tag name for the invalid fields
*/
_setValuesJSON: function(data, valuesTagName, invalidFieldsTagName)
{
this._initializeTabsStatus();
// Sort repeaters to get parent repeaters first.
var sortedRepeaters = Ext.Array.sort(data.repeaters || [], function(rep1, rep2) {
var rep1Name = rep1.prefix + rep1.name;
var rep2Name = rep2.prefix + rep2.name;
return rep1Name < rep2Name ? -1 : 1;
});
var values = data[valuesTagName];
// Initialize repeater entries before setting the values.
for (var i = 0; i < sortedRepeaters.length; i++)
{
var name = sortedRepeaters[i].name;
var prefix = this._getRepeaterPanelPrefix(sortedRepeaters[i]);
var count = sortedRepeaters[i].count;
var repeaterPanel = this.down("panel[isRepeater][name=" + name + "][prefix/=^" + prefix + "$]");
if (repeaterPanel != null)
{
if (values && values.hasOwnProperty("__externalDisableConditionsValues"))
{
repeaterPanel.setNewItemExternalDisableConditionsValues(values["__externalDisableConditionsValues"]);
}
var itemsDifference = count - repeaterPanel.getItemCount();
if (itemsDifference < 0)
{
// Entries were initialized (initial or min-size) but the repeater contain less:
// we need to remove the exceeding ones.
repeaterPanel.getItems().each(function(panel, index, length) {
if (index >= count)
{
repeaterPanel.removeItem(panel);
}
});
}
else if (itemsDifference > 0)
{
// Collapse all existing items and expand the last one.
repeaterPanel.collapseAll();
for (var j = 0; j < itemsDifference; j++)
{
var collapsed = j < (itemsDifference-1);
repeaterPanel.addRepeaterItem({animate: false, collapsed: collapsed, fireRepeaterEntryReadyEvent: true});
}
}
// Set previous position and extract external disable conditions on each repeater item
repeaterPanel.getItems().each(function(panel, index, length) {
let previousPosition = values["_" + sortedRepeaters[i].prefix + sortedRepeaters[i].name + "[" + (index + 1) + "]/previous-position"];
repeaterPanel.setItemPreviousPosition(index, previousPosition !== undefined ? previousPosition - 1 : index);
let extname = name + "[" + (index+1) + "]/__externalDisableConditionsValues";
if (values && values.hasOwnProperty(extname))
{
repeaterPanel.setItemExternalDisableConditionsValues(index, values[extname]);
}
});
}
}
if (values && values.hasOwnProperty("__externalDisableConditionsValues"))
{
delete values["__externalDisableConditionsValues"];
}
this._initiallyNotNullFieldNames = Ext.Object.getKeys(values);
// Set the field values.
this._setValuesJSONForField(values);
// Set the invalid field values (raw mode) and validate the fields afterwards.
this._setValuesJSONForField(data[invalidFieldsTagName], true, true);
},
/**
* @private
* Retrieves the repeater prefix to use to find the field panel
* @param {Object} repeater the repeater
* @return {String} the repeater's prefix
*/
_getRepeaterPanelPrefix: function(repeater)
{
var prefix = this.getFieldNamePrefix() + repeater.prefix;
return prefix.replace(/\./g, '\\.').replace(/\[|\]/g, '.');
},
/**
* @private
* Set the values from an object.
* @param {Object} values The object containing the values, indexed by name.
* @param {Boolean} [rawMode=false] `true` to set the value in raw mode, `false` otherwise.
* @param {Boolean} [validate=false] `true` to validate the value after setting it, `false` otherwise.
*/
_setValuesJSONForField: function(values, rawMode, validate)
{
for (var name in values)
{
var value = this._getValueFromJSON(values[name]);
var field = this.getForm().findField(this.getFieldNamePrefix() + name);
if (field != null)
{
var oldLastValue = field.lastValue;
if (rawMode && field.setRawValue)
{
field.setRawValue(value);
}
else
{
field.setValue(value);
}
// We set the lastValue to avoid later "change" event on ajax components (such as combobox)
// We set it after the setValue because we do want the initial change event for internal listening systems
if (oldLastValue == field.lastValue)
{
field.lastValue = value;
}
if (validate)
{
field.validate();
}
}
}
},
/**
* @private
* Gets data value from JSON
* @param {String|Object} jsonValue The JSON representing the value
* @return {String|String[]} A array of values
*/
_getValueFromJSON: function (jsonValue)
{
let value;
if (Ext.isArray(jsonValue))
{
value = [];
for (jsonVal of jsonValue)
{
value.push(this._getSingleValueFromJSON(jsonVal));
}
}
else
{
value = this._getSingleValueFromJSON(jsonValue);
}
return value;
},
/**
* @private
* Gets single data value from JSON
* @param {String|Object} jsonValue The JSON representing the value
* @return {String|String[]} A array of values
*/
_getSingleValueFromJSON: function (jsonValue)
{
let value = jsonValue;
// Detect the case of enumerated and extract the value
// mirror of org.ametys.core.model.type.AbstractElementType._isEnumeratedJSONObject(Object)
if (Ext.isObject(jsonValue) && Object.keys(jsonValue).length == 2 && jsonValue.value && jsonValue.label)
{
value = jsonValue.value;
}
return value;
},
/**
* @private
* Sets a data values into the field
* @param {HTMLElement} metadataNode The DOM node representing the metadata value
* @param {String} fieldName The name of concerned field
*/
_setValuesXMLMetadata: function (metadataNode, fieldName)
{
var metaName = metadataNode.tagName;
var prefix = this.getFieldNamePrefix();
if (fieldName.lastIndexOf(this.defaultPathSeparator) > 0)
{
prefix = fieldName.substring(0, fieldName.lastIndexOf(this.defaultPathSeparator) + 1);
}
var childNodes = Ext.dom.Query.selectDirectElements(null, metadataNode);
var repeaterItemCount = Ext.dom.Query.selectNumber('@entryCount', metadataNode, -1);
var predicateMetaName = this._escapeForXpathPredicate(metaName);
var predicatePrefix = this._escapeForXpathPredicate(this._escapeForRegexPredicate(prefix));
var repeaterPanel = this.down("panel[isRepeater][name=" + predicateMetaName + "][prefix/=^" + predicatePrefix + "$]");
// Case of a repeater metadata.
if (repeaterItemCount >= 0 && repeaterPanel != null)
{
// Set the external disable conditions values for new entries
repeaterPanel.setNewItemExternalDisableConditionsValues(this._extractExternalDisableConditionsValuesFromXML(metadataNode));
if (repeaterItemCount < repeaterPanel.getItemCount())
{
// Entries were initialized (initial or min-size) but the repeater contain less:
// we need to remove the exceeding ones.
repeaterPanel.getItems().each(function(panel, index, length) {
if (index >= repeaterItemCount)
{
repeaterPanel.removeItem(panel);
}
});
}
else if (repeaterItemCount > repeaterPanel.getItemCount())
{
// We're going to add some entries: collapse all existing ones.
repeaterPanel.collapseAll();
}
for (var i=0; i < childNodes.length; i++)
{
if (childNodes[i].tagName == 'entry' && childNodes[i].getAttribute('name') != null)
{
// Repeater value.
var entryPos = childNodes[i].getAttribute('name');
// Add repeater items if they were not already created
// (initial or min-size).
if (i >= repeaterPanel.getItemCount())
{
// Expand the last item.
var collapsed = (i < childNodes.length-1);
repeaterPanel.addRepeaterItem({previousPosition: i, animate: false, collapsed: collapsed, fireRepeaterEntryReadyEvent: true});
}
else
{
// Set the previous position.
repeaterPanel.setItemPreviousPosition(i, i);
}
// Set the external disable conditions values for each entry
repeaterPanel.setItemExternalDisableConditionsValues(i, this._extractExternalDisableConditionsValuesFromXML(childNodes[i]));
var repeaterEntryName = Ametys.form.ConfigurableFormPanel.Repeater.getNameAtIndex(fieldName, this.defaultPathSeparator, entryPos);
this._setValuesXMLMetadata(childNodes[i], repeaterEntryName);
}
}
}
else if (childNodes.length == 0)
{
// Non-composite metadata.
var previousSibling = metadataNode.previousElementSibling || metadataNode.previousSibling;
if (!previousSibling || previousSibling.tagName != metadataNode.tagName)
{
this._initiallyNotNullFieldNames.push(fieldName);
var field = this.getForm().findField(fieldName);
if (field != null)
{
var valueToSet = this._getValuesFromXML(metadataNode, field);
var oldLastValue = field.lastValue;
field.setValue(valueToSet);
// We set the lastValue to avoid later "change" event on ajax components (such as combobox)
// We set it after the setValue because we do want the initial change event for internal listening systems
if (oldLastValue == field.lastValue)
{
field.lastValue = valueToSet;
}
}
}
}
else
{
// Standard composite metadata.
for (var i=0; i < childNodes.length; i++)
{
this._setValuesXMLMetadata(childNodes[i], fieldName + this.defaultPathSeparator + childNodes[i].tagName);
}
}
},
/**
* @private
* Gets data values from DOM
* @param {HTMLElement} metadataNode The DOM node representing the metadata value
* @param {Ext.form.field.Field} field The field where the value will be set
* @return {String[]} A array of values
*/
_getValuesFromXML: function (metadataNode, field)
{
if (metadataNode.getAttribute('json') == 'true')
{
// the metadata was sent has json in a single element
// read the string value directly from the node
var str = Ext.dom.Query.selectValue('', metadataNode, '');
// then process the value has a JSON value
// return directly the value no matter its type or the
// cardinality of field.
return this._getValueFromJSON(Ext.JSON.decode(str, true));
}
var values = [];
// We get children with the same name of the same parent to have all tags with the same name for multiple values.
var nodes = Ext.dom.Query.selectDirectElements("*", metadataNode.parentNode); // We cannot make a better selector because of possible "." in the tagName.
for (var i=0; i < nodes.length; i++)
{
if (nodes[i].tagName == metadataNode.tagName)
{
var value = nodes[i].getAttribute('value') == null ? Ext.dom.Query.selectValue('', nodes[i], '') : nodes[i].getAttribute('value');
value = value || '';
if (value.length > 0)
{
values.push(value);
}
}
}
return field.multiple ? values : values[0];
},
/**
* Get the values of the invalid fields.
* @return {Object} The invalid field values, indexed by field name.
*/
getInvalidFieldValues: function()
{
var invalidValues = {};
var fields = this.getForm().getFields().items;
for (var i = 0; i < fields.length; i++)
{
if (!fields[i].isHidden() && fields[i].getErrors().length > 0)
{
var name = fields[i].getName();
if (fields[i].getRawValue)
{
invalidValues[name] = fields[i].getRawValue();
}
else
{
Ext.applyIf(invalidValues, fields[i].getSubmitData());
}
}
}
return invalidValues;
},
/**
* Get the invalid repeaters
* @return {String[]} The labels of invalid repeaters
*/
getInvalidRepeaters: function ()
{
var invalidRepeaters = [];
var repeaters = this.getRepeaters(null);
for (var i = 0; i < repeaters.length; i++)
{
var repeater = repeaters[i];
if (!repeater.isValid())
{
invalidRepeaters.push(repeater.getLabel());
}
else
{
repeater.clearInvalid();
}
}
return invalidRepeaters;
},
/**
* Get the invalid fields
* @return {String[]} The names of invalid fields
*/
getInvalidFields: function ()
{
var invalidFields = [];
for (var i = 0; i < this._fields.length; i++)
{
var fd = this.getForm().findField(this._fields[i]);
if (!fd.isValid())
{
invalidFields.push(this._getFullLabel(fd));
}
else
{
fd.clearInvalid();
}
}
this._updateTabsStatus(true);
return invalidFields;
},
/**
* Get the fields with warnings
* @return {Object} The mapping of warned field names with their warning message
*/
getWarnedFields: function()
{
var warnedFields = {};
for (var i = 0; i < this._fields.length; i++)
{
var fd = this.getForm().findField(this._fields[i]);
if (!Ext.isEmpty(fd.getActiveWarning()))
{
warnedFields[this._getFullLabel(fd)] = fd.getActiveWarnings();
}
}
this._updateTabsStatus();
return warnedFields;
},
/**
* @private
* Escapes the given name for inserting into XPath Predicate
* @param {String} name The name to escape
* @return {String} The escaped name
*/
_escapeForXpathPredicate: function(name)
{
// escape '[' and ']' for valid XPath predicate
return name
.replace(/\[/g, '\\[')
.replace(/\]/g, '\\]');
},
/**
* @private
* Escapes the given name for inserting into Regex
* @param {String} name The name to escape
* @return {String} The escaped name
*/
_escapeForRegexPredicate: function(name)
{
// escape '[' and ']' for valid regex predicate
return name
.replace(/\[/g, '\\[')
.replace(/\]/g, '\\]');
},
/**
* Get the form's message targets, including the test results.
* @return {Object} The current message target configuration for the current selection selection
*/
getMessageTargetConf: function()
{
var form = this.getForm();
var messageTargets = {
'id': Ametys.message.MessageTarget.FORM,
'parameters': {
'object': form,
'test-results': this._fieldCheckersManager ? this._fieldCheckersManager._testResults : {}
}
};
var focusField;
if (this._focusFieldId != null && (focusField = form.findField(this._focusFieldId)))
{
messageTargets['subtargets'] = {
'id': Ametys.message.MessageTarget.FORM_FIELD,
'parameters': {
name: focusField.getName()
}
}
if (focusField.isRichText)
{
var node = focusField.getNode();
if (node != null)
{
messageTargets['subtargets']['subtargets'] = {
'id': Ametys.message.MessageTarget.FORM_FIELD_RICHTEXTNODE,
'parameters': {
'object': node
}
};
}
}
}
return messageTargets;
},
/**
* @private
* Executes the given function on every form's fields
* The function will also be executed on each new repeater entry's fields
* @param {Function} fn The function to execute
*/
_executeOnFields: function(fn)
{
// Executes function on fields
this.getForm().getFields().each(fn, this);
// Executes function on repeaters
let repeaters = this.getRepeaters(null);
Ext.Array.forEach(repeaters, fn, this);
// Executes function on composites
let composites = this.getComposites(null);
Ext.Array.forEach(composites, fn, this);
// Executes function on fields repeaters when entries are ready
this.on('repeaterEntryReady', function(repeater) {
// Executes function on fields
this.getChildrenFields(repeater).each(fn, this);
// Executes function on repeaters
let repeaters = this.getRepeaters(repeater.getId());
Ext.Array.forEach(repeaters, fn, this);
// Executes function on composites
let composites = this.getComposites(repeater.getId());
Ext.Array.forEach(composites, fn, this);
}, this);
},
/**
* @private
* Add listeners to evaluate the disable conditions of the given field
* @param {Object} field the field on which the disable conditions applies
*/
_addDisableConditionsListeners: function(field)
{
let disableConditions = field.disableCondition;
// fields that are initially disabled can never be enabled See #cfg-helpBoxId
if (disableConditions != null && !field.disabled)
{
this._doAddDisableConditionsListeners(typeof disableConditions === 'string' ? JSON.parse(disableConditions) : disableConditions, field);
}
},
/**
* @private
* Add listeners to evaluate the disable conditions dynamically
* @param {Object} disableConditions the disable condition
* @param {Object[]} disableConditions.conditions an array of conditions that can contain several condition objects or other conditions
* @param {Object[]} disableConditions.condition an array of condition objects
* @param {String} disableConditions.condition.id the id of the field that will be evaluated
* @param {String} disableConditions.condition.operator the operator used to evaluate the field
* @param {String} disableConditions.condition.value the value on which the field will be compared to
* @param {Object} disablingField the field on which the disable condition applies
*/
_doAddDisableConditionsListeners: function(disableConditions, disablingField)
{
if (disableConditions.conditions)
{
var conditionsList = disableConditions.conditions,
conditionsListLength = conditionsList.length;
for (var i = 0; i < conditionsListLength; i++)
{
this._doAddDisableConditionsListeners(conditionsList[i], disablingField);
}
}
if (disableConditions.condition)
{
var conditionList = disableConditions.condition,
conditionListLength = conditionList.length;
for (var i = 0; i < conditionListLength; i++)
{
var conditionId = conditionList[i]['id'];
if (!conditionId.startsWith("__external"))
{
var field = this.getRelativeField(conditionId, disablingField);
if (field)
{
let listeners = field.on('change', Ext.bind(this._disableField, this, [disablingField], false), null, { destroyable: true });
disablingField.on('destroy', Ext.bind(listeners.destroy, listeners));
}
else
{
this.getLogger().error('Cannot evaluate the condition ' + conditionList[i]['id'] + ' for ' + disablingField.getInitialConfig('name'));
}
}
}
}
},
/**
* @private
* Activate the disable conditions of the given field
* @param {Object} field the field on which the disable condition applies
*/
_activateDisableConditions: function(field)
{
let disableConditions = field.disableCondition;
if (disableConditions != null)
{
this._disableField(field);
}
},
/**
* @private
* Enables/disables the field.
* @param {Object} field the field to(not to) disable.
*/
_disableField: function(field)
{
// Hidden and readonly elements (can-notwrite) should not be enabled here
if (field.getInitialConfig('disabled') === true || field.getInitialConfig('hidden') === true)
{
return;
}
let disable = Ametys.form.Widget.evaluateDisableCondition(field);
field.setDisabled(disable);
let hide = disable && Ametys.form.Widget.shouldHideDisabledField(field);
if (field.repeaterMode && field.repeaterMode == 'table')
{
field.toggleCls('a-item-hidden', hide);
}
else
{
field.setHidden(hide);
}
},
/* --------------------------------------------------------------------- */
/* Misc helper methods */
/* --------------------------------------------------------------------- */
/**
* Helper method to be used to execute a function while being sure that the form and all the repeater entry are ready.
* @param {Function} initiliazeFn The initialize function to execute. Will be executed immediately if the form is ready.
* @param {Object} scope The scope handler. Default to the form.
* @param {Object} args Optional function arguments.
*/
executeFormReady: function(initiliazeFn, scope, args)
{
if (this._addingRepeaterEntry)
{
this.on('repeaterEntryReady', Ext.bind(this._executeFormReadyCb, this, [initiliazeFn, scope, args]), undefined, {single: true});
}
else if(this._formReady)
{
this._executeFormReadyCb(initiliazeFn, scope, args);
}
else
{
this.on('formready', Ext.bind(this._executeFormReadyCb, this, [initiliazeFn, scope, args]), undefined, {single: true});
}
},
/**
* @private
* Internal callback used for executeFormReady.
* @param {Function} initiliazeFn The initialize function to execute
* @param {Object} scope The scope handler. Default to the form.
* @param {Object} args Optional function arguments.
*/
_executeFormReadyCb: function(initiliazeFn, scope, args)
{
if (Ext.isFunction(initiliazeFn))
{
initiliazeFn.apply(scope || this, args);
}
},
/* --------------------------------------------------------------------- */
/* Helper methods to work on relative fields */
/* --------------------------------------------------------------------- */
/**
* Helper method to get a relative field
* @param {String} fieldPath The path of the relative field, which is a relative path (e.g. a/b/c or ../../e/f)
* @param {Ext.form.field.Field} field The field of reference
* @param {boolean} silently Do not send error message if no relative field is found. Default to false.
* @return {Ext.form.field.Field} The relative field or null if the field has not been found or if the form is not ready yet.
*/
getRelativeField: function(fieldPath, field, silently)
{
var relativeField = null;
if (fieldPath)
{
// try to get the relative field from the field cache
var cache = field['__relativeFields'];
if (cache && fieldPath in cache)
{
var relativeFieldId = cache[fieldPath];
relativeField = relativeFieldId ? Ext.getCmp(relativeFieldId) : null;
}
else
{
if (!cache)
{
cache = field['__relativeFields'] = {};
}
let originalFieldPath = fieldPath;
if (!relativeField)
{
var fieldName = (field.prefix ? field.prefix : '') + field.name;
var prefix = fieldName.substring(0, fieldName.lastIndexOf(this.defaultPathSeparator));
// Handling '..' in field name.
var me = this;
Ext.Array.forEach(fieldPath.split('/'), function(pathPart) {
if (pathPart == '..')
{
prefix = prefix.substring(0, prefix.lastIndexOf(me.defaultPathSeparator));
fieldPath = fieldPath.substring(3);
}
});
// Separator in composites path is '/' whereas javascript path separator must be '.'
fieldPath = fieldPath.replace('/', this.defaultPathSeparator);
var relativeFieldPath = prefix == '' ? this.getFieldNamePrefix() + fieldPath : (prefix + this.defaultPathSeparator + fieldPath);
relativeField = this.getField(relativeFieldPath);
if (silently !== true && !relativeField)
{
var message = "{{i18n PLUGINS_CORE_UI_WIDGET_UNKNOWN_FIELD}}" + relativeFieldPath;
this.getLogger().error(message);
}
}
// Populate cache
cache[originalFieldPath] = relativeField ? relativeField.getId() : null;
}
}
return relativeField;
},
/**
* Helper method to get relative fields
* @param {String[]} fieldPaths An array of path to relative fields. Each path is relative (e.g. a/b/c or ../../e/f).
* @param {Ext.form.field.Field} field The field of reference
* @return {Ext.form.field.Field[]} The array of the relative fields in the same order than the fieldPaths array, if a field is not found, its corresponding entry in the array will be null. If the form is not ready, the empty array will be returned.
*/
getRelativeFields: function(fieldPaths, field)
{
var relativeFields = [];
if (fieldPaths)
{
Ext.Array.forEach(fieldPaths, function(fieldPath) {
relativeFields.push(this.getRelativeField(fieldPath, field));
}, this);
}
return relativeFields;
},
/**
* Helper method to get all children fields of a repeater
* @param {Ametys.form.ConfigurableFormPanel.Repeater} repeater The repeater
* @return {Ext.util.MixedCollection} The children fields of this repeater
*/
getChildrenFields: function(repeater)
{
return this.getForm().getFields().filter('name', repeater.prefix + repeater._getNameAtIndex(repeater._lastInsertItemPosition) + this.defaultPathSeparator);
},
/**
* Helper method to listen to the change event of a relative field.
* @param {String/String[]} fieldPaths The path of the relative field, which is a relative path (e.g. a/b/c or ../../e/f). An array of path can also be provided.
* @param {Ext.form.field.Field} field The field who is searching for a relative field
* @param {Function} handler The on change handler
* @param {Ext.form.field.Field} handler.field The relative field that has triggered the on change event.
* @param {Object} handler.newValue The new value
* @param {Object} handler.oldValue The old value
* @param {Object} scope The scope handler. Default to the field.
*/
onRelativeFieldsChange: function(fieldPaths, field, handler, scope)
{
if (!this._formReady)
{
this.on('formready', Ext.bind(this._onRelativeFieldsChangeReady, this, [fieldPaths, field, handler, scope]), undefined, {single: true});
}
else if (this._addingRepeaterEntry)
{
this.on('repeaterEntryReady', Ext.bind(this._onRelativeFieldsChangeReady, this, [fieldPaths, field, handler, scope]), undefined, {single: true});
}
else
{
this._onRelativeFieldsChangeReady(fieldPaths, field, handler, scope);
}
},
/**
* @private
* Internal callback used for onRelativeFieldsChange.
* @param {String/String[]} fieldPaths The path of the relative field, which is a relative path (e.g. a/b/c or ../../e/f). An array of path can also be provided.
* @param {Ext.form.field.Field} field The field who is searching for a relative field
* @param {Function} handler The on change handler
* @param {Ext.form.field.Field} handler.field The relative field that has triggered the on change event.
* @param {Object} handler.newValue The new value
* @param {Object} handler.oldValue The old value
* @param {Object} scope The scope handler. Default to the field.
*/
_onRelativeFieldsChangeReady: function(fieldPaths, field, handler, scope)
{
fieldPaths = Ext.Array.from(fieldPaths);
var relativeFields = this.getRelativeFields(fieldPaths, field);
Ext.Array.forEach(relativeFields, function(relativeField) {
if (relativeField)
{
field.mon(relativeField, 'change', handler, scope || field);
handler.apply((scope || field), [relativeField, relativeField.getValue(), null]);
}
});
}
});
Ext.define("Ametys.message.ConfigurableFormPanelMessageTarget",
{
override: "Ametys.message.MessageTarget",
statics:
{
/**
* @member Ametys.message.MessageTarget
* @readonly
* @property {String} FORM The target of the message is a form. It has at least the parameters: **form** the form object and **test-results** the results of the test of the parameters checkers.
*/
FORM: "form",
/**
* @member Ametys.message.MessageTarget
* @readonly
* @property {String} FORM_FIELD The target of the message is the field of a #FORM. The parent Ametys.message.MessageTarget must be a #FORM. The parameter provided is the **name** of the selected field in the parent target form.
*/
FORM_FIELD: "field",
/**
* @member Ametys.message.MessageTarget
* @readonly
* @property {String} FORM_FIELD_RICHTEXTNODE The target of the message is a node of a richtext #FIELD of a #FORM. The parent Ametys.message.MessageTarget must be a #FORM_FIELD providing a field of type 'richtext'. The parameter provided is **object** that is the HTMLElement node in the rich text.
*/
FORM_FIELD_RICHTEXTNODE: "node"
}
}
);