/**
* Performs value validation
* @param {Rule} rule
* @param {string|string[]} value
* @returns {array|boolean} true or error array
* @fires QueryBuilder.changer:validateValue
*/
QueryBuilder.prototype.validateValue = function(rule, value) {
var validation = rule.filter.validation || {};
var result = true;
if (validation.callback) {
result = validation.callback.call(this, value, rule);
}
else {
result = this._validateValue(rule, value);
}
/**
* Modifies the result of the rule validation method
* @event changer:validateValue
* @memberof QueryBuilder
* @param {array|boolean} result - true or an error array
* @param {*} value
* @param {Rule} rule
* @returns {array|boolean}
*/
return this.change('validateValue', result, value, rule);
};
/**
* Default validation function
* @param {Rule} rule
* @param {string|string[]} value
* @returns {array|boolean} true or error array
* @throws ConfigError
* @private
*/
QueryBuilder.prototype._validateValue = function(rule, value) {
var filter = rule.filter;
var operator = rule.operator;
var validation = filter.validation || {};
var result = true;
var tmp, tempValue;
if (rule.operator.nb_inputs === 1) {
value = [value];
}
for (var i = 0; i < operator.nb_inputs; i++) {
if (!operator.multiple && $.isArray(value[i]) && value[i].length > 1) {
result = ['operator_not_multiple', operator.type, this.translate('operators', operator.type)];
break;
}
switch (filter.input) {
case 'radio':
if (value[i] === undefined || value[i].length === 0) {
if (!validation.allow_empty_value) {
result = ['radio_empty'];
}
break;
}
break;
case 'checkbox':
if (value[i] === undefined || value[i].length === 0) {
if (!validation.allow_empty_value) {
result = ['checkbox_empty'];
}
break;
}
break;
case 'select':
if (value[i] === undefined || value[i].length === 0 || (filter.placeholder && value[i] == filter.placeholder_value)) {
if (!validation.allow_empty_value) {
result = ['select_empty'];
}
break;
}
break;
default:
tempValue = $.isArray(value[i]) ? value[i] : [value[i]];
for (var j = 0; j < tempValue.length; j++) {
switch (QueryBuilder.types[filter.type]) {
case 'string':
if (tempValue[j] === undefined || tempValue[j].length === 0) {
if (!validation.allow_empty_value) {
result = ['string_empty'];
}
break;
}
if (validation.min !== undefined) {
if (tempValue[j].length < parseInt(validation.min)) {
result = [this.getValidationMessage(validation, 'min', 'string_exceed_min_length'), validation.min];
break;
}
}
if (validation.max !== undefined) {
if (tempValue[j].length > parseInt(validation.max)) {
result = [this.getValidationMessage(validation, 'max', 'string_exceed_max_length'), validation.max];
break;
}
}
if (validation.format) {
if (typeof validation.format == 'string') {
validation.format = new RegExp(validation.format);
}
if (!validation.format.test(tempValue[j])) {
result = [this.getValidationMessage(validation, 'format', 'string_invalid_format'), validation.format];
break;
}
}
break;
case 'number':
if (tempValue[j] === undefined || tempValue[j].length === 0) {
if (!validation.allow_empty_value) {
result = ['number_nan'];
}
break;
}
if (isNaN(tempValue[j])) {
result = ['number_nan'];
break;
}
if (filter.type == 'integer') {
if (parseInt(tempValue[j]) != tempValue[j]) {
result = ['number_not_integer'];
break;
}
}
else {
if (parseFloat(tempValue[j]) != tempValue[j]) {
result = ['number_not_double'];
break;
}
}
if (validation.min !== undefined) {
if (tempValue[j] < parseFloat(validation.min)) {
result = [this.getValidationMessage(validation, 'min', 'number_exceed_min'), validation.min];
break;
}
}
if (validation.max !== undefined) {
if (tempValue[j] > parseFloat(validation.max)) {
result = [this.getValidationMessage(validation, 'max', 'number_exceed_max'), validation.max];
break;
}
}
if (validation.step !== undefined && validation.step !== 'any') {
var v = (tempValue[j] / validation.step).toPrecision(14);
if (parseInt(v) != v) {
result = [this.getValidationMessage(validation, 'step', 'number_wrong_step'), validation.step];
break;
}
}
break;
case 'datetime':
if (tempValue[j] === undefined || tempValue[j].length === 0) {
if (!validation.allow_empty_value) {
result = ['datetime_empty'];
}
break;
}
// we need MomentJS
if (validation.format) {
if (!('moment' in window)) {
Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
}
var datetime = moment(tempValue[j], validation.format);
if (!datetime.isValid()) {
result = [this.getValidationMessage(validation, 'format', 'datetime_invalid'), validation.format];
break;
}
else {
if (validation.min) {
if (datetime < moment(validation.min, validation.format)) {
result = [this.getValidationMessage(validation, 'min', 'datetime_exceed_min'), validation.min];
break;
}
}
if (validation.max) {
if (datetime > moment(validation.max, validation.format)) {
result = [this.getValidationMessage(validation, 'max', 'datetime_exceed_max'), validation.max];
break;
}
}
}
}
break;
case 'boolean':
if (tempValue[j] === undefined || tempValue[j].length === 0) {
if (!validation.allow_empty_value) {
result = ['boolean_not_valid'];
}
break;
}
tmp = ('' + tempValue[j]).trim().toLowerCase();
if (tmp !== 'true' && tmp !== 'false' && tmp !== '1' && tmp !== '0' && tempValue[j] !== 1 && tempValue[j] !== 0) {
result = ['boolean_not_valid'];
break;
}
}
if (result !== true) {
break;
}
}
}
if (result !== true) {
break;
}
}
if ((rule.operator.type === 'between' || rule.operator.type === 'not_between') && value.length === 2) {
switch (QueryBuilder.types[filter.type]) {
case 'number':
if (value[0] > value[1]) {
result = ['number_between_invalid', value[0], value[1]];
}
break;
case 'datetime':
// we need MomentJS
if (validation.format) {
if (!('moment' in window)) {
Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
}
if (moment(value[0], validation.format).isAfter(moment(value[1], validation.format))) {
result = ['datetime_between_invalid', value[0], value[1]];
}
}
break;
}
}
return result;
};
/**
* Returns an incremented group ID
* @returns {string}
* @private
*/
QueryBuilder.prototype.nextGroupId = function() {
return this.status.id + '_group_' + (this.status.group_id++);
};
/**
* Returns an incremented rule ID
* @returns {string}
* @private
*/
QueryBuilder.prototype.nextRuleId = function() {
return this.status.id + '_rule_' + (this.status.rule_id++);
};
/**
* Returns the operators for a filter
* @param {string|object} filter - filter id or filter object
* @returns {object[]}
* @fires QueryBuilder.changer:getOperators
* @private
*/
QueryBuilder.prototype.getOperators = function(filter) {
if (typeof filter == 'string') {
filter = this.getFilterById(filter);
}
var result = [];
for (var i = 0, l = this.operators.length; i < l; i++) {
// filter operators check
if (filter.operators) {
if (filter.operators.indexOf(this.operators[i].type) == -1) {
continue;
}
}
// type check
else if (this.operators[i].apply_to.indexOf(QueryBuilder.types[filter.type]) == -1) {
continue;
}
result.push(this.operators[i]);
}
// keep sort order defined for the filter
if (filter.operators) {
result.sort(function(a, b) {
return filter.operators.indexOf(a.type) - filter.operators.indexOf(b.type);
});
}
/**
* Modifies the operators available for a filter
* @event changer:getOperators
* @memberof QueryBuilder
* @param {QueryBuilder.Operator[]} operators
* @param {QueryBuilder.Filter} filter
* @returns {QueryBuilder.Operator[]}
*/
return this.change('getOperators', result, filter);
};
/**
* Returns a particular filter by its id
* @param {string} id
* @param {boolean} [doThrow=true]
* @returns {object|null}
* @throws UndefinedFilterError
* @private
*/
QueryBuilder.prototype.getFilterById = function(id, doThrow) {
if (id == '-1') {
return null;
}
for (var i = 0, l = this.filters.length; i < l; i++) {
if (this.filters[i].id == id) {
return this.filters[i];
}
}
Utils.error(doThrow !== false, 'UndefinedFilter', 'Undefined filter "{0}"', id);
return null;
};
/**
* Returns a particular operator by its type
* @param {string} type
* @param {boolean} [doThrow=true]
* @returns {object|null}
* @throws UndefinedOperatorError
* @private
*/
QueryBuilder.prototype.getOperatorByType = function(type, doThrow) {
if (type == '-1') {
return null;
}
for (var i = 0, l = this.operators.length; i < l; i++) {
if (this.operators[i].type == type) {
return this.operators[i];
}
}
Utils.error(doThrow !== false, 'UndefinedOperator', 'Undefined operator "{0}"', type);
return null;
};
/**
* Returns rule's current input value
* @param {Rule} rule
* @returns {*}
* @fires QueryBuilder.changer:getRuleValue
* @private
*/
QueryBuilder.prototype.getRuleInputValue = function(rule) {
var filter = rule.filter;
var operator = rule.operator;
var value = [];
if (filter.valueGetter) {
value = filter.valueGetter.call(this, rule);
}
else {
var $value = rule.$el.find(QueryBuilder.selectors.value_container);
for (var i = 0; i < operator.nb_inputs; i++) {
var name = Utils.escapeElementId(rule.id + '_value_' + i);
var tmp;
switch (filter.input) {
case 'radio':
value.push($value.find('[name=' + name + ']:checked').val());
break;
case 'checkbox':
tmp = [];
// jshint loopfunc:true
$value.find('[name=' + name + ']:checked').each(function() {
tmp.push($(this).val());
});
// jshint loopfunc:false
value.push(tmp);
break;
case 'select':
if (filter.multiple) {
tmp = [];
// jshint loopfunc:true
$value.find('[name=' + name + '] option:selected').each(function() {
tmp.push($(this).val());
});
// jshint loopfunc:false
value.push(tmp);
}
else {
value.push($value.find('[name=' + name + '] option:selected').val());
}
break;
default:
value.push($value.find('[name=' + name + ']').val());
}
}
value = value.map(function(val) {
if (operator.multiple && filter.value_separator && typeof val == 'string') {
val = val.split(filter.value_separator);
}
if ($.isArray(val)) {
return val.map(function(subval) {
return Utils.changeType(subval, filter.type);
});
}
else {
return Utils.changeType(val, filter.type);
}
});
if (operator.nb_inputs === 1) {
value = value[0];
}
// @deprecated
if (filter.valueParser) {
value = filter.valueParser.call(this, rule, value);
}
}
/**
* Modifies the rule's value grabbed from the DOM
* @event changer:getRuleValue
* @memberof QueryBuilder
* @param {*} value
* @param {Rule} rule
* @returns {*}
*/
return this.change('getRuleValue', value, rule);
};
/**
* Sets the value of a rule's input
* @param {Rule} rule
* @param {*} value
* @private
*/
QueryBuilder.prototype.setRuleInputValue = function(rule, value) {
var filter = rule.filter;
var operator = rule.operator;
if (!filter || !operator) {
return;
}
rule._updating_input = true;
if (filter.valueSetter) {
filter.valueSetter.call(this, rule, value);
}
else {
var $value = rule.$el.find(QueryBuilder.selectors.value_container);
if (operator.nb_inputs == 1) {
value = [value];
}
for (var i = 0; i < operator.nb_inputs; i++) {
var name = Utils.escapeElementId(rule.id + '_value_' + i);
switch (filter.input) {
case 'radio':
$value.find('[name=' + name + '][value="' + value[i] + '"]').prop('checked', true).trigger('change');
break;
case 'checkbox':
if (!$.isArray(value[i])) {
value[i] = [value[i]];
}
// jshint loopfunc:true
value[i].forEach(function(value) {
$value.find('[name=' + name + '][value="' + value + '"]').prop('checked', true).trigger('change');
});
// jshint loopfunc:false
break;
default:
if (operator.multiple && filter.value_separator && $.isArray(value[i])) {
value[i] = value[i].join(filter.value_separator);
}
$value.find('[name=' + name + ']').val(value[i]).trigger('change');
break;
}
}
}
rule._updating_input = false;
};
/**
* Parses rule flags
* @param {object} rule
* @returns {object}
* @fires QueryBuilder.changer:parseRuleFlags
* @private
*/
QueryBuilder.prototype.parseRuleFlags = function(rule) {
var flags = $.extend({}, this.settings.default_rule_flags);
if (rule.readonly) {
$.extend(flags, {
filter_readonly: true,
operator_readonly: true,
value_readonly: true,
no_delete: true
});
}
if (rule.flags) {
$.extend(flags, rule.flags);
}
/**
* Modifies the consolidated rule's flags
* @event changer:parseRuleFlags
* @memberof QueryBuilder
* @param {object} flags
* @param {object} rule - <b>not</b> a Rule object
* @returns {object}
*/
return this.change('parseRuleFlags', flags, rule);
};
/**
* Gets a copy of flags of a rule
* @param {object} flags
* @param {boolean} [all=false] - return all flags or only changes from default flags
* @returns {object}
* @private
*/
QueryBuilder.prototype.getRuleFlags = function(flags, all) {
if (all) {
return $.extend({}, flags);
}
else {
var ret = {};
$.each(this.settings.default_rule_flags, function(key, value) {
if (flags[key] !== value) {
ret[key] = flags[key];
}
});
return ret;
}
};
/**
* Parses group flags
* @param {object} group
* @returns {object}
* @fires QueryBuilder.changer:parseGroupFlags
* @private
*/
QueryBuilder.prototype.parseGroupFlags = function(group) {
var flags = $.extend({}, this.settings.default_group_flags);
if (group.readonly) {
$.extend(flags, {
condition_readonly: true,
no_add_rule: true,
no_add_group: true,
no_delete: true
});
}
if (group.flags) {
$.extend(flags, group.flags);
}
/**
* Modifies the consolidated group's flags
* @event changer:parseGroupFlags
* @memberof QueryBuilder
* @param {object} flags
* @param {object} group - <b>not</b> a Group object
* @returns {object}
*/
return this.change('parseGroupFlags', flags, group);
};
/**
* Gets a copy of flags of a group
* @param {object} flags
* @param {boolean} [all=false] - return all flags or only changes from default flags
* @returns {object}
* @private
*/
QueryBuilder.prototype.getGroupFlags = function(flags, all) {
if (all) {
return $.extend({}, flags);
}
else {
var ret = {};
$.each(this.settings.default_group_flags, function(key, value) {
if (flags[key] !== value) {
ret[key] = flags[key];
}
});
return ret;
}
};
/**
* Translate a label either by looking in the `lang` object or in itself if it's an object where keys are language codes
* @param {string} [category]
* @param {string|object} key
* @returns {string}
* @fires QueryBuilder.changer:translate
*/
QueryBuilder.prototype.translate = function(category, key) {
if (!key) {
key = category;
category = undefined;
}
var translation;
if (typeof key === 'object') {
translation = key[this.settings.lang_code] || key['en'];
}
else {
translation = (category ? this.lang[category] : this.lang)[key] || key;
}
/**
* Modifies the translated label
* @event changer:translate
* @memberof QueryBuilder
* @param {string} translation
* @param {string|object} key
* @param {string} [category]
* @returns {string}
*/
return this.change('translate', translation, key, category);
};
/**
* Returns a validation message
* @param {object} validation
* @param {string} type
* @param {string} def
* @returns {string}
* @private
*/
QueryBuilder.prototype.getValidationMessage = function(validation, type, def) {
return validation.messages && validation.messages[type] || def;
};