kopia lustrzana https://github.com/magicbug/Cloudlog
6201 wiersze
187 KiB
JavaScript
6201 wiersze
187 KiB
JavaScript
/*!
|
|
* jQuery QueryBuilder 2.5.2
|
|
* Copyright 2014-2018 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
|
|
* Licensed under MIT (https://opensource.org/licenses/MIT)
|
|
*/
|
|
(function(root, factory) {
|
|
if (typeof define == 'function' && define.amd) {
|
|
define(['jquery', 'dot/doT', 'jquery-extendext'], factory);
|
|
}
|
|
else if (typeof module === 'object' && module.exports) {
|
|
module.exports = factory(require('jquery'), require('dot/doT'), require('jquery-extendext'));
|
|
}
|
|
else {
|
|
factory(root.jQuery, root.doT);
|
|
}
|
|
}(this, function($, doT) {
|
|
"use strict";
|
|
|
|
/**
|
|
* @typedef {object} Filter
|
|
* @memberof QueryBuilder
|
|
* @description See {@link http://querybuilder.js.org/index.html#filters}
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} Operator
|
|
* @memberof QueryBuilder
|
|
* @description See {@link http://querybuilder.js.org/index.html#operators}
|
|
*/
|
|
|
|
/**
|
|
* @param {jQuery} $el
|
|
* @param {object} options - see {@link http://querybuilder.js.org/#options}
|
|
* @constructor
|
|
*/
|
|
var QueryBuilder = function($el, options) {
|
|
$el[0].queryBuilder = this;
|
|
|
|
/**
|
|
* Element container
|
|
* @member {jQuery}
|
|
* @readonly
|
|
*/
|
|
this.$el = $el;
|
|
|
|
/**
|
|
* Configuration object
|
|
* @member {object}
|
|
* @readonly
|
|
*/
|
|
this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options);
|
|
|
|
/**
|
|
* Internal model
|
|
* @member {Model}
|
|
* @readonly
|
|
*/
|
|
this.model = new Model();
|
|
|
|
/**
|
|
* Internal status
|
|
* @member {object}
|
|
* @property {string} id - id of the container
|
|
* @property {boolean} generated_id - if the container id has been generated
|
|
* @property {int} group_id - current group id
|
|
* @property {int} rule_id - current rule id
|
|
* @property {boolean} has_optgroup - if filters have optgroups
|
|
* @property {boolean} has_operator_optgroup - if operators have optgroups
|
|
* @readonly
|
|
* @private
|
|
*/
|
|
this.status = {
|
|
id: null,
|
|
generated_id: false,
|
|
group_id: 0,
|
|
rule_id: 0,
|
|
has_optgroup: false,
|
|
has_operator_optgroup: false
|
|
};
|
|
|
|
/**
|
|
* List of filters
|
|
* @member {QueryBuilder.Filter[]}
|
|
* @readonly
|
|
*/
|
|
this.filters = this.settings.filters;
|
|
|
|
/**
|
|
* List of icons
|
|
* @member {object.<string, string>}
|
|
* @readonly
|
|
*/
|
|
this.icons = this.settings.icons;
|
|
|
|
/**
|
|
* List of operators
|
|
* @member {QueryBuilder.Operator[]}
|
|
* @readonly
|
|
*/
|
|
this.operators = this.settings.operators;
|
|
|
|
/**
|
|
* List of templates
|
|
* @member {object.<string, function>}
|
|
* @readonly
|
|
*/
|
|
this.templates = this.settings.templates;
|
|
|
|
/**
|
|
* Plugins configuration
|
|
* @member {object.<string, object>}
|
|
* @readonly
|
|
*/
|
|
this.plugins = this.settings.plugins;
|
|
|
|
/**
|
|
* Translations object
|
|
* @member {object}
|
|
* @readonly
|
|
*/
|
|
this.lang = null;
|
|
|
|
// translations : english << 'lang_code' << custom
|
|
if (QueryBuilder.regional['en'] === undefined) {
|
|
Utils.error('Config', '"i18n/en.js" not loaded.');
|
|
}
|
|
this.lang = $.extendext(true, 'replace', {}, QueryBuilder.regional['en'], QueryBuilder.regional[this.settings.lang_code], this.settings.lang);
|
|
|
|
// "allow_groups" can be boolean or int
|
|
if (this.settings.allow_groups === false) {
|
|
this.settings.allow_groups = 0;
|
|
}
|
|
else if (this.settings.allow_groups === true) {
|
|
this.settings.allow_groups = -1;
|
|
}
|
|
|
|
// init templates
|
|
Object.keys(this.templates).forEach(function(tpl) {
|
|
if (!this.templates[tpl]) {
|
|
this.templates[tpl] = QueryBuilder.templates[tpl];
|
|
}
|
|
if (typeof this.templates[tpl] == 'string') {
|
|
this.templates[tpl] = doT.template(this.templates[tpl]);
|
|
}
|
|
}, this);
|
|
|
|
// ensure we have a container id
|
|
if (!this.$el.attr('id')) {
|
|
this.$el.attr('id', 'qb_' + Math.floor(Math.random() * 99999));
|
|
this.status.generated_id = true;
|
|
}
|
|
this.status.id = this.$el.attr('id');
|
|
|
|
// INIT
|
|
this.$el.addClass('query-builder form-inline');
|
|
|
|
this.filters = this.checkFilters(this.filters);
|
|
this.operators = this.checkOperators(this.operators);
|
|
this.bindEvents();
|
|
this.initPlugins();
|
|
};
|
|
|
|
$.extend(QueryBuilder.prototype, /** @lends QueryBuilder.prototype */ {
|
|
/**
|
|
* Triggers an event on the builder container
|
|
* @param {string} type
|
|
* @returns {$.Event}
|
|
*/
|
|
trigger: function(type) {
|
|
var event = new $.Event(this._tojQueryEvent(type), {
|
|
builder: this
|
|
});
|
|
|
|
this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 1));
|
|
|
|
return event;
|
|
},
|
|
|
|
/**
|
|
* Triggers an event on the builder container and returns the modified value
|
|
* @param {string} type
|
|
* @param {*} value
|
|
* @returns {*}
|
|
*/
|
|
change: function(type, value) {
|
|
var event = new $.Event(this._tojQueryEvent(type, true), {
|
|
builder: this,
|
|
value: value
|
|
});
|
|
|
|
this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 2));
|
|
|
|
return event.value;
|
|
},
|
|
|
|
/**
|
|
* Attaches an event listener on the builder container
|
|
* @param {string} type
|
|
* @param {function} cb
|
|
* @returns {QueryBuilder}
|
|
*/
|
|
on: function(type, cb) {
|
|
this.$el.on(this._tojQueryEvent(type), cb);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Removes an event listener from the builder container
|
|
* @param {string} type
|
|
* @param {function} [cb]
|
|
* @returns {QueryBuilder}
|
|
*/
|
|
off: function(type, cb) {
|
|
this.$el.off(this._tojQueryEvent(type), cb);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Attaches an event listener called once on the builder container
|
|
* @param {string} type
|
|
* @param {function} cb
|
|
* @returns {QueryBuilder}
|
|
*/
|
|
once: function(type, cb) {
|
|
this.$el.one(this._tojQueryEvent(type), cb);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Appends `.queryBuilder` and optionally `.filter` to the events names
|
|
* @param {string} name
|
|
* @param {boolean} [filter=false]
|
|
* @returns {string}
|
|
* @private
|
|
*/
|
|
_tojQueryEvent: function(name, filter) {
|
|
return name.split(' ').map(function(type) {
|
|
return type + '.queryBuilder' + (filter ? '.filter' : '');
|
|
}).join(' ');
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* Allowed types and their internal representation
|
|
* @type {object.<string, string>}
|
|
* @readonly
|
|
* @private
|
|
*/
|
|
QueryBuilder.types = {
|
|
'string': 'string',
|
|
'integer': 'number',
|
|
'double': 'number',
|
|
'date': 'datetime',
|
|
'time': 'datetime',
|
|
'datetime': 'datetime',
|
|
'boolean': 'boolean'
|
|
};
|
|
|
|
/**
|
|
* Allowed inputs
|
|
* @type {string[]}
|
|
* @readonly
|
|
* @private
|
|
*/
|
|
QueryBuilder.inputs = [
|
|
'text',
|
|
'number',
|
|
'textarea',
|
|
'radio',
|
|
'checkbox',
|
|
'select'
|
|
];
|
|
|
|
/**
|
|
* Runtime modifiable options with `setOptions` method
|
|
* @type {string[]}
|
|
* @readonly
|
|
* @private
|
|
*/
|
|
QueryBuilder.modifiable_options = [
|
|
'display_errors',
|
|
'allow_groups',
|
|
'allow_empty',
|
|
'default_condition',
|
|
'default_filter'
|
|
];
|
|
|
|
/**
|
|
* CSS selectors for common components
|
|
* @type {object.<string, string>}
|
|
* @readonly
|
|
*/
|
|
QueryBuilder.selectors = {
|
|
group_container: '.rules-group-container',
|
|
rule_container: '.rule-container',
|
|
filter_container: '.rule-filter-container',
|
|
operator_container: '.rule-operator-container',
|
|
value_container: '.rule-value-container',
|
|
error_container: '.error-container',
|
|
condition_container: '.rules-group-header .group-conditions',
|
|
|
|
rule_header: '.rule-header',
|
|
group_header: '.rules-group-header',
|
|
group_actions: '.group-actions',
|
|
rule_actions: '.rule-actions',
|
|
|
|
rules_list: '.rules-group-body>.rules-list',
|
|
|
|
group_condition: '.rules-group-header [name$=_cond]',
|
|
rule_filter: '.rule-filter-container [name$=_filter]',
|
|
rule_operator: '.rule-operator-container [name$=_operator]',
|
|
rule_value: '.rule-value-container [name*=_value_]',
|
|
|
|
add_rule: '[data-add=rule]',
|
|
delete_rule: '[data-delete=rule]',
|
|
add_group: '[data-add=group]',
|
|
delete_group: '[data-delete=group]'
|
|
};
|
|
|
|
/**
|
|
* Template strings (see template.js)
|
|
* @type {object.<string, string>}
|
|
* @readonly
|
|
*/
|
|
QueryBuilder.templates = {};
|
|
|
|
/**
|
|
* Localized strings (see i18n/)
|
|
* @type {object.<string, object>}
|
|
* @readonly
|
|
*/
|
|
QueryBuilder.regional = {};
|
|
|
|
/**
|
|
* Default operators
|
|
* @type {object.<string, object>}
|
|
* @readonly
|
|
*/
|
|
QueryBuilder.OPERATORS = {
|
|
equal: { type: 'equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] },
|
|
not_equal: { type: 'not_equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] },
|
|
in: { type: 'in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime'] },
|
|
not_in: { type: 'not_in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime'] },
|
|
less: { type: 'less', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
|
|
less_or_equal: { type: 'less_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
|
|
greater: { type: 'greater', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
|
|
greater_or_equal: { type: 'greater_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
|
|
between: { type: 'between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] },
|
|
not_between: { type: 'not_between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] },
|
|
begins_with: { type: 'begins_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
|
|
not_begins_with: { type: 'not_begins_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
|
|
contains: { type: 'contains', nb_inputs: 1, multiple: false, apply_to: ['string'] },
|
|
not_contains: { type: 'not_contains', nb_inputs: 1, multiple: false, apply_to: ['string'] },
|
|
ends_with: { type: 'ends_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
|
|
not_ends_with: { type: 'not_ends_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
|
|
is_empty: { type: 'is_empty', nb_inputs: 0, multiple: false, apply_to: ['string'] },
|
|
is_not_empty: { type: 'is_not_empty', nb_inputs: 0, multiple: false, apply_to: ['string'] },
|
|
is_null: { type: 'is_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] },
|
|
is_not_null: { type: 'is_not_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] }
|
|
};
|
|
|
|
/**
|
|
* Default configuration
|
|
* @type {object}
|
|
* @readonly
|
|
*/
|
|
QueryBuilder.DEFAULTS = {
|
|
filters: [],
|
|
plugins: [],
|
|
|
|
sort_filters: false,
|
|
display_errors: true,
|
|
allow_groups: -1,
|
|
allow_empty: false,
|
|
conditions: ['AND', 'OR'],
|
|
default_condition: 'AND',
|
|
inputs_separator: ' , ',
|
|
select_placeholder: '------',
|
|
display_empty_filter: true,
|
|
default_filter: null,
|
|
optgroups: {},
|
|
|
|
default_rule_flags: {
|
|
filter_readonly: false,
|
|
operator_readonly: false,
|
|
value_readonly: false,
|
|
no_delete: false
|
|
},
|
|
|
|
default_group_flags: {
|
|
condition_readonly: false,
|
|
no_add_rule: false,
|
|
no_add_group: false,
|
|
no_delete: false
|
|
},
|
|
|
|
templates: {
|
|
group: null,
|
|
rule: null,
|
|
filterSelect: null,
|
|
operatorSelect: null,
|
|
ruleValueSelect: null
|
|
},
|
|
|
|
lang_code: 'en',
|
|
lang: {},
|
|
|
|
operators: [
|
|
'equal',
|
|
'not_equal',
|
|
'in',
|
|
'not_in',
|
|
'less',
|
|
'less_or_equal',
|
|
'greater',
|
|
'greater_or_equal',
|
|
'between',
|
|
'not_between',
|
|
'begins_with',
|
|
'not_begins_with',
|
|
'contains',
|
|
'not_contains',
|
|
'ends_with',
|
|
'not_ends_with',
|
|
'is_empty',
|
|
'is_not_empty',
|
|
'is_null',
|
|
'is_not_null'
|
|
],
|
|
|
|
icons: {
|
|
add_group: 'glyphicon glyphicon-plus-sign',
|
|
add_rule: 'glyphicon glyphicon-plus',
|
|
remove_group: 'glyphicon glyphicon-remove',
|
|
remove_rule: 'glyphicon glyphicon-remove',
|
|
error: 'glyphicon glyphicon-warning-sign'
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @module plugins
|
|
*/
|
|
|
|
/**
|
|
* Definition of available plugins
|
|
* @type {object.<String, object>}
|
|
*/
|
|
QueryBuilder.plugins = {};
|
|
|
|
/**
|
|
* Gets or extends the default configuration
|
|
* @param {object} [options] - new configuration
|
|
* @returns {undefined|object} nothing or configuration object (copy)
|
|
*/
|
|
QueryBuilder.defaults = function(options) {
|
|
if (typeof options == 'object') {
|
|
$.extendext(true, 'replace', QueryBuilder.DEFAULTS, options);
|
|
}
|
|
else if (typeof options == 'string') {
|
|
if (typeof QueryBuilder.DEFAULTS[options] == 'object') {
|
|
return $.extend(true, {}, QueryBuilder.DEFAULTS[options]);
|
|
}
|
|
else {
|
|
return QueryBuilder.DEFAULTS[options];
|
|
}
|
|
}
|
|
else {
|
|
return $.extend(true, {}, QueryBuilder.DEFAULTS);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Registers a new plugin
|
|
* @param {string} name
|
|
* @param {function} fct - init function
|
|
* @param {object} [def] - default options
|
|
*/
|
|
QueryBuilder.define = function(name, fct, def) {
|
|
QueryBuilder.plugins[name] = {
|
|
fct: fct,
|
|
def: def || {}
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Adds new methods to QueryBuilder prototype
|
|
* @param {object.<string, function>} methods
|
|
*/
|
|
QueryBuilder.extend = function(methods) {
|
|
$.extend(QueryBuilder.prototype, methods);
|
|
};
|
|
|
|
/**
|
|
* Initializes plugins for an instance
|
|
* @throws ConfigError
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.initPlugins = function() {
|
|
if (!this.plugins) {
|
|
return;
|
|
}
|
|
|
|
if ($.isArray(this.plugins)) {
|
|
var tmp = {};
|
|
this.plugins.forEach(function(plugin) {
|
|
tmp[plugin] = null;
|
|
});
|
|
this.plugins = tmp;
|
|
}
|
|
|
|
Object.keys(this.plugins).forEach(function(plugin) {
|
|
if (plugin in QueryBuilder.plugins) {
|
|
this.plugins[plugin] = $.extend(true, {},
|
|
QueryBuilder.plugins[plugin].def,
|
|
this.plugins[plugin] || {}
|
|
);
|
|
|
|
QueryBuilder.plugins[plugin].fct.call(this, this.plugins[plugin]);
|
|
}
|
|
else {
|
|
Utils.error('Config', 'Unable to find plugin "{0}"', plugin);
|
|
}
|
|
}, this);
|
|
};
|
|
|
|
/**
|
|
* Returns the config of a plugin, if the plugin is not loaded, returns the default config.
|
|
* @param {string} name
|
|
* @param {string} [property]
|
|
* @throws ConfigError
|
|
* @returns {*}
|
|
*/
|
|
QueryBuilder.prototype.getPluginOptions = function(name, property) {
|
|
var plugin;
|
|
if (this.plugins && this.plugins[name]) {
|
|
plugin = this.plugins[name];
|
|
}
|
|
else if (QueryBuilder.plugins[name]) {
|
|
plugin = QueryBuilder.plugins[name].def;
|
|
}
|
|
|
|
if (plugin) {
|
|
if (property) {
|
|
return plugin[property];
|
|
}
|
|
else {
|
|
return plugin;
|
|
}
|
|
}
|
|
else {
|
|
Utils.error('Config', 'Unable to find plugin "{0}"', name);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Final initialisation of the builder
|
|
* @param {object} [rules]
|
|
* @fires QueryBuilder.afterInit
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.init = function(rules) {
|
|
/**
|
|
* When the initilization is done, just before creating the root group
|
|
* @event afterInit
|
|
* @memberof QueryBuilder
|
|
*/
|
|
this.trigger('afterInit');
|
|
|
|
if (rules) {
|
|
this.setRules(rules);
|
|
delete this.settings.rules;
|
|
}
|
|
else {
|
|
this.setRoot(true);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Checks the configuration of each filter
|
|
* @param {QueryBuilder.Filter[]} filters
|
|
* @returns {QueryBuilder.Filter[]}
|
|
* @throws ConfigError
|
|
*/
|
|
QueryBuilder.prototype.checkFilters = function(filters) {
|
|
var definedFilters = [];
|
|
|
|
if (!filters || filters.length === 0) {
|
|
Utils.error('Config', 'Missing filters list');
|
|
}
|
|
|
|
filters.forEach(function(filter, i) {
|
|
if (!filter.id) {
|
|
Utils.error('Config', 'Missing filter {0} id', i);
|
|
}
|
|
if (definedFilters.indexOf(filter.id) != -1) {
|
|
Utils.error('Config', 'Filter "{0}" already defined', filter.id);
|
|
}
|
|
definedFilters.push(filter.id);
|
|
|
|
if (!filter.type) {
|
|
filter.type = 'string';
|
|
}
|
|
else if (!QueryBuilder.types[filter.type]) {
|
|
Utils.error('Config', 'Invalid type "{0}"', filter.type);
|
|
}
|
|
|
|
if (!filter.input) {
|
|
filter.input = QueryBuilder.types[filter.type] === 'number' ? 'number' : 'text';
|
|
}
|
|
else if (typeof filter.input != 'function' && QueryBuilder.inputs.indexOf(filter.input) == -1) {
|
|
Utils.error('Config', 'Invalid input "{0}"', filter.input);
|
|
}
|
|
|
|
if (filter.operators) {
|
|
filter.operators.forEach(function(operator) {
|
|
if (typeof operator != 'string') {
|
|
Utils.error('Config', 'Filter operators must be global operators types (string)');
|
|
}
|
|
});
|
|
}
|
|
|
|
if (!filter.field) {
|
|
filter.field = filter.id;
|
|
}
|
|
if (!filter.label) {
|
|
filter.label = filter.field;
|
|
}
|
|
|
|
if (!filter.optgroup) {
|
|
filter.optgroup = null;
|
|
}
|
|
else {
|
|
this.status.has_optgroup = true;
|
|
|
|
// register optgroup if needed
|
|
if (!this.settings.optgroups[filter.optgroup]) {
|
|
this.settings.optgroups[filter.optgroup] = filter.optgroup;
|
|
}
|
|
}
|
|
|
|
switch (filter.input) {
|
|
case 'radio':
|
|
case 'checkbox':
|
|
if (!filter.values || filter.values.length < 1) {
|
|
Utils.error('Config', 'Missing filter "{0}" values', filter.id);
|
|
}
|
|
break;
|
|
|
|
case 'select':
|
|
var cleanValues = [];
|
|
filter.has_optgroup = false;
|
|
|
|
Utils.iterateOptions(filter.values, function(value, label, optgroup) {
|
|
cleanValues.push({
|
|
value: value,
|
|
label: label,
|
|
optgroup: optgroup || null
|
|
});
|
|
|
|
if (optgroup) {
|
|
filter.has_optgroup = true;
|
|
|
|
// register optgroup if needed
|
|
if (!this.settings.optgroups[optgroup]) {
|
|
this.settings.optgroups[optgroup] = optgroup;
|
|
}
|
|
}
|
|
}.bind(this));
|
|
|
|
if (filter.has_optgroup) {
|
|
filter.values = Utils.groupSort(cleanValues, 'optgroup');
|
|
}
|
|
else {
|
|
filter.values = cleanValues;
|
|
}
|
|
|
|
if (filter.placeholder) {
|
|
if (filter.placeholder_value === undefined) {
|
|
filter.placeholder_value = -1;
|
|
}
|
|
|
|
filter.values.forEach(function(entry) {
|
|
if (entry.value == filter.placeholder_value) {
|
|
Utils.error('Config', 'Placeholder of filter "{0}" overlaps with one of its values', filter.id);
|
|
}
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
}, this);
|
|
|
|
if (this.settings.sort_filters) {
|
|
if (typeof this.settings.sort_filters == 'function') {
|
|
filters.sort(this.settings.sort_filters);
|
|
}
|
|
else {
|
|
var self = this;
|
|
filters.sort(function(a, b) {
|
|
return self.translate(a.label).localeCompare(self.translate(b.label));
|
|
});
|
|
}
|
|
}
|
|
|
|
if (this.status.has_optgroup) {
|
|
filters = Utils.groupSort(filters, 'optgroup');
|
|
}
|
|
|
|
return filters;
|
|
};
|
|
|
|
/**
|
|
* Checks the configuration of each operator
|
|
* @param {QueryBuilder.Operator[]} operators
|
|
* @returns {QueryBuilder.Operator[]}
|
|
* @throws ConfigError
|
|
*/
|
|
QueryBuilder.prototype.checkOperators = function(operators) {
|
|
var definedOperators = [];
|
|
|
|
operators.forEach(function(operator, i) {
|
|
if (typeof operator == 'string') {
|
|
if (!QueryBuilder.OPERATORS[operator]) {
|
|
Utils.error('Config', 'Unknown operator "{0}"', operator);
|
|
}
|
|
|
|
operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator]);
|
|
}
|
|
else {
|
|
if (!operator.type) {
|
|
Utils.error('Config', 'Missing "type" for operator {0}', i);
|
|
}
|
|
|
|
if (QueryBuilder.OPERATORS[operator.type]) {
|
|
operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator.type], operator);
|
|
}
|
|
|
|
if (operator.nb_inputs === undefined || operator.apply_to === undefined) {
|
|
Utils.error('Config', 'Missing "nb_inputs" and/or "apply_to" for operator "{0}"', operator.type);
|
|
}
|
|
}
|
|
|
|
if (definedOperators.indexOf(operator.type) != -1) {
|
|
Utils.error('Config', 'Operator "{0}" already defined', operator.type);
|
|
}
|
|
definedOperators.push(operator.type);
|
|
|
|
if (!operator.optgroup) {
|
|
operator.optgroup = null;
|
|
}
|
|
else {
|
|
this.status.has_operator_optgroup = true;
|
|
|
|
// register optgroup if needed
|
|
if (!this.settings.optgroups[operator.optgroup]) {
|
|
this.settings.optgroups[operator.optgroup] = operator.optgroup;
|
|
}
|
|
}
|
|
}, this);
|
|
|
|
if (this.status.has_operator_optgroup) {
|
|
operators = Utils.groupSort(operators, 'optgroup');
|
|
}
|
|
|
|
return operators;
|
|
};
|
|
|
|
/**
|
|
* Adds all events listeners to the builder
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.bindEvents = function() {
|
|
var self = this;
|
|
var Selectors = QueryBuilder.selectors;
|
|
|
|
// group condition change
|
|
this.$el.on('change.queryBuilder', Selectors.group_condition, function() {
|
|
if ($(this).is(':checked')) {
|
|
var $group = $(this).closest(Selectors.group_container);
|
|
self.getModel($group).condition = $(this).val();
|
|
}
|
|
});
|
|
|
|
// rule filter change
|
|
this.$el.on('change.queryBuilder', Selectors.rule_filter, function() {
|
|
var $rule = $(this).closest(Selectors.rule_container);
|
|
self.getModel($rule).filter = self.getFilterById($(this).val());
|
|
});
|
|
|
|
// rule operator change
|
|
this.$el.on('change.queryBuilder', Selectors.rule_operator, function() {
|
|
var $rule = $(this).closest(Selectors.rule_container);
|
|
self.getModel($rule).operator = self.getOperatorByType($(this).val());
|
|
});
|
|
|
|
// add rule button
|
|
this.$el.on('click.queryBuilder', Selectors.add_rule, function() {
|
|
var $group = $(this).closest(Selectors.group_container);
|
|
self.addRule(self.getModel($group));
|
|
});
|
|
|
|
// delete rule button
|
|
this.$el.on('click.queryBuilder', Selectors.delete_rule, function() {
|
|
var $rule = $(this).closest(Selectors.rule_container);
|
|
self.deleteRule(self.getModel($rule));
|
|
});
|
|
|
|
if (this.settings.allow_groups !== 0) {
|
|
// add group button
|
|
this.$el.on('click.queryBuilder', Selectors.add_group, function() {
|
|
var $group = $(this).closest(Selectors.group_container);
|
|
self.addGroup(self.getModel($group));
|
|
});
|
|
|
|
// delete group button
|
|
this.$el.on('click.queryBuilder', Selectors.delete_group, function() {
|
|
var $group = $(this).closest(Selectors.group_container);
|
|
self.deleteGroup(self.getModel($group));
|
|
});
|
|
}
|
|
|
|
// model events
|
|
this.model.on({
|
|
'drop': function(e, node) {
|
|
node.$el.remove();
|
|
self.refreshGroupsConditions();
|
|
},
|
|
'add': function(e, parent, node, index) {
|
|
if (index === 0) {
|
|
node.$el.prependTo(parent.$el.find('>' + QueryBuilder.selectors.rules_list));
|
|
}
|
|
else {
|
|
node.$el.insertAfter(parent.rules[index - 1].$el);
|
|
}
|
|
self.refreshGroupsConditions();
|
|
},
|
|
'move': function(e, node, group, index) {
|
|
node.$el.detach();
|
|
|
|
if (index === 0) {
|
|
node.$el.prependTo(group.$el.find('>' + QueryBuilder.selectors.rules_list));
|
|
}
|
|
else {
|
|
node.$el.insertAfter(group.rules[index - 1].$el);
|
|
}
|
|
self.refreshGroupsConditions();
|
|
},
|
|
'update': function(e, node, field, value, oldValue) {
|
|
if (node instanceof Rule) {
|
|
switch (field) {
|
|
case 'error':
|
|
self.updateError(node);
|
|
break;
|
|
|
|
case 'flags':
|
|
self.applyRuleFlags(node);
|
|
break;
|
|
|
|
case 'filter':
|
|
self.updateRuleFilter(node, oldValue);
|
|
break;
|
|
|
|
case 'operator':
|
|
self.updateRuleOperator(node, oldValue);
|
|
break;
|
|
|
|
case 'value':
|
|
self.updateRuleValue(node, oldValue);
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
switch (field) {
|
|
case 'error':
|
|
self.updateError(node);
|
|
break;
|
|
|
|
case 'flags':
|
|
self.applyGroupFlags(node);
|
|
break;
|
|
|
|
case 'condition':
|
|
self.updateGroupCondition(node, oldValue);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Creates the root group
|
|
* @param {boolean} [addRule=true] - adds a default empty rule
|
|
* @param {object} [data] - group custom data
|
|
* @param {object} [flags] - flags to apply to the group
|
|
* @returns {Group} root group
|
|
* @fires QueryBuilder.afterAddGroup
|
|
*/
|
|
QueryBuilder.prototype.setRoot = function(addRule, data, flags) {
|
|
addRule = (addRule === undefined || addRule === true);
|
|
|
|
var group_id = this.nextGroupId();
|
|
var $group = $(this.getGroupTemplate(group_id, 1));
|
|
|
|
this.$el.append($group);
|
|
this.model.root = new Group(null, $group);
|
|
this.model.root.model = this.model;
|
|
|
|
this.model.root.data = data;
|
|
this.model.root.flags = $.extend({}, this.settings.default_group_flags, flags);
|
|
this.model.root.condition = this.settings.default_condition;
|
|
|
|
this.trigger('afterAddGroup', this.model.root);
|
|
|
|
if (addRule) {
|
|
this.addRule(this.model.root);
|
|
}
|
|
|
|
return this.model.root;
|
|
};
|
|
|
|
/**
|
|
* Adds a new group
|
|
* @param {Group} parent
|
|
* @param {boolean} [addRule=true] - adds a default empty rule
|
|
* @param {object} [data] - group custom data
|
|
* @param {object} [flags] - flags to apply to the group
|
|
* @returns {Group}
|
|
* @fires QueryBuilder.beforeAddGroup
|
|
* @fires QueryBuilder.afterAddGroup
|
|
*/
|
|
QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) {
|
|
addRule = (addRule === undefined || addRule === true);
|
|
|
|
var level = parent.level + 1;
|
|
|
|
/**
|
|
* Just before adding a group, can be prevented.
|
|
* @event beforeAddGroup
|
|
* @memberof QueryBuilder
|
|
* @param {Group} parent
|
|
* @param {boolean} addRule - if an empty rule will be added in the group
|
|
* @param {int} level - nesting level of the group, 1 is the root group
|
|
*/
|
|
var e = this.trigger('beforeAddGroup', parent, addRule, level);
|
|
if (e.isDefaultPrevented()) {
|
|
return null;
|
|
}
|
|
|
|
var group_id = this.nextGroupId();
|
|
var $group = $(this.getGroupTemplate(group_id, level));
|
|
var model = parent.addGroup($group);
|
|
|
|
model.data = data;
|
|
model.flags = $.extend({}, this.settings.default_group_flags, flags);
|
|
model.condition = this.settings.default_condition;
|
|
|
|
/**
|
|
* Just after adding a group
|
|
* @event afterAddGroup
|
|
* @memberof QueryBuilder
|
|
* @param {Group} group
|
|
*/
|
|
this.trigger('afterAddGroup', model);
|
|
|
|
/**
|
|
* After any change in the rules
|
|
* @event rulesChanged
|
|
* @memberof QueryBuilder
|
|
*/
|
|
this.trigger('rulesChanged');
|
|
|
|
if (addRule) {
|
|
this.addRule(model);
|
|
}
|
|
|
|
return model;
|
|
};
|
|
|
|
/**
|
|
* Tries to delete a group. The group is not deleted if at least one rule is flagged `no_delete`.
|
|
* @param {Group} group
|
|
* @returns {boolean} if the group has been deleted
|
|
* @fires QueryBuilder.beforeDeleteGroup
|
|
* @fires QueryBuilder.afterDeleteGroup
|
|
*/
|
|
QueryBuilder.prototype.deleteGroup = function(group) {
|
|
if (group.isRoot()) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Just before deleting a group, can be prevented
|
|
* @event beforeDeleteGroup
|
|
* @memberof QueryBuilder
|
|
* @param {Group} parent
|
|
*/
|
|
var e = this.trigger('beforeDeleteGroup', group);
|
|
if (e.isDefaultPrevented()) {
|
|
return false;
|
|
}
|
|
|
|
var del = true;
|
|
|
|
group.each('reverse', function(rule) {
|
|
del &= this.deleteRule(rule);
|
|
}, function(group) {
|
|
del &= this.deleteGroup(group);
|
|
}, this);
|
|
|
|
if (del) {
|
|
group.drop();
|
|
|
|
/**
|
|
* Just after deleting a group
|
|
* @event afterDeleteGroup
|
|
* @memberof QueryBuilder
|
|
*/
|
|
this.trigger('afterDeleteGroup');
|
|
|
|
this.trigger('rulesChanged');
|
|
}
|
|
|
|
return del;
|
|
};
|
|
|
|
/**
|
|
* Performs actions when a group's condition changes
|
|
* @param {Group} group
|
|
* @param {object} previousCondition
|
|
* @fires QueryBuilder.afterUpdateGroupCondition
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.updateGroupCondition = function(group, previousCondition) {
|
|
group.$el.find('>' + QueryBuilder.selectors.group_condition).each(function() {
|
|
var $this = $(this);
|
|
$this.prop('checked', $this.val() === group.condition);
|
|
$this.parent().toggleClass('active', $this.val() === group.condition);
|
|
});
|
|
|
|
/**
|
|
* After the group condition has been modified
|
|
* @event afterUpdateGroupCondition
|
|
* @memberof QueryBuilder
|
|
* @param {Group} group
|
|
* @param {object} previousCondition
|
|
*/
|
|
this.trigger('afterUpdateGroupCondition', group, previousCondition);
|
|
|
|
this.trigger('rulesChanged');
|
|
};
|
|
|
|
/**
|
|
* Updates the visibility of conditions based on number of rules inside each group
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.refreshGroupsConditions = function() {
|
|
(function walk(group) {
|
|
if (!group.flags || (group.flags && !group.flags.condition_readonly)) {
|
|
group.$el.find('>' + QueryBuilder.selectors.group_condition).prop('disabled', group.rules.length <= 1)
|
|
.parent().toggleClass('disabled', group.rules.length <= 1);
|
|
}
|
|
|
|
group.each(null, function(group) {
|
|
walk(group);
|
|
}, this);
|
|
}(this.model.root));
|
|
};
|
|
|
|
/**
|
|
* Adds a new rule
|
|
* @param {Group} parent
|
|
* @param {object} [data] - rule custom data
|
|
* @param {object} [flags] - flags to apply to the rule
|
|
* @returns {Rule}
|
|
* @fires QueryBuilder.beforeAddRule
|
|
* @fires QueryBuilder.afterAddRule
|
|
* @fires QueryBuilder.changer:getDefaultFilter
|
|
*/
|
|
QueryBuilder.prototype.addRule = function(parent, data, flags) {
|
|
/**
|
|
* Just before adding a rule, can be prevented
|
|
* @event beforeAddRule
|
|
* @memberof QueryBuilder
|
|
* @param {Group} parent
|
|
*/
|
|
var e = this.trigger('beforeAddRule', parent);
|
|
if (e.isDefaultPrevented()) {
|
|
return null;
|
|
}
|
|
|
|
var rule_id = this.nextRuleId();
|
|
var $rule = $(this.getRuleTemplate(rule_id));
|
|
var model = parent.addRule($rule);
|
|
|
|
model.data = data;
|
|
model.flags = $.extend({}, this.settings.default_rule_flags, flags);
|
|
|
|
/**
|
|
* Just after adding a rule
|
|
* @event afterAddRule
|
|
* @memberof QueryBuilder
|
|
* @param {Rule} rule
|
|
*/
|
|
this.trigger('afterAddRule', model);
|
|
|
|
this.trigger('rulesChanged');
|
|
|
|
this.createRuleFilters(model);
|
|
|
|
if (this.settings.default_filter || !this.settings.display_empty_filter) {
|
|
/**
|
|
* Modifies the default filter for a rule
|
|
* @event changer:getDefaultFilter
|
|
* @memberof QueryBuilder
|
|
* @param {QueryBuilder.Filter} filter
|
|
* @param {Rule} rule
|
|
* @returns {QueryBuilder.Filter}
|
|
*/
|
|
model.filter = this.change('getDefaultFilter',
|
|
this.getFilterById(this.settings.default_filter || this.filters[0].id),
|
|
model
|
|
);
|
|
}
|
|
|
|
return model;
|
|
};
|
|
|
|
/**
|
|
* Tries to delete a rule
|
|
* @param {Rule} rule
|
|
* @returns {boolean} if the rule has been deleted
|
|
* @fires QueryBuilder.beforeDeleteRule
|
|
* @fires QueryBuilder.afterDeleteRule
|
|
*/
|
|
QueryBuilder.prototype.deleteRule = function(rule) {
|
|
if (rule.flags.no_delete) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Just before deleting a rule, can be prevented
|
|
* @event beforeDeleteRule
|
|
* @memberof QueryBuilder
|
|
* @param {Rule} rule
|
|
*/
|
|
var e = this.trigger('beforeDeleteRule', rule);
|
|
if (e.isDefaultPrevented()) {
|
|
return false;
|
|
}
|
|
|
|
rule.drop();
|
|
|
|
/**
|
|
* Just after deleting a rule
|
|
* @event afterDeleteRule
|
|
* @memberof QueryBuilder
|
|
*/
|
|
this.trigger('afterDeleteRule');
|
|
|
|
this.trigger('rulesChanged');
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Creates the filters for a rule
|
|
* @param {Rule} rule
|
|
* @fires QueryBuilder.changer:getRuleFilters
|
|
* @fires QueryBuilder.afterCreateRuleFilters
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.createRuleFilters = function(rule) {
|
|
/**
|
|
* Modifies the list a filters available for a rule
|
|
* @event changer:getRuleFilters
|
|
* @memberof QueryBuilder
|
|
* @param {QueryBuilder.Filter[]} filters
|
|
* @param {Rule} rule
|
|
* @returns {QueryBuilder.Filter[]}
|
|
*/
|
|
var filters = this.change('getRuleFilters', this.filters, rule);
|
|
var $filterSelect = $(this.getRuleFilterSelect(rule, filters));
|
|
|
|
rule.$el.find(QueryBuilder.selectors.filter_container).html($filterSelect);
|
|
|
|
/**
|
|
* After creating the dropdown for filters
|
|
* @event afterCreateRuleFilters
|
|
* @memberof QueryBuilder
|
|
* @param {Rule} rule
|
|
*/
|
|
this.trigger('afterCreateRuleFilters', rule);
|
|
|
|
this.applyRuleFlags(rule);
|
|
};
|
|
|
|
/**
|
|
* Creates the operators for a rule and init the rule operator
|
|
* @param {Rule} rule
|
|
* @fires QueryBuilder.afterCreateRuleOperators
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.createRuleOperators = function(rule) {
|
|
var $operatorContainer = rule.$el.find(QueryBuilder.selectors.operator_container).empty();
|
|
|
|
if (!rule.filter) {
|
|
return;
|
|
}
|
|
|
|
var operators = this.getOperators(rule.filter);
|
|
var $operatorSelect = $(this.getRuleOperatorSelect(rule, operators));
|
|
|
|
$operatorContainer.html($operatorSelect);
|
|
|
|
// set the operator without triggering update event
|
|
if (rule.filter.default_operator) {
|
|
rule.__.operator = this.getOperatorByType(rule.filter.default_operator);
|
|
}
|
|
else {
|
|
rule.__.operator = operators[0];
|
|
}
|
|
|
|
rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
|
|
|
|
/**
|
|
* After creating the dropdown for operators
|
|
* @event afterCreateRuleOperators
|
|
* @memberof QueryBuilder
|
|
* @param {Rule} rule
|
|
* @param {QueryBuilder.Operator[]} operators - allowed operators for this rule
|
|
*/
|
|
this.trigger('afterCreateRuleOperators', rule, operators);
|
|
|
|
this.applyRuleFlags(rule);
|
|
};
|
|
|
|
/**
|
|
* Creates the main input for a rule
|
|
* @param {Rule} rule
|
|
* @fires QueryBuilder.afterCreateRuleInput
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.createRuleInput = function(rule) {
|
|
var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container).empty();
|
|
|
|
rule.__.value = undefined;
|
|
|
|
if (!rule.filter || !rule.operator || rule.operator.nb_inputs === 0) {
|
|
return;
|
|
}
|
|
|
|
var self = this;
|
|
var $inputs = $();
|
|
var filter = rule.filter;
|
|
|
|
for (var i = 0; i < rule.operator.nb_inputs; i++) {
|
|
var $ruleInput = $(this.getRuleInput(rule, i));
|
|
if (i > 0) $valueContainer.append(this.settings.inputs_separator);
|
|
$valueContainer.append($ruleInput);
|
|
$inputs = $inputs.add($ruleInput);
|
|
}
|
|
|
|
$valueContainer.css('display', '');
|
|
|
|
$inputs.on('change ' + (filter.input_event || ''), function() {
|
|
if (!rule._updating_input) {
|
|
rule._updating_value = true;
|
|
rule.value = self.getRuleInputValue(rule);
|
|
rule._updating_value = false;
|
|
}
|
|
});
|
|
|
|
if (filter.plugin) {
|
|
$inputs[filter.plugin](filter.plugin_config || {});
|
|
}
|
|
|
|
/**
|
|
* After creating the input for a rule and initializing optional plugin
|
|
* @event afterCreateRuleInput
|
|
* @memberof QueryBuilder
|
|
* @param {Rule} rule
|
|
*/
|
|
this.trigger('afterCreateRuleInput', rule);
|
|
|
|
if (filter.default_value !== undefined) {
|
|
rule.value = filter.default_value;
|
|
}
|
|
else {
|
|
rule._updating_value = true;
|
|
rule.value = self.getRuleInputValue(rule);
|
|
rule._updating_value = false;
|
|
}
|
|
|
|
this.applyRuleFlags(rule);
|
|
};
|
|
|
|
/**
|
|
* Performs action when a rule's filter changes
|
|
* @param {Rule} rule
|
|
* @param {object} previousFilter
|
|
* @fires QueryBuilder.afterUpdateRuleFilter
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.updateRuleFilter = function(rule, previousFilter) {
|
|
this.createRuleOperators(rule);
|
|
this.createRuleInput(rule);
|
|
|
|
rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
|
|
|
|
// clear rule data if the filter changed
|
|
if (previousFilter && rule.filter && previousFilter.id !== rule.filter.id) {
|
|
rule.data = undefined;
|
|
}
|
|
|
|
/**
|
|
* After the filter has been updated and the operators and input re-created
|
|
* @event afterUpdateRuleFilter
|
|
* @memberof QueryBuilder
|
|
* @param {Rule} rule
|
|
* @param {object} previousFilter
|
|
*/
|
|
this.trigger('afterUpdateRuleFilter', rule, previousFilter);
|
|
|
|
this.trigger('rulesChanged');
|
|
};
|
|
|
|
/**
|
|
* Performs actions when a rule's operator changes
|
|
* @param {Rule} rule
|
|
* @param {object} previousOperator
|
|
* @fires QueryBuilder.afterUpdateRuleOperator
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) {
|
|
var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container);
|
|
|
|
if (!rule.operator || rule.operator.nb_inputs === 0) {
|
|
$valueContainer.hide();
|
|
|
|
rule.__.value = undefined;
|
|
}
|
|
else {
|
|
$valueContainer.css('display', '');
|
|
|
|
if ($valueContainer.is(':empty') || !previousOperator ||
|
|
rule.operator.nb_inputs !== previousOperator.nb_inputs ||
|
|
rule.operator.optgroup !== previousOperator.optgroup
|
|
) {
|
|
this.createRuleInput(rule);
|
|
}
|
|
}
|
|
|
|
if (rule.operator) {
|
|
rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
|
|
|
|
// refresh value if the format changed for this operator
|
|
rule.__.value = this.getRuleInputValue(rule);
|
|
}
|
|
|
|
/**
|
|
* After the operator has been updated and the input optionally re-created
|
|
* @event afterUpdateRuleOperator
|
|
* @memberof QueryBuilder
|
|
* @param {Rule} rule
|
|
* @param {object} previousOperator
|
|
*/
|
|
this.trigger('afterUpdateRuleOperator', rule, previousOperator);
|
|
|
|
this.trigger('rulesChanged');
|
|
};
|
|
|
|
/**
|
|
* Performs actions when rule's value changes
|
|
* @param {Rule} rule
|
|
* @param {object} previousValue
|
|
* @fires QueryBuilder.afterUpdateRuleValue
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.updateRuleValue = function(rule, previousValue) {
|
|
if (!rule._updating_value) {
|
|
this.setRuleInputValue(rule, rule.value);
|
|
}
|
|
|
|
/**
|
|
* After the rule value has been modified
|
|
* @event afterUpdateRuleValue
|
|
* @memberof QueryBuilder
|
|
* @param {Rule} rule
|
|
* @param {*} previousValue
|
|
*/
|
|
this.trigger('afterUpdateRuleValue', rule, previousValue);
|
|
|
|
this.trigger('rulesChanged');
|
|
};
|
|
|
|
/**
|
|
* Changes a rule's properties depending on its flags
|
|
* @param {Rule} rule
|
|
* @fires QueryBuilder.afterApplyRuleFlags
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.applyRuleFlags = function(rule) {
|
|
var flags = rule.flags;
|
|
var Selectors = QueryBuilder.selectors;
|
|
|
|
rule.$el.find(Selectors.rule_filter).prop('disabled', flags.filter_readonly);
|
|
rule.$el.find(Selectors.rule_operator).prop('disabled', flags.operator_readonly);
|
|
rule.$el.find(Selectors.rule_value).prop('disabled', flags.value_readonly);
|
|
|
|
if (flags.no_delete) {
|
|
rule.$el.find(Selectors.delete_rule).remove();
|
|
}
|
|
|
|
/**
|
|
* After rule's flags has been applied
|
|
* @event afterApplyRuleFlags
|
|
* @memberof QueryBuilder
|
|
* @param {Rule} rule
|
|
*/
|
|
this.trigger('afterApplyRuleFlags', rule);
|
|
};
|
|
|
|
/**
|
|
* Changes group's properties depending on its flags
|
|
* @param {Group} group
|
|
* @fires QueryBuilder.afterApplyGroupFlags
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.applyGroupFlags = function(group) {
|
|
var flags = group.flags;
|
|
var Selectors = QueryBuilder.selectors;
|
|
|
|
group.$el.find('>' + Selectors.group_condition).prop('disabled', flags.condition_readonly)
|
|
.parent().toggleClass('readonly', flags.condition_readonly);
|
|
|
|
if (flags.no_add_rule) {
|
|
group.$el.find(Selectors.add_rule).remove();
|
|
}
|
|
if (flags.no_add_group) {
|
|
group.$el.find(Selectors.add_group).remove();
|
|
}
|
|
if (flags.no_delete) {
|
|
group.$el.find(Selectors.delete_group).remove();
|
|
}
|
|
|
|
/**
|
|
* After group's flags has been applied
|
|
* @event afterApplyGroupFlags
|
|
* @memberof QueryBuilder
|
|
* @param {Group} group
|
|
*/
|
|
this.trigger('afterApplyGroupFlags', group);
|
|
};
|
|
|
|
/**
|
|
* Clears all errors markers
|
|
* @param {Node} [node] default is root Group
|
|
*/
|
|
QueryBuilder.prototype.clearErrors = function(node) {
|
|
node = node || this.model.root;
|
|
|
|
if (!node) {
|
|
return;
|
|
}
|
|
|
|
node.error = null;
|
|
|
|
if (node instanceof Group) {
|
|
node.each(function(rule) {
|
|
rule.error = null;
|
|
}, function(group) {
|
|
this.clearErrors(group);
|
|
}, this);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Adds/Removes error on a Rule or Group
|
|
* @param {Node} node
|
|
* @fires QueryBuilder.changer:displayError
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.updateError = function(node) {
|
|
if (this.settings.display_errors) {
|
|
if (node.error === null) {
|
|
node.$el.removeClass('has-error');
|
|
}
|
|
else {
|
|
var errorMessage = this.translate('errors', node.error[0]);
|
|
errorMessage = Utils.fmt(errorMessage, node.error.slice(1));
|
|
|
|
/**
|
|
* Modifies an error message before display
|
|
* @event changer:displayError
|
|
* @memberof QueryBuilder
|
|
* @param {string} errorMessage - the error message (translated and formatted)
|
|
* @param {array} error - the raw error array (error code and optional arguments)
|
|
* @param {Node} node
|
|
* @returns {string}
|
|
*/
|
|
errorMessage = this.change('displayError', errorMessage, node.error, node);
|
|
|
|
node.$el.addClass('has-error')
|
|
.find(QueryBuilder.selectors.error_container).eq(0)
|
|
.attr('title', errorMessage);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Triggers a validation error event
|
|
* @param {Node} node
|
|
* @param {string|array} error
|
|
* @param {*} value
|
|
* @fires QueryBuilder.validationError
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.triggerValidationError = function(node, error, value) {
|
|
if (!$.isArray(error)) {
|
|
error = [error];
|
|
}
|
|
|
|
/**
|
|
* Fired when a validation error occurred, can be prevented
|
|
* @event validationError
|
|
* @memberof QueryBuilder
|
|
* @param {Node} node
|
|
* @param {string} error
|
|
* @param {*} value
|
|
*/
|
|
var e = this.trigger('validationError', node, error, value);
|
|
if (!e.isDefaultPrevented()) {
|
|
node.error = error;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Destroys the builder
|
|
* @fires QueryBuilder.beforeDestroy
|
|
*/
|
|
QueryBuilder.prototype.destroy = function() {
|
|
/**
|
|
* Before the {@link QueryBuilder#destroy} method
|
|
* @event beforeDestroy
|
|
* @memberof QueryBuilder
|
|
*/
|
|
this.trigger('beforeDestroy');
|
|
|
|
if (this.status.generated_id) {
|
|
this.$el.removeAttr('id');
|
|
}
|
|
|
|
this.clear();
|
|
this.model = null;
|
|
|
|
this.$el
|
|
.off('.queryBuilder')
|
|
.removeClass('query-builder')
|
|
.removeData('queryBuilder');
|
|
|
|
delete this.$el[0].queryBuilder;
|
|
};
|
|
|
|
/**
|
|
* Clear all rules and resets the root group
|
|
* @fires QueryBuilder.beforeReset
|
|
* @fires QueryBuilder.afterReset
|
|
*/
|
|
QueryBuilder.prototype.reset = function() {
|
|
/**
|
|
* Before the {@link QueryBuilder#reset} method, can be prevented
|
|
* @event beforeReset
|
|
* @memberof QueryBuilder
|
|
*/
|
|
var e = this.trigger('beforeReset');
|
|
if (e.isDefaultPrevented()) {
|
|
return;
|
|
}
|
|
|
|
this.status.group_id = 1;
|
|
this.status.rule_id = 0;
|
|
|
|
this.model.root.empty();
|
|
|
|
this.model.root.data = undefined;
|
|
this.model.root.flags = $.extend({}, this.settings.default_group_flags);
|
|
this.model.root.condition = this.settings.default_condition;
|
|
|
|
this.addRule(this.model.root);
|
|
|
|
/**
|
|
* After the {@link QueryBuilder#reset} method
|
|
* @event afterReset
|
|
* @memberof QueryBuilder
|
|
*/
|
|
this.trigger('afterReset');
|
|
|
|
this.trigger('rulesChanged');
|
|
};
|
|
|
|
/**
|
|
* Clears all rules and removes the root group
|
|
* @fires QueryBuilder.beforeClear
|
|
* @fires QueryBuilder.afterClear
|
|
*/
|
|
QueryBuilder.prototype.clear = function() {
|
|
/**
|
|
* Before the {@link QueryBuilder#clear} method, can be prevented
|
|
* @event beforeClear
|
|
* @memberof QueryBuilder
|
|
*/
|
|
var e = this.trigger('beforeClear');
|
|
if (e.isDefaultPrevented()) {
|
|
return;
|
|
}
|
|
|
|
this.status.group_id = 0;
|
|
this.status.rule_id = 0;
|
|
|
|
if (this.model.root) {
|
|
this.model.root.drop();
|
|
this.model.root = null;
|
|
}
|
|
|
|
/**
|
|
* After the {@link QueryBuilder#clear} method
|
|
* @event afterClear
|
|
* @memberof QueryBuilder
|
|
*/
|
|
this.trigger('afterClear');
|
|
|
|
this.trigger('rulesChanged');
|
|
};
|
|
|
|
/**
|
|
* Modifies the builder configuration.<br>
|
|
* Only options defined in QueryBuilder.modifiable_options are modifiable
|
|
* @param {object} options
|
|
*/
|
|
QueryBuilder.prototype.setOptions = function(options) {
|
|
$.each(options, function(opt, value) {
|
|
if (QueryBuilder.modifiable_options.indexOf(opt) !== -1) {
|
|
this.settings[opt] = value;
|
|
}
|
|
}.bind(this));
|
|
};
|
|
|
|
/**
|
|
* Returns the model associated to a DOM object, or the root model
|
|
* @param {jQuery} [target]
|
|
* @returns {Node}
|
|
*/
|
|
QueryBuilder.prototype.getModel = function(target) {
|
|
if (!target) {
|
|
return this.model.root;
|
|
}
|
|
else if (target instanceof Node) {
|
|
return target;
|
|
}
|
|
else {
|
|
return $(target).data('queryBuilderModel');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Validates the whole builder
|
|
* @param {object} [options]
|
|
* @param {boolean} [options.skip_empty=false] - skips validating rules that have no filter selected
|
|
* @returns {boolean}
|
|
* @fires QueryBuilder.changer:validate
|
|
*/
|
|
QueryBuilder.prototype.validate = function(options) {
|
|
options = $.extend({
|
|
skip_empty: false
|
|
}, options);
|
|
|
|
this.clearErrors();
|
|
|
|
var self = this;
|
|
|
|
var valid = (function parse(group) {
|
|
var done = 0;
|
|
var errors = 0;
|
|
|
|
group.each(function(rule) {
|
|
if (!rule.filter && options.skip_empty) {
|
|
return;
|
|
}
|
|
|
|
if (!rule.filter) {
|
|
self.triggerValidationError(rule, 'no_filter', null);
|
|
errors++;
|
|
return;
|
|
}
|
|
|
|
if (!rule.operator) {
|
|
self.triggerValidationError(rule, 'no_operator', null);
|
|
errors++;
|
|
return;
|
|
}
|
|
|
|
if (rule.operator.nb_inputs !== 0) {
|
|
var valid = self.validateValue(rule, rule.value);
|
|
|
|
if (valid !== true) {
|
|
self.triggerValidationError(rule, valid, rule.value);
|
|
errors++;
|
|
return;
|
|
}
|
|
}
|
|
|
|
done++;
|
|
|
|
}, function(group) {
|
|
var res = parse(group);
|
|
if (res === true) {
|
|
done++;
|
|
}
|
|
else if (res === false) {
|
|
errors++;
|
|
}
|
|
});
|
|
|
|
if (errors > 0) {
|
|
return false;
|
|
}
|
|
else if (done === 0 && !group.isRoot() && options.skip_empty) {
|
|
return null;
|
|
}
|
|
else if (done === 0 && (!self.settings.allow_empty || !group.isRoot())) {
|
|
self.triggerValidationError(group, 'empty_group', null);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
|
|
}(this.model.root));
|
|
|
|
/**
|
|
* Modifies the result of the {@link QueryBuilder#validate} method
|
|
* @event changer:validate
|
|
* @memberof QueryBuilder
|
|
* @param {boolean} valid
|
|
* @returns {boolean}
|
|
*/
|
|
return this.change('validate', valid);
|
|
};
|
|
|
|
/**
|
|
* Gets an object representing current rules
|
|
* @param {object} [options]
|
|
* @param {boolean|string} [options.get_flags=false] - export flags, true: only changes from default flags or 'all'
|
|
* @param {boolean} [options.allow_invalid=false] - returns rules even if they are invalid
|
|
* @param {boolean} [options.skip_empty=false] - remove rules that have no filter selected
|
|
* @returns {object}
|
|
* @fires QueryBuilder.changer:ruleToJson
|
|
* @fires QueryBuilder.changer:groupToJson
|
|
* @fires QueryBuilder.changer:getRules
|
|
*/
|
|
QueryBuilder.prototype.getRules = function(options) {
|
|
options = $.extend({
|
|
get_flags: false,
|
|
allow_invalid: false,
|
|
skip_empty: false
|
|
}, options);
|
|
|
|
var valid = this.validate(options);
|
|
if (!valid && !options.allow_invalid) {
|
|
return null;
|
|
}
|
|
|
|
var self = this;
|
|
|
|
var out = (function parse(group) {
|
|
var groupData = {
|
|
condition: group.condition,
|
|
rules: []
|
|
};
|
|
|
|
if (group.data) {
|
|
groupData.data = $.extendext(true, 'replace', {}, group.data);
|
|
}
|
|
|
|
if (options.get_flags) {
|
|
var flags = self.getGroupFlags(group.flags, options.get_flags === 'all');
|
|
if (!$.isEmptyObject(flags)) {
|
|
groupData.flags = flags;
|
|
}
|
|
}
|
|
|
|
group.each(function(rule) {
|
|
if (!rule.filter && options.skip_empty) {
|
|
return;
|
|
}
|
|
|
|
var value = null;
|
|
if (!rule.operator || rule.operator.nb_inputs !== 0) {
|
|
value = rule.value;
|
|
}
|
|
|
|
var ruleData = {
|
|
id: rule.filter ? rule.filter.id : null,
|
|
field: rule.filter ? rule.filter.field : null,
|
|
type: rule.filter ? rule.filter.type : null,
|
|
input: rule.filter ? rule.filter.input : null,
|
|
operator: rule.operator ? rule.operator.type : null,
|
|
value: value
|
|
};
|
|
|
|
if (rule.filter && rule.filter.data || rule.data) {
|
|
ruleData.data = $.extendext(true, 'replace', {}, rule.filter.data, rule.data);
|
|
}
|
|
|
|
if (options.get_flags) {
|
|
var flags = self.getRuleFlags(rule.flags, options.get_flags === 'all');
|
|
if (!$.isEmptyObject(flags)) {
|
|
ruleData.flags = flags;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Modifies the JSON generated from a Rule object
|
|
* @event changer:ruleToJson
|
|
* @memberof QueryBuilder
|
|
* @param {object} json
|
|
* @param {Rule} rule
|
|
* @returns {object}
|
|
*/
|
|
groupData.rules.push(self.change('ruleToJson', ruleData, rule));
|
|
|
|
}, function(model) {
|
|
var data = parse(model);
|
|
if (data.rules.length !== 0 || !options.skip_empty) {
|
|
groupData.rules.push(data);
|
|
}
|
|
}, this);
|
|
|
|
/**
|
|
* Modifies the JSON generated from a Group object
|
|
* @event changer:groupToJson
|
|
* @memberof QueryBuilder
|
|
* @param {object} json
|
|
* @param {Group} group
|
|
* @returns {object}
|
|
*/
|
|
return self.change('groupToJson', groupData, group);
|
|
|
|
}(this.model.root));
|
|
|
|
out.valid = valid;
|
|
|
|
/**
|
|
* Modifies the result of the {@link QueryBuilder#getRules} method
|
|
* @event changer:getRules
|
|
* @memberof QueryBuilder
|
|
* @param {object} json
|
|
* @returns {object}
|
|
*/
|
|
return this.change('getRules', out);
|
|
};
|
|
|
|
/**
|
|
* Sets rules from object
|
|
* @param {object} data
|
|
* @param {object} [options]
|
|
* @param {boolean} [options.allow_invalid=false] - silent-fail if the data are invalid
|
|
* @throws RulesError, UndefinedConditionError
|
|
* @fires QueryBuilder.changer:setRules
|
|
* @fires QueryBuilder.changer:jsonToRule
|
|
* @fires QueryBuilder.changer:jsonToGroup
|
|
* @fires QueryBuilder.afterSetRules
|
|
*/
|
|
QueryBuilder.prototype.setRules = function(data, options) {
|
|
options = $.extend({
|
|
allow_invalid: false
|
|
}, options);
|
|
|
|
if ($.isArray(data)) {
|
|
data = {
|
|
condition: this.settings.default_condition,
|
|
rules: data
|
|
};
|
|
}
|
|
|
|
if (!data || !data.rules || (data.rules.length === 0 && !this.settings.allow_empty)) {
|
|
Utils.error('RulesParse', 'Incorrect data object passed');
|
|
}
|
|
|
|
this.clear();
|
|
this.setRoot(false, data.data, this.parseGroupFlags(data));
|
|
|
|
/**
|
|
* Modifies data before the {@link QueryBuilder#setRules} method
|
|
* @event changer:setRules
|
|
* @memberof QueryBuilder
|
|
* @param {object} json
|
|
* @param {object} options
|
|
* @returns {object}
|
|
*/
|
|
data = this.change('setRules', data, options);
|
|
|
|
var self = this;
|
|
|
|
(function add(data, group) {
|
|
if (group === null) {
|
|
return;
|
|
}
|
|
|
|
if (data.condition === undefined) {
|
|
data.condition = self.settings.default_condition;
|
|
}
|
|
else if (self.settings.conditions.indexOf(data.condition) == -1) {
|
|
Utils.error(!options.allow_invalid, 'UndefinedCondition', 'Invalid condition "{0}"', data.condition);
|
|
data.condition = self.settings.default_condition;
|
|
}
|
|
|
|
group.condition = data.condition;
|
|
|
|
data.rules.forEach(function(item) {
|
|
var model;
|
|
|
|
if (item.rules !== undefined) {
|
|
if (self.settings.allow_groups !== -1 && self.settings.allow_groups < group.level) {
|
|
Utils.error(!options.allow_invalid, 'RulesParse', 'No more than {0} groups are allowed', self.settings.allow_groups);
|
|
self.reset();
|
|
}
|
|
else {
|
|
model = self.addGroup(group, false, item.data, self.parseGroupFlags(item));
|
|
if (model === null) {
|
|
return;
|
|
}
|
|
|
|
add(item, model);
|
|
}
|
|
}
|
|
else {
|
|
if (!item.empty) {
|
|
if (item.id === undefined) {
|
|
Utils.error(!options.allow_invalid, 'RulesParse', 'Missing rule field id');
|
|
item.empty = true;
|
|
}
|
|
if (item.operator === undefined) {
|
|
item.operator = 'equal';
|
|
}
|
|
}
|
|
|
|
model = self.addRule(group, item.data, self.parseRuleFlags(item));
|
|
if (model === null) {
|
|
return;
|
|
}
|
|
|
|
if (!item.empty) {
|
|
model.filter = self.getFilterById(item.id, !options.allow_invalid);
|
|
}
|
|
|
|
if (model.filter) {
|
|
model.operator = self.getOperatorByType(item.operator, !options.allow_invalid);
|
|
|
|
if (!model.operator) {
|
|
model.operator = self.getOperators(model.filter)[0];
|
|
}
|
|
}
|
|
|
|
if (model.operator && model.operator.nb_inputs !== 0) {
|
|
if (item.value !== undefined) {
|
|
model.value = item.value;
|
|
}
|
|
else if (model.filter.default_value !== undefined) {
|
|
model.value = model.filter.default_value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Modifies the Rule object generated from the JSON
|
|
* @event changer:jsonToRule
|
|
* @memberof QueryBuilder
|
|
* @param {Rule} rule
|
|
* @param {object} json
|
|
* @returns {Rule} the same rule
|
|
*/
|
|
if (self.change('jsonToRule', model, item) != model) {
|
|
Utils.error('RulesParse', 'Plugin tried to change rule reference');
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Modifies the Group object generated from the JSON
|
|
* @event changer:jsonToGroup
|
|
* @memberof QueryBuilder
|
|
* @param {Group} group
|
|
* @param {object} json
|
|
* @returns {Group} the same group
|
|
*/
|
|
if (self.change('jsonToGroup', group, data) != group) {
|
|
Utils.error('RulesParse', 'Plugin tried to change group reference');
|
|
}
|
|
|
|
}(data, this.model.root));
|
|
|
|
/**
|
|
* After the {@link QueryBuilder#setRules} method
|
|
* @event afterSetRules
|
|
* @memberof QueryBuilder
|
|
*/
|
|
this.trigger('afterSetRules');
|
|
};
|
|
|
|
|
|
/**
|
|
* 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;
|
|
};
|
|
|
|
|
|
QueryBuilder.templates.group = '\
|
|
<div id="{{= it.group_id }}" class="rules-group-container"> \
|
|
<div class="rules-group-header"> \
|
|
<div class="btn-group pull-right group-actions"> \
|
|
<button type="button" class="btn btn-xs btn-success" data-add="rule"> \
|
|
<i class="{{= it.icons.add_rule }}"></i> {{= it.translate("add_rule") }} \
|
|
</button> \
|
|
{{? it.settings.allow_groups===-1 || it.settings.allow_groups>=it.level }} \
|
|
<button type="button" class="btn btn-xs btn-success" data-add="group"> \
|
|
<i class="{{= it.icons.add_group }}"></i> {{= it.translate("add_group") }} \
|
|
</button> \
|
|
{{?}} \
|
|
{{? it.level>1 }} \
|
|
<button type="button" class="btn btn-xs btn-danger" data-delete="group"> \
|
|
<i class="{{= it.icons.remove_group }}"></i> {{= it.translate("delete_group") }} \
|
|
</button> \
|
|
{{?}} \
|
|
</div> \
|
|
<div class="btn-group group-conditions"> \
|
|
{{~ it.conditions: condition }} \
|
|
<label class="btn btn-xs btn-primary"> \
|
|
<input type="radio" name="{{= it.group_id }}_cond" value="{{= condition }}"> {{= it.translate("conditions", condition) }} \
|
|
</label> \
|
|
{{~}} \
|
|
</div> \
|
|
{{? it.settings.display_errors }} \
|
|
<div class="error-container"><i class="{{= it.icons.error }}"></i></div> \
|
|
{{?}} \
|
|
</div> \
|
|
<div class=rules-group-body> \
|
|
<div class=rules-list></div> \
|
|
</div> \
|
|
</div>';
|
|
|
|
QueryBuilder.templates.rule = '\
|
|
<div id="{{= it.rule_id }}" class="rule-container"> \
|
|
<div class="rule-header"> \
|
|
<div class="btn-group pull-right rule-actions"> \
|
|
<button type="button" class="btn btn-xs btn-danger" data-delete="rule"> \
|
|
<i class="{{= it.icons.remove_rule }}"></i> {{= it.translate("delete_rule") }} \
|
|
</button> \
|
|
</div> \
|
|
</div> \
|
|
{{? it.settings.display_errors }} \
|
|
<div class="error-container"><i class="{{= it.icons.error }}"></i></div> \
|
|
{{?}} \
|
|
<div class="rule-filter-container"></div> \
|
|
<div class="rule-operator-container"></div> \
|
|
<div class="rule-value-container"></div> \
|
|
</div>';
|
|
|
|
QueryBuilder.templates.filterSelect = '\
|
|
{{ var optgroup = null; }} \
|
|
<select class="form-control" name="{{= it.rule.id }}_filter"> \
|
|
{{? it.settings.display_empty_filter }} \
|
|
<option value="-1">{{= it.settings.select_placeholder }}</option> \
|
|
{{?}} \
|
|
{{~ it.filters: filter }} \
|
|
{{? optgroup !== filter.optgroup }} \
|
|
{{? optgroup !== null }}</optgroup>{{?}} \
|
|
{{? (optgroup = filter.optgroup) !== null }} \
|
|
<optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
|
|
{{?}} \
|
|
{{?}} \
|
|
<option value="{{= filter.id }}" {{? filter.icon}}data-icon="{{= filter.icon}}"{{?}}>{{= it.translate(filter.label) }}</option> \
|
|
{{~}} \
|
|
{{? optgroup !== null }}</optgroup>{{?}} \
|
|
</select>';
|
|
|
|
QueryBuilder.templates.operatorSelect = '\
|
|
{{? it.operators.length === 1 }} \
|
|
<span> \
|
|
{{= it.translate("operators", it.operators[0].type) }} \
|
|
</span> \
|
|
{{?}} \
|
|
{{ var optgroup = null; }} \
|
|
<select class="form-control {{? it.operators.length === 1 }}hide{{?}}" name="{{= it.rule.id }}_operator"> \
|
|
{{~ it.operators: operator }} \
|
|
{{? optgroup !== operator.optgroup }} \
|
|
{{? optgroup !== null }}</optgroup>{{?}} \
|
|
{{? (optgroup = operator.optgroup) !== null }} \
|
|
<optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
|
|
{{?}} \
|
|
{{?}} \
|
|
<option value="{{= operator.type }}" {{? operator.icon}}data-icon="{{= operator.icon}}"{{?}}>{{= it.translate("operators", operator.type) }}</option> \
|
|
{{~}} \
|
|
{{? optgroup !== null }}</optgroup>{{?}} \
|
|
</select>';
|
|
|
|
QueryBuilder.templates.ruleValueSelect = '\
|
|
{{ var optgroup = null; }} \
|
|
<select class="form-control" name="{{= it.name }}" {{? it.rule.filter.multiple }}multiple{{?}}> \
|
|
{{? it.rule.filter.placeholder }} \
|
|
<option value="{{= it.rule.filter.placeholder_value }}" disabled selected>{{= it.rule.filter.placeholder }}</option> \
|
|
{{?}} \
|
|
{{~ it.rule.filter.values: entry }} \
|
|
{{? optgroup !== entry.optgroup }} \
|
|
{{? optgroup !== null }}</optgroup>{{?}} \
|
|
{{? (optgroup = entry.optgroup) !== null }} \
|
|
<optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
|
|
{{?}} \
|
|
{{?}} \
|
|
<option value="{{= entry.value }}">{{= entry.label }}</option> \
|
|
{{~}} \
|
|
{{? optgroup !== null }}</optgroup>{{?}} \
|
|
</select>';
|
|
|
|
/**
|
|
* Returns group's HTML
|
|
* @param {string} group_id
|
|
* @param {int} level
|
|
* @returns {string}
|
|
* @fires QueryBuilder.changer:getGroupTemplate
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.getGroupTemplate = function(group_id, level) {
|
|
var h = this.templates.group({
|
|
builder: this,
|
|
group_id: group_id,
|
|
level: level,
|
|
conditions: this.settings.conditions,
|
|
icons: this.icons,
|
|
settings: this.settings,
|
|
translate: this.translate.bind(this)
|
|
});
|
|
|
|
/**
|
|
* Modifies the raw HTML of a group
|
|
* @event changer:getGroupTemplate
|
|
* @memberof QueryBuilder
|
|
* @param {string} html
|
|
* @param {int} level
|
|
* @returns {string}
|
|
*/
|
|
return this.change('getGroupTemplate', h, level);
|
|
};
|
|
|
|
/**
|
|
* Returns rule's HTML
|
|
* @param {string} rule_id
|
|
* @returns {string}
|
|
* @fires QueryBuilder.changer:getRuleTemplate
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.getRuleTemplate = function(rule_id) {
|
|
var h = this.templates.rule({
|
|
builder: this,
|
|
rule_id: rule_id,
|
|
icons: this.icons,
|
|
settings: this.settings,
|
|
translate: this.translate.bind(this)
|
|
});
|
|
|
|
/**
|
|
* Modifies the raw HTML of a rule
|
|
* @event changer:getRuleTemplate
|
|
* @memberof QueryBuilder
|
|
* @param {string} html
|
|
* @returns {string}
|
|
*/
|
|
return this.change('getRuleTemplate', h);
|
|
};
|
|
|
|
/**
|
|
* Returns rule's filter HTML
|
|
* @param {Rule} rule
|
|
* @param {object[]} filters
|
|
* @returns {string}
|
|
* @fires QueryBuilder.changer:getRuleFilterTemplate
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.getRuleFilterSelect = function(rule, filters) {
|
|
var h = this.templates.filterSelect({
|
|
builder: this,
|
|
rule: rule,
|
|
filters: filters,
|
|
icons: this.icons,
|
|
settings: this.settings,
|
|
translate: this.translate.bind(this)
|
|
});
|
|
|
|
/**
|
|
* Modifies the raw HTML of the rule's filter dropdown
|
|
* @event changer:getRuleFilterSelect
|
|
* @memberof QueryBuilder
|
|
* @param {string} html
|
|
* @param {Rule} rule
|
|
* @param {QueryBuilder.Filter[]} filters
|
|
* @returns {string}
|
|
*/
|
|
return this.change('getRuleFilterSelect', h, rule, filters);
|
|
};
|
|
|
|
/**
|
|
* Returns rule's operator HTML
|
|
* @param {Rule} rule
|
|
* @param {object[]} operators
|
|
* @returns {string}
|
|
* @fires QueryBuilder.changer:getRuleOperatorTemplate
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.getRuleOperatorSelect = function(rule, operators) {
|
|
var h = this.templates.operatorSelect({
|
|
builder: this,
|
|
rule: rule,
|
|
operators: operators,
|
|
icons: this.icons,
|
|
settings: this.settings,
|
|
translate: this.translate.bind(this)
|
|
});
|
|
|
|
/**
|
|
* Modifies the raw HTML of the rule's operator dropdown
|
|
* @event changer:getRuleOperatorSelect
|
|
* @memberof QueryBuilder
|
|
* @param {string} html
|
|
* @param {Rule} rule
|
|
* @param {QueryBuilder.Operator[]} operators
|
|
* @returns {string}
|
|
*/
|
|
return this.change('getRuleOperatorSelect', h, rule, operators);
|
|
};
|
|
|
|
/**
|
|
* Returns the rule's value select HTML
|
|
* @param {string} name
|
|
* @param {Rule} rule
|
|
* @returns {string}
|
|
* @fires QueryBuilder.changer:getRuleValueSelect
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.getRuleValueSelect = function(name, rule) {
|
|
var h = this.templates.ruleValueSelect({
|
|
builder: this,
|
|
name: name,
|
|
rule: rule,
|
|
icons: this.icons,
|
|
settings: this.settings,
|
|
translate: this.translate.bind(this)
|
|
});
|
|
|
|
/**
|
|
* Modifies the raw HTML of the rule's value dropdown (in case of a "select filter)
|
|
* @event changer:getRuleValueSelect
|
|
* @memberof QueryBuilder
|
|
* @param {string} html
|
|
* @param [string} name
|
|
* @param {Rule} rule
|
|
* @returns {string}
|
|
*/
|
|
return this.change('getRuleValueSelect', h, name, rule);
|
|
};
|
|
|
|
/**
|
|
* Returns the rule's value HTML
|
|
* @param {Rule} rule
|
|
* @param {int} value_id
|
|
* @returns {string}
|
|
* @fires QueryBuilder.changer:getRuleInput
|
|
* @private
|
|
*/
|
|
QueryBuilder.prototype.getRuleInput = function(rule, value_id) {
|
|
var filter = rule.filter;
|
|
var validation = rule.filter.validation || {};
|
|
var name = rule.id + '_value_' + value_id;
|
|
var c = filter.vertical ? ' class=block' : '';
|
|
var h = '';
|
|
|
|
if (typeof filter.input == 'function') {
|
|
h = filter.input.call(this, rule, name);
|
|
}
|
|
else {
|
|
switch (filter.input) {
|
|
case 'radio':
|
|
case 'checkbox':
|
|
Utils.iterateOptions(filter.values, function(key, val) {
|
|
h += '<label' + c + '><input type="' + filter.input + '" name="' + name + '" value="' + key + '"> ' + val + '</label> ';
|
|
});
|
|
break;
|
|
|
|
case 'select':
|
|
h = this.getRuleValueSelect(name, rule);
|
|
break;
|
|
|
|
case 'textarea':
|
|
h += '<textarea class="form-control" name="' + name + '"';
|
|
if (filter.size) h += ' cols="' + filter.size + '"';
|
|
if (filter.rows) h += ' rows="' + filter.rows + '"';
|
|
if (validation.min !== undefined) h += ' minlength="' + validation.min + '"';
|
|
if (validation.max !== undefined) h += ' maxlength="' + validation.max + '"';
|
|
if (filter.placeholder) h += ' placeholder="' + filter.placeholder + '"';
|
|
h += '></textarea>';
|
|
break;
|
|
|
|
case 'number':
|
|
h += '<input class="form-control" type="number" name="' + name + '"';
|
|
if (validation.step !== undefined) h += ' step="' + validation.step + '"';
|
|
if (validation.min !== undefined) h += ' min="' + validation.min + '"';
|
|
if (validation.max !== undefined) h += ' max="' + validation.max + '"';
|
|
if (filter.placeholder) h += ' placeholder="' + filter.placeholder + '"';
|
|
if (filter.size) h += ' size="' + filter.size + '"';
|
|
h += '>';
|
|
break;
|
|
|
|
default:
|
|
h += '<input class="form-control" type="text" name="' + name + '"';
|
|
if (filter.placeholder) h += ' placeholder="' + filter.placeholder + '"';
|
|
if (filter.type === 'string' && validation.min !== undefined) h += ' minlength="' + validation.min + '"';
|
|
if (filter.type === 'string' && validation.max !== undefined) h += ' maxlength="' + validation.max + '"';
|
|
if (filter.size) h += ' size="' + filter.size + '"';
|
|
h += '>';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Modifies the raw HTML of the rule's input
|
|
* @event changer:getRuleInput
|
|
* @memberof QueryBuilder
|
|
* @param {string} html
|
|
* @param {Rule} rule
|
|
* @param {string} name - the name that the input must have
|
|
* @returns {string}
|
|
*/
|
|
return this.change('getRuleInput', h, rule, name);
|
|
};
|
|
|
|
|
|
/**
|
|
* @namespace
|
|
*/
|
|
var Utils = {};
|
|
|
|
/**
|
|
* @member {object}
|
|
* @memberof QueryBuilder
|
|
* @see Utils
|
|
*/
|
|
QueryBuilder.utils = Utils;
|
|
|
|
/**
|
|
* @callback Utils#OptionsIteratee
|
|
* @param {string} key
|
|
* @param {string} value
|
|
* @param {string} [optgroup]
|
|
*/
|
|
|
|
/**
|
|
* Iterates over radio/checkbox/selection options, it accept four formats
|
|
*
|
|
* @example
|
|
* // array of values
|
|
* options = ['one', 'two', 'three']
|
|
* @example
|
|
* // simple key-value map
|
|
* options = {1: 'one', 2: 'two', 3: 'three'}
|
|
* @example
|
|
* // array of 1-element maps
|
|
* options = [{1: 'one'}, {2: 'two'}, {3: 'three'}]
|
|
* @example
|
|
* // array of elements
|
|
* options = [{value: 1, label: 'one', optgroup: 'group'}, {value: 2, label: 'two'}]
|
|
*
|
|
* @param {object|array} options
|
|
* @param {Utils#OptionsIteratee} tpl
|
|
*/
|
|
Utils.iterateOptions = function(options, tpl) {
|
|
if (options) {
|
|
if ($.isArray(options)) {
|
|
options.forEach(function(entry) {
|
|
if ($.isPlainObject(entry)) {
|
|
// array of elements
|
|
if ('value' in entry) {
|
|
tpl(entry.value, entry.label || entry.value, entry.optgroup);
|
|
}
|
|
// array of one-element maps
|
|
else {
|
|
$.each(entry, function(key, val) {
|
|
tpl(key, val);
|
|
return false; // break after first entry
|
|
});
|
|
}
|
|
}
|
|
// array of values
|
|
else {
|
|
tpl(entry, entry);
|
|
}
|
|
});
|
|
}
|
|
// unordered map
|
|
else {
|
|
$.each(options, function(key, val) {
|
|
tpl(key, val);
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Replaces {0}, {1}, ... in a string
|
|
* @param {string} str
|
|
* @param {...*} args
|
|
* @returns {string}
|
|
*/
|
|
Utils.fmt = function(str, args) {
|
|
if (!Array.isArray(args)) {
|
|
args = Array.prototype.slice.call(arguments, 1);
|
|
}
|
|
|
|
return str.replace(/{([0-9]+)}/g, function(m, i) {
|
|
return args[parseInt(i)];
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Throws an Error object with custom name or logs an error
|
|
* @param {boolean} [doThrow=true]
|
|
* @param {string} type
|
|
* @param {string} message
|
|
* @param {...*} args
|
|
*/
|
|
Utils.error = function() {
|
|
var i = 0;
|
|
var doThrow = typeof arguments[i] === 'boolean' ? arguments[i++] : true;
|
|
var type = arguments[i++];
|
|
var message = arguments[i++];
|
|
var args = Array.isArray(arguments[i]) ? arguments[i] : Array.prototype.slice.call(arguments, i);
|
|
|
|
if (doThrow) {
|
|
var err = new Error(Utils.fmt(message, args));
|
|
err.name = type + 'Error';
|
|
err.args = args;
|
|
throw err;
|
|
}
|
|
else {
|
|
console.error(type + 'Error: ' + Utils.fmt(message, args));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Changes the type of a value to int, float or bool
|
|
* @param {*} value
|
|
* @param {string} type - 'integer', 'double', 'boolean' or anything else (passthrough)
|
|
* @returns {*}
|
|
*/
|
|
Utils.changeType = function(value, type) {
|
|
if (value === '' || value === undefined) {
|
|
return undefined;
|
|
}
|
|
|
|
switch (type) {
|
|
// @formatter:off
|
|
case 'integer':
|
|
if (typeof value === 'string' && !/^-?\d+$/.test(value)) {
|
|
return value;
|
|
}
|
|
return parseInt(value);
|
|
case 'double':
|
|
if (typeof value === 'string' && !/^-?\d+\.?\d*$/.test(value)) {
|
|
return value;
|
|
}
|
|
return parseFloat(value);
|
|
case 'boolean':
|
|
if (typeof value === 'string' && !/^(0|1|true|false){1}$/i.test(value)) {
|
|
return value;
|
|
}
|
|
return value === true || value === 1 || value.toLowerCase() === 'true' || value === '1';
|
|
default: return value;
|
|
// @formatter:on
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Escapes a string like PHP's mysql_real_escape_string does
|
|
* @param {string} value
|
|
* @returns {string}
|
|
*/
|
|
Utils.escapeString = function(value) {
|
|
if (typeof value != 'string') {
|
|
return value;
|
|
}
|
|
|
|
return value
|
|
.replace(/[\0\n\r\b\\\'\"]/g, function(s) {
|
|
switch (s) {
|
|
// @formatter:off
|
|
case '\0': return '\\0';
|
|
case '\n': return '\\n';
|
|
case '\r': return '\\r';
|
|
case '\b': return '\\b';
|
|
default: return '\\' + s;
|
|
// @formatter:off
|
|
}
|
|
})
|
|
// uglify compliant
|
|
.replace(/\t/g, '\\t')
|
|
.replace(/\x1a/g, '\\Z');
|
|
};
|
|
|
|
/**
|
|
* Escapes a string for use in regex
|
|
* @param {string} str
|
|
* @returns {string}
|
|
*/
|
|
Utils.escapeRegExp = function(str) {
|
|
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
|
|
};
|
|
|
|
/**
|
|
* Escapes a string for use in HTML element id
|
|
* @param {string} str
|
|
* @returns {string}
|
|
*/
|
|
Utils.escapeElementId = function(str) {
|
|
// Regex based on that suggested by:
|
|
// https://learn.jquery.com/using-jquery-core/faq/how-do-i-select-an-element-by-an-id-that-has-characters-used-in-css-notation/
|
|
// - escapes : . [ ] ,
|
|
// - avoids escaping already escaped values
|
|
return (str) ? str.replace(/(\\)?([:.\[\],])/g,
|
|
function( $0, $1, $2 ) { return $1 ? $0 : '\\' + $2; }) : str;
|
|
};
|
|
|
|
/**
|
|
* Sorts objects by grouping them by `key`, preserving initial order when possible
|
|
* @param {object[]} items
|
|
* @param {string} key
|
|
* @returns {object[]}
|
|
*/
|
|
Utils.groupSort = function(items, key) {
|
|
var optgroups = [];
|
|
var newItems = [];
|
|
|
|
items.forEach(function(item) {
|
|
var idx;
|
|
|
|
if (item[key]) {
|
|
idx = optgroups.lastIndexOf(item[key]);
|
|
|
|
if (idx == -1) {
|
|
idx = optgroups.length;
|
|
}
|
|
else {
|
|
idx++;
|
|
}
|
|
}
|
|
else {
|
|
idx = optgroups.length;
|
|
}
|
|
|
|
optgroups.splice(idx, 0, item[key]);
|
|
newItems.splice(idx, 0, item);
|
|
});
|
|
|
|
return newItems;
|
|
};
|
|
|
|
/**
|
|
* Defines properties on an Node prototype with getter and setter.<br>
|
|
* Update events are emitted in the setter through root Model (if any).<br>
|
|
* The object must have a `__` object, non enumerable property to store values.
|
|
* @param {function} obj
|
|
* @param {string[]} fields
|
|
*/
|
|
Utils.defineModelProperties = function(obj, fields) {
|
|
fields.forEach(function(field) {
|
|
Object.defineProperty(obj.prototype, field, {
|
|
enumerable: true,
|
|
get: function() {
|
|
return this.__[field];
|
|
},
|
|
set: function(value) {
|
|
var previousValue = (this.__[field] !== null && typeof this.__[field] == 'object') ?
|
|
$.extend({}, this.__[field]) :
|
|
this.__[field];
|
|
|
|
this.__[field] = value;
|
|
|
|
if (this.model !== null) {
|
|
/**
|
|
* After a value of the model changed
|
|
* @event model:update
|
|
* @memberof Model
|
|
* @param {Node} node
|
|
* @param {string} field
|
|
* @param {*} value
|
|
* @param {*} previousValue
|
|
*/
|
|
this.model.trigger('update', this, field, value, previousValue);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* Main object storing data model and emitting model events
|
|
* @constructor
|
|
*/
|
|
function Model() {
|
|
/**
|
|
* @member {Group}
|
|
* @readonly
|
|
*/
|
|
this.root = null;
|
|
|
|
/**
|
|
* Base for event emitting
|
|
* @member {jQuery}
|
|
* @readonly
|
|
* @private
|
|
*/
|
|
this.$ = $(this);
|
|
}
|
|
|
|
$.extend(Model.prototype, /** @lends Model.prototype */ {
|
|
/**
|
|
* Triggers an event on the model
|
|
* @param {string} type
|
|
* @returns {$.Event}
|
|
*/
|
|
trigger: function(type) {
|
|
var event = new $.Event(type);
|
|
this.$.triggerHandler(event, Array.prototype.slice.call(arguments, 1));
|
|
return event;
|
|
},
|
|
|
|
/**
|
|
* Attaches an event listener on the model
|
|
* @param {string} type
|
|
* @param {function} cb
|
|
* @returns {Model}
|
|
*/
|
|
on: function() {
|
|
this.$.on.apply(this.$, Array.prototype.slice.call(arguments));
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Removes an event listener from the model
|
|
* @param {string} type
|
|
* @param {function} [cb]
|
|
* @returns {Model}
|
|
*/
|
|
off: function() {
|
|
this.$.off.apply(this.$, Array.prototype.slice.call(arguments));
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Attaches an event listener called once on the model
|
|
* @param {string} type
|
|
* @param {function} cb
|
|
* @returns {Model}
|
|
*/
|
|
once: function() {
|
|
this.$.one.apply(this.$, Array.prototype.slice.call(arguments));
|
|
return this;
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* Root abstract object
|
|
* @constructor
|
|
* @param {Node} [parent]
|
|
* @param {jQuery} $el
|
|
*/
|
|
var Node = function(parent, $el) {
|
|
if (!(this instanceof Node)) {
|
|
return new Node(parent, $el);
|
|
}
|
|
|
|
Object.defineProperty(this, '__', { value: {} });
|
|
|
|
$el.data('queryBuilderModel', this);
|
|
|
|
/**
|
|
* @name level
|
|
* @member {int}
|
|
* @memberof Node
|
|
* @instance
|
|
* @readonly
|
|
*/
|
|
this.__.level = 1;
|
|
|
|
/**
|
|
* @name error
|
|
* @member {string}
|
|
* @memberof Node
|
|
* @instance
|
|
*/
|
|
this.__.error = null;
|
|
|
|
/**
|
|
* @name flags
|
|
* @member {object}
|
|
* @memberof Node
|
|
* @instance
|
|
* @readonly
|
|
*/
|
|
this.__.flags = {};
|
|
|
|
/**
|
|
* @name data
|
|
* @member {object}
|
|
* @memberof Node
|
|
* @instance
|
|
*/
|
|
this.__.data = undefined;
|
|
|
|
/**
|
|
* @member {jQuery}
|
|
* @readonly
|
|
*/
|
|
this.$el = $el;
|
|
|
|
/**
|
|
* @member {string}
|
|
* @readonly
|
|
*/
|
|
this.id = $el[0].id;
|
|
|
|
/**
|
|
* @member {Model}
|
|
* @readonly
|
|
*/
|
|
this.model = null;
|
|
|
|
/**
|
|
* @member {Group}
|
|
* @readonly
|
|
*/
|
|
this.parent = parent;
|
|
};
|
|
|
|
Utils.defineModelProperties(Node, ['level', 'error', 'data', 'flags']);
|
|
|
|
Object.defineProperty(Node.prototype, 'parent', {
|
|
enumerable: true,
|
|
get: function() {
|
|
return this.__.parent;
|
|
},
|
|
set: function(value) {
|
|
this.__.parent = value;
|
|
this.level = value === null ? 1 : value.level + 1;
|
|
this.model = value === null ? null : value.model;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Checks if this Node is the root
|
|
* @returns {boolean}
|
|
*/
|
|
Node.prototype.isRoot = function() {
|
|
return (this.level === 1);
|
|
};
|
|
|
|
/**
|
|
* Returns the node position inside its parent
|
|
* @returns {int}
|
|
*/
|
|
Node.prototype.getPos = function() {
|
|
if (this.isRoot()) {
|
|
return -1;
|
|
}
|
|
else {
|
|
return this.parent.getNodePos(this);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Deletes self
|
|
* @fires Model.model:drop
|
|
*/
|
|
Node.prototype.drop = function() {
|
|
var model = this.model;
|
|
|
|
if (!!this.parent) {
|
|
this.parent.removeNode(this);
|
|
}
|
|
|
|
this.$el.removeData('queryBuilderModel');
|
|
|
|
if (model !== null) {
|
|
/**
|
|
* After a node of the model has been removed
|
|
* @event model:drop
|
|
* @memberof Model
|
|
* @param {Node} node
|
|
*/
|
|
model.trigger('drop', this);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Moves itself after another Node
|
|
* @param {Node} target
|
|
* @fires Model.model:move
|
|
*/
|
|
Node.prototype.moveAfter = function(target) {
|
|
if (!this.isRoot()) {
|
|
this.move(target.parent, target.getPos() + 1);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Moves itself at the beginning of parent or another Group
|
|
* @param {Group} [target]
|
|
* @fires Model.model:move
|
|
*/
|
|
Node.prototype.moveAtBegin = function(target) {
|
|
if (!this.isRoot()) {
|
|
if (target === undefined) {
|
|
target = this.parent;
|
|
}
|
|
|
|
this.move(target, 0);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Moves itself at the end of parent or another Group
|
|
* @param {Group} [target]
|
|
* @fires Model.model:move
|
|
*/
|
|
Node.prototype.moveAtEnd = function(target) {
|
|
if (!this.isRoot()) {
|
|
if (target === undefined) {
|
|
target = this.parent;
|
|
}
|
|
|
|
this.move(target, target.length() === 0 ? 0 : target.length() - 1);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Moves itself at specific position of Group
|
|
* @param {Group} target
|
|
* @param {int} index
|
|
* @fires Model.model:move
|
|
*/
|
|
Node.prototype.move = function(target, index) {
|
|
if (!this.isRoot()) {
|
|
if (typeof target === 'number') {
|
|
index = target;
|
|
target = this.parent;
|
|
}
|
|
|
|
this.parent.removeNode(this);
|
|
target.insertNode(this, index, false);
|
|
|
|
if (this.model !== null) {
|
|
/**
|
|
* After a node of the model has been moved
|
|
* @event model:move
|
|
* @memberof Model
|
|
* @param {Node} node
|
|
* @param {Node} target
|
|
* @param {int} index
|
|
*/
|
|
this.model.trigger('move', this, target, index);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Group object
|
|
* @constructor
|
|
* @extends Node
|
|
* @param {Group} [parent]
|
|
* @param {jQuery} $el
|
|
*/
|
|
var Group = function(parent, $el) {
|
|
if (!(this instanceof Group)) {
|
|
return new Group(parent, $el);
|
|
}
|
|
|
|
Node.call(this, parent, $el);
|
|
|
|
/**
|
|
* @member {object[]}
|
|
* @readonly
|
|
*/
|
|
this.rules = [];
|
|
|
|
/**
|
|
* @name condition
|
|
* @member {string}
|
|
* @memberof Group
|
|
* @instance
|
|
*/
|
|
this.__.condition = null;
|
|
};
|
|
|
|
Group.prototype = Object.create(Node.prototype);
|
|
Group.prototype.constructor = Group;
|
|
|
|
Utils.defineModelProperties(Group, ['condition']);
|
|
|
|
/**
|
|
* Removes group's content
|
|
*/
|
|
Group.prototype.empty = function() {
|
|
this.each('reverse', function(rule) {
|
|
rule.drop();
|
|
}, function(group) {
|
|
group.drop();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Deletes self
|
|
*/
|
|
Group.prototype.drop = function() {
|
|
this.empty();
|
|
Node.prototype.drop.call(this);
|
|
};
|
|
|
|
/**
|
|
* Returns the number of children
|
|
* @returns {int}
|
|
*/
|
|
Group.prototype.length = function() {
|
|
return this.rules.length;
|
|
};
|
|
|
|
/**
|
|
* Adds a Node at specified index
|
|
* @param {Node} node
|
|
* @param {int} [index=end]
|
|
* @param {boolean} [trigger=false] - fire 'add' event
|
|
* @returns {Node} the inserted node
|
|
* @fires Model.model:add
|
|
*/
|
|
Group.prototype.insertNode = function(node, index, trigger) {
|
|
if (index === undefined) {
|
|
index = this.length();
|
|
}
|
|
|
|
this.rules.splice(index, 0, node);
|
|
node.parent = this;
|
|
|
|
if (trigger && this.model !== null) {
|
|
/**
|
|
* After a node of the model has been added
|
|
* @event model:add
|
|
* @memberof Model
|
|
* @param {Node} parent
|
|
* @param {Node} node
|
|
* @param {int} index
|
|
*/
|
|
this.model.trigger('add', this, node, index);
|
|
}
|
|
|
|
return node;
|
|
};
|
|
|
|
/**
|
|
* Adds a new Group at specified index
|
|
* @param {jQuery} $el
|
|
* @param {int} [index=end]
|
|
* @returns {Group}
|
|
* @fires Model.model:add
|
|
*/
|
|
Group.prototype.addGroup = function($el, index) {
|
|
return this.insertNode(new Group(this, $el), index, true);
|
|
};
|
|
|
|
/**
|
|
* Adds a new Rule at specified index
|
|
* @param {jQuery} $el
|
|
* @param {int} [index=end]
|
|
* @returns {Rule}
|
|
* @fires Model.model:add
|
|
*/
|
|
Group.prototype.addRule = function($el, index) {
|
|
return this.insertNode(new Rule(this, $el), index, true);
|
|
};
|
|
|
|
/**
|
|
* Deletes a specific Node
|
|
* @param {Node} node
|
|
*/
|
|
Group.prototype.removeNode = function(node) {
|
|
var index = this.getNodePos(node);
|
|
if (index !== -1) {
|
|
node.parent = null;
|
|
this.rules.splice(index, 1);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns the position of a child Node
|
|
* @param {Node} node
|
|
* @returns {int}
|
|
*/
|
|
Group.prototype.getNodePos = function(node) {
|
|
return this.rules.indexOf(node);
|
|
};
|
|
|
|
/**
|
|
* @callback Model#GroupIteratee
|
|
* @param {Node} node
|
|
* @returns {boolean} stop the iteration
|
|
*/
|
|
|
|
/**
|
|
* Iterate over all Nodes
|
|
* @param {boolean} [reverse=false] - iterate in reverse order, required if you delete nodes
|
|
* @param {Model#GroupIteratee} cbRule - callback for Rules (can be `null` but not omitted)
|
|
* @param {Model#GroupIteratee} [cbGroup] - callback for Groups
|
|
* @param {object} [context] - context for callbacks
|
|
* @returns {boolean} if the iteration has been stopped by a callback
|
|
*/
|
|
Group.prototype.each = function(reverse, cbRule, cbGroup, context) {
|
|
if (typeof reverse !== 'boolean' && typeof reverse !== 'string') {
|
|
context = cbGroup;
|
|
cbGroup = cbRule;
|
|
cbRule = reverse;
|
|
reverse = false;
|
|
}
|
|
context = context === undefined ? null : context;
|
|
|
|
var i = reverse ? this.rules.length - 1 : 0;
|
|
var l = reverse ? 0 : this.rules.length - 1;
|
|
var c = reverse ? -1 : 1;
|
|
var next = function() {
|
|
return reverse ? i >= l : i <= l;
|
|
};
|
|
var stop = false;
|
|
|
|
for (; next(); i += c) {
|
|
if (this.rules[i] instanceof Group) {
|
|
if (!!cbGroup) {
|
|
stop = cbGroup.call(context, this.rules[i]) === false;
|
|
}
|
|
}
|
|
else if (!!cbRule) {
|
|
stop = cbRule.call(context, this.rules[i]) === false;
|
|
}
|
|
|
|
if (stop) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return !stop;
|
|
};
|
|
|
|
/**
|
|
* Checks if the group contains a particular Node
|
|
* @param {Node} node
|
|
* @param {boolean} [recursive=false]
|
|
* @returns {boolean}
|
|
*/
|
|
Group.prototype.contains = function(node, recursive) {
|
|
if (this.getNodePos(node) !== -1) {
|
|
return true;
|
|
}
|
|
else if (!recursive) {
|
|
return false;
|
|
}
|
|
else {
|
|
// the loop will return with false as soon as the Node is found
|
|
return !this.each(function() {
|
|
return true;
|
|
}, function(group) {
|
|
return !group.contains(node, true);
|
|
});
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Rule object
|
|
* @constructor
|
|
* @extends Node
|
|
* @param {Group} parent
|
|
* @param {jQuery} $el
|
|
*/
|
|
var Rule = function(parent, $el) {
|
|
if (!(this instanceof Rule)) {
|
|
return new Rule(parent, $el);
|
|
}
|
|
|
|
Node.call(this, parent, $el);
|
|
|
|
this._updating_value = false;
|
|
this._updating_input = false;
|
|
|
|
/**
|
|
* @name filter
|
|
* @member {QueryBuilder.Filter}
|
|
* @memberof Rule
|
|
* @instance
|
|
*/
|
|
this.__.filter = null;
|
|
|
|
/**
|
|
* @name operator
|
|
* @member {QueryBuilder.Operator}
|
|
* @memberof Rule
|
|
* @instance
|
|
*/
|
|
this.__.operator = null;
|
|
|
|
/**
|
|
* @name value
|
|
* @member {*}
|
|
* @memberof Rule
|
|
* @instance
|
|
*/
|
|
this.__.value = undefined;
|
|
};
|
|
|
|
Rule.prototype = Object.create(Node.prototype);
|
|
Rule.prototype.constructor = Rule;
|
|
|
|
Utils.defineModelProperties(Rule, ['filter', 'operator', 'value']);
|
|
|
|
/**
|
|
* Checks if this Node is the root
|
|
* @returns {boolean} always false
|
|
*/
|
|
Rule.prototype.isRoot = function() {
|
|
return false;
|
|
};
|
|
|
|
|
|
/**
|
|
* @member {function}
|
|
* @memberof QueryBuilder
|
|
* @see Group
|
|
*/
|
|
QueryBuilder.Group = Group;
|
|
|
|
/**
|
|
* @member {function}
|
|
* @memberof QueryBuilder
|
|
* @see Rule
|
|
*/
|
|
QueryBuilder.Rule = Rule;
|
|
|
|
|
|
/**
|
|
* The {@link http://learn.jquery.com/plugins/|jQuery Plugins} namespace
|
|
* @external "jQuery.fn"
|
|
*/
|
|
|
|
/**
|
|
* Instanciates or accesses the {@link QueryBuilder} on an element
|
|
* @function
|
|
* @memberof external:"jQuery.fn"
|
|
* @param {*} option - initial configuration or method name
|
|
* @param {...*} args - method arguments
|
|
*
|
|
* @example
|
|
* $('#builder').queryBuilder({ /** configuration object *\/ });
|
|
* @example
|
|
* $('#builder').queryBuilder('methodName', methodParam1, methodParam2);
|
|
*/
|
|
$.fn.queryBuilder = function(option) {
|
|
if (this.length === 0) {
|
|
Utils.error('Config', 'No target defined');
|
|
}
|
|
if (this.length > 1) {
|
|
Utils.error('Config', 'Unable to initialize on multiple target');
|
|
}
|
|
|
|
var data = this.data('queryBuilder');
|
|
var options = (typeof option == 'object' && option) || {};
|
|
|
|
if (!data && option == 'destroy') {
|
|
return this;
|
|
}
|
|
if (!data) {
|
|
var builder = new QueryBuilder(this, options);
|
|
this.data('queryBuilder', builder);
|
|
builder.init(options.rules);
|
|
}
|
|
if (typeof option == 'string') {
|
|
return data[option].apply(data, Array.prototype.slice.call(arguments, 1));
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* @function
|
|
* @memberof external:"jQuery.fn"
|
|
* @see QueryBuilder
|
|
*/
|
|
$.fn.queryBuilder.constructor = QueryBuilder;
|
|
|
|
/**
|
|
* @function
|
|
* @memberof external:"jQuery.fn"
|
|
* @see QueryBuilder.defaults
|
|
*/
|
|
$.fn.queryBuilder.defaults = QueryBuilder.defaults;
|
|
|
|
/**
|
|
* @function
|
|
* @memberof external:"jQuery.fn"
|
|
* @see QueryBuilder.defaults
|
|
*/
|
|
$.fn.queryBuilder.extend = QueryBuilder.extend;
|
|
|
|
/**
|
|
* @function
|
|
* @memberof external:"jQuery.fn"
|
|
* @see QueryBuilder.define
|
|
*/
|
|
$.fn.queryBuilder.define = QueryBuilder.define;
|
|
|
|
/**
|
|
* @function
|
|
* @memberof external:"jQuery.fn"
|
|
* @see QueryBuilder.regional
|
|
*/
|
|
$.fn.queryBuilder.regional = QueryBuilder.regional;
|
|
|
|
|
|
/**
|
|
* @class BtCheckbox
|
|
* @memberof module:plugins
|
|
* @description Applies Awesome Bootstrap Checkbox for checkbox and radio inputs.
|
|
* @param {object} [options]
|
|
* @param {string} [options.font='glyphicons']
|
|
* @param {string} [options.color='default']
|
|
*/
|
|
QueryBuilder.define('bt-checkbox', function(options) {
|
|
if (options.font == 'glyphicons') {
|
|
this.$el.addClass('bt-checkbox-glyphicons');
|
|
}
|
|
|
|
this.on('getRuleInput.filter', function(h, rule, name) {
|
|
var filter = rule.filter;
|
|
|
|
if ((filter.input === 'radio' || filter.input === 'checkbox') && !filter.plugin) {
|
|
h.value = '';
|
|
|
|
if (!filter.colors) {
|
|
filter.colors = {};
|
|
}
|
|
if (filter.color) {
|
|
filter.colors._def_ = filter.color;
|
|
}
|
|
|
|
var style = filter.vertical ? ' style="display:block"' : '';
|
|
var i = 0;
|
|
|
|
Utils.iterateOptions(filter.values, function(key, val) {
|
|
var color = filter.colors[key] || filter.colors._def_ || options.color;
|
|
var id = name + '_' + (i++);
|
|
|
|
h.value+= '\
|
|
<div' + style + ' class="' + filter.input + ' ' + filter.input + '-' + color + '"> \
|
|
<input type="' + filter.input + '" name="' + name + '" id="' + id + '" value="' + key + '"> \
|
|
<label for="' + id + '">' + val + '</label> \
|
|
</div>';
|
|
});
|
|
}
|
|
});
|
|
}, {
|
|
font: 'glyphicons',
|
|
color: 'default'
|
|
});
|
|
|
|
|
|
/**
|
|
* @class BtSelectpicker
|
|
* @memberof module:plugins
|
|
* @descriptioon Applies Bootstrap Select on filters and operators combo-boxes.
|
|
* @param {object} [options]
|
|
* @param {string} [options.container='body']
|
|
* @param {string} [options.style='btn-inverse btn-xs']
|
|
* @param {int|string} [options.width='auto']
|
|
* @param {boolean} [options.showIcon=false]
|
|
* @throws MissingLibraryError
|
|
*/
|
|
QueryBuilder.define('bt-selectpicker', function(options) {
|
|
if (!$.fn.selectpicker || !$.fn.selectpicker.Constructor) {
|
|
Utils.error('MissingLibrary', 'Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select');
|
|
}
|
|
|
|
var Selectors = QueryBuilder.selectors;
|
|
|
|
// init selectpicker
|
|
this.on('afterCreateRuleFilters', function(e, rule) {
|
|
rule.$el.find(Selectors.rule_filter).removeClass('form-control').selectpicker(options);
|
|
});
|
|
|
|
this.on('afterCreateRuleOperators', function(e, rule) {
|
|
rule.$el.find(Selectors.rule_operator).removeClass('form-control').selectpicker(options);
|
|
});
|
|
|
|
// update selectpicker on change
|
|
this.on('afterUpdateRuleFilter', function(e, rule) {
|
|
rule.$el.find(Selectors.rule_filter).selectpicker('render');
|
|
});
|
|
|
|
this.on('afterUpdateRuleOperator', function(e, rule) {
|
|
rule.$el.find(Selectors.rule_operator).selectpicker('render');
|
|
});
|
|
|
|
this.on('beforeDeleteRule', function(e, rule) {
|
|
rule.$el.find(Selectors.rule_filter).selectpicker('destroy');
|
|
rule.$el.find(Selectors.rule_operator).selectpicker('destroy');
|
|
});
|
|
}, {
|
|
container: 'body',
|
|
style: 'btn-inverse btn-xs',
|
|
width: 'auto',
|
|
showIcon: false
|
|
});
|
|
|
|
|
|
/**
|
|
* @class BtTooltipErrors
|
|
* @memberof module:plugins
|
|
* @description Applies Bootstrap Tooltips on validation error messages.
|
|
* @param {object} [options]
|
|
* @param {string} [options.placement='right']
|
|
* @throws MissingLibraryError
|
|
*/
|
|
QueryBuilder.define('bt-tooltip-errors', function(options) {
|
|
if (!$.fn.tooltip || !$.fn.tooltip.Constructor || !$.fn.tooltip.Constructor.prototype.fixTitle) {
|
|
Utils.error('MissingLibrary', 'Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com');
|
|
}
|
|
|
|
var self = this;
|
|
|
|
// add BT Tooltip data
|
|
this.on('getRuleTemplate.filter getGroupTemplate.filter', function(h) {
|
|
var $h = $(h.value);
|
|
$h.find(QueryBuilder.selectors.error_container).attr('data-toggle', 'tooltip');
|
|
h.value = $h.prop('outerHTML');
|
|
});
|
|
|
|
// init/refresh tooltip when title changes
|
|
this.model.on('update', function(e, node, field) {
|
|
if (field == 'error' && self.settings.display_errors) {
|
|
node.$el.find(QueryBuilder.selectors.error_container).eq(0)
|
|
.tooltip(options)
|
|
.tooltip('hide')
|
|
.tooltip('fixTitle');
|
|
}
|
|
});
|
|
}, {
|
|
placement: 'right'
|
|
});
|
|
|
|
|
|
/**
|
|
* @class ChangeFilters
|
|
* @memberof module:plugins
|
|
* @description Allows to change available filters after plugin initialization.
|
|
*/
|
|
|
|
QueryBuilder.extend(/** @lends module:plugins.ChangeFilters.prototype */ {
|
|
/**
|
|
* Change the filters of the builder
|
|
* @param {boolean} [deleteOrphans=false] - delete rules using old filters
|
|
* @param {QueryBuilder[]} filters
|
|
* @fires module:plugins.ChangeFilters.changer:setFilters
|
|
* @fires module:plugins.ChangeFilters.afterSetFilters
|
|
* @throws ChangeFilterError
|
|
*/
|
|
setFilters: function(deleteOrphans, filters) {
|
|
var self = this;
|
|
|
|
if (filters === undefined) {
|
|
filters = deleteOrphans;
|
|
deleteOrphans = false;
|
|
}
|
|
|
|
filters = this.checkFilters(filters);
|
|
|
|
/**
|
|
* Modifies the filters before {@link module:plugins.ChangeFilters.setFilters} method
|
|
* @event changer:setFilters
|
|
* @memberof module:plugins.ChangeFilters
|
|
* @param {QueryBuilder.Filter[]} filters
|
|
* @returns {QueryBuilder.Filter[]}
|
|
*/
|
|
filters = this.change('setFilters', filters);
|
|
|
|
var filtersIds = filters.map(function(filter) {
|
|
return filter.id;
|
|
});
|
|
|
|
// check for orphans
|
|
if (!deleteOrphans) {
|
|
(function checkOrphans(node) {
|
|
node.each(
|
|
function(rule) {
|
|
if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
|
|
Utils.error('ChangeFilter', 'A rule is using filter "{0}"', rule.filter.id);
|
|
}
|
|
},
|
|
checkOrphans
|
|
);
|
|
}(this.model.root));
|
|
}
|
|
|
|
// replace filters
|
|
this.filters = filters;
|
|
|
|
// apply on existing DOM
|
|
(function updateBuilder(node) {
|
|
node.each(true,
|
|
function(rule) {
|
|
if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
|
|
rule.drop();
|
|
|
|
self.trigger('rulesChanged');
|
|
}
|
|
else {
|
|
self.createRuleFilters(rule);
|
|
|
|
rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
|
|
self.trigger('afterUpdateRuleFilter', rule);
|
|
}
|
|
},
|
|
updateBuilder
|
|
);
|
|
}(this.model.root));
|
|
|
|
// update plugins
|
|
if (this.settings.plugins) {
|
|
if (this.settings.plugins['unique-filter']) {
|
|
this.updateDisabledFilters();
|
|
}
|
|
if (this.settings.plugins['bt-selectpicker']) {
|
|
this.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render');
|
|
}
|
|
}
|
|
|
|
// reset the default_filter if does not exist anymore
|
|
if (this.settings.default_filter) {
|
|
try {
|
|
this.getFilterById(this.settings.default_filter);
|
|
}
|
|
catch (e) {
|
|
this.settings.default_filter = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* After {@link module:plugins.ChangeFilters.setFilters} method
|
|
* @event afterSetFilters
|
|
* @memberof module:plugins.ChangeFilters
|
|
* @param {QueryBuilder.Filter[]} filters
|
|
*/
|
|
this.trigger('afterSetFilters', filters);
|
|
},
|
|
|
|
/**
|
|
* Adds a new filter to the builder
|
|
* @param {QueryBuilder.Filter|Filter[]} newFilters
|
|
* @param {int|string} [position=#end] - index or '#start' or '#end'
|
|
* @fires module:plugins.ChangeFilters.changer:setFilters
|
|
* @fires module:plugins.ChangeFilters.afterSetFilters
|
|
* @throws ChangeFilterError
|
|
*/
|
|
addFilter: function(newFilters, position) {
|
|
if (position === undefined || position == '#end') {
|
|
position = this.filters.length;
|
|
}
|
|
else if (position == '#start') {
|
|
position = 0;
|
|
}
|
|
|
|
if (!$.isArray(newFilters)) {
|
|
newFilters = [newFilters];
|
|
}
|
|
|
|
var filters = $.extend(true, [], this.filters);
|
|
|
|
// numeric position
|
|
if (parseInt(position) == position) {
|
|
Array.prototype.splice.apply(filters, [position, 0].concat(newFilters));
|
|
}
|
|
else {
|
|
// after filter by its id
|
|
if (this.filters.some(function(filter, index) {
|
|
if (filter.id == position) {
|
|
position = index + 1;
|
|
return true;
|
|
}
|
|
})
|
|
) {
|
|
Array.prototype.splice.apply(filters, [position, 0].concat(newFilters));
|
|
}
|
|
// defaults to end of list
|
|
else {
|
|
Array.prototype.push.apply(filters, newFilters);
|
|
}
|
|
}
|
|
|
|
this.setFilters(filters);
|
|
},
|
|
|
|
/**
|
|
* Removes a filter from the builder
|
|
* @param {string|string[]} filterIds
|
|
* @param {boolean} [deleteOrphans=false] delete rules using old filters
|
|
* @fires module:plugins.ChangeFilters.changer:setFilters
|
|
* @fires module:plugins.ChangeFilters.afterSetFilters
|
|
* @throws ChangeFilterError
|
|
*/
|
|
removeFilter: function(filterIds, deleteOrphans) {
|
|
var filters = $.extend(true, [], this.filters);
|
|
if (typeof filterIds === 'string') {
|
|
filterIds = [filterIds];
|
|
}
|
|
|
|
filters = filters.filter(function(filter) {
|
|
return filterIds.indexOf(filter.id) === -1;
|
|
});
|
|
|
|
this.setFilters(deleteOrphans, filters);
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* @class ChosenSelectpicker
|
|
* @memberof module:plugins
|
|
* @descriptioon Applies chosen-js Select on filters and operators combo-boxes.
|
|
* @param {object} [options] Supports all the options for chosen
|
|
* @throws MissingLibraryError
|
|
*/
|
|
QueryBuilder.define('chosen-selectpicker', function(options) {
|
|
|
|
if (!$.fn.chosen) {
|
|
Utils.error('MissingLibrary', 'chosen is required to use "chosen-selectpicker" plugin. Get it here: https://github.com/harvesthq/chosen');
|
|
}
|
|
|
|
if (this.settings.plugins['bt-selectpicker']) {
|
|
Utils.error('Conflict', 'bt-selectpicker is already selected as the dropdown plugin. Please remove chosen-selectpicker from the plugin list');
|
|
}
|
|
|
|
var Selectors = QueryBuilder.selectors;
|
|
|
|
// init selectpicker
|
|
this.on('afterCreateRuleFilters', function(e, rule) {
|
|
rule.$el.find(Selectors.rule_filter).removeClass('form-control').chosen(options);
|
|
});
|
|
|
|
this.on('afterCreateRuleOperators', function(e, rule) {
|
|
rule.$el.find(Selectors.rule_operator).removeClass('form-control').chosen(options);
|
|
});
|
|
|
|
// update selectpicker on change
|
|
this.on('afterUpdateRuleFilter', function(e, rule) {
|
|
rule.$el.find(Selectors.rule_filter).trigger('chosen:updated');
|
|
});
|
|
|
|
this.on('afterUpdateRuleOperator', function(e, rule) {
|
|
rule.$el.find(Selectors.rule_operator).trigger('chosen:updated');
|
|
});
|
|
|
|
this.on('beforeDeleteRule', function(e, rule) {
|
|
rule.$el.find(Selectors.rule_filter).chosen('destroy');
|
|
rule.$el.find(Selectors.rule_operator).chosen('destroy');
|
|
});
|
|
});
|
|
|
|
|
|
/**
|
|
* @class FilterDescription
|
|
* @memberof module:plugins
|
|
* @description Provides three ways to display a description about a filter: inline, Bootsrap Popover or Bootbox.
|
|
* @param {object} [options]
|
|
* @param {string} [options.icon='glyphicon glyphicon-info-sign']
|
|
* @param {string} [options.mode='popover'] - inline, popover or bootbox
|
|
* @throws ConfigError
|
|
*/
|
|
QueryBuilder.define('filter-description', function(options) {
|
|
// INLINE
|
|
if (options.mode === 'inline') {
|
|
this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
|
|
var $p = rule.$el.find('p.filter-description');
|
|
var description = e.builder.getFilterDescription(rule.filter, rule);
|
|
|
|
if (!description) {
|
|
$p.hide();
|
|
}
|
|
else {
|
|
if ($p.length === 0) {
|
|
$p = $('<p class="filter-description"></p>');
|
|
$p.appendTo(rule.$el);
|
|
}
|
|
else {
|
|
$p.css('display', '');
|
|
}
|
|
|
|
$p.html('<i class="' + options.icon + '"></i> ' + description);
|
|
}
|
|
});
|
|
}
|
|
// POPOVER
|
|
else if (options.mode === 'popover') {
|
|
if (!$.fn.popover || !$.fn.popover.Constructor || !$.fn.popover.Constructor.prototype.fixTitle) {
|
|
Utils.error('MissingLibrary', 'Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com');
|
|
}
|
|
|
|
this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
|
|
var $b = rule.$el.find('button.filter-description');
|
|
var description = e.builder.getFilterDescription(rule.filter, rule);
|
|
|
|
if (!description) {
|
|
$b.hide();
|
|
|
|
if ($b.data('bs.popover')) {
|
|
$b.popover('hide');
|
|
}
|
|
}
|
|
else {
|
|
if ($b.length === 0) {
|
|
$b = $('<button type="button" class="btn btn-xs btn-info filter-description" data-toggle="popover"><i class="' + options.icon + '"></i></button>');
|
|
$b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions));
|
|
|
|
$b.popover({
|
|
placement: 'left',
|
|
container: 'body',
|
|
html: true
|
|
});
|
|
|
|
$b.on('mouseout', function() {
|
|
$b.popover('hide');
|
|
});
|
|
}
|
|
else {
|
|
$b.css('display', '');
|
|
}
|
|
|
|
$b.data('bs.popover').options.content = description;
|
|
|
|
if ($b.attr('aria-describedby')) {
|
|
$b.popover('show');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
// BOOTBOX
|
|
else if (options.mode === 'bootbox') {
|
|
if (!('bootbox' in window)) {
|
|
Utils.error('MissingLibrary', 'Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com');
|
|
}
|
|
|
|
this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
|
|
var $b = rule.$el.find('button.filter-description');
|
|
var description = e.builder.getFilterDescription(rule.filter, rule);
|
|
|
|
if (!description) {
|
|
$b.hide();
|
|
}
|
|
else {
|
|
if ($b.length === 0) {
|
|
$b = $('<button type="button" class="btn btn-xs btn-info filter-description" data-toggle="bootbox"><i class="' + options.icon + '"></i></button>');
|
|
$b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions));
|
|
|
|
$b.on('click', function() {
|
|
bootbox.alert($b.data('description'));
|
|
});
|
|
}
|
|
else {
|
|
$b.css('display', '');
|
|
}
|
|
|
|
$b.data('description', description);
|
|
}
|
|
});
|
|
}
|
|
}, {
|
|
icon: 'glyphicon glyphicon-info-sign',
|
|
mode: 'popover'
|
|
});
|
|
|
|
QueryBuilder.extend(/** @lends module:plugins.FilterDescription.prototype */ {
|
|
/**
|
|
* Returns the description of a filter for a particular rule (if present)
|
|
* @param {object} filter
|
|
* @param {Rule} [rule]
|
|
* @returns {string}
|
|
* @private
|
|
*/
|
|
getFilterDescription: function(filter, rule) {
|
|
if (!filter) {
|
|
return undefined;
|
|
}
|
|
else if (typeof filter.description == 'function') {
|
|
return filter.description.call(this, rule);
|
|
}
|
|
else {
|
|
return filter.description;
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* @class Invert
|
|
* @memberof module:plugins
|
|
* @description Allows to invert a rule operator, a group condition or the entire builder.
|
|
* @param {object} [options]
|
|
* @param {string} [options.icon='glyphicon glyphicon-random']
|
|
* @param {boolean} [options.recursive=true]
|
|
* @param {boolean} [options.invert_rules=true]
|
|
* @param {boolean} [options.display_rules_button=false]
|
|
* @param {boolean} [options.silent_fail=false]
|
|
*/
|
|
QueryBuilder.define('invert', function(options) {
|
|
var self = this;
|
|
var Selectors = QueryBuilder.selectors;
|
|
|
|
// Bind events
|
|
this.on('afterInit', function() {
|
|
self.$el.on('click.queryBuilder', '[data-invert=group]', function() {
|
|
var $group = $(this).closest(Selectors.group_container);
|
|
self.invert(self.getModel($group), options);
|
|
});
|
|
|
|
if (options.display_rules_button && options.invert_rules) {
|
|
self.$el.on('click.queryBuilder', '[data-invert=rule]', function() {
|
|
var $rule = $(this).closest(Selectors.rule_container);
|
|
self.invert(self.getModel($rule), options);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Modify templates
|
|
if (!options.disable_template) {
|
|
this.on('getGroupTemplate.filter', function(h) {
|
|
var $h = $(h.value);
|
|
$h.find(Selectors.condition_container).after(
|
|
'<button type="button" class="btn btn-xs btn-default" data-invert="group">' +
|
|
'<i class="' + options.icon + '"></i> ' + self.translate('invert') +
|
|
'</button>'
|
|
);
|
|
h.value = $h.prop('outerHTML');
|
|
});
|
|
|
|
if (options.display_rules_button && options.invert_rules) {
|
|
this.on('getRuleTemplate.filter', function(h) {
|
|
var $h = $(h.value);
|
|
$h.find(Selectors.rule_actions).prepend(
|
|
'<button type="button" class="btn btn-xs btn-default" data-invert="rule">' +
|
|
'<i class="' + options.icon + '"></i> ' + self.translate('invert') +
|
|
'</button>'
|
|
);
|
|
h.value = $h.prop('outerHTML');
|
|
});
|
|
}
|
|
}
|
|
}, {
|
|
icon: 'glyphicon glyphicon-random',
|
|
recursive: true,
|
|
invert_rules: true,
|
|
display_rules_button: false,
|
|
silent_fail: false,
|
|
disable_template: false
|
|
});
|
|
|
|
QueryBuilder.defaults({
|
|
operatorOpposites: {
|
|
'equal': 'not_equal',
|
|
'not_equal': 'equal',
|
|
'in': 'not_in',
|
|
'not_in': 'in',
|
|
'less': 'greater_or_equal',
|
|
'less_or_equal': 'greater',
|
|
'greater': 'less_or_equal',
|
|
'greater_or_equal': 'less',
|
|
'between': 'not_between',
|
|
'not_between': 'between',
|
|
'begins_with': 'not_begins_with',
|
|
'not_begins_with': 'begins_with',
|
|
'contains': 'not_contains',
|
|
'not_contains': 'contains',
|
|
'ends_with': 'not_ends_with',
|
|
'not_ends_with': 'ends_with',
|
|
'is_empty': 'is_not_empty',
|
|
'is_not_empty': 'is_empty',
|
|
'is_null': 'is_not_null',
|
|
'is_not_null': 'is_null'
|
|
},
|
|
|
|
conditionOpposites: {
|
|
'AND': 'OR',
|
|
'OR': 'AND'
|
|
}
|
|
});
|
|
|
|
QueryBuilder.extend(/** @lends module:plugins.Invert.prototype */ {
|
|
/**
|
|
* Invert a Group, a Rule or the whole builder
|
|
* @param {Node} [node]
|
|
* @param {object} [options] {@link module:plugins.Invert}
|
|
* @fires module:plugins.Invert.afterInvert
|
|
* @throws InvertConditionError, InvertOperatorError
|
|
*/
|
|
invert: function(node, options) {
|
|
if (!(node instanceof Node)) {
|
|
if (!this.model.root) return;
|
|
options = node;
|
|
node = this.model.root;
|
|
}
|
|
|
|
if (typeof options != 'object') options = {};
|
|
if (options.recursive === undefined) options.recursive = true;
|
|
if (options.invert_rules === undefined) options.invert_rules = true;
|
|
if (options.silent_fail === undefined) options.silent_fail = false;
|
|
if (options.trigger === undefined) options.trigger = true;
|
|
|
|
if (node instanceof Group) {
|
|
// invert group condition
|
|
if (this.settings.conditionOpposites[node.condition]) {
|
|
node.condition = this.settings.conditionOpposites[node.condition];
|
|
}
|
|
else if (!options.silent_fail) {
|
|
Utils.error('InvertCondition', 'Unknown inverse of condition "{0}"', node.condition);
|
|
}
|
|
|
|
// recursive call
|
|
if (options.recursive) {
|
|
var tempOpts = $.extend({}, options, { trigger: false });
|
|
node.each(function(rule) {
|
|
if (options.invert_rules) {
|
|
this.invert(rule, tempOpts);
|
|
}
|
|
}, function(group) {
|
|
this.invert(group, tempOpts);
|
|
}, this);
|
|
}
|
|
}
|
|
else if (node instanceof Rule) {
|
|
if (node.operator && !node.filter.no_invert) {
|
|
// invert rule operator
|
|
if (this.settings.operatorOpposites[node.operator.type]) {
|
|
var invert = this.settings.operatorOpposites[node.operator.type];
|
|
// check if the invert is "authorized"
|
|
if (!node.filter.operators || node.filter.operators.indexOf(invert) != -1) {
|
|
node.operator = this.getOperatorByType(invert);
|
|
}
|
|
}
|
|
else if (!options.silent_fail) {
|
|
Utils.error('InvertOperator', 'Unknown inverse of operator "{0}"', node.operator.type);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (options.trigger) {
|
|
/**
|
|
* After {@link module:plugins.Invert.invert} method
|
|
* @event afterInvert
|
|
* @memberof module:plugins.Invert
|
|
* @param {Node} node - the main group or rule that has been modified
|
|
* @param {object} options
|
|
*/
|
|
this.trigger('afterInvert', node, options);
|
|
|
|
this.trigger('rulesChanged');
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* @class MongoDbSupport
|
|
* @memberof module:plugins
|
|
* @description Allows to export rules as a MongoDB find object as well as populating the builder from a MongoDB object.
|
|
*/
|
|
|
|
QueryBuilder.defaults({
|
|
mongoOperators: {
|
|
// @formatter:off
|
|
equal: function(v) { return v[0]; },
|
|
not_equal: function(v) { return { '$ne': v[0] }; },
|
|
in: function(v) { return { '$in': v }; },
|
|
not_in: function(v) { return { '$nin': v }; },
|
|
less: function(v) { return { '$lt': v[0] }; },
|
|
less_or_equal: function(v) { return { '$lte': v[0] }; },
|
|
greater: function(v) { return { '$gt': v[0] }; },
|
|
greater_or_equal: function(v) { return { '$gte': v[0] }; },
|
|
between: function(v) { return { '$gte': v[0], '$lte': v[1] }; },
|
|
not_between: function(v) { return { '$lt': v[0], '$gt': v[1] }; },
|
|
begins_with: function(v) { return { '$regex': '^' + Utils.escapeRegExp(v[0]) }; },
|
|
not_begins_with: function(v) { return { '$regex': '^(?!' + Utils.escapeRegExp(v[0]) + ')' }; },
|
|
contains: function(v) { return { '$regex': Utils.escapeRegExp(v[0]) }; },
|
|
not_contains: function(v) { return { '$regex': '^((?!' + Utils.escapeRegExp(v[0]) + ').)*$', '$options': 's' }; },
|
|
ends_with: function(v) { return { '$regex': Utils.escapeRegExp(v[0]) + '$' }; },
|
|
not_ends_with: function(v) { return { '$regex': '(?<!' + Utils.escapeRegExp(v[0]) + ')$' }; },
|
|
is_empty: function(v) { return ''; },
|
|
is_not_empty: function(v) { return { '$ne': '' }; },
|
|
is_null: function(v) { return null; },
|
|
is_not_null: function(v) { return { '$ne': null }; }
|
|
// @formatter:on
|
|
},
|
|
|
|
mongoRuleOperators: {
|
|
$eq: function(v) {
|
|
return {
|
|
'val': v,
|
|
'op': v === null ? 'is_null' : (v === '' ? 'is_empty' : 'equal')
|
|
};
|
|
},
|
|
$ne: function(v) {
|
|
v = v.$ne;
|
|
return {
|
|
'val': v,
|
|
'op': v === null ? 'is_not_null' : (v === '' ? 'is_not_empty' : 'not_equal')
|
|
};
|
|
},
|
|
$regex: function(v) {
|
|
v = v.$regex;
|
|
if (v.slice(0, 4) == '^(?!' && v.slice(-1) == ')') {
|
|
return { 'val': v.slice(4, -1), 'op': 'not_begins_with' };
|
|
}
|
|
else if (v.slice(0, 5) == '^((?!' && v.slice(-5) == ').)*$') {
|
|
return { 'val': v.slice(5, -5), 'op': 'not_contains' };
|
|
}
|
|
else if (v.slice(0, 4) == '(?<!' && v.slice(-2) == ')$') {
|
|
return { 'val': v.slice(4, -2), 'op': 'not_ends_with' };
|
|
}
|
|
else if (v.slice(-1) == '$') {
|
|
return { 'val': v.slice(0, -1), 'op': 'ends_with' };
|
|
}
|
|
else if (v.slice(0, 1) == '^') {
|
|
return { 'val': v.slice(1), 'op': 'begins_with' };
|
|
}
|
|
else {
|
|
return { 'val': v, 'op': 'contains' };
|
|
}
|
|
},
|
|
between: function(v) {
|
|
return { 'val': [v.$gte, v.$lte], 'op': 'between' };
|
|
},
|
|
not_between: function(v) {
|
|
return { 'val': [v.$lt, v.$gt], 'op': 'not_between' };
|
|
},
|
|
$in: function(v) {
|
|
return { 'val': v.$in, 'op': 'in' };
|
|
},
|
|
$nin: function(v) {
|
|
return { 'val': v.$nin, 'op': 'not_in' };
|
|
},
|
|
$lt: function(v) {
|
|
return { 'val': v.$lt, 'op': 'less' };
|
|
},
|
|
$lte: function(v) {
|
|
return { 'val': v.$lte, 'op': 'less_or_equal' };
|
|
},
|
|
$gt: function(v) {
|
|
return { 'val': v.$gt, 'op': 'greater' };
|
|
},
|
|
$gte: function(v) {
|
|
return { 'val': v.$gte, 'op': 'greater_or_equal' };
|
|
}
|
|
}
|
|
});
|
|
|
|
QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ {
|
|
/**
|
|
* Returns rules as a MongoDB query
|
|
* @param {object} [data] - current rules by default
|
|
* @returns {object}
|
|
* @fires module:plugins.MongoDbSupport.changer:getMongoDBField
|
|
* @fires module:plugins.MongoDbSupport.changer:ruleToMongo
|
|
* @fires module:plugins.MongoDbSupport.changer:groupToMongo
|
|
* @throws UndefinedMongoConditionError, UndefinedMongoOperatorError
|
|
*/
|
|
getMongo: function(data) {
|
|
data = (data === undefined) ? this.getRules() : data;
|
|
|
|
if (!data) {
|
|
return null;
|
|
}
|
|
|
|
var self = this;
|
|
|
|
return (function parse(group) {
|
|
if (!group.condition) {
|
|
group.condition = self.settings.default_condition;
|
|
}
|
|
if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) {
|
|
Utils.error('UndefinedMongoCondition', 'Unable to build MongoDB query with condition "{0}"', group.condition);
|
|
}
|
|
|
|
if (!group.rules) {
|
|
return {};
|
|
}
|
|
|
|
var parts = [];
|
|
|
|
group.rules.forEach(function(rule) {
|
|
if (rule.rules && rule.rules.length > 0) {
|
|
parts.push(parse(rule));
|
|
}
|
|
else {
|
|
var mdb = self.settings.mongoOperators[rule.operator];
|
|
var ope = self.getOperatorByType(rule.operator);
|
|
|
|
if (mdb === undefined) {
|
|
Utils.error('UndefinedMongoOperator', 'Unknown MongoDB operation for operator "{0}"', rule.operator);
|
|
}
|
|
|
|
if (ope.nb_inputs !== 0) {
|
|
if (!(rule.value instanceof Array)) {
|
|
rule.value = [rule.value];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Modifies the MongoDB field used by a rule
|
|
* @event changer:getMongoDBField
|
|
* @memberof module:plugins.MongoDbSupport
|
|
* @param {string} field
|
|
* @param {Rule} rule
|
|
* @returns {string}
|
|
*/
|
|
var field = self.change('getMongoDBField', rule.field, rule);
|
|
|
|
var ruleExpression = {};
|
|
ruleExpression[field] = mdb.call(self, rule.value);
|
|
|
|
/**
|
|
* Modifies the MongoDB expression generated for a rul
|
|
* @event changer:ruleToMongo
|
|
* @memberof module:plugins.MongoDbSupport
|
|
* @param {object} expression
|
|
* @param {Rule} rule
|
|
* @param {*} value
|
|
* @param {function} valueWrapper - function that takes the value and adds the operator
|
|
* @returns {object}
|
|
*/
|
|
parts.push(self.change('ruleToMongo', ruleExpression, rule, rule.value, mdb));
|
|
}
|
|
});
|
|
|
|
var groupExpression = {};
|
|
groupExpression['$' + group.condition.toLowerCase()] = parts;
|
|
|
|
/**
|
|
* Modifies the MongoDB expression generated for a group
|
|
* @event changer:groupToMongo
|
|
* @memberof module:plugins.MongoDbSupport
|
|
* @param {object} expression
|
|
* @param {Group} group
|
|
* @returns {object}
|
|
*/
|
|
return self.change('groupToMongo', groupExpression, group);
|
|
}(data));
|
|
},
|
|
|
|
/**
|
|
* Converts a MongoDB query to rules
|
|
* @param {object} query
|
|
* @returns {object}
|
|
* @fires module:plugins.MongoDbSupport.changer:parseMongoNode
|
|
* @fires module:plugins.MongoDbSupport.changer:getMongoDBFieldID
|
|
* @fires module:plugins.MongoDbSupport.changer:mongoToRule
|
|
* @fires module:plugins.MongoDbSupport.changer:mongoToGroup
|
|
* @throws MongoParseError, UndefinedMongoConditionError, UndefinedMongoOperatorError
|
|
*/
|
|
getRulesFromMongo: function(query) {
|
|
if (query === undefined || query === null) {
|
|
return null;
|
|
}
|
|
|
|
var self = this;
|
|
|
|
/**
|
|
* Custom parsing of a MongoDB expression, you can return a sub-part of the expression, or a well formed group or rule JSON
|
|
* @event changer:parseMongoNode
|
|
* @memberof module:plugins.MongoDbSupport
|
|
* @param {object} expression
|
|
* @returns {object} expression, rule or group
|
|
*/
|
|
query = self.change('parseMongoNode', query);
|
|
|
|
// a plugin returned a group
|
|
if ('rules' in query && 'condition' in query) {
|
|
return query;
|
|
}
|
|
|
|
// a plugin returned a rule
|
|
if ('id' in query && 'operator' in query && 'value' in query) {
|
|
return {
|
|
condition: this.settings.default_condition,
|
|
rules: [query]
|
|
};
|
|
}
|
|
|
|
var key = self.getMongoCondition(query);
|
|
if (!key) {
|
|
Utils.error('MongoParse', 'Invalid MongoDB query format');
|
|
}
|
|
|
|
return (function parse(data, topKey) {
|
|
var rules = data[topKey];
|
|
var parts = [];
|
|
|
|
rules.forEach(function(data) {
|
|
// allow plugins to manually parse or handle special cases
|
|
data = self.change('parseMongoNode', data);
|
|
|
|
// a plugin returned a group
|
|
if ('rules' in data && 'condition' in data) {
|
|
parts.push(data);
|
|
return;
|
|
}
|
|
|
|
// a plugin returned a rule
|
|
if ('id' in data && 'operator' in data && 'value' in data) {
|
|
parts.push(data);
|
|
return;
|
|
}
|
|
|
|
var key = self.getMongoCondition(data);
|
|
if (key) {
|
|
parts.push(parse(data, key));
|
|
}
|
|
else {
|
|
var field = Object.keys(data)[0];
|
|
var value = data[field];
|
|
|
|
var operator = self.getMongoOperator(value);
|
|
if (operator === undefined) {
|
|
Utils.error('MongoParse', 'Invalid MongoDB query format');
|
|
}
|
|
|
|
var mdbrl = self.settings.mongoRuleOperators[operator];
|
|
if (mdbrl === undefined) {
|
|
Utils.error('UndefinedMongoOperator', 'JSON Rule operation unknown for operator "{0}"', operator);
|
|
}
|
|
|
|
var opVal = mdbrl.call(self, value);
|
|
|
|
var id = self.getMongoDBFieldID(field, value);
|
|
|
|
/**
|
|
* Modifies the rule generated from the MongoDB expression
|
|
* @event changer:mongoToRule
|
|
* @memberof module:plugins.MongoDbSupport
|
|
* @param {object} rule
|
|
* @param {object} expression
|
|
* @returns {object}
|
|
*/
|
|
var rule = self.change('mongoToRule', {
|
|
id: id,
|
|
field: field,
|
|
operator: opVal.op,
|
|
value: opVal.val
|
|
}, data);
|
|
|
|
parts.push(rule);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Modifies the group generated from the MongoDB expression
|
|
* @event changer:mongoToGroup
|
|
* @memberof module:plugins.MongoDbSupport
|
|
* @param {object} group
|
|
* @param {object} expression
|
|
* @returns {object}
|
|
*/
|
|
return self.change('mongoToGroup', {
|
|
condition: topKey.replace('$', '').toUpperCase(),
|
|
rules: parts
|
|
}, data);
|
|
}(query, key));
|
|
},
|
|
|
|
/**
|
|
* Sets rules a from MongoDB query
|
|
* @see module:plugins.MongoDbSupport.getRulesFromMongo
|
|
*/
|
|
setRulesFromMongo: function(query) {
|
|
this.setRules(this.getRulesFromMongo(query));
|
|
},
|
|
|
|
/**
|
|
* Returns a filter identifier from the MongoDB field.
|
|
* Automatically use the only one filter with a matching field, fires a changer otherwise.
|
|
* @param {string} field
|
|
* @param {*} value
|
|
* @fires module:plugins.MongoDbSupport:changer:getMongoDBFieldID
|
|
* @returns {string}
|
|
* @private
|
|
*/
|
|
getMongoDBFieldID: function(field, value) {
|
|
var matchingFilters = this.filters.filter(function(filter) {
|
|
return filter.field === field;
|
|
});
|
|
|
|
var id;
|
|
if (matchingFilters.length === 1) {
|
|
id = matchingFilters[0].id;
|
|
}
|
|
else {
|
|
/**
|
|
* Returns a filter identifier from the MongoDB field
|
|
* @event changer:getMongoDBFieldID
|
|
* @memberof module:plugins.MongoDbSupport
|
|
* @param {string} field
|
|
* @param {*} value
|
|
* @returns {string}
|
|
*/
|
|
id = this.change('getMongoDBFieldID', field, value);
|
|
}
|
|
|
|
return id;
|
|
},
|
|
|
|
/**
|
|
* Finds which operator is used in a MongoDB sub-object
|
|
* @param {*} data
|
|
* @returns {string|undefined}
|
|
* @private
|
|
*/
|
|
getMongoOperator: function(data) {
|
|
if (data !== null && typeof data === 'object') {
|
|
if (data.$gte !== undefined && data.$lte !== undefined) {
|
|
return 'between';
|
|
}
|
|
if (data.$lt !== undefined && data.$gt !== undefined) {
|
|
return 'not_between';
|
|
}
|
|
|
|
var knownKeys = Object.keys(data).filter(function(key) {
|
|
return !!this.settings.mongoRuleOperators[key];
|
|
}.bind(this));
|
|
|
|
if (knownKeys.length === 1) {
|
|
return knownKeys[0];
|
|
}
|
|
}
|
|
else {
|
|
return '$eq';
|
|
}
|
|
},
|
|
|
|
|
|
/**
|
|
* Returns the key corresponding to "$or" or "$and"
|
|
* @param {object} data
|
|
* @returns {string|undefined}
|
|
* @private
|
|
*/
|
|
getMongoCondition: function(data) {
|
|
var keys = Object.keys(data);
|
|
|
|
for (var i = 0, l = keys.length; i < l; i++) {
|
|
if (keys[i].toLowerCase() === '$or' || keys[i].toLowerCase() === '$and') {
|
|
return keys[i];
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* @class NotGroup
|
|
* @memberof module:plugins
|
|
* @description Adds a "Not" checkbox in front of group conditions.
|
|
* @param {object} [options]
|
|
* @param {string} [options.icon_checked='glyphicon glyphicon-checked']
|
|
* @param {string} [options.icon_unchecked='glyphicon glyphicon-unchecked']
|
|
*/
|
|
QueryBuilder.define('not-group', function(options) {
|
|
var self = this;
|
|
|
|
// Bind events
|
|
this.on('afterInit', function() {
|
|
self.$el.on('click.queryBuilder', '[data-not=group]', function() {
|
|
var $group = $(this).closest(QueryBuilder.selectors.group_container);
|
|
var group = self.getModel($group);
|
|
group.not = !group.not;
|
|
});
|
|
|
|
self.model.on('update', function(e, node, field) {
|
|
if (node instanceof Group && field === 'not') {
|
|
self.updateGroupNot(node);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Init "not" property
|
|
this.on('afterAddGroup', function(e, group) {
|
|
group.__.not = false;
|
|
});
|
|
|
|
// Modify templates
|
|
if (!options.disable_template) {
|
|
this.on('getGroupTemplate.filter', function(h) {
|
|
var $h = $(h.value);
|
|
$h.find(QueryBuilder.selectors.condition_container).prepend(
|
|
'<button type="button" class="btn btn-xs btn-default" data-not="group">' +
|
|
'<i class="' + options.icon_unchecked + '"></i> ' + self.translate('NOT') +
|
|
'</button>'
|
|
);
|
|
h.value = $h.prop('outerHTML');
|
|
});
|
|
}
|
|
|
|
// Export "not" to JSON
|
|
this.on('groupToJson.filter', function(e, group) {
|
|
e.value.not = group.not;
|
|
});
|
|
|
|
// Read "not" from JSON
|
|
this.on('jsonToGroup.filter', function(e, json) {
|
|
e.value.not = !!json.not;
|
|
});
|
|
|
|
// Export "not" to SQL
|
|
this.on('groupToSQL.filter', function(e, group) {
|
|
if (group.not) {
|
|
e.value = 'NOT ( ' + e.value + ' )';
|
|
}
|
|
});
|
|
|
|
// Parse "NOT" function from sqlparser
|
|
this.on('parseSQLNode.filter', function(e) {
|
|
if (e.value.name && e.value.name.toUpperCase() == 'NOT') {
|
|
e.value = e.value.arguments.value[0];
|
|
|
|
// if the there is no sub-group, create one
|
|
if (['AND', 'OR'].indexOf(e.value.operation.toUpperCase()) === -1) {
|
|
e.value = new SQLParser.nodes.Op(
|
|
self.settings.default_condition,
|
|
e.value,
|
|
null
|
|
);
|
|
}
|
|
|
|
e.value.not = true;
|
|
}
|
|
});
|
|
|
|
// Request to create sub-group if the "not" flag is set
|
|
this.on('sqlGroupsDistinct.filter', function(e, group, data, i) {
|
|
if (data.not && i > 0) {
|
|
e.value = true;
|
|
}
|
|
});
|
|
|
|
// Read "not" from parsed SQL
|
|
this.on('sqlToGroup.filter', function(e, data) {
|
|
e.value.not = !!data.not;
|
|
});
|
|
|
|
// Export "not" to Mongo
|
|
this.on('groupToMongo.filter', function(e, group) {
|
|
var key = '$' + group.condition.toLowerCase();
|
|
if (group.not && e.value[key]) {
|
|
e.value = { '$nor': [e.value] };
|
|
}
|
|
});
|
|
|
|
// Parse "$nor" operator from Mongo
|
|
this.on('parseMongoNode.filter', function(e) {
|
|
var keys = Object.keys(e.value);
|
|
|
|
if (keys[0] == '$nor') {
|
|
e.value = e.value[keys[0]][0];
|
|
e.value.not = true;
|
|
}
|
|
});
|
|
|
|
// Read "not" from parsed Mongo
|
|
this.on('mongoToGroup.filter', function(e, data) {
|
|
e.value.not = !!data.not;
|
|
});
|
|
}, {
|
|
icon_unchecked: 'glyphicon glyphicon-unchecked',
|
|
icon_checked: 'glyphicon glyphicon-check',
|
|
disable_template: false
|
|
});
|
|
|
|
/**
|
|
* From {@link module:plugins.NotGroup}
|
|
* @name not
|
|
* @member {boolean}
|
|
* @memberof Group
|
|
* @instance
|
|
*/
|
|
Utils.defineModelProperties(Group, ['not']);
|
|
|
|
QueryBuilder.selectors.group_not = QueryBuilder.selectors.group_header + ' [data-not=group]';
|
|
|
|
QueryBuilder.extend(/** @lends module:plugins.NotGroup.prototype */ {
|
|
/**
|
|
* Performs actions when a group's not changes
|
|
* @param {Group} group
|
|
* @fires module:plugins.NotGroup.afterUpdateGroupNot
|
|
* @private
|
|
*/
|
|
updateGroupNot: function(group) {
|
|
var options = this.plugins['not-group'];
|
|
group.$el.find('>' + QueryBuilder.selectors.group_not)
|
|
.toggleClass('active', group.not)
|
|
.find('i').attr('class', group.not ? options.icon_checked : options.icon_unchecked);
|
|
|
|
/**
|
|
* After the group's not flag has been modified
|
|
* @event afterUpdateGroupNot
|
|
* @memberof module:plugins.NotGroup
|
|
* @param {Group} group
|
|
*/
|
|
this.trigger('afterUpdateGroupNot', group);
|
|
|
|
this.trigger('rulesChanged');
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* @class Sortable
|
|
* @memberof module:plugins
|
|
* @description Enables drag & drop sort of rules.
|
|
* @param {object} [options]
|
|
* @param {boolean} [options.inherit_no_drop=true]
|
|
* @param {boolean} [options.inherit_no_sortable=true]
|
|
* @param {string} [options.icon='glyphicon glyphicon-sort']
|
|
* @throws MissingLibraryError, ConfigError
|
|
*/
|
|
QueryBuilder.define('sortable', function(options) {
|
|
if (!('interact' in window)) {
|
|
Utils.error('MissingLibrary', 'interact.js is required to use "sortable" plugin. Get it here: http://interactjs.io');
|
|
}
|
|
|
|
if (options.default_no_sortable !== undefined) {
|
|
Utils.error(false, 'Config', 'Sortable plugin : "default_no_sortable" options is deprecated, use standard "default_rule_flags" and "default_group_flags" instead');
|
|
this.settings.default_rule_flags.no_sortable = this.settings.default_group_flags.no_sortable = options.default_no_sortable;
|
|
}
|
|
|
|
// recompute drop-zones during drag (when a rule is hidden)
|
|
interact.dynamicDrop(true);
|
|
|
|
// set move threshold to 10px
|
|
interact.pointerMoveTolerance(10);
|
|
|
|
var placeholder;
|
|
var ghost;
|
|
var src;
|
|
var moved;
|
|
|
|
// Init drag and drop
|
|
this.on('afterAddRule afterAddGroup', function(e, node) {
|
|
if (node == placeholder) {
|
|
return;
|
|
}
|
|
|
|
var self = e.builder;
|
|
|
|
// Inherit flags
|
|
if (options.inherit_no_sortable && node.parent && node.parent.flags.no_sortable) {
|
|
node.flags.no_sortable = true;
|
|
}
|
|
if (options.inherit_no_drop && node.parent && node.parent.flags.no_drop) {
|
|
node.flags.no_drop = true;
|
|
}
|
|
|
|
// Configure drag
|
|
if (!node.flags.no_sortable) {
|
|
interact(node.$el[0])
|
|
.draggable({
|
|
allowFrom: QueryBuilder.selectors.drag_handle,
|
|
onstart: function(event) {
|
|
moved = false;
|
|
|
|
// get model of dragged element
|
|
src = self.getModel(event.target);
|
|
|
|
// create ghost
|
|
ghost = src.$el.clone()
|
|
.appendTo(src.$el.parent())
|
|
.width(src.$el.outerWidth())
|
|
.addClass('dragging');
|
|
|
|
// create drop placeholder
|
|
var ph = $('<div class="rule-placeholder"> </div>')
|
|
.height(src.$el.outerHeight());
|
|
|
|
placeholder = src.parent.addRule(ph, src.getPos());
|
|
|
|
// hide dragged element
|
|
src.$el.hide();
|
|
},
|
|
onmove: function(event) {
|
|
// make the ghost follow the cursor
|
|
ghost[0].style.top = event.clientY - 15 + 'px';
|
|
ghost[0].style.left = event.clientX - 15 + 'px';
|
|
},
|
|
onend: function(event) {
|
|
// starting from Interact 1.3.3, onend is called before ondrop
|
|
if (event.dropzone) {
|
|
moveSortableToTarget(src, $(event.relatedTarget), self);
|
|
moved = true;
|
|
}
|
|
|
|
// remove ghost
|
|
ghost.remove();
|
|
ghost = undefined;
|
|
|
|
// remove placeholder
|
|
placeholder.drop();
|
|
placeholder = undefined;
|
|
|
|
// show element
|
|
src.$el.css('display', '');
|
|
|
|
/**
|
|
* After a node has been moved with {@link module:plugins.Sortable}
|
|
* @event afterMove
|
|
* @memberof module:plugins.Sortable
|
|
* @param {Node} node
|
|
*/
|
|
self.trigger('afterMove', src);
|
|
|
|
self.trigger('rulesChanged');
|
|
}
|
|
});
|
|
}
|
|
|
|
if (!node.flags.no_drop) {
|
|
// Configure drop on groups and rules
|
|
interact(node.$el[0])
|
|
.dropzone({
|
|
accept: QueryBuilder.selectors.rule_and_group_containers,
|
|
ondragenter: function(event) {
|
|
moveSortableToTarget(placeholder, $(event.target), self);
|
|
},
|
|
ondrop: function(event) {
|
|
if (!moved) {
|
|
moveSortableToTarget(src, $(event.target), self);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Configure drop on group headers
|
|
if (node instanceof Group) {
|
|
interact(node.$el.find(QueryBuilder.selectors.group_header)[0])
|
|
.dropzone({
|
|
accept: QueryBuilder.selectors.rule_and_group_containers,
|
|
ondragenter: function(event) {
|
|
moveSortableToTarget(placeholder, $(event.target), self);
|
|
},
|
|
ondrop: function(event) {
|
|
if (!moved) {
|
|
moveSortableToTarget(src, $(event.target), self);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Detach interactables
|
|
this.on('beforeDeleteRule beforeDeleteGroup', function(e, node) {
|
|
if (!e.isDefaultPrevented()) {
|
|
interact(node.$el[0]).unset();
|
|
|
|
if (node instanceof Group) {
|
|
interact(node.$el.find(QueryBuilder.selectors.group_header)[0]).unset();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Remove drag handle from non-sortable items
|
|
this.on('afterApplyRuleFlags afterApplyGroupFlags', function(e, node) {
|
|
if (node.flags.no_sortable) {
|
|
node.$el.find('.drag-handle').remove();
|
|
}
|
|
});
|
|
|
|
// Modify templates
|
|
if (!options.disable_template) {
|
|
this.on('getGroupTemplate.filter', function(h, level) {
|
|
if (level > 1) {
|
|
var $h = $(h.value);
|
|
$h.find(QueryBuilder.selectors.condition_container).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>');
|
|
h.value = $h.prop('outerHTML');
|
|
}
|
|
});
|
|
|
|
this.on('getRuleTemplate.filter', function(h) {
|
|
var $h = $(h.value);
|
|
$h.find(QueryBuilder.selectors.rule_header).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>');
|
|
h.value = $h.prop('outerHTML');
|
|
});
|
|
}
|
|
}, {
|
|
inherit_no_sortable: true,
|
|
inherit_no_drop: true,
|
|
icon: 'glyphicon glyphicon-sort',
|
|
disable_template: false
|
|
});
|
|
|
|
QueryBuilder.selectors.rule_and_group_containers = QueryBuilder.selectors.rule_container + ', ' + QueryBuilder.selectors.group_container;
|
|
QueryBuilder.selectors.drag_handle = '.drag-handle';
|
|
|
|
QueryBuilder.defaults({
|
|
default_rule_flags: {
|
|
no_sortable: false,
|
|
no_drop: false
|
|
},
|
|
default_group_flags: {
|
|
no_sortable: false,
|
|
no_drop: false
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Moves an element (placeholder or actual object) depending on active target
|
|
* @memberof module:plugins.Sortable
|
|
* @param {Node} node
|
|
* @param {jQuery} target
|
|
* @param {QueryBuilder} [builder]
|
|
* @private
|
|
*/
|
|
function moveSortableToTarget(node, target, builder) {
|
|
var parent, method;
|
|
var Selectors = QueryBuilder.selectors;
|
|
|
|
// on rule
|
|
parent = target.closest(Selectors.rule_container);
|
|
if (parent.length) {
|
|
method = 'moveAfter';
|
|
}
|
|
|
|
// on group header
|
|
if (!method) {
|
|
parent = target.closest(Selectors.group_header);
|
|
if (parent.length) {
|
|
parent = target.closest(Selectors.group_container);
|
|
method = 'moveAtBegin';
|
|
}
|
|
}
|
|
|
|
// on group
|
|
if (!method) {
|
|
parent = target.closest(Selectors.group_container);
|
|
if (parent.length) {
|
|
method = 'moveAtEnd';
|
|
}
|
|
}
|
|
|
|
if (method) {
|
|
node[method](builder.getModel(parent));
|
|
|
|
// refresh radio value
|
|
if (builder && node instanceof Rule) {
|
|
builder.setRuleInputValue(node, node.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @class SqlSupport
|
|
* @memberof module:plugins
|
|
* @description Allows to export rules as a SQL WHERE statement as well as populating the builder from an SQL query.
|
|
* @param {object} [options]
|
|
* @param {boolean} [options.boolean_as_integer=true] - `true` to convert boolean values to integer in the SQL output
|
|
*/
|
|
QueryBuilder.define('sql-support', function(options) {
|
|
|
|
}, {
|
|
boolean_as_integer: true
|
|
});
|
|
|
|
QueryBuilder.defaults({
|
|
// operators for internal -> SQL conversion
|
|
sqlOperators: {
|
|
equal: { op: '= ?' },
|
|
not_equal: { op: '!= ?' },
|
|
in: { op: 'IN(?)', sep: ', ' },
|
|
not_in: { op: 'NOT IN(?)', sep: ', ' },
|
|
less: { op: '< ?' },
|
|
less_or_equal: { op: '<= ?' },
|
|
greater: { op: '> ?' },
|
|
greater_or_equal: { op: '>= ?' },
|
|
between: { op: 'BETWEEN ?', sep: ' AND ' },
|
|
not_between: { op: 'NOT BETWEEN ?', sep: ' AND ' },
|
|
begins_with: { op: 'LIKE(?)', mod: '{0}%' },
|
|
not_begins_with: { op: 'NOT LIKE(?)', mod: '{0}%' },
|
|
contains: { op: 'LIKE(?)', mod: '%{0}%' },
|
|
not_contains: { op: 'NOT LIKE(?)', mod: '%{0}%' },
|
|
ends_with: { op: 'LIKE(?)', mod: '%{0}' },
|
|
not_ends_with: { op: 'NOT LIKE(?)', mod: '%{0}' },
|
|
is_empty: { op: '= \'\'' },
|
|
is_not_empty: { op: '!= \'\'' },
|
|
is_null: { op: 'IS NULL' },
|
|
is_not_null: { op: 'IS NOT NULL' }
|
|
},
|
|
|
|
// operators for SQL -> internal conversion
|
|
sqlRuleOperator: {
|
|
'=': function(v) {
|
|
return {
|
|
val: v,
|
|
op: v === '' ? 'is_empty' : 'equal'
|
|
};
|
|
},
|
|
'!=': function(v) {
|
|
return {
|
|
val: v,
|
|
op: v === '' ? 'is_not_empty' : 'not_equal'
|
|
};
|
|
},
|
|
'LIKE': function(v) {
|
|
if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
|
|
return {
|
|
val: v.slice(1, -1),
|
|
op: 'contains'
|
|
};
|
|
}
|
|
else if (v.slice(0, 1) == '%') {
|
|
return {
|
|
val: v.slice(1),
|
|
op: 'ends_with'
|
|
};
|
|
}
|
|
else if (v.slice(-1) == '%') {
|
|
return {
|
|
val: v.slice(0, -1),
|
|
op: 'begins_with'
|
|
};
|
|
}
|
|
else {
|
|
Utils.error('SQLParse', 'Invalid value for LIKE operator "{0}"', v);
|
|
}
|
|
},
|
|
'NOT LIKE': function(v) {
|
|
if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
|
|
return {
|
|
val: v.slice(1, -1),
|
|
op: 'not_contains'
|
|
};
|
|
}
|
|
else if (v.slice(0, 1) == '%') {
|
|
return {
|
|
val: v.slice(1),
|
|
op: 'not_ends_with'
|
|
};
|
|
}
|
|
else if (v.slice(-1) == '%') {
|
|
return {
|
|
val: v.slice(0, -1),
|
|
op: 'not_begins_with'
|
|
};
|
|
}
|
|
else {
|
|
Utils.error('SQLParse', 'Invalid value for NOT LIKE operator "{0}"', v);
|
|
}
|
|
},
|
|
'IN': function(v) {
|
|
return { val: v, op: 'in' };
|
|
},
|
|
'NOT IN': function(v) {
|
|
return { val: v, op: 'not_in' };
|
|
},
|
|
'<': function(v) {
|
|
return { val: v, op: 'less' };
|
|
},
|
|
'<=': function(v) {
|
|
return { val: v, op: 'less_or_equal' };
|
|
},
|
|
'>': function(v) {
|
|
return { val: v, op: 'greater' };
|
|
},
|
|
'>=': function(v) {
|
|
return { val: v, op: 'greater_or_equal' };
|
|
},
|
|
'BETWEEN': function(v) {
|
|
return { val: v, op: 'between' };
|
|
},
|
|
'NOT BETWEEN': function(v) {
|
|
return { val: v, op: 'not_between' };
|
|
},
|
|
'IS': function(v) {
|
|
if (v !== null) {
|
|
Utils.error('SQLParse', 'Invalid value for IS operator');
|
|
}
|
|
return { val: null, op: 'is_null' };
|
|
},
|
|
'IS NOT': function(v) {
|
|
if (v !== null) {
|
|
Utils.error('SQLParse', 'Invalid value for IS operator');
|
|
}
|
|
return { val: null, op: 'is_not_null' };
|
|
}
|
|
},
|
|
|
|
// statements for internal -> SQL conversion
|
|
sqlStatements: {
|
|
'question_mark': function() {
|
|
var params = [];
|
|
return {
|
|
add: function(rule, value) {
|
|
params.push(value);
|
|
return '?';
|
|
},
|
|
run: function() {
|
|
return params;
|
|
}
|
|
};
|
|
},
|
|
|
|
'numbered': function(char) {
|
|
if (!char || char.length > 1) char = '$';
|
|
var index = 0;
|
|
var params = [];
|
|
return {
|
|
add: function(rule, value) {
|
|
params.push(value);
|
|
index++;
|
|
return char + index;
|
|
},
|
|
run: function() {
|
|
return params;
|
|
}
|
|
};
|
|
},
|
|
|
|
'named': function(char) {
|
|
if (!char || char.length > 1) char = ':';
|
|
var indexes = {};
|
|
var params = {};
|
|
return {
|
|
add: function(rule, value) {
|
|
if (!indexes[rule.field]) indexes[rule.field] = 1;
|
|
var key = rule.field + '_' + (indexes[rule.field]++);
|
|
params[key] = value;
|
|
return char + key;
|
|
},
|
|
run: function() {
|
|
return params;
|
|
}
|
|
};
|
|
}
|
|
},
|
|
|
|
// statements for SQL -> internal conversion
|
|
sqlRuleStatement: {
|
|
'question_mark': function(values) {
|
|
var index = 0;
|
|
return {
|
|
parse: function(v) {
|
|
return v == '?' ? values[index++] : v;
|
|
},
|
|
esc: function(sql) {
|
|
return sql.replace(/\?/g, '\'?\'');
|
|
}
|
|
};
|
|
},
|
|
|
|
'numbered': function(values, char) {
|
|
if (!char || char.length > 1) char = '$';
|
|
var regex1 = new RegExp('^\\' + char + '[0-9]+$');
|
|
var regex2 = new RegExp('\\' + char + '([0-9]+)', 'g');
|
|
return {
|
|
parse: function(v) {
|
|
return regex1.test(v) ? values[v.slice(1) - 1] : v;
|
|
},
|
|
esc: function(sql) {
|
|
return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\'');
|
|
}
|
|
};
|
|
},
|
|
|
|
'named': function(values, char) {
|
|
if (!char || char.length > 1) char = ':';
|
|
var regex1 = new RegExp('^\\' + char);
|
|
var regex2 = new RegExp('\\' + char + '(' + Object.keys(values).join('|') + ')', 'g');
|
|
return {
|
|
parse: function(v) {
|
|
return regex1.test(v) ? values[v.slice(1)] : v;
|
|
},
|
|
esc: function(sql) {
|
|
return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\'');
|
|
}
|
|
};
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @typedef {object} SqlQuery
|
|
* @memberof module:plugins.SqlSupport
|
|
* @property {string} sql
|
|
* @property {object} params
|
|
*/
|
|
|
|
QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ {
|
|
/**
|
|
* Returns rules as a SQL query
|
|
* @param {boolean|string} [stmt] - use prepared statements: false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)'
|
|
* @param {boolean} [nl=false] output with new lines
|
|
* @param {object} [data] - current rules by default
|
|
* @returns {module:plugins.SqlSupport.SqlQuery}
|
|
* @fires module:plugins.SqlSupport.changer:getSQLField
|
|
* @fires module:plugins.SqlSupport.changer:ruleToSQL
|
|
* @fires module:plugins.SqlSupport.changer:groupToSQL
|
|
* @throws UndefinedSQLConditionError, UndefinedSQLOperatorError
|
|
*/
|
|
getSQL: function(stmt, nl, data) {
|
|
data = (data === undefined) ? this.getRules() : data;
|
|
|
|
if (!data) {
|
|
return null;
|
|
}
|
|
|
|
nl = !!nl ? '\n' : ' ';
|
|
var boolean_as_integer = this.getPluginOptions('sql-support', 'boolean_as_integer');
|
|
|
|
if (stmt === true) {
|
|
stmt = 'question_mark';
|
|
}
|
|
if (typeof stmt == 'string') {
|
|
var config = getStmtConfig(stmt);
|
|
stmt = this.settings.sqlStatements[config[1]](config[2]);
|
|
}
|
|
|
|
var self = this;
|
|
|
|
var sql = (function parse(group) {
|
|
if (!group.condition) {
|
|
group.condition = self.settings.default_condition;
|
|
}
|
|
if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) {
|
|
Utils.error('UndefinedSQLCondition', 'Unable to build SQL query with condition "{0}"', group.condition);
|
|
}
|
|
|
|
if (!group.rules) {
|
|
return '';
|
|
}
|
|
|
|
var parts = [];
|
|
|
|
group.rules.forEach(function(rule) {
|
|
if (rule.rules && rule.rules.length > 0) {
|
|
parts.push('(' + nl + parse(rule) + nl + ')' + nl);
|
|
}
|
|
else {
|
|
var sql = self.settings.sqlOperators[rule.operator];
|
|
var ope = self.getOperatorByType(rule.operator);
|
|
var value = '';
|
|
|
|
if (sql === undefined) {
|
|
Utils.error('UndefinedSQLOperator', 'Unknown SQL operation for operator "{0}"', rule.operator);
|
|
}
|
|
|
|
if (ope.nb_inputs !== 0) {
|
|
if (!(rule.value instanceof Array)) {
|
|
rule.value = [rule.value];
|
|
}
|
|
|
|
rule.value.forEach(function(v, i) {
|
|
if (i > 0) {
|
|
value += sql.sep;
|
|
}
|
|
|
|
if (rule.type == 'boolean' && boolean_as_integer) {
|
|
v = v ? 1 : 0;
|
|
}
|
|
else if (!stmt && rule.type !== 'integer' && rule.type !== 'double' && rule.type !== 'boolean') {
|
|
v = Utils.escapeString(v);
|
|
}
|
|
|
|
if (sql.mod) {
|
|
v = Utils.fmt(sql.mod, v);
|
|
}
|
|
|
|
if (stmt) {
|
|
value += stmt.add(rule, v);
|
|
}
|
|
else {
|
|
if (typeof v == 'string') {
|
|
v = '\'' + v + '\'';
|
|
}
|
|
|
|
value += v;
|
|
}
|
|
});
|
|
}
|
|
|
|
var sqlFn = function(v) {
|
|
return sql.op.replace('?', function() {
|
|
return v;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Modifies the SQL field used by a rule
|
|
* @event changer:getSQLField
|
|
* @memberof module:plugins.SqlSupport
|
|
* @param {string} field
|
|
* @param {Rule} rule
|
|
* @returns {string}
|
|
*/
|
|
var field = self.change('getSQLField', rule.field, rule);
|
|
|
|
var ruleExpression = field + ' ' + sqlFn(value);
|
|
|
|
/**
|
|
* Modifies the SQL generated for a rule
|
|
* @event changer:ruleToSQL
|
|
* @memberof module:plugins.SqlSupport
|
|
* @param {string} expression
|
|
* @param {Rule} rule
|
|
* @param {*} value
|
|
* @param {function} valueWrapper - function that takes the value and adds the operator
|
|
* @returns {string}
|
|
*/
|
|
parts.push(self.change('ruleToSQL', ruleExpression, rule, value, sqlFn));
|
|
}
|
|
});
|
|
|
|
var groupExpression = parts.join(' ' + group.condition + nl);
|
|
|
|
/**
|
|
* Modifies the SQL generated for a group
|
|
* @event changer:groupToSQL
|
|
* @memberof module:plugins.SqlSupport
|
|
* @param {string} expression
|
|
* @param {Group} group
|
|
* @returns {string}
|
|
*/
|
|
return self.change('groupToSQL', groupExpression, group);
|
|
}(data));
|
|
|
|
if (stmt) {
|
|
return {
|
|
sql: sql,
|
|
params: stmt.run()
|
|
};
|
|
}
|
|
else {
|
|
return {
|
|
sql: sql
|
|
};
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Convert a SQL query to rules
|
|
* @param {string|module:plugins.SqlSupport.SqlQuery} query
|
|
* @param {boolean|string} stmt
|
|
* @returns {object}
|
|
* @fires module:plugins.SqlSupport.changer:parseSQLNode
|
|
* @fires module:plugins.SqlSupport.changer:getSQLFieldID
|
|
* @fires module:plugins.SqlSupport.changer:sqlToRule
|
|
* @fires module:plugins.SqlSupport.changer:sqlToGroup
|
|
* @throws MissingLibraryError, SQLParseError, UndefinedSQLOperatorError
|
|
*/
|
|
getRulesFromSQL: function(query, stmt) {
|
|
if (!('SQLParser' in window)) {
|
|
Utils.error('MissingLibrary', 'SQLParser is required to parse SQL queries. Get it here https://github.com/mistic100/sql-parser');
|
|
}
|
|
|
|
var self = this;
|
|
|
|
if (typeof query == 'string') {
|
|
query = { sql: query };
|
|
}
|
|
|
|
if (stmt === true) stmt = 'question_mark';
|
|
if (typeof stmt == 'string') {
|
|
var config = getStmtConfig(stmt);
|
|
stmt = this.settings.sqlRuleStatement[config[1]](query.params, config[2]);
|
|
}
|
|
|
|
if (stmt) {
|
|
query.sql = stmt.esc(query.sql);
|
|
}
|
|
|
|
if (query.sql.toUpperCase().indexOf('SELECT') !== 0) {
|
|
query.sql = 'SELECT * FROM table WHERE ' + query.sql;
|
|
}
|
|
|
|
var parsed = SQLParser.parse(query.sql);
|
|
|
|
if (!parsed.where) {
|
|
Utils.error('SQLParse', 'No WHERE clause found');
|
|
}
|
|
|
|
/**
|
|
* Custom parsing of an AST node generated by SQLParser, you can return a sub-part of the tree, or a well formed group or rule JSON
|
|
* @event changer:parseSQLNode
|
|
* @memberof module:plugins.SqlSupport
|
|
* @param {object} AST node
|
|
* @returns {object} tree, rule or group
|
|
*/
|
|
var data = self.change('parseSQLNode', parsed.where.conditions);
|
|
|
|
// a plugin returned a group
|
|
if ('rules' in data && 'condition' in data) {
|
|
return data;
|
|
}
|
|
|
|
// a plugin returned a rule
|
|
if ('id' in data && 'operator' in data && 'value' in data) {
|
|
return {
|
|
condition: this.settings.default_condition,
|
|
rules: [data]
|
|
};
|
|
}
|
|
|
|
// create root group
|
|
var out = self.change('sqlToGroup', {
|
|
condition: this.settings.default_condition,
|
|
rules: []
|
|
}, data);
|
|
|
|
// keep track of current group
|
|
var curr = out;
|
|
|
|
(function flatten(data, i) {
|
|
if (data === null) {
|
|
return;
|
|
}
|
|
|
|
// allow plugins to manually parse or handle special cases
|
|
data = self.change('parseSQLNode', data);
|
|
|
|
// a plugin returned a group
|
|
if ('rules' in data && 'condition' in data) {
|
|
curr.rules.push(data);
|
|
return;
|
|
}
|
|
|
|
// a plugin returned a rule
|
|
if ('id' in data && 'operator' in data && 'value' in data) {
|
|
curr.rules.push(data);
|
|
return;
|
|
}
|
|
|
|
// data must be a SQL parser node
|
|
if (!('left' in data) || !('right' in data) || !('operation' in data)) {
|
|
Utils.error('SQLParse', 'Unable to parse WHERE clause');
|
|
}
|
|
|
|
// it's a node
|
|
if (['AND', 'OR'].indexOf(data.operation.toUpperCase()) !== -1) {
|
|
// create a sub-group if the condition is not the same and it's not the first level
|
|
|
|
/**
|
|
* Given an existing group and an AST node, determines if a sub-group must be created
|
|
* @event changer:sqlGroupsDistinct
|
|
* @memberof module:plugins.SqlSupport
|
|
* @param {boolean} create - true by default if the group condition is different
|
|
* @param {object} group
|
|
* @param {object} AST
|
|
* @param {int} current group level
|
|
* @returns {boolean}
|
|
*/
|
|
var createGroup = self.change('sqlGroupsDistinct', i > 0 && curr.condition != data.operation.toUpperCase(), curr, data, i);
|
|
|
|
if (createGroup) {
|
|
/**
|
|
* Modifies the group generated from the SQL expression (this is called before the group is filled with rules)
|
|
* @event changer:sqlToGroup
|
|
* @memberof module:plugins.SqlSupport
|
|
* @param {object} group
|
|
* @param {object} AST
|
|
* @returns {object}
|
|
*/
|
|
var group = self.change('sqlToGroup', {
|
|
condition: self.settings.default_condition,
|
|
rules: []
|
|
}, data);
|
|
|
|
curr.rules.push(group);
|
|
curr = group;
|
|
}
|
|
|
|
curr.condition = data.operation.toUpperCase();
|
|
i++;
|
|
|
|
// some magic !
|
|
var next = curr;
|
|
flatten(data.left, i);
|
|
|
|
curr = next;
|
|
flatten(data.right, i);
|
|
}
|
|
// it's a leaf
|
|
else {
|
|
if ($.isPlainObject(data.right.value)) {
|
|
Utils.error('SQLParse', 'Value format not supported for {0}.', data.left.value);
|
|
}
|
|
|
|
// convert array
|
|
var value;
|
|
if ($.isArray(data.right.value)) {
|
|
value = data.right.value.map(function(v) {
|
|
return v.value;
|
|
});
|
|
}
|
|
else {
|
|
value = data.right.value;
|
|
}
|
|
|
|
// get actual values
|
|
if (stmt) {
|
|
if ($.isArray(value)) {
|
|
value = value.map(stmt.parse);
|
|
}
|
|
else {
|
|
value = stmt.parse(value);
|
|
}
|
|
}
|
|
|
|
// convert operator
|
|
var operator = data.operation.toUpperCase();
|
|
if (operator == '<>') {
|
|
operator = '!=';
|
|
}
|
|
|
|
var sqlrl = self.settings.sqlRuleOperator[operator];
|
|
if (sqlrl === undefined) {
|
|
Utils.error('UndefinedSQLOperator', 'Invalid SQL operation "{0}".', data.operation);
|
|
}
|
|
|
|
var opVal = sqlrl.call(this, value, data.operation);
|
|
|
|
// find field name
|
|
var field;
|
|
if ('values' in data.left) {
|
|
field = data.left.values.join('.');
|
|
}
|
|
else if ('value' in data.left) {
|
|
field = data.left.value;
|
|
}
|
|
else {
|
|
Utils.error('SQLParse', 'Cannot find field name in {0}', JSON.stringify(data.left));
|
|
}
|
|
|
|
var id = self.getSQLFieldID(field, value);
|
|
|
|
/**
|
|
* Modifies the rule generated from the SQL expression
|
|
* @event changer:sqlToRule
|
|
* @memberof module:plugins.SqlSupport
|
|
* @param {object} rule
|
|
* @param {object} AST
|
|
* @returns {object}
|
|
*/
|
|
var rule = self.change('sqlToRule', {
|
|
id: id,
|
|
field: field,
|
|
operator: opVal.op,
|
|
value: opVal.val
|
|
}, data);
|
|
|
|
curr.rules.push(rule);
|
|
}
|
|
}(data, 0));
|
|
|
|
return out;
|
|
},
|
|
|
|
/**
|
|
* Sets the builder's rules from a SQL query
|
|
* @see module:plugins.SqlSupport.getRulesFromSQL
|
|
*/
|
|
setRulesFromSQL: function(query, stmt) {
|
|
this.setRules(this.getRulesFromSQL(query, stmt));
|
|
},
|
|
|
|
/**
|
|
* Returns a filter identifier from the SQL field.
|
|
* Automatically use the only one filter with a matching field, fires a changer otherwise.
|
|
* @param {string} field
|
|
* @param {*} value
|
|
* @fires module:plugins.SqlSupport:changer:getSQLFieldID
|
|
* @returns {string}
|
|
* @private
|
|
*/
|
|
getSQLFieldID: function(field, value) {
|
|
var matchingFilters = this.filters.filter(function(filter) {
|
|
return filter.field.toLowerCase() === field.toLowerCase();
|
|
});
|
|
|
|
var id;
|
|
if (matchingFilters.length === 1) {
|
|
id = matchingFilters[0].id;
|
|
}
|
|
else {
|
|
/**
|
|
* Returns a filter identifier from the SQL field
|
|
* @event changer:getSQLFieldID
|
|
* @memberof module:plugins.SqlSupport
|
|
* @param {string} field
|
|
* @param {*} value
|
|
* @returns {string}
|
|
*/
|
|
id = this.change('getSQLFieldID', field, value);
|
|
}
|
|
|
|
return id;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Parses the statement configuration
|
|
* @memberof module:plugins.SqlSupport
|
|
* @param {string} stmt
|
|
* @returns {Array} null, mode, option
|
|
* @private
|
|
*/
|
|
function getStmtConfig(stmt) {
|
|
var config = stmt.match(/(question_mark|numbered|named)(?:\((.)\))?/);
|
|
if (!config) config = [null, 'question_mark', undefined];
|
|
return config;
|
|
}
|
|
|
|
|
|
/**
|
|
* @class UniqueFilter
|
|
* @memberof module:plugins
|
|
* @description Allows to define some filters as "unique": ie which can be used for only one rule, globally or in the same group.
|
|
*/
|
|
QueryBuilder.define('unique-filter', function() {
|
|
this.status.used_filters = {};
|
|
|
|
this.on('afterUpdateRuleFilter', this.updateDisabledFilters);
|
|
this.on('afterDeleteRule', this.updateDisabledFilters);
|
|
this.on('afterCreateRuleFilters', this.applyDisabledFilters);
|
|
this.on('afterReset', this.clearDisabledFilters);
|
|
this.on('afterClear', this.clearDisabledFilters);
|
|
|
|
// Ensure that the default filter is not already used if unique
|
|
this.on('getDefaultFilter.filter', function(e, model) {
|
|
var self = e.builder;
|
|
|
|
self.updateDisabledFilters();
|
|
|
|
if (e.value.id in self.status.used_filters) {
|
|
var found = self.filters.some(function(filter) {
|
|
if (!(filter.id in self.status.used_filters) || self.status.used_filters[filter.id].length > 0 && self.status.used_filters[filter.id].indexOf(model.parent) === -1) {
|
|
e.value = filter;
|
|
return true;
|
|
}
|
|
});
|
|
|
|
if (!found) {
|
|
Utils.error(false, 'UniqueFilter', 'No more non-unique filters available');
|
|
e.value = undefined;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
QueryBuilder.extend(/** @lends module:plugins.UniqueFilter.prototype */ {
|
|
/**
|
|
* Updates the list of used filters
|
|
* @param {$.Event} [e]
|
|
* @private
|
|
*/
|
|
updateDisabledFilters: function(e) {
|
|
var self = e ? e.builder : this;
|
|
|
|
self.status.used_filters = {};
|
|
|
|
if (!self.model) {
|
|
return;
|
|
}
|
|
|
|
// get used filters
|
|
(function walk(group) {
|
|
group.each(function(rule) {
|
|
if (rule.filter && rule.filter.unique) {
|
|
if (!self.status.used_filters[rule.filter.id]) {
|
|
self.status.used_filters[rule.filter.id] = [];
|
|
}
|
|
if (rule.filter.unique == 'group') {
|
|
self.status.used_filters[rule.filter.id].push(rule.parent);
|
|
}
|
|
}
|
|
}, function(group) {
|
|
walk(group);
|
|
});
|
|
}(self.model.root));
|
|
|
|
self.applyDisabledFilters(e);
|
|
},
|
|
|
|
/**
|
|
* Clear the list of used filters
|
|
* @param {$.Event} [e]
|
|
* @private
|
|
*/
|
|
clearDisabledFilters: function(e) {
|
|
var self = e ? e.builder : this;
|
|
|
|
self.status.used_filters = {};
|
|
|
|
self.applyDisabledFilters(e);
|
|
},
|
|
|
|
/**
|
|
* Disabled filters depending on the list of used ones
|
|
* @param {$.Event} [e]
|
|
* @private
|
|
*/
|
|
applyDisabledFilters: function(e) {
|
|
var self = e ? e.builder : this;
|
|
|
|
// re-enable everything
|
|
self.$el.find(QueryBuilder.selectors.filter_container + ' option').prop('disabled', false);
|
|
|
|
// disable some
|
|
$.each(self.status.used_filters, function(filterId, groups) {
|
|
if (groups.length === 0) {
|
|
self.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true);
|
|
}
|
|
else {
|
|
groups.forEach(function(group) {
|
|
group.each(function(rule) {
|
|
rule.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true);
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
// update Selectpicker
|
|
if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) {
|
|
self.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render');
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
/*!
|
|
* jQuery QueryBuilder 2.5.2
|
|
* Locale: English (en)
|
|
* Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr
|
|
* Licensed under MIT (https://opensource.org/licenses/MIT)
|
|
*/
|
|
|
|
QueryBuilder.regional['en'] = {
|
|
"__locale": "English (en)",
|
|
"__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr",
|
|
"add_rule": "Add rule",
|
|
"add_group": "Add group",
|
|
"delete_rule": "Delete",
|
|
"delete_group": "Delete",
|
|
"conditions": {
|
|
"AND": "AND",
|
|
"OR": "OR"
|
|
},
|
|
"operators": {
|
|
"equal": "equal",
|
|
"not_equal": "not equal",
|
|
"in": "in",
|
|
"not_in": "not in",
|
|
"less": "less",
|
|
"less_or_equal": "less or equal",
|
|
"greater": "greater",
|
|
"greater_or_equal": "greater or equal",
|
|
"between": "between",
|
|
"not_between": "not between",
|
|
"begins_with": "begins with",
|
|
"not_begins_with": "doesn't begin with",
|
|
"contains": "contains",
|
|
"not_contains": "doesn't contain",
|
|
"ends_with": "ends with",
|
|
"not_ends_with": "doesn't end with",
|
|
"is_empty": "is empty",
|
|
"is_not_empty": "is not empty",
|
|
"is_null": "is null",
|
|
"is_not_null": "is not null"
|
|
},
|
|
"errors": {
|
|
"no_filter": "No filter selected",
|
|
"empty_group": "The group is empty",
|
|
"radio_empty": "No value selected",
|
|
"checkbox_empty": "No value selected",
|
|
"select_empty": "No value selected",
|
|
"string_empty": "Empty value",
|
|
"string_exceed_min_length": "Must contain at least {0} characters",
|
|
"string_exceed_max_length": "Must not contain more than {0} characters",
|
|
"string_invalid_format": "Invalid format ({0})",
|
|
"number_nan": "Not a number",
|
|
"number_not_integer": "Not an integer",
|
|
"number_not_double": "Not a real number",
|
|
"number_exceed_min": "Must be greater than {0}",
|
|
"number_exceed_max": "Must be lower than {0}",
|
|
"number_wrong_step": "Must be a multiple of {0}",
|
|
"number_between_invalid": "Invalid values, {0} is greater than {1}",
|
|
"datetime_empty": "Empty value",
|
|
"datetime_invalid": "Invalid date format ({0})",
|
|
"datetime_exceed_min": "Must be after {0}",
|
|
"datetime_exceed_max": "Must be before {0}",
|
|
"datetime_between_invalid": "Invalid values, {0} is greater than {1}",
|
|
"boolean_not_valid": "Not a boolean",
|
|
"operator_not_multiple": "Operator \"{1}\" cannot accept multiple values"
|
|
},
|
|
"invert": "Invert",
|
|
"NOT": "NOT"
|
|
};
|
|
|
|
QueryBuilder.defaults({ lang_code: 'en' });
|
|
return QueryBuilder;
|
|
|
|
}));
|