/*
* Copyright 2014 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.
*/
/**
* Custom form Panel for advanced search.
*/
Ext.define('Ametys.plugins.cms.search.advanced.AdvancedSearchFormPanel', {
extend: 'Ext.form.Panel',
/**
* @private
* @property {Number} _fieldIndex the field index used to identify the fields
*/
/**
* @private
* @property {Number} _rowCount the number of rows of this advanced search form panel
*/
/**
* @private
* @property {Object} _simpleCriteria the configuration object for simple criteria
*/
/**
* @private
* @property {Object} _criteria the configuration object for advanced criteria
*/
/**
* @private
* @property {Object} _language the configuration object for the language
*/
/**
* @private
* @property {String} _defaultCriterionId the id of the default criterion (which is the first criterion of the model)
*/
/**
* @private
* @property {Ext.data.Store} _languagesEnumerationStore the {@link Ext.data.Store} of available languages. Can be null if no language is defined
*/
/**
* @private
* @property {Ext.data.Store} _logicalOperatorStore the store of logical operators linking two consecutive criteria
*/
/**
* @private
* @property {Ext.data.Store} _andOperatorStore the store containing solely the 'and' operator
*/
/**
* @private
* @property {Object[]} _advancedCriteriaFieldNames Array of criteria for the criterion field's store
*/
/**
* @private
* @property {Object} _operatorStores the mapping of operator type with the corresponding operator {@link Ext.data.Store}
*/
/**
* @private
* @property {Object} _enumerationStores the mapping of criterion id with its corresponding enumeration {@link Ext.data.Store} for the value field
*/
/**
* @cfg {Object[]} additionalFields The configurations of fields to add in each row.
* @cfg {String} additionalFields.itemId The item id of the field to add.
* @cfg {String} additionalFields.after The item id of the field after which the one to add will be inserted.
* @cfg {Function} [additionalFields.getValueFn] The function used to retrieve the value of the field when calling {@link #getValueTree}. Its passed argument will be the field itself. Can be undefined to not retrieve the value.
* @cfg {Object/Function} additionalFields.config The configuration (or a function returning a configuration object) of the field to add (including xtype, flex, etc.).
* @cfg {Number} [additionalFields.config.rowIndex] If config is a function, the first argument is the index of the row
* @cfg {String} [additionalFields.config.criterionName] If config is a function, the second argument is the name of the criterion field
*/
/**
* @private
* @property {Object[]} _additionalFields The configurations of additional fields.
*/
/**
* @cfg {Boolean} [allowBlank=false] True if a panel with no row is allowed.
*/
/**
* @private
* @property {Boolean} _allowBlank True if a panel with no row is allowed.
*/
_allowBlank: false,
statics: {
/**
* List of logical operators.
*/
LOGICAL_OPERATORS: [
{
value: "and",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_LOGICALOP_AND}}",
str: " && "
}, {
value: "or",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_LOGICALOP_OR}}",
str: " || "
}, {
value: "and-par",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_LOGICALOP_AND_PAR}}",
str: ") && ("
}
],
/**
* List of available operators, by criterion type.
*/
OPERATORS: {
'string': [{
value: "search",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_SEARCH}}",
shortLabel: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_SEARCH_SHORT}}"
}, {
value: "searchStemmed",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_SEARCH_STEMMED}}",
shortLabel: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_SEARCH_STEMMED_SHORT}}"
}, {
value: "contains",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_CONTAINS}}"
}, {
value: "not-contains",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_NOT_CONTAINS}}"
}, {
value: "eq",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_EQ}}"
}, {
value: "ne",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_NE}}"
}, {
value: "starts-with",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_STARTS_WITH}}"
}, {
value: "not-starts-with",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_NOT_STARTS_WITH}}"
}, {
value: "ends-with",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_ENDS_WITH}}"
}, {
value: "not-ends-with",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_NOT_ENDS_WITH}}"
}, {
value: "is-empty",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_EMPTY}}"
}, {
value: "is-not-empty",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_NOT_EMPTY}}"
}],
'rich-text': [{
value: "search",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_SEARCH}}",
shortLabel: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_SEARCH_SHORT}}"
}, {
value: "searchStemmed",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_SEARCH_STEMMED}}",
shortLabel: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_SEARCH_STEMMED_SHORT}}"
}, {
value: "is-empty",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_EMPTY}}"
}, {
value: "is-not-empty",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_NOT_EMPTY}}"
}],
'enumeration': [{
value: "eq",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_EQ}}",
labelMultiple: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_EQ_MULTIPLE}}"
}, {
value: "ne",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_NE}}",
labelMultiple: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_NE_MULTIPLE}}"
}, {
value: "is-empty",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_EMPTY}}"
}, {
value: "is-not-empty",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_NOT_EMPTY}}"
}],
'date': [{
value: "eq",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS}}"
}, {
value: "ne",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_NOT}}"
}, {
value: "ge",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_AFTER_INCLUDED}}"
}, {
value: "le",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_BEFORE_INCLUDED}}"
}, {
value: "is-empty",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_EMPTY}}"
}, {
value: "is-not-empty",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_NOT_EMPTY}}"
}],
'boolean': [{
value: "eq",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS}}"
}],
'number': [{
value: "eq",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_EQ}}"
}, {
value: "ne",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_NE}}"
}, {
value: "gt",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_GT}}"
}, {
value: "lt",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_LT}}"
}, {
value: "ge",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_GTE}}"
}, {
value: "le",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_LTE}}"
}, {
value: "is-empty",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_EMPTY}}"
}, {
value: "is-not-empty",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_NOT_EMPTY}}"
}],
'geocode': [{
value: "eq",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_EQ}}"
}, {
value: "ne",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_NE}}"
},{
value: "is-empty",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_EMPTY}}"
}, {
value: "is-not-empty",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_NOT_EMPTY}}"
}]
},
/**
* Template used for the operator combobox (for non-multiple criterion).
*/
OPERATOR_TEMPLATE: Ext.create('Ext.XTemplate',
'<tpl for=".">',
'{shortLabel}',
'</tpl>'
),
/**
* Template used for the operator combobox (for multiple criterion).
*/
OPERATOR_TEMPLATE_MULTIPLE: Ext.create('Ext.XTemplate',
'<tpl for=".">',
'{shortLabelMultiple}',
'</tpl>'
),
/**
* @property {Number} DEFAULT_LOGICAL_OPERATOR_FIELD_WITH The default width of the logical operator field.
* @static
* @readonly
*/
DEFAULT_LOGICAL_OPERATOR_FIELD_WITH: 70,
/**
* @property {String} OPENING_PARENTHESIS_FIELD_ITEM_ID The item id of the opening parenthesis field.
* @static
* @readonly
*/
OPENING_PARENTHESIS_FIELD_ITEM_ID: 'opening-parenthesis',
/**
* @property {String} LOGICAL_OPERATOR_FIELD_ITEM_ID The item id of the criterion field.
* @static
* @readonly
*/
LOGICAL_OPERATOR_FIELD_ITEM_ID: 'logical-operator',
/**
* @property {String} CRITERION_FIELD_ITEM_ID The item id of the criterion field.
* @static
* @readonly
*/
CRITERION_FIELD_ITEM_ID: 'criterion',
/**
* @property {String} OPERATOR_FIELD_ITEM_ID The item id of the operator field.
* @static
* @readonly
*/
OPERATOR_FIELD_ITEM_ID: 'operator',
/**
* @property {String} MODE_COMBOBOX_FIELD_ITEM_ID The item id of the value field.
* @static
* @readonly
*/
VALUE_FIELD_ITEM_ID: 'value',
/**
* @property {Number} VALUE_FIELD_FLEX The flex configuration of the value field.
* @static
* @readonly
*/
VALUE_FIELD_FLEX: 1,
/**
* @property {String} REMOVE_BUTTON_ITEM_ID The item id of the remove button.
* @static
* @readonly
*/
REMOVE_BUTTON_ITEM_ID: 'remove-button',
/**
* @property {String} ADD_BUTTON_ITEM_ID The item id of the add button.
* @static
* @readonly
*/
ADD_BUTTON_ITEM_ID: 'add-button',
/**
* @property {String} MOVEUP_BUTTON_ITEM_ID The item id of the moveup button.
* @static
* @readonly
*/
MOVEUP_BUTTON_ITEM_ID: 'moveup-button',
/**
* @property {String} MOVEDOWN_BUTTON_ITEM_ID The item id of the movedown button.
* @static
* @readonly
*/
MOVEDOWN_BUTTON_ITEM_ID: 'movedown-button'
},
constructor: function(config)
{
this._additionalFields = config.additionalFields || [];
if (config.allowBlank != null)
{
this._allowBlank = config.allowBlank === true;
}
this.callParent(arguments);
},
/**
* Initialize the form, providing the list of advanced criteria.
*/
initialize: function(model)
{
this._fieldIndex = 0;
// Store the simple criteria to be able to initialize the form from a simple values map.
this._simpleCriteria = model['simple-criteria'] || {fieldsets: []};
this._criteria = model['advanced-criteria'].criteria;
this._language = model['advanced-criteria'].language;
this._defaultCriterionId = Ext.Object.getKeys(this._criteria)[0];
// Initialize the criteria stores.
this._buildCriteriaStores();
// Draw the language criterion if not hidden
this._drawLanguageLine();
// Draw the defaut line
if (!this._allowBlank)
{
this._drawDefaultLine();
this._fieldIndex++;
}
// Draw the last container line (closing parenthesis + hint).
this._drawClosingLine(this._fieldIndex);
},
/**
* Gets the (advanced) criteria
* @return {Object} the (advanced) criteria
*/
getCriteria: function()
{
return this._criteria;
},
/**
* Initialize the search form
* @param {Object} values The criteria values into logical expressions
* @param {String} language The language value. Can be null
*/
initForm: function (values, language)
{
try
{
Ext.suspendLayouts();
this._rowCount = 0;
var languageIsPresent = this.items.get('container-language') != null;
if (!this._allowBlank)
{
this._removeOldCriteria(languageIsPresent);
}
this._setLanguageValue(language);
if (this._allowBlank && Ext.Object.isEmpty(values))
{
// nothing more to do
return;
}
if (this._allowBlank && this.items.getCount() == 1)
{
// need a first row, there is only 1 item which is the closing line
this.addCriterion();
}
var firstRowIndex = languageIsPresent ? 1 : 0;
if (values.expressions)
{
// Saved query
this._logTreeValues(values);
this._drawExpression(values.expressions, null, values.type, firstRowIndex);
}
else if (values.simpleValues)
{
this._initSimpleValues(values.simpleValues, firstRowIndex);
}
else
{
// Only one criterion, fill first row
this._setRowValues(firstRowIndex, values.id, this._convertOperator(values.op, values.value), values.rawValue, null, values);
this._rowCount++;
}
}
finally
{
Ext.resumeLayouts(true);
}
},
/**
* @private
* Removes old criteria
* @param {Boolean} languageIsPresent true if language is present
*/
_removeOldCriteria: function(languageIsPresent)
{
var minIndex = languageIsPresent ? 1 : 0;
var criterionItemId = this.statics().CRITERION_FIELD_ITEM_ID;
var me = this;
function initFirstLine()
{
var firstLine = me.items.get('container-' + minIndex);
if (firstLine)
{
me._replaceOperatorFieldByOpeningField(firstLine);
var criterionField = firstLine.items.get(criterionItemId);
if (criterionField)
{
var firstCriterionValue = criterionField.getStore().getAt(0);
criterionField.setValue(null); // force null before in case the first criterion value was already selected
criterionField.setValue(firstCriterionValue);
}
}
return firstLine;
}
// Remove old criteria rows (except language if present)
for (var i = this._fieldIndex; i > minIndex; i--)
{
this.remove('container-' + i);
}
var firstLine = initFirstLine();
this._fieldIndex = languageIsPresent ? 2 : 1;
this._drawClosingLine(this._fieldIndex);
this._updateRowUi(firstLine);
},
/**
* Initialize the language row with the given value
* @param {String} value the value of the language
*/
_setLanguageValue: function(value)
{
var row = this.items.get('container-language');
if (row)
{
var valueField = row.items.get(this.statics().VALUE_FIELD_ITEM_ID);
valueField.setValue(value);
}
},
/**
* Initialize the advanced search form from simple search form values.
* @param {Object} values The simple search values, indexed by simple criterion ID.
* @param {Number} firstRowIndex The index of the first row
*/
_initSimpleValues: function(values, firstRowIndex)
{
Ext.Object.each(values, function(simpleCriterionId, value) {
if (value != null && value !== '' && !(Ext.isArray(value) && value.length == 0))
{
// Get the simple criterion definition from the ID.
var simpleCriterion = this._getSimpleCriterion(simpleCriterionId);
if (simpleCriterion != null)
{
// Get the advanced criterion definition corresponding to the simple criterion.
var advCriterion = this._getCorrespondingAdvancedCriterion(simpleCriterion);
if (advCriterion != null)
{
// Use the advanced criterion ID but the operator from the simple criterion.
var id = advCriterion.name;
var type = advCriterion.type;
var simpleOp = (simpleCriterion.criterionOperator || '').toLowerCase();
var operator = this._convertOperator(simpleOp, value, type);
if (this._rowCount == 0)
{
// Fill first row (start after language line if present)
this._setRowValues(firstRowIndex, id, operator, value, null, {});
}
else
{
// Add 'AND' row.
this.addCriterion();
this._setRowValues(this._fieldIndex - 1, id, operator, value, 'and', {});
}
}
this._rowCount++;
}
}
}, this);
},
/**
* Get a simple criterion definition.
* @param {String} id The simple criterion ID.
* @return {Object} The simple criterion definition, or `null` if not found.
*/
_getSimpleCriterion: function(id)
{
for (var uuid in this._simpleCriteria) {
if (this._simpleCriteria[uuid].elements[id])
{
return this._simpleCriteria[uuid].elements[id];
}
}
return null;
},
/**
* Get the definition of the advanced criterion corresponding to a simple criterion.
* @param {Object} simpleCriterion The simple criterion definition.
* @return {Object} The corresponding advanced criterion definition, or `null` if not found.
*/
_getCorrespondingAdvancedCriterion: function(simpleCriterion)
{
var simpleType = simpleCriterion.criterionType;
var simpleProp = simpleCriterion.criterionProperty;
for (var advId in this._criteria)
{
var advCriterion = this._criteria[advId];
var advType = advCriterion.criterionType;
var advProp = advCriterion.criterionProperty;
// Consider the advanced criterion equal to the other if they are of the same type and target ("property").
if (simpleType == advType && simpleProp != null && simpleProp == advProp)
{
return advCriterion;
}
}
return null;
},
/**
* Convert the operator value into value for operators combo box
* @param {String} op The operator's value
* @param {Object} value The field value
* @param {String} type The field type
* @private
*/
_convertOperator: function(op, value, type)
{
if (!op)
{
return null;
}
if (op == 'like' && type == 'string')
{
return 'contains';
}
else if (op == 'gt' && (type == 'date' || type == 'datetime'))
{
return 'ge';
}
else if (op == 'lt' && (type == 'date' || type == 'datetime'))
{
return 'le';
}
else
{
return op;
}
},
/**
* Draw criteria in logical expressions
* @param {Object[]} expressions The criteria into the logical expression or other logical expressions
* @param {String} outerType The outer type, i.e. the logical operator between expressions as a single expression and its siblings : "AND" or "OR". Can be null if root expressions
* @param {String} innerType The inner type, i.e. the logical operator between all expressions : "AND" or "OR"
* @param {Number} firstRowIndex The index of the first row
* @param {Boolean} [outerPutParentheses=false] True if outer type needs parentheses (used in the case of the first expression among expressions)
* @private
*/
_drawExpression: function(expressions, outerType, innerType, firstRowIndex, outerPutParentheses)
{
var me = this;
var innerPutParentheses = this._oneOperandNeedsParentheses(expressions, innerType); // even though only one needs parentheses, put on every expression as only ') AND (' and ') OR (' operators exist
Ext.Array.each(expressions, function(object, index) {
var isFirstAmongExpressions = index == 0;
if (object.type == 'criterion')
{
if (me._rowCount == 0)
{
// Fill first row
me._setRowValues(firstRowIndex, object.id, me._convertOperator(object.op, object.value), object.rawValue, null, object);
}
else
{
var operator = me._computeCorrectOperator(isFirstAmongExpressions, outerPutParentheses, innerPutParentheses, outerType, innerType);
// Add row
me.addCriterion();
me._setRowValues(me._fieldIndex - 1, object.id, me._convertOperator(object.op, object.value), object.rawValue, operator, object);
}
me._rowCount++;
}
else
{
var subOuterType = isFirstAmongExpressions
? outerType
: innerType;
var subOuterPutParentheses = isFirstAmongExpressions
? outerPutParentheses
: innerPutParentheses;
me._drawExpression (object.expressions, subOuterType, object.type, firstRowIndex, subOuterPutParentheses);
}
});
},
/**
* @private
* Determines if one operand among thos in expressions needs some parentheses
* @param {Object[]} expressions The expressions (i.e. operands)
* @param {String} operator The logical operator between all expressions
* @return {Boolean} true if at least one operand needs parentheses
*/
_oneOperandNeedsParentheses: function(expressions, operator)
{
for (var i in expressions)
{
var expression = expressions[i];
if (needsParentheses(expression, operator))
{
return true;
}
}
return false;
function needsParentheses(operand, outerOperator)
{
// because parenthesis will thus enable for operator OR to have priority on AND
return operand.type === 'OR' && outerOperator === 'AND';
}
},
/**
* @private
* Computes the correct logical operator to draw
* @param {Boolean} isFirstAmongExpressions true if is first expression among siblings
* @param {Boolean} outerPutParentheses true if outer level needs to put parentheses
* @param {Boolean} innerPutParentheses true if inner level needs to put parentheses
* @param {String} outerType The outer type, i.e. the logical operator between expressions as a single expression and its siblings : "AND" or "OR"
* @param {String} innerType The inner type, i.e. the logical operator between all expressions : "AND" or "OR"
* @return {String} The correct logical operar (can be 'and', 'or', 'and-par')
*/
_computeCorrectOperator: function(isFirstAmongExpressions, outerPutParentheses, innerPutParentheses, outerType, innerType)
{
var type = isFirstAmongExpressions
? outerType
: innerType;
var putParentheses = isFirstAmongExpressions
? outerPutParentheses
: innerPutParentheses;
return type.toLowerCase() + (putParentheses ? '-par' : '');
},
/**
* @private
* For debug purposes, log the string representation of the given logical tree
* @param {Object} values The values of the logical tree
*/
_logTreeValues: function(values)
{
var me = this;
var logger = this.getLogger();
if (logger.isInfoEnabled())
{
logger.info('String representation of logical tree ');
console.info(values); // because wannot use syntax logger.info('aa %o zz', obj);
logger.info('is: ');
var criterionIndex = 1;
logger.info(traverseNodeOrLeaf(values));
}
function traverseNodeOrLeaf(node)
{
if (node.type === 'criterion')
{
return '$' + criterionIndex++;
}
else
{
return traverseIntermediateNode(node);
}
}
function traverseIntermediateNode(node)
{
var result = '';
var expressions = node.expressions;
var operator = node.type;
var atLeastOneOperandNeedsParenthesis = me._oneOperandNeedsParentheses(expressions, operator);
for (var subIndex in expressions)
{
var isFirst = subIndex == 0;
if (!isFirst)
{
result += (' ' + operator + ' ');
}
var subNode = expressions[subIndex];
if (atLeastOneOperandNeedsParenthesis)
{
result += '(';
}
var builtNode = traverseNodeOrLeaf(subNode);
result += builtNode;
if (atLeastOneOperandNeedsParenthesis)
{
result += ')';
}
}
return result;
}
},
/**
* Initialize the row values
* @param {Number} index The row index
* @param {String} criterion The id of criterion (first combobox)
* @param {String} operator The operator's value (2nd combobox)
* @param {Object} value The field's value (3rd field)
* @param {String} logicalOp The logical operator.
* @param {Object} object The object which can contain additional values.
* @private
*/
_setRowValues: function(index, criterion, operator, value, logicalOp, object)
{
var row = this.items.get('container-' + index);
var criterionCombo = row.items.get(this.statics().CRITERION_FIELD_ITEM_ID),
isFoundCriterion = false;
if (criterionCombo)
{
criterionCombo.store.clearFilter(); // clear filter to get all values
var record = criterionCombo.store.findRecord("name", criterion);
criterionCombo.setValue(criterion);
isFoundCriterion = record != null;
}
var operatorCombo = row.items.get(this.statics().OPERATOR_FIELD_ITEM_ID);
if (operator && operatorCombo)
{
operatorCombo.setValue(isFoundCriterion ? operator : null/*force to empty if no criterion found*/);
}
var valueCombo = row.items.get(this.statics().VALUE_FIELD_ITEM_ID);
if (valueCombo)
{
valueCombo.setValue(isFoundCriterion ? value : null/*force to empty if no criterion found*/);
}
if (index != 0 && logicalOp)
{
var logicalOperatorCombo = row.items.get(this.statics().LOGICAL_OPERATOR_FIELD_ITEM_ID);
if (logicalOperatorCombo)
{
logicalOperatorCombo.setValue(logicalOp);
}
}
this._additionalFields.forEach(function(additionalField) {
var itemId = additionalField.itemId,
field = row.items.get(itemId),
val = object[itemId];
if (field && Ext.isFunction(field.setValue) && val !== undefined)
{
field.setValue(isFoundCriterion ? val : null/*force to empty if no criterion found*/);
}
});
},
/**
* Reset the fields of a row.
* @param {Number} index The row index
* @private
*/
_resetRowValues: function(index)
{
var row = this.items.get('container-' + index);
var combo = row.items.get(this.statics().CRITERION_FIELD_ITEM_ID);
combo.store.clearFilter(); // clear filter to get all values
combo.reset();
var opCombo = row.items.get(this.statics().OPERATOR_FIELD_ITEM_ID);
opCombo.select(opCombo.getStore().getAt(0));
row.items.get(this.statics().VALUE_FIELD_ITEM_ID).reset();
},
/**
* Build all the stores.
* @private
*/
_buildCriteriaStores: function()
{
// Build the logical operator store (shared).
this._logicalOperatorStore = Ext.create('Ext.data.Store', {
fields: ['value', 'label', 'str'],
data: Ametys.plugins.cms.search.advanced.AdvancedSearchFormPanel.LOGICAL_OPERATORS
});
// The store with the 'and' operator exclusively
this._andOperatorStore = Ext.create('Ext.data.Store', {
fields: ['value', 'label', 'str'],
data: [{
value: "and",
label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_LOGICALOP_AND}}",
str: " && "
}]
});
// Build the criteria store and the enumeration stores.
this._advancedCriteriaFieldNames = [];
this._enumerationStores = {};
var advancedCriteria = [];
for (var name in this._criteria)
{
var criterion = this._criteria[name];
this._advancedCriteriaFieldNames.push({
name: name,
label: criterion.label
});
if (criterion.enumeration)
{
this._enumerationStores[name] = Ext.create('Ext.data.Store', {
fields: ['value', 'label'],
data: criterion.enumeration
});
}
if (this._language)
{
this._languagesEnumerationStore = Ext.create('Ext.data.Store', {
fields: ['value', 'label'],
data: this._language.enumeration
});
}
}
// Build the operator stores.
this._operatorStores = {};
for (var type in Ametys.plugins.cms.search.advanced.AdvancedSearchFormPanel.OPERATORS)
{
this._operatorStores[type] = Ext.create('Ext.data.Store', {
fields: ['value', 'label', 'shortLabel', 'labelMultiple', 'shortLabelMultiple'],
data: this.getOperators(type)
});
}
},
/**
* @protected
* Gets the operators for the given type
* @param {String} type The type
* @return {Object[]} An array of objects of operators (with at least the keys 'value', 'label', 'labelMultiple', 'shortLabel' and 'shortLabelMultiple')
*/
getOperators: function(type)
{
return Ametys.plugins.cms.search.advanced.AdvancedSearchFormPanel.OPERATORS[type]
.map(function(obj) {
var res = Ext.apply({}, obj);
if (!res.shortLabel)
{
res.shortLabel = res.label;
}
if (!res.labelMultiple)
{
res.labelMultiple = res.label;
}
if (!res.shortLabelMultiple)
{
res.shortLabelMultiple = res.labelMultiple;
}
return res;
});
},
/**
* Get the language criteria submit value
* @return {String} the language criteria submit value
*/
getLanguage: function()
{
if (this.items.get('container-language'))
{
var languageContainer = this.items.get('container-language');
var valueField = languageContainer.items.get(this.statics().VALUE_FIELD_ITEM_ID);
return valueField.getSubmitValue();
}
return null;
},
/**
* Get the value tree.
* @return {Object} the user advanced search, organized in a criterion tree.
*/
getValueTree: function()
{
var criteria = {};
// Build the expression to parse and index the criteria in the map.
var expression = this._buildExpression(criteria);
if (this._allowBlank && expression == '()')
{
return {};
}
// Parse the expression and build a syntax tree from it.
var syntaxTree = Ametys.plugins.cms.search.advanced.AdvancedSearchParser.parse(expression);
// Normalize the syntax tree by removing unnecessary levels (nested AND/OR).
this._normalizeSyntaxTree(syntaxTree);
// Build and return the value tree from the normalized syntax tree and the criteria map.
return this._getValues(syntaxTree, criteria);
},
/**
* Build the expression to parse and index the criteria in the map.
* The expression will look like '(criterion-1 || criterion-2) && (criterion-3)'.
* @param {Object} criteria The criteria map to fill.
* @return {String} the expression.
* @private
*/
_buildExpression: function(criteria)
{
var minIndex = this.items.get('container-language') ? 1 : 0; // exclude language line if present
var expression = '(';
var me = this,
logicalOpItemId = this.statics().LOGICAL_OPERATOR_FIELD_ITEM_ID,
criterionItemId = this.statics().CRITERION_FIELD_ITEM_ID,
operatorItemId = this.statics().OPERATOR_FIELD_ITEM_ID,
valueItemId = this.statics().VALUE_FIELD_ITEM_ID;
this.items.each(function(container, index, length) {
if (index >= minIndex)
{
var containerItems = container.items,
logicalOpField = containerItems.get(logicalOpItemId),
criterionField = containerItems.get(criterionItemId),
operatorField = containerItems.get(operatorItemId),
valueField = containerItems.get(valueItemId);
if (criterionField != null)
{
// Generate a local criterion ID.
var critId = 'criterion-' + index;
// handle additional fields
var additionalValues = {};
me._additionalFields.forEach(function(additionalField) {
var itemId = additionalField.itemId,
field = containerItems.get(itemId),
getValueFn = field && additionalField.getValueFn;
if (Ext.isFunction(getValueFn))
{
var val = getValueFn(field);
additionalValues[itemId] = val;
}
});
// Put the criterion ID, operator and value.
criteria[critId] = {
id: criterionField.getValue(),
op: operatorField.getValue(),
val: valueField.getJsonValue(),
additionalValues: additionalValues
}
// Build the expression.
if (logicalOpField != null)
{
var record = logicalOpField.findRecordByValue(logicalOpField.getValue());
expression = expression + record.get('str');
}
expression = expression + critId;
}
}
});
expression = expression + ')';
return expression;
},
/**
* Normalize the syntax tree by removing unnecessary levels (nested AND/OR).
* @param {Object} node the syntax tree node to normalize.
* @private
*/
_normalizeSyntaxTree: function(node)
{
// Normalize only AND/OR nodes.
if ((node.type == 'and' || node.type == 'or') && node.expressions)
{
// Start by recursing (reduce 'bottom-up').
for (var i = 0; i < node.expressions.length; i++)
{
this._normalizeSyntaxTree(node.expressions[i]);
}
var toRemove = [];
var count = node.expressions.length;
for (var i = 0; i < count; i++)
{
var childNode = node.expressions[i];
// Test if the child node is of the same type ('and' or 'or').
if (childNode.type == node.type && childNode.expressions)
{
// If the child node is of the same type, store its index in the 'to remove' list
// and copy its expressions to the current node.
toRemove.push(i);
for (var j = 0; j < childNode.expressions.length; j++)
{
node.expressions.push(childNode.expressions[j]);
}
}
}
// Remove the children node backwards (to avoid messing up indexes).
for (var i = (toRemove.length - 1); i >= 0; i--)
{
node.expressions.splice(toRemove[i], 1);
}
}
},
/**
* Build the value tree from a normalized syntax tree node and the criteria map.
* @param {Object} node The syntax tree node.
* @param {Object} criteria The criteria map.
* @private
*/
_getValues: function(node, criteria)
{
if (node.type == 'criterion')
{
return this._getCriterionValue(criteria[node.id]);
}
else if (node.expressions)
{
var expressions = [];
for (var i = 0; i < node.expressions.length; i++)
{
expressions.push(this._getValues(node.expressions[i], criteria));
}
return {
type: node.type.toUpperCase(),
expressions: expressions
};
}
},
/**
* Build a criterion value node.
* @param {Object} criterion The selected criterion data.
* @param {String} criterion.id The selected criterion ID.
* @param {String} criterion.op The selected operator.
* @param {String} criterion.val The user-submitted value.
* @private
*/
_getCriterionValue: function(criterion)
{
var value = {
type: 'criterion',
id: criterion.id,
op: criterion.op,
rawValue: criterion.val,
value: criterion.val
};
Ext.Object.each(criterion.additionalValues, function(fieldItemId, fieldValue) {
value[fieldItemId] = fieldValue;
});
return value;
},
/**
* Add a criterion (i.e. draw a new row).
* @param {Number} [position] the position of the new row. Can be null to insert criterion at last position.
*/
addCriterion: function(position)
{
var container;
if (position != null)
{
var index = this._fieldIndex + 1;
var container = this.insert(position, this._lineConfig(index, [this._getLogicalOperatorField(index)]));
}
else
{
// Insert to last position
container = this.items.getAt(this.items.getCount() - 1);
}
// Draw the new field row.
this._drawFieldRow(container);
if (this._allowBlank && this.items.getCount() === 1)
{
this._replaceOperatorFieldByOpeningField(container)
}
// Increment the index
this._fieldIndex++;
if (position == null)
{
// and draw the new 'closing parenthesis' line.
this._drawClosingLine(this._fieldIndex);
}
// previous & added lines => update UI
this._updateRowUi(container);
this._updateRowUi(container.previousSibling());
},
/**
* Replace the first field of the row (combobox of logical operators) by an opening field (opening parenthesis)
* @param {Ext.container.Container} container The row container
* @private
*/
_replaceOperatorFieldByOpeningField: function(container)
{
var index = container.fieldIndex;
container.remove(this.statics().OPENING_PARENTHESIS_FIELD_ITEM_ID);
container.remove(this.statics().LOGICAL_OPERATOR_FIELD_ITEM_ID);
var hasPreviousSibling = container.previousSibling() != null; // if true, there is probably "language line" before
var value = hasPreviousSibling
? "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_LOGICALOP_START_AND_PAR}}"
: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_LOGICALOP_START_PAR}}";
container.insert(0, this._getOpeningField(index, value));
},
/**
* Replace an opening field (opening parenthesis) by a combobox of logical operators)
* @param {Ext.container.Container} container The row container
* @private
*/
_replaceOpeningFieldByOperatorField: function(container)
{
var index = container.fieldIndex;
container.remove(container.getComponent(0));
container.insert(0, this._getLogicalOperatorField(index));
},
/**
* Insert additional field configurations in the given array of line items
* @param {Object[]} items The items where to insert additional field configurations
* @param {Number} rowIndex The index of the row
* @param {String} criterionName The criterion name
* @private
*/
_insertAdditionalFields: function(items, rowIndex, criterionName)
{
var logger = this.getLogger();
Ext.Array.each(this._additionalFields, function(additionalField) {
var itemIds = items.map(function(item) {return item['itemId'];}),
after = additionalField['after'],
itemId = additionalField['itemId'],
cfg = additionalField['config'] || {};
if (Ext.isFunction(cfg))
{
cfg = cfg(rowIndex, criterionName);
}
if (!itemId)
{
logger.error("Cannot insert additional field without the configuration 'itemId'");
return true;
}
if (!after)
{
logger.error("Cannot insert additional field without the configuration 'after'");
return true;
}
if (Ext.Array.contains(itemIds, itemId))
{
logger.error("Cannot insert additional field with itemId '" + itemId + "' as it already exists.");
return true;
}
if (Ext.Array.contains(['type', 'id', 'op', 'rawValue', 'value'], itemId))
{
// those ids can conflict and thus are forbidden
logger.error("Cannot insert additional field with forbidden itemId '" + itemId + "'.");
return true;
}
// Find position
var position = -1;
Ext.Array.findBy(itemIds, function(currentItemId, currentIndex) {
var isFound = currentItemId == after;
if (isFound)
{
position = currentIndex + 1;
}
return isFound;
});
if (position == -1)
{
logger.error("Cannot insert additional field after itemId '" + after + "' as it does not exists.");
return true;
}
cfg['itemId'] = itemId;
cfg['name'] = itemId + '-' + rowIndex;
items.splice(position, 0, cfg);
});
},
/**
* Draw the language criterion line
* @private
*/
_drawLanguageLine: function()
{
if (this._language && !this._language.hidden)
{
var items = Ext.Array.flatten([
this._getOpeningField(this._fieldIndex, ''),
this._getLanguageCriterionField(),
this._getLanguageOperatorField(),
this.getValueField(this._fieldIndex, this._language),
this._getDeleteButton(this._fieldIndex, true),
this._getAddRowUpButton(this._fieldIndex, true),
this._getMoveUpButton(this._fieldIndex, true),
this._getMoveDownButton(this._fieldIndex, true)
]);
this._insertAdditionalFields(items, this._fieldIndex, null);
// Display the language criterion (cannot delete it)
var container = this.add({
xtype: 'fieldcontainer',
itemId: 'container-language',
layout: { type: 'hbox' },
defaults: {margin:'0 5 5 0'},
fieldIndex: this._fieldIndex,
items: items
});
this._fieldIndex++;
}
// else, if not present the language will be forced by server side (using the default value if hidden)
},
/**
* Draw the first criterion line (with an opening parenthesis instead of the logical operator).
* @private
*/
_drawDefaultLine: function()
{
// Get the default criterion.
var criterion = this._criteria[this._defaultCriterionId];
var index = this._fieldIndex;
var items = Ext.Array.flatten([
this._getOpeningField(index, index > 0 ? "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_LOGICALOP_START_AND_PAR}}" : "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_LOGICALOP_START_PAR}}"),
this._getCriterionField(index, this._defaultCriterionId),
this._getOperatorField(index, criterion),
this.getValueField(index, criterion),
this._getDeleteButton(index, true),
this._getAddRowUpButton(index, true),
this._getMoveUpButton(index, true),
this._getMoveDownButton(index, true)
]);
this._insertAdditionalFields(items, index, this._defaultCriterionId);
var container = this.add({
xtype: 'fieldcontainer',
itemId: 'container-' + index,
layout: { type: 'hbox' },
defaults: {margin:'0 5 0 0'},
fieldIndex: index,
items: items
});
},
/**
* Add the last container line (closing parenthesis + hint).
* @param {Number} index The line index.
* @private
*/
_drawClosingLine: function(index)
{
var items;
if (this.items.getCount() == 0)
{
items = [this._getStartButton()];
}
else
{
items = [
this._getLogicalOperatorField(index),
this._getHintField(index)
];
}
this.add(this._lineConfig(index, items));
},
/**
* Gets the configuration of a line
* @param {Number} index The line index.
* @param {Object[]} items the child items
* @return {Object} The configuration of a line
* @private
*/
_lineConfig: function(index, items)
{
return {
xtype: 'fieldcontainer',
itemId: 'container-' + index,
layout: { type: 'hbox' },
defaults: {margin:'0 5 0 0'},
fieldIndex: index,
items: items
};
},
/**
* Draw a new criterion row (from the default criterion).
* Called when a logical operator was chosen or 'insert row up' button was clicked.
* @param {Ext.container.Container} container The row container
* @private
*/
_drawFieldRow: function(container)
{
// Get the default criterion.
var criterion = this._criteria[this._defaultCriterionId];
// Remove and destroy the hint if exists
container.remove('hint', true);
// Remove and destroy the start button if exists
container.remove('start-button', true);
// Add the other fields.
var index = container.fieldIndex;
var items = Ext.Array.flatten([
this._getCriterionField(index, this._defaultCriterionId),
this._getOperatorField(index, criterion),
this.getValueField(index, criterion),
this._getDeleteButton(index, false),
this._getAddRowUpButton(index, false),
this._getMoveUpButton(index, true /* will be shown if necessary after adding, in #_updateRowUi */),
this._getMoveDownButton(index, true /* will be shown if necessary after adding, in #_updateRowUi */)
]);
this._insertAdditionalFields(items, index, this._defaultCriterionId);
container.add(items);
},
/**
* Re-generate the operator and value field when the criterion is changed.
* @param {Number} index The line index.
* @param {Ext.form.FieldContainer} container The field container.
* @param {String} name The criterion name.
* @param {Object} criterion The criterion data.
* @private
*/
_changeCriterion: function(index, container, name, criterion)
{
// Remove and destroy the current criterion-dependent fields.
container.remove(this.statics().OPERATOR_FIELD_ITEM_ID);
container.remove(this.statics().VALUE_FIELD_ITEM_ID);
container.remove(this.statics().REMOVE_BUTTON_ITEM_ID);
container.remove(this.statics().REMOVE_BUTTON_ITEM_ID + '-placeholder');
container.remove(this.statics().ADD_BUTTON_ITEM_ID);
container.remove(this.statics().ADD_BUTTON_ITEM_ID + '-placeholder');
container.remove(this.statics().MOVEUP_BUTTON_ITEM_ID);
container.remove(this.statics().MOVEUP_BUTTON_ITEM_ID + '-placeholder');
container.remove(this.statics().MOVEDOWN_BUTTON_ITEM_ID);
container.remove(this.statics().MOVEDOWN_BUTTON_ITEM_ID + '-placeholder');
this._additionalFields
.map(function(additionalField) {return additionalField['itemId'];})
.filter(function(itemId) {return itemId != null;})
.forEach(function(itemId) {
container.remove(itemId);
});
var minIndex = this.items.get('container-language') ? 1 : 0;
// Add the other fields, corresponding to the selected criterion.
var hidden = !this._allowBlank && index === minIndex;
var items = Ext.Array.flatten([
this._getOperatorField(index, criterion),
this.getValueField(index, criterion),
this._getDeleteButton(index, hidden),
this._getAddRowUpButton(index, hidden),
this._getMoveUpButton(index, true /* will be shown if necessary after adding, in #_updateRowUi */),
this._getMoveDownButton(index, true /* will be shown if necessary after adding, in #_updateRowUi */)
]);
this._insertAdditionalFields(items, index, name);
container.add(items);
this._updateRowUi(container);
},
/**
* Get the opening parenthesis field.
* @param {Number} index the field's index
* @param {String} value the value to initialize the field with
* @return {Object} the opening parenthesis field configuration.
* @private
*/
_getOpeningField: function(index, value)
{
return {
xtype: 'displayfield',
cls: 'ametys',
margin: '0 5 0 5',
itemId: this.statics().OPENING_PARENTHESIS_FIELD_ITEM_ID,
fieldIndex: index,
value: value,
width: this.statics().DEFAULT_LOGICAL_OPERATOR_FIELD_WITH
}
},
/**
* Get a logical operator field.
* @param {Number} index The line index.
* @return {Object} the field configuration.
* @private
*/
_getLogicalOperatorField: function(index)
{
return {
xtype: 'combobox',
fieldIndex: index,
name: this.statics().LOGICAL_OPERATOR_FIELD_ITEM_ID + '-' + index,
itemId: this.statics().LOGICAL_OPERATOR_FIELD_ITEM_ID,
triggerAction: 'all',
queryMode: 'local',
editable: false,
forceSelection: true,
width: this.statics().DEFAULT_LOGICAL_OPERATOR_FIELD_WITH,
emptyText: ')',
cls: 'ametys',
margin: '0 5 0 5',
isParenthesis: true,
store: this._logicalOperatorStore,
valueField: 'value',
displayField: 'label',
listeners: {
'select': this._onSelectLogicalOperator,
'change': this._onChangeLogicalOperator,
scope: this
}
}
},
/**
* Get a criterion field.
* @param {Number} index The line index.
* @param {String} criterionId The criterion ID to preselect.
* @return {Object} the field configuration.
* @private
*/
_getCriterionField: function(index, criterionId)
{
return {
xtype: 'combobox',
fieldIndex: index,
cls: 'ametys',
name: this.statics().CRITERION_FIELD_ITEM_ID + '-' + index,
itemId: this.statics().CRITERION_FIELD_ITEM_ID,
flex: 1,
queryMode: 'local',
triggerAction: 'all',
allQuery: '',
editable: true,
anyMatch: true,
forceSelection: true,
clearFilterOnBlur: true,
store: Ext.create('Ext.data.Store', {
fields: ['name', 'label'],
data: this._advancedCriteriaFieldNames,
sorters: [{property: 'label', direction:'ASC'}]
}),
valueField: 'name',
displayField: 'label',
value: criterionId || '',
listeners: {
'change': this._onChangeCriterion,
'focus': function(combo) {
// Clear filter on focus.
combo.getStore().clearFilter();
},
scope: this
}
}
},
/**
* @private
* Get the configuration object for the language criterion field
* @return {Object} the configuration object
*/
_getLanguageCriterionField: function()
{
return {
xtype: 'textfield',
fieldIndex: 0,
margin: '0 5 5 0',
readOnly: true,
cls: 'ametys',
triggerWrapCls: 'border-transparent',
name: this.statics().CRITERION_FIELD_ITEM_ID + '-0',
itemId: this.statics().CRITERION_FIELD_ITEM_ID,
flex: 1,
value: this._language.label
};
},
/**
* Get a criterion operator field.
* @param {Number} index The line index.
* @param {Object} criterion The criterion data.
* @param {String} [opValue] The operator value. Can be null.
* @return {Object} the field configuration.
* @private
*/
_getOperatorField: function(index, criterion, opValue)
{
var store;
if (criterion)
{
var id = criterion.name;
var type = criterion.type.toLowerCase() || 'string';
var enumerated = criterion.enumeration != null;
var widget = criterion.widget != null;
var multiple = criterion.multiple === true;
if (type == 'date' || type == 'datetime')
{
store = this._operatorStores['date'];
}
else if (type == 'rich-text')
{
store = this._operatorStores['rich-text'];
}
else if (enumerated || widget)
{
store = this._operatorStores['enumeration'];
}
else if (type == 'string' || type == 'reference')
{
store = this._operatorStores['string'];
}
else if (type == 'long' || type == 'double')
{
store = this._operatorStores['number'];
}
else if (type == 'boolean')
{
store = this._operatorStores['boolean'];
}
else if (type == 'content' || type == 'sub_content' || type == 'user')
{
store = this._operatorStores['enumeration'];
}
else if (type == 'geocode')
{
store = this._operatorStores['geocode'];
}
}
else
{
store = this._operatorStores[null];
}
return {
xtype: 'combobox',
fieldIndex: index,
cls: 'ametys',
name: this.statics().OPERATOR_FIELD_ITEM_ID + '-' + index,
itemId: this.statics().OPERATOR_FIELD_ITEM_ID,
triggerAction: 'all',
queryMode: 'local',
forceSelection: true,
width: 150,
store: store,
valueField: 'value',
displayField: multiple ? 'labelMultiple' : 'label',
displayTpl: multiple ? this.statics().OPERATOR_TEMPLATE_MULTIPLE : this.statics().OPERATOR_TEMPLATE,
value: opValue,
listeners: {
'change': this._onChangeOperator,
'render': function(combo) {
if (combo.getSelection() == null)
{
combo.select(combo.getStore().getAt(0));
}
},
scope: this
}
}
},
/**
* Get the language operator field configuration object
* @return {Object} the configuration object
*/
_getLanguageOperatorField: function()
{
return {
xtype: 'textfield',
margin: '0 5 5 0',
readOnly: true,
cls: 'ametys',
triggerWrapCls: 'border-transparent',
name: this.statics().OPERATOR_FIELD_ITEM_ID + '-0',
itemId: this.statics().OPERATOR_FIELD_ITEM_ID,
width: 150,
value: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_EQ}}"
};
},
/**
* Get a criterion value field.
* @param {Number} index The line index.
* @param {Object} criterion The criterion data.
* @param {Object} [value] The init value. Can be null.
* @return {Object} the field configuration or the field itself.
*/
getValueField: function(index, criterion, value)
{
criterion = criterion || {};
var type = criterion.type && criterion.type.toLowerCase() || 'string';
var cfg = {
fieldIndex: index,
name: 'value-' + index,
itemId: this.statics().VALUE_FIELD_ITEM_ID,
flex: this.statics().VALUE_FIELD_FLEX,
cls: 'ametys',
value: value,
type: type,
contentType: criterion.contentType,
multiple: criterion.multiple,
widget: criterion.widget,
hidden: criterion.hidden,
ametysDescription: criterion.description || ''
};
if (criterion.validation)
{
var validation = criterion.validation;
cfg.regexp = validation.regexp || null;
if (validation.invalidText)
{
cfg.invalidText = validation.invalidText;
}
if (validation.regexText)
{
cfg.regexText = validation.regexText;
}
}
if (criterion.enumeration)
{
var enumeration = [];
var entries = criterion.enumeration;
for (var j=0; j < entries.length; j++)
{
enumeration.push([entries[j].value, entries[j].label]);
}
cfg.enumeration = enumeration;
}
if (criterion['widget-params'])
{
Ext.merge(cfg, criterion['widget-params']);
}
if (cfg.regexp)
{
cfg.regex = new RegExp(cfg.regexp);
cfg.regexText = cfg.regexText || cfg.invalidText || "{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_FORM_INVALID_REGEXP}}" + cfg.regexp;
}
return Ametys.form.WidgetManager.getWidget(cfg.widget, cfg.type, cfg);
},
/**
* @private
* Gets the configuration of a placeholder
* @param {Boolean} hidden true to hide
* @param {String} btnPlaceHolderItemId The item id of the placeholder
* @return {Object} The configuration
*/
_getButtonPlaceholderCfg: function(hidden, btnPlaceHolderItemId)
{
return {
xtype: 'displayfield',
itemId: btnPlaceHolderItemId,
hidden: hidden
};
},
/**
* @private
* Hides the button, shows a placeholder
* @param {Ext.button.Button} button The button to hide
*/
_hideBtn: function(button)
{
var container = button.up(),
btnPlaceHolderItemId = button.getItemId() + '-placeholder',
placeHolder = container.getComponent(btnPlaceHolderItemId);
placeHolder.show();
button.hide();
},
/**
* @private
* Shows the button, hides the placeholder
* @param {Ext.button.Button} button The button to show
*/
_showBtn: function(button)
{
var container = button.up(),
btnPlaceHolderItemId = button.getItemId() + '-placeholder',
placeHolder = container.getComponent(btnPlaceHolderItemId);
placeHolder.hide();
button.show();
},
/**
* Get a row delete button configuration
* @param {Number} index The line index.
* @param {Boolean} hidden True to hide the button (used for the first line).
* @return {Object[]} the button configuration(s).
* @private
*/
_getDeleteButton: function(index, hidden)
{
var itemId = this.statics().REMOVE_BUTTON_ITEM_ID;
var removeBtnPlaceHolderItemId = itemId + '-placeholder';
return [{
xtype: 'button',
fieldIndex: index,
itemId: itemId,
iconCls: 'ametysicon-sign-raw-cross',
tooltip: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_REMOVE_CRITERION}}",
handler: this._removeRow,
scope: this,
hidden: hidden,
listeners: {
'afterrender': {
fn: function(button) {
button.up().getComponent(removeBtnPlaceHolderItemId).setWidth(button.getWidth());
},
scope: this
}
}
}, this._getButtonPlaceholderCfg(!hidden, removeBtnPlaceHolderItemId) ];
},
/**
* Get a 'add row' button configuration
* @param {Number} index The line index.
* @param {Boolean} hidden True to hide the button (used for the first line).
* @return {Object[]} the button configuration(s).
* @private
*/
_getAddRowUpButton: function(index, hidden)
{
var itemId = this.statics().ADD_BUTTON_ITEM_ID;
var addBtnPlaceHolderItemId = itemId + '-placeholder';
return [{
xtype: 'button',
fieldIndex: index,
itemId: itemId,
iconCls: 'ametysicon-sign-raw-add',
tooltip: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_INSERT_CRITERION}}",
handler: this._insertRowUp,
scope: this,
hidden: hidden,
listeners: {
'afterrender': {
fn: function(button) {
button.up().getComponent(addBtnPlaceHolderItemId).setWidth(button.getWidth());
},
scope: this
}
}
}, this._getButtonPlaceholderCfg(!hidden, addBtnPlaceHolderItemId) ];
},
/**
* Get a 'move row up' button configuration
* @param {Number} index The line index.
* @param {Boolean} hidden True to hide the button.
* @return {Object[]} the button configuration(s).
* @private
*/
_getMoveUpButton: function(index, hidden)
{
var itemId = this.statics().MOVEUP_BUTTON_ITEM_ID;
var btnPlaceHolderItemId = itemId + '-placeholder';
return [{
xtype: 'button',
fieldIndex: index,
itemId: itemId,
iconCls: 'ametysicon-arrow-up-simple',
tooltip: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_MOVEUP_CRITERION}}",
handler: this._moveRowUp,
scope: this,
hidden: hidden,
listeners: {
'afterrender': {
fn: function(button) {
button.up().getComponent(btnPlaceHolderItemId).setWidth(button.getWidth());
},
scope: this
}
}
}, this._getButtonPlaceholderCfg(!hidden, btnPlaceHolderItemId) ];
},
/**
* Get a 'move row down' button configuration
* @param {Number} index The line index.
* @param {Boolean} hidden True to hide the button.
* @return {Object[]} the button configuration(s).
* @private
*/
_getMoveDownButton: function(index, hidden)
{
var itemId = this.statics().MOVEDOWN_BUTTON_ITEM_ID;
var btnPlaceHolderItemId = itemId + '-placeholder';
return [{
xtype: 'button',
fieldIndex: index,
itemId: this.statics().MOVEDOWN_BUTTON_ITEM_ID,
iconCls: 'ametysicon-arrow-down-simple',
tooltip: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_MOVEDOWN_CRITERION}}",
handler: this._moveRowDown,
scope: this,
hidden: hidden,
listeners: {
'afterrender': {
fn: function(button) {
button.up().getComponent(btnPlaceHolderItemId).setWidth(button.getWidth());
},
scope: this
}
}
}, this._getButtonPlaceholderCfg(!hidden, btnPlaceHolderItemId) ];
},
/**
* Get the hint field configuration.
* @param {Number} index The line index.
* @return {Object} the field configuration.
* @private
*/
_getHintField: function(index)
{
return {
xtype: 'displayfield',
itemId: 'hint',
fieldIndex: index,
cls: 'advanced-search-form-hint',
value: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_ADD_HINT}}"
}
},
/**
* Get the 'start button' button configuration
* @return {Object} The button configuration
* @private
*/
_getStartButton: function()
{
return {
xtype: 'button',
itemId: 'start-button',
text: '{{i18n PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_START_BUTTON_TEXT}}',
iconCls: 'ametysicon-sign-raw-add',
handler: function() {
this.addCriterion();
this._rowCount++;
},
scope: this
};
},
/**
* The first time a logical operator is selected, add a new line.
* @param {Ext.form.field.ComboBox} combo The combo box.
* @param {Array} records The selected records.
* @param {Object} eOpts The event options.
* @private
*/
_onSelectLogicalOperator: function(combo, records, eOpts)
{
// Set a marker
if (combo.isParenthesis === true)
{
this.addCriterion();
this._rowCount++;
Ext.defer(function() {combo.isParenthesis = false}, 1);
}
},
/**
* Triggered when the value of the combo is changed via the setValue method.
* @param {Ext.form.field.ComboBox} combo The combo box.
* @param {Object} newValue The new value.
* @param {Object} oldValue The old value.
* @param {Object} eOpts The event options.
* @private
*/
_onChangeLogicalOperator: function(combo, newValue, oldValue, eOpts)
{
// Set a marker
if (combo.isParenthesis === true)
{
Ext.defer(function() {combo.isParenthesis = false}, 1);
}
},
/**
* Re-draw the operator and value fields when a new criterion is selected.
* @param {Ext.form.field.ComboBox} combo The combo box.
* @param {Object} newValue The new value.
* @param {Object} oldValue The old value.
* @param {Object} eOpts The event options.
* @private
*/
_onChangeCriterion: function(combo, newValue, oldValue, eOpts)
{
if (newValue != null)
{
// Re-draw the fields
var criterion = this._criteria[newValue];
var container = this.items.get('container-' + combo.fieldIndex); //+ combo.up('fieldcontainer');
this._changeCriterion(combo.fieldIndex, container, newValue, criterion);
}
},
/**
* Remove a line.
* @param {Ext.button.Button} button The clicked 'remove' button.
* @param {Object} eOpts The event options.
* @protected
*/
_removeRow: function(button, eOpts)
{
var container = button.up('fieldcontainer'),
isFirst = this.items.getAt(0) === container,
nextContainer = container.nextSibling(),
previousContainer = container.previousSibling();
this.remove(container);
this._rowCount--;
if (this._allowBlank && isFirst && nextContainer && this._rowCount > 0)
{
// next container is now the first one, and is not alone
this._replaceOperatorFieldByOpeningField(nextContainer)
}
else if (this._rowCount == 0 && nextContainer)
{
// nextContainer is the closing line
this.remove(nextContainer);
this._drawClosingLine(this._fieldIndex);
}
// previous & next lines => update UI
this._updateRowUi(previousContainer);
this._updateRowUi(nextContainer);
},
/**
* Insert a row up to the current position
* @param {Ext.button.Button} button The clicked 'insert row up' button.
* @param {Object} eOpts The event options.
* @protected
*/
_insertRowUp: function(button, eOpts)
{
var container = button.up('fieldcontainer');
var position = this.items.indexOf(container);
// Draw the new field row.
this.addCriterion(position);
// the created row
var createdRow = this.items.getAt(position);
// Set logical operator to default value 'and'
if (this._allowBlank && position == 0)
{
this._replaceOperatorFieldByOpeningField(createdRow);
// the row which was clicked in 1st position and which will be in 2nd
var secondRow = container;
this._replaceOpeningFieldByOperatorField(secondRow);
var logicalOpField = secondRow.items.get(this.statics().LOGICAL_OPERATOR_FIELD_ITEM_ID);
logicalOpField.setValue("and");
}
else
{
var logicalOpField = createdRow.items.get(this.statics().LOGICAL_OPERATOR_FIELD_ITEM_ID);
logicalOpField.setValue("and");
}
// next line => update UI
this._updateRowUi(createdRow.nextSibling());
this._rowCount++;
},
/**
* @private
* Swaps the logical operators of the two given row containers
* @param {Ext.form.FieldContainer} firstRow The first row container
* @param {Ext.form.FieldContainer} secondRow The second row container
*/
_swapLogicalOperators: function(firstRow, secondRow)
{
var firstLogicalOp = firstRow.items.first(),
secondLogicalOp = secondRow.items.first();
firstRow.insert(0, secondLogicalOp);
secondRow.insert(0, firstLogicalOp);
// and that's all, the components are automatically detached from their former container
},
/**
* Moves up a row
* @param {Ext.button.Button} button The clicked button.
* @protected
*/
_moveRowUp: function(button)
{
var container = button.up('fieldcontainer'),
prev = container.previousSibling(),
minIndex = this.items.get('container-language') ? 1 : 0,
prevPosition = this.items.indexOf(prev),
prevIsFirstCriterion = prevPosition === minIndex;
if (prev)
{
this.moveBefore(container, prev);
if (prevIsFirstCriterion)
{
this._swapLogicalOperators(container, prev);
}
this._updateRowUi(container);
this._updateRowUi(prev);
}
else
{
// unexpected => button should not be there, at least hide it
this._hideBtn(button);
}
},
/**
* Moves down a row
* @param {Ext.button.Button} button The clicked button.
* @protected
*/
_moveRowDown: function(button)
{
var container = button.up('fieldcontainer'),
next = container.nextSibling(),
minIndex = this.items.get('container-language') ? 1 : 0,
position = this.items.indexOf(container),
isFirstCriterion = position === minIndex;
if (next)
{
this.moveAfter(container, next);
if (isFirstCriterion)
{
this._swapLogicalOperators(next, container);
}
this._updateRowUi(container);
this._updateRowUi(next);
}
else
{
// unexpected => button should not be there, at least hide it
this._hideBtn(button);
}
},
/**
* @private
* Updates the UI of the given row (show/hide buttons...)
* @param {Ext.form.FieldContainer} rowContainer The row container to update
*/
_updateRowUi: function(rowContainer)
{
if (!rowContainer)
{
return;
}
var position = this.items.indexOf(rowContainer),
maxIndex = this.items.length - 2/* last item is not a criterion, so last criterion is at index 'length-2' */;
isLastCriterion = position === maxIndex,
minIndex = this.items.get('container-language') ? 1 : 0,
isFirstCriterion = position === minIndex;
// Delete button
var deleteBtn = rowContainer && !rowContainer.isDestroyed && rowContainer.getComponent(this.statics().REMOVE_BUTTON_ITEM_ID);
if (deleteBtn)
{
var hide = !this._allowBlank && isFirstCriterion;
this[hide ? '_hideBtn' : '_showBtn'](deleteBtn);
}
// Insert button
var addBtn = rowContainer && !rowContainer.isDestroyed && rowContainer.getComponent(this.statics().ADD_BUTTON_ITEM_ID);
if (addBtn)
{
var hide = !this._allowBlank && isFirstCriterion;
this[hide ? '_hideBtn' : '_showBtn'](addBtn);
}
// Move Down button
var rowMoveDownBtn = rowContainer && !rowContainer.isDestroyed && rowContainer.getComponent(this.statics().MOVEDOWN_BUTTON_ITEM_ID);
if (rowMoveDownBtn)
{
var hide = isLastCriterion;
this[hide ? '_hideBtn' : '_showBtn'](rowMoveDownBtn);
}
// Move Up button
var rowMoveUpBtn = rowContainer && !rowContainer.isDestroyed && rowContainer.getComponent(this.statics().MOVEUP_BUTTON_ITEM_ID);
if (rowMoveUpBtn)
{
var hide = isFirstCriterion;
this[hide ? '_hideBtn' : '_showBtn'](rowMoveUpBtn);
}
},
/**
* Re-draw the value field when a new operator is selected.
* @param {Ext.form.field.ComboBox} combo The combo box.
* @param {Object} newValue The new value.
* @param {Object} oldValue The old value.
* @private
*/
_onChangeOperator: function(combo, newValue, oldValue)
{
if (newValue != null && oldValue != null)
{
var container = this.items.get('container-' + combo.fieldIndex);
var valueField = container.items.get(this.statics().VALUE_FIELD_ITEM_ID);
//check if widget can be set to read only mode
if (typeof valueField.setReadOnly !== "undefined") {
valueField.setReadOnly(newValue == "is-empty" || newValue == "is-not-empty")
}
// clear widget and store value
if (oldValue != "is-empty" && oldValue != "is-not-empty" && (newValue == "is-empty" || newValue == "is-not-empty"))
{
valueField.oldValue = valueField.getValue();
valueField.setValue(null)
}
// fill widget with old value
if (newValue != "is-empty" && newValue != "is-not-empty" && (oldValue == "is-empty" || oldValue == "is-not-empty"))
{
valueField.setValue(valueField.oldValue)
}
}
}
});