2 * jQuery.extendext 0.1.2
4 * Copyright 2014-2016 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
5 * Licensed under MIT (http://opensource.org/licenses/MIT)
7 * Based on jQuery.extend by jQuery Foundation, Inc. and other contributors
10 (function (root, factory) {
11 if (typeof define === 'function' && define.amd) {
12 define('jQuery.extendext', ['jquery'], factory);
14 else if (typeof module === 'object' && module.exports) {
15 module.exports = factory(require('jquery'));
20 }(this, function ($) {
23 $.extendext = function () {
24 var options, name, src, copy, copyIsArray, clone,
25 target = arguments[0] || {},
27 length = arguments.length,
29 arrayMode = 'default';
31 // Handle a deep copy situation
32 if (typeof target === "boolean") {
35 // Skip the boolean and the target
36 target = arguments[i++] || {};
39 // Handle array mode parameter
40 if (typeof target === "string") {
41 arrayMode = target.toLowerCase();
42 if (arrayMode !== 'concat' && arrayMode !== 'replace' && arrayMode !== 'extend') {
43 arrayMode = 'default';
46 // Skip the string param
47 target = arguments[i++] || {};
50 // Handle case when target is a string or something (possible in deep copy)
51 if (typeof target !== "object" && !$.isFunction(target)) {
55 // Extend jQuery itself if only one argument is passed
61 for (; i < length; i++) {
62 // Only deal with non-null/undefined values
63 if ((options = arguments[i]) !== null) {
64 // Special operations for arrays
65 if ($.isArray(options) && arrayMode !== 'default') {
66 clone = target && $.isArray(target) ? target : [];
70 target = clone.concat($.extend(deep, [], options));
74 target = $.extend(deep, [], options);
78 options.forEach(function (e, i) {
79 if (typeof e === 'object') {
80 var type = $.isArray(e) ? [] : {};
81 clone[i] = $.extendext(deep, arrayMode, clone[i] || type, e);
83 } else if (clone.indexOf(e) === -1) {
93 // Extend the base object
94 for (name in options) {
98 // Prevent never-ending loop
99 if (target === copy) {
103 // Recurse if we're merging plain objects or arrays
104 if (deep && copy && ( $.isPlainObject(copy) ||
105 (copyIsArray = $.isArray(copy)) )) {
109 clone = src && $.isArray(src) ? src : [];
112 clone = src && $.isPlainObject(src) ? src : {};
115 // Never move original objects, clone them
116 target[name] = $.extendext(deep, arrayMode, clone, copy);
118 // Don't bring in undefined values
119 } else if (copy !== undefined) {
127 // Return the modified object
133 // 2011-2014, Laura Doktorova, https://github.com/olado/doT
134 // Licensed under the MIT license.
143 evaluate: /\{\{([\s\S]+?(\}?)+)\}\}/g,
144 interpolate: /\{\{=([\s\S]+?)\}\}/g,
145 encode: /\{\{!([\s\S]+?)\}\}/g,
146 use: /\{\{#([\s\S]+?)\}\}/g,
147 useParams: /(^|[^\w$])def(?:\.|\[[\'\"])([\w$\.]+)(?:[\'\"]\])?\s*\:\s*([\w$\.]+|\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})/g,
148 define: /\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g,
149 defineParams:/^\s*([\w$]+):([\s\S]+)/,
150 conditional: /\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g,
151 iterate: /\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/g,
155 selfcontained: false,
156 doNotSkipEncoded: false
158 template: undefined, //fn, compile template
159 compile: undefined, //fn, for express
163 doT.encodeHTMLSource = function(doNotSkipEncoded) {
164 var encodeHTMLRules = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "/": "/" },
165 matchHTML = doNotSkipEncoded ? /[&<>"'\/]/g : /&(?!#?\w+;)|<|>|"|'|\//g;
166 return function(code) {
167 return code ? code.toString().replace(matchHTML, function(m) {return encodeHTMLRules[m] || m;}) : "";
171 _globals = (function(){ return this || (0,eval)("this"); }());
173 /* istanbul ignore else */
174 if (typeof module !== "undefined" && module.exports) {
175 module.exports = doT;
176 } else if (typeof define === "function" && define.amd) {
177 define('doT', function(){return doT;});
183 append: { start: "'+(", end: ")+'", startencode: "'+encodeHTML(" },
184 split: { start: "';out+=(", end: ");out+='", startencode: "';out+=encodeHTML(" }
187 function resolveDefs(c, block, def) {
188 return ((typeof block === "string") ? block : block.toString())
189 .replace(c.define || skip, function(m, code, assign, value) {
190 if (code.indexOf("def.") === 0) {
191 code = code.substring(4);
193 if (!(code in def)) {
194 if (assign === ":") {
195 if (c.defineParams) value.replace(c.defineParams, function(m, param, v) {
196 def[code] = {arg: param, text: v};
198 if (!(code in def)) def[code]= value;
200 new Function("def", "def['"+code+"']=" + value)(def);
205 .replace(c.use || skip, function(m, code) {
206 if (c.useParams) code = code.replace(c.useParams, function(m, s, d, param) {
207 if (def[d] && def[d].arg && param) {
208 var rw = (d+":"+param).replace(/'|\\/g, "_");
209 def.__exp = def.__exp || {};
210 def.__exp[rw] = def[d].text.replace(new RegExp("(^|[^\\w$])" + def[d].arg + "([^\\w$])", "g"), "$1" + param + "$2");
211 return s + "def.__exp['"+rw+"']";
214 var v = new Function("def", "return " + code)(def);
215 return v ? resolveDefs(c, v, def) : v;
219 function unescape(code) {
220 return code.replace(/\\('|\\)/g, "$1").replace(/[\r\t\n]/g, " ");
223 doT.template = function(tmpl, c, def) {
224 c = c || doT.templateSettings;
225 var cse = c.append ? startend.append : startend.split, needhtmlencode, sid = 0, indv,
226 str = (c.use || c.define) ? resolveDefs(c, tmpl, def || {}) : tmpl;
228 str = ("var out='" + (c.strip ? str.replace(/(^|\r|\n)\t* +| +\t*(\r|\n|$)/g," ")
229 .replace(/\r|\n|\t|\/\*[\s\S]*?\*\//g,""): str)
230 .replace(/'|\\/g, "\\$&")
231 .replace(c.interpolate || skip, function(m, code) {
232 return cse.start + unescape(code) + cse.end;
234 .replace(c.encode || skip, function(m, code) {
235 needhtmlencode = true;
236 return cse.startencode + unescape(code) + cse.end;
238 .replace(c.conditional || skip, function(m, elsecase, code) {
240 (code ? "';}else if(" + unescape(code) + "){out+='" : "';}else{out+='") :
241 (code ? "';if(" + unescape(code) + "){out+='" : "';}out+='");
243 .replace(c.iterate || skip, function(m, iterate, vname, iname) {
244 if (!iterate) return "';} } out+='";
245 sid+=1; indv=iname || "i"+sid; iterate=unescape(iterate);
246 return "';var arr"+sid+"="+iterate+";if(arr"+sid+"){var "+vname+","+indv+"=-1,l"+sid+"=arr"+sid+".length-1;while("+indv+"<l"+sid+"){"
247 +vname+"=arr"+sid+"["+indv+"+=1];out+='";
249 .replace(c.evaluate || skip, function(m, code) {
250 return "';" + unescape(code) + "out+='";
253 .replace(/\n/g, "\\n").replace(/\t/g, '\\t').replace(/\r/g, "\\r")
254 .replace(/(\s|;|\}|^|\{)out\+='';/g, '$1').replace(/\+''/g, "");
255 //.replace(/(\s|;|\}|^|\{)out\+=''\+/g,'$1out+=');
257 if (needhtmlencode) {
258 if (!c.selfcontained && _globals && !_globals._encodeHTML) _globals._encodeHTML = doT.encodeHTMLSource(c.doNotSkipEncoded);
259 str = "var encodeHTML = typeof _encodeHTML !== 'undefined' ? _encodeHTML : ("
260 + doT.encodeHTMLSource.toString() + "(" + (c.doNotSkipEncoded || '') + "));"
264 return new Function(c.varname, str);
266 /* istanbul ignore else */
267 if (typeof console !== "undefined") console.log("Could not create a template function: " + str);
272 doT.compile = function(tmpl, def) {
273 return doT.template(tmpl, null, def);
279 * jQuery QueryBuilder 2.5.2
280 * Copyright 2014-2018 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
281 * Licensed under MIT (https://opensource.org/licenses/MIT)
283 (function(root, factory) {
284 if (typeof define == 'function' && define.amd) {
285 define('query-builder', ['jquery', 'dot/doT', 'jquery-extendext'], factory);
287 else if (typeof module === 'object' && module.exports) {
288 module.exports = factory(require('jquery'), require('dot/doT'), require('jquery-extendext'));
291 factory(root.jQuery, root.doT);
293 }(this, function($, doT) {
297 * @typedef {object} Filter
298 * @memberof QueryBuilder
299 * @description See {@link http://querybuilder.js.org/index.html#filters}
303 * @typedef {object} Operator
304 * @memberof QueryBuilder
305 * @description See {@link http://querybuilder.js.org/index.html#operators}
309 * @param {jQuery} $el
310 * @param {object} options - see {@link http://querybuilder.js.org/#options}
313 var QueryBuilder = function($el, options) {
314 $el[0].queryBuilder = this;
324 * Configuration object
328 this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options);
335 this.model = new Model();
340 * @property {string} id - id of the container
341 * @property {boolean} generated_id - if the container id has been generated
342 * @property {int} group_id - current group id
343 * @property {int} rule_id - current rule id
344 * @property {boolean} has_optgroup - if filters have optgroups
345 * @property {boolean} has_operator_optgroup - if operators have optgroups
355 has_operator_optgroup: false
360 * @member {QueryBuilder.Filter[]}
363 this.filters = this.settings.filters;
367 * @member {object.<string, string>}
370 this.icons = this.settings.icons;
374 * @member {QueryBuilder.Operator[]}
377 this.operators = this.settings.operators;
381 * @member {object.<string, function>}
384 this.templates = this.settings.templates;
387 * Plugins configuration
388 * @member {object.<string, object>}
391 this.plugins = this.settings.plugins;
394 * Translations object
400 // translations : english << 'lang_code' << custom
401 if (QueryBuilder.regional['en'] === undefined) {
402 Utils.error('Config', '"i18n/en.js" not loaded.');
404 this.lang = $.extendext(true, 'replace', {}, QueryBuilder.regional['en'], QueryBuilder.regional[this.settings.lang_code], this.settings.lang);
406 // "allow_groups" can be boolean or int
407 if (this.settings.allow_groups === false) {
408 this.settings.allow_groups = 0;
410 else if (this.settings.allow_groups === true) {
411 this.settings.allow_groups = -1;
415 Object.keys(this.templates).forEach(function(tpl) {
416 if (!this.templates[tpl]) {
417 this.templates[tpl] = QueryBuilder.templates[tpl];
419 if (typeof this.templates[tpl] == 'string') {
420 this.templates[tpl] = doT.template(this.templates[tpl]);
424 // ensure we have a container id
425 if (!this.$el.attr('id')) {
426 this.$el.attr('id', 'qb_' + Math.floor(Math.random() * 99999));
427 this.status.generated_id = true;
429 this.status.id = this.$el.attr('id');
432 this.$el.addClass('query-builder form-inline');
434 this.filters = this.checkFilters(this.filters);
435 this.operators = this.checkOperators(this.operators);
440 $.extend(QueryBuilder.prototype, /** @lends QueryBuilder.prototype */ {
442 * Triggers an event on the builder container
443 * @param {string} type
446 trigger: function(type) {
447 var event = new $.Event(this._tojQueryEvent(type), {
451 this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 1));
457 * Triggers an event on the builder container and returns the modified value
458 * @param {string} type
462 change: function(type, value) {
463 var event = new $.Event(this._tojQueryEvent(type, true), {
468 this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 2));
474 * Attaches an event listener on the builder container
475 * @param {string} type
476 * @param {function} cb
477 * @returns {QueryBuilder}
479 on: function(type, cb) {
480 this.$el.on(this._tojQueryEvent(type), cb);
485 * Removes an event listener from the builder container
486 * @param {string} type
487 * @param {function} [cb]
488 * @returns {QueryBuilder}
490 off: function(type, cb) {
491 this.$el.off(this._tojQueryEvent(type), cb);
496 * Attaches an event listener called once on the builder container
497 * @param {string} type
498 * @param {function} cb
499 * @returns {QueryBuilder}
501 once: function(type, cb) {
502 this.$el.one(this._tojQueryEvent(type), cb);
507 * Appends `.queryBuilder` and optionally `.filter` to the events names
508 * @param {string} name
509 * @param {boolean} [filter=false]
513 _tojQueryEvent: function(name, filter) {
514 return name.split(' ').map(function(type) {
515 return type + '.queryBuilder' + (filter ? '.filter' : '');
522 * Allowed types and their internal representation
523 * @type {object.<string, string>}
527 QueryBuilder.types = {
533 'datetime': 'datetime',
534 'boolean': 'boolean',
544 QueryBuilder.inputs = [
554 * Runtime modifiable options with `setOptions` method
559 QueryBuilder.modifiable_options = [
568 * CSS selectors for common components
569 * @type {object.<string, string>}
572 QueryBuilder.selectors = {
573 group_container: '.rules-group-container',
574 rule_container: '.rule-container',
575 filter_container: '.rule-filter-container',
576 operator_container: '.rule-operator-container',
577 value_container: '.rule-value-container',
578 error_container: '.error-container',
579 condition_container: '.rules-group-header .group-conditions',
581 rule_header: '.rule-header',
582 group_header: '.rules-group-header',
583 group_actions: '.group-actions',
584 rule_actions: '.rule-actions',
586 rules_list: '.rules-group-body>.rules-list',
588 group_condition: '.rules-group-header [name$=_cond]',
589 rule_filter: '.rule-filter-container [name$=_filter]',
590 rule_operator: '.rule-operator-container [name$=_operator]',
591 rule_value: '.rule-value-container [name*=_value_]',
593 add_rule: '[data-add=rule]',
594 delete_rule: '[data-delete=rule]',
595 add_group: '[data-add=group]',
596 delete_group: '[data-delete=group]'
600 * Template strings (see template.js)
601 * @type {object.<string, string>}
604 QueryBuilder.templates = {};
607 * Localized strings (see i18n/)
608 * @type {object.<string, object>}
611 QueryBuilder.regional = {};
615 * @type {object.<string, object>}
618 QueryBuilder.OPERATORS = {
619 equal: { type: 'equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean', 'map'] },
620 not_equal: { type: 'not_equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean', 'map'] },
621 in: { type: 'in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime', 'map'] },
622 not_in: { type: 'not_in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime', 'map'] },
623 less: { type: 'less', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
624 less_or_equal: { type: 'less_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
625 greater: { type: 'greater', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
626 greater_or_equal: { type: 'greater_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
627 between: { type: 'between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] },
628 not_between: { type: 'not_between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] },
629 begins_with: { type: 'begins_with', nb_inputs: 1, multiple: false, apply_to: ['string', 'map'] },
630 not_begins_with: { type: 'not_begins_with', nb_inputs: 1, multiple: false, apply_to: ['string', 'map'] },
631 contains: { type: 'contains', nb_inputs: 1, multiple: false, apply_to: ['string', 'map'] },
632 not_contains: { type: 'not_contains', nb_inputs: 1, multiple: false, apply_to: ['string', 'map'] },
633 ends_with: { type: 'ends_with', nb_inputs: 1, multiple: false, apply_to: ['string', 'map'] },
634 not_ends_with: { type: 'not_ends_with', nb_inputs: 1, multiple: false, apply_to: ['string', 'map'] },
635 is_empty: { type: 'is_empty', nb_inputs: 0, multiple: false, apply_to: ['string', 'map'] },
636 is_not_empty: { type: 'is_not_empty', nb_inputs: 0, multiple: false, apply_to: ['string', 'map'] },
637 is_null: { type: 'is_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean', 'map'] },
638 is_not_null: { type: 'is_not_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean', 'map'] }
642 * Default configuration
646 QueryBuilder.DEFAULTS = {
651 display_errors: true,
654 conditions: ['AND', 'OR'],
655 default_condition: 'AND',
656 inputs_separator: ' , ',
657 select_placeholder: '------',
658 display_empty_filter: true,
659 default_filter: null,
662 default_rule_flags: {
663 filter_readonly: false,
664 operator_readonly: false,
665 value_readonly: false,
669 default_group_flags: {
670 condition_readonly: false,
680 operatorSelect: null,
681 ruleValueSelect: null
711 add_group: 'glyphicon glyphicon-plus-sign',
712 add_rule: 'glyphicon glyphicon-plus',
713 remove_group: 'glyphicon glyphicon-trash',
714 remove_rule: 'glyphicon glyphicon-trash',
715 error: 'glyphicon glyphicon-warning-sign'
725 * Definition of available plugins
726 * @type {object.<String, object>}
728 QueryBuilder.plugins = {};
731 * Gets or extends the default configuration
732 * @param {object} [options] - new configuration
733 * @returns {undefined|object} nothing or configuration object (copy)
735 QueryBuilder.defaults = function(options) {
736 if (typeof options == 'object') {
737 $.extendext(true, 'replace', QueryBuilder.DEFAULTS, options);
739 else if (typeof options == 'string') {
740 if (typeof QueryBuilder.DEFAULTS[options] == 'object') {
741 return $.extend(true, {}, QueryBuilder.DEFAULTS[options]);
744 return QueryBuilder.DEFAULTS[options];
748 return $.extend(true, {}, QueryBuilder.DEFAULTS);
753 * Registers a new plugin
754 * @param {string} name
755 * @param {function} fct - init function
756 * @param {object} [def] - default options
758 QueryBuilder.define = function(name, fct, def) {
759 QueryBuilder.plugins[name] = {
766 * Adds new methods to QueryBuilder prototype
767 * @param {object.<string, function>} methods
769 QueryBuilder.extend = function(methods) {
770 $.extend(QueryBuilder.prototype, methods);
774 * Initializes plugins for an instance
775 * @throws ConfigError
778 QueryBuilder.prototype.initPlugins = function() {
783 if ($.isArray(this.plugins)) {
785 this.plugins.forEach(function(plugin) {
791 Object.keys(this.plugins).forEach(function(plugin) {
792 if (plugin in QueryBuilder.plugins) {
793 this.plugins[plugin] = $.extend(true, {},
794 QueryBuilder.plugins[plugin].def,
795 this.plugins[plugin] || {}
798 QueryBuilder.plugins[plugin].fct.call(this, this.plugins[plugin]);
801 Utils.error('Config', 'Unable to find plugin "{0}"', plugin);
807 * Returns the config of a plugin, if the plugin is not loaded, returns the default config.
808 * @param {string} name
809 * @param {string} [property]
810 * @throws ConfigError
813 QueryBuilder.prototype.getPluginOptions = function(name, property) {
815 if (this.plugins && this.plugins[name]) {
816 plugin = this.plugins[name];
818 else if (QueryBuilder.plugins[name]) {
819 plugin = QueryBuilder.plugins[name].def;
824 return plugin[property];
831 Utils.error('Config', 'Unable to find plugin "{0}"', name);
837 * Final initialisation of the builder
838 * @param {object} [rules]
839 * @fires QueryBuilder.afterInit
842 QueryBuilder.prototype.init = function(rules) {
844 * When the initilization is done, just before creating the root group
846 * @memberof QueryBuilder
848 this.trigger('afterInit');
851 this.setRules(rules);
852 delete this.settings.rules;
860 * Checks the configuration of each filter
861 * @param {QueryBuilder.Filter[]} filters
862 * @returns {QueryBuilder.Filter[]}
863 * @throws ConfigError
865 QueryBuilder.prototype.checkFilters = function(filters) {
866 var definedFilters = [];
868 if (!filters || filters.length === 0) {
869 Utils.error('Config', 'Missing filters list');
872 filters.forEach(function(filter, i) {
874 Utils.error('Config', 'Missing filter {0} id', i);
876 if (definedFilters.indexOf(filter.id) != -1) {
877 Utils.error('Config', 'Filter "{0}" already defined', filter.id);
879 definedFilters.push(filter.id);
882 filter.type = 'string';
884 else if (!QueryBuilder.types[filter.type]) {
885 Utils.error('Config', 'Invalid type "{0}"', filter.type);
889 filter.input = QueryBuilder.types[filter.type] === 'number' ? 'number' : 'text';
891 else if (typeof filter.input != 'function' && QueryBuilder.inputs.indexOf(filter.input) == -1) {
892 Utils.error('Config', 'Invalid input "{0}"', filter.input);
895 if (filter.operators) {
896 filter.operators.forEach(function(operator) {
897 if (typeof operator != 'string') {
898 Utils.error('Config', 'Filter operators must be global operators types (string)');
904 filter.field = filter.id;
907 filter.label = filter.field;
910 if (!filter.optgroup) {
911 filter.optgroup = null;
914 this.status.has_optgroup = true;
916 // register optgroup if needed
917 if (!this.settings.optgroups[filter.optgroup]) {
918 this.settings.optgroups[filter.optgroup] = filter.optgroup;
922 switch (filter.input) {
925 if (!filter.values || filter.values.length < 1) {
926 Utils.error('Config', 'Missing filter "{0}" values', filter.id);
931 var cleanValues = [];
932 filter.has_optgroup = false;
934 Utils.iterateOptions(filter.values, function(value, label, optgroup) {
938 optgroup: optgroup || null
942 filter.has_optgroup = true;
944 // register optgroup if needed
945 if (!this.settings.optgroups[optgroup]) {
946 this.settings.optgroups[optgroup] = optgroup;
951 if (filter.has_optgroup) {
952 filter.values = Utils.groupSort(cleanValues, 'optgroup');
955 filter.values = cleanValues;
958 if (filter.placeholder) {
959 if (filter.placeholder_value === undefined) {
960 filter.placeholder_value = -1;
963 filter.values.forEach(function(entry) {
964 if (entry.value == filter.placeholder_value) {
965 Utils.error('Config', 'Placeholder of filter "{0}" overlaps with one of its values', filter.id);
973 if (this.settings.sort_filters) {
974 if (typeof this.settings.sort_filters == 'function') {
975 filters.sort(this.settings.sort_filters);
979 filters.sort(function(a, b) {
980 return self.translate(a.label).localeCompare(self.translate(b.label));
985 if (this.status.has_optgroup) {
986 filters = Utils.groupSort(filters, 'optgroup');
993 * Checks the configuration of each operator
994 * @param {QueryBuilder.Operator[]} operators
995 * @returns {QueryBuilder.Operator[]}
996 * @throws ConfigError
998 QueryBuilder.prototype.checkOperators = function(operators) {
999 var definedOperators = [];
1001 operators.forEach(function(operator, i) {
1002 if (typeof operator == 'string') {
1003 if (!QueryBuilder.OPERATORS[operator]) {
1004 Utils.error('Config', 'Unknown operator "{0}"', operator);
1007 operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator]);
1010 if (!operator.type) {
1011 Utils.error('Config', 'Missing "type" for operator {0}', i);
1014 if (QueryBuilder.OPERATORS[operator.type]) {
1015 operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator.type], operator);
1018 if (operator.nb_inputs === undefined || operator.apply_to === undefined) {
1019 Utils.error('Config', 'Missing "nb_inputs" and/or "apply_to" for operator "{0}"', operator.type);
1023 if (definedOperators.indexOf(operator.type) != -1) {
1024 Utils.error('Config', 'Operator "{0}" already defined', operator.type);
1026 definedOperators.push(operator.type);
1028 if (!operator.optgroup) {
1029 operator.optgroup = null;
1032 this.status.has_operator_optgroup = true;
1034 // register optgroup if needed
1035 if (!this.settings.optgroups[operator.optgroup]) {
1036 this.settings.optgroups[operator.optgroup] = operator.optgroup;
1041 if (this.status.has_operator_optgroup) {
1042 operators = Utils.groupSort(operators, 'optgroup');
1049 * Adds all events listeners to the builder
1052 QueryBuilder.prototype.bindEvents = function() {
1054 var Selectors = QueryBuilder.selectors;
1056 // group condition change
1057 this.$el.on('change.queryBuilder', Selectors.group_condition, function() {
1058 if ($(this).is(':checked')) {
1059 var $group = $(this).closest(Selectors.group_container);
1060 self.getModel($group).condition = $(this).val();
1064 // rule filter change
1065 this.$el.on('change.queryBuilder', Selectors.rule_filter, function() {
1066 var $rule = $(this).closest(Selectors.rule_container);
1067 self.getModel($rule).filter = self.getFilterById($(this).val());
1070 // rule operator change
1071 this.$el.on('change.queryBuilder', Selectors.rule_operator, function() {
1072 var $rule = $(this).closest(Selectors.rule_container);
1073 self.getModel($rule).operator = self.getOperatorByType($(this).val());
1077 this.$el.on('click.queryBuilder', Selectors.add_rule, function() {
1078 var $group = $(this).closest(Selectors.group_container);
1079 self.addRule(self.getModel($group));
1082 // delete rule button
1083 this.$el.on('click.queryBuilder', Selectors.delete_rule, function() {
1084 var $rule = $(this).closest(Selectors.rule_container);
1085 self.deleteRule(self.getModel($rule));
1088 if (this.settings.allow_groups !== 0) {
1090 this.$el.on('click.queryBuilder', Selectors.add_group, function() {
1091 var $group = $(this).closest(Selectors.group_container);
1092 self.addGroup(self.getModel($group));
1095 // delete group button
1096 this.$el.on('click.queryBuilder', Selectors.delete_group, function() {
1097 var $group = $(this).closest(Selectors.group_container);
1098 self.deleteGroup(self.getModel($group));
1104 'drop': function(e, node) {
1106 self.refreshGroupsConditions();
1108 'add': function(e, parent, node, index) {
1110 node.$el.prependTo(parent.$el.find('>' + QueryBuilder.selectors.rules_list));
1113 node.$el.insertAfter(parent.rules[index - 1].$el);
1115 self.refreshGroupsConditions();
1117 'move': function(e, node, group, index) {
1121 node.$el.prependTo(group.$el.find('>' + QueryBuilder.selectors.rules_list));
1124 node.$el.insertAfter(group.rules[index - 1].$el);
1126 self.refreshGroupsConditions();
1128 'update': function(e, node, field, value, oldValue) {
1129 if (node instanceof Rule) {
1132 self.updateError(node);
1136 self.applyRuleFlags(node);
1140 self.updateRuleFilter(node, oldValue);
1144 self.updateRuleOperator(node, oldValue);
1148 self.updateRuleValue(node, oldValue);
1155 self.updateError(node);
1159 self.applyGroupFlags(node);
1163 self.updateGroupCondition(node, oldValue);
1172 * Creates the root group
1173 * @param {boolean} [addRule=true] - adds a default empty rule
1174 * @param {object} [data] - group custom data
1175 * @param {object} [flags] - flags to apply to the group
1176 * @returns {Group} root group
1177 * @fires QueryBuilder.afterAddGroup
1179 QueryBuilder.prototype.setRoot = function(addRule, data, flags) {
1180 addRule = (addRule === undefined || addRule === true);
1182 var group_id = this.nextGroupId();
1183 var $group = $(this.getGroupTemplate(group_id, 1));
1185 this.$el.append($group);
1186 this.model.root = new Group(null, $group);
1187 this.model.root.model = this.model;
1189 this.model.root.data = data;
1190 this.model.root.flags = $.extend({}, this.settings.default_group_flags, flags);
1191 this.model.root.condition = this.settings.default_condition;
1193 this.trigger('afterAddGroup', this.model.root);
1196 this.addRule(this.model.root);
1199 return this.model.root;
1204 * @param {Group} parent
1205 * @param {boolean} [addRule=true] - adds a default empty rule
1206 * @param {object} [data] - group custom data
1207 * @param {object} [flags] - flags to apply to the group
1209 * @fires QueryBuilder.beforeAddGroup
1210 * @fires QueryBuilder.afterAddGroup
1212 QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) {
1213 addRule = (addRule === undefined || addRule === true);
1215 var level = parent.level + 1;
1218 * Just before adding a group, can be prevented.
1219 * @event beforeAddGroup
1220 * @memberof QueryBuilder
1221 * @param {Group} parent
1222 * @param {boolean} addRule - if an empty rule will be added in the group
1223 * @param {int} level - nesting level of the group, 1 is the root group
1225 var e = this.trigger('beforeAddGroup', parent, addRule, level);
1226 if (e.isDefaultPrevented()) {
1230 var group_id = this.nextGroupId();
1231 var $group = $(this.getGroupTemplate(group_id, level));
1232 var model = parent.addGroup($group);
1235 model.flags = $.extend({}, this.settings.default_group_flags, flags);
1236 model.condition = this.settings.default_condition;
1239 * Just after adding a group
1240 * @event afterAddGroup
1241 * @memberof QueryBuilder
1242 * @param {Group} group
1244 this.trigger('afterAddGroup', model);
1247 * After any change in the rules
1248 * @event rulesChanged
1249 * @memberof QueryBuilder
1251 this.trigger('rulesChanged');
1254 this.addRule(model);
1261 * Tries to delete a group. The group is not deleted if at least one rule is flagged `no_delete`.
1262 * @param {Group} group
1263 * @returns {boolean} if the group has been deleted
1264 * @fires QueryBuilder.beforeDeleteGroup
1265 * @fires QueryBuilder.afterDeleteGroup
1267 QueryBuilder.prototype.deleteGroup = function(group) {
1268 if (group.isRoot()) {
1273 * Just before deleting a group, can be prevented
1274 * @event beforeDeleteGroup
1275 * @memberof QueryBuilder
1276 * @param {Group} parent
1278 var e = this.trigger('beforeDeleteGroup', group);
1279 if (e.isDefaultPrevented()) {
1285 group.each('reverse', function(rule) {
1286 del &= this.deleteRule(rule);
1287 }, function(group) {
1288 del &= this.deleteGroup(group);
1295 * Just after deleting a group
1296 * @event afterDeleteGroup
1297 * @memberof QueryBuilder
1299 this.trigger('afterDeleteGroup');
1301 this.trigger('rulesChanged');
1308 * Performs actions when a group's condition changes
1309 * @param {Group} group
1310 * @param {object} previousCondition
1311 * @fires QueryBuilder.afterUpdateGroupCondition
1314 QueryBuilder.prototype.updateGroupCondition = function(group, previousCondition) {
1315 group.$el.find('>' + QueryBuilder.selectors.group_condition).each(function() {
1316 var $this = $(this);
1317 $this.prop('checked', $this.val() === group.condition);
1318 $this.parent().toggleClass('active', $this.val() === group.condition);
1322 * After the group condition has been modified
1323 * @event afterUpdateGroupCondition
1324 * @memberof QueryBuilder
1325 * @param {Group} group
1326 * @param {object} previousCondition
1328 this.trigger('afterUpdateGroupCondition', group, previousCondition);
1330 this.trigger('rulesChanged');
1334 * Updates the visibility of conditions based on number of rules inside each group
1337 QueryBuilder.prototype.refreshGroupsConditions = function() {
1338 (function walk(group) {
1339 if (!group.flags || (group.flags && !group.flags.condition_readonly)) {
1340 group.$el.find('>' + QueryBuilder.selectors.group_condition).prop('disabled', group.rules.length <= 1)
1341 .parent().toggleClass('disabled', group.rules.length <= 1);
1344 group.each(null, function(group) {
1347 }(this.model.root));
1352 * @param {Group} parent
1353 * @param {object} [data] - rule custom data
1354 * @param {object} [flags] - flags to apply to the rule
1356 * @fires QueryBuilder.beforeAddRule
1357 * @fires QueryBuilder.afterAddRule
1358 * @fires QueryBuilder.changer:getDefaultFilter
1360 QueryBuilder.prototype.addRule = function(parent, data, flags) {
1362 * Just before adding a rule, can be prevented
1363 * @event beforeAddRule
1364 * @memberof QueryBuilder
1365 * @param {Group} parent
1367 var e = this.trigger('beforeAddRule', parent);
1368 if (e.isDefaultPrevented()) {
1372 var rule_id = this.nextRuleId();
1373 var $rule = $(this.getRuleTemplate(rule_id));
1374 var model = parent.addRule($rule);
1377 model.flags = $.extend({}, this.settings.default_rule_flags, flags);
1380 * Just after adding a rule
1381 * @event afterAddRule
1382 * @memberof QueryBuilder
1383 * @param {Rule} rule
1385 this.trigger('afterAddRule', model);
1387 this.trigger('rulesChanged');
1389 this.createRuleFilters(model);
1391 if (this.settings.default_filter || !this.settings.display_empty_filter) {
1393 * Modifies the default filter for a rule
1394 * @event changer:getDefaultFilter
1395 * @memberof QueryBuilder
1396 * @param {QueryBuilder.Filter} filter
1397 * @param {Rule} rule
1398 * @returns {QueryBuilder.Filter}
1400 model.filter = this.change('getDefaultFilter',
1401 this.getFilterById(this.settings.default_filter || this.filters[0].id),
1410 * Tries to delete a rule
1411 * @param {Rule} rule
1412 * @returns {boolean} if the rule has been deleted
1413 * @fires QueryBuilder.beforeDeleteRule
1414 * @fires QueryBuilder.afterDeleteRule
1416 QueryBuilder.prototype.deleteRule = function(rule) {
1417 if (rule.flags.no_delete) {
1422 * Just before deleting a rule, can be prevented
1423 * @event beforeDeleteRule
1424 * @memberof QueryBuilder
1425 * @param {Rule} rule
1427 var e = this.trigger('beforeDeleteRule', rule);
1428 if (e.isDefaultPrevented()) {
1435 * Just after deleting a rule
1436 * @event afterDeleteRule
1437 * @memberof QueryBuilder
1439 this.trigger('afterDeleteRule');
1441 this.trigger('rulesChanged');
1447 * Creates the filters for a rule
1448 * @param {Rule} rule
1449 * @fires QueryBuilder.changer:getRuleFilters
1450 * @fires QueryBuilder.afterCreateRuleFilters
1453 QueryBuilder.prototype.createRuleFilters = function(rule) {
1455 * Modifies the list a filters available for a rule
1456 * @event changer:getRuleFilters
1457 * @memberof QueryBuilder
1458 * @param {QueryBuilder.Filter[]} filters
1459 * @param {Rule} rule
1460 * @returns {QueryBuilder.Filter[]}
1462 var filters = this.change('getRuleFilters', this.filters, rule);
1463 var $filterSelect = $(this.getRuleFilterSelect(rule, filters));
1465 rule.$el.find(QueryBuilder.selectors.filter_container).html($filterSelect);
1468 * After creating the dropdown for filters
1469 * @event afterCreateRuleFilters
1470 * @memberof QueryBuilder
1471 * @param {Rule} rule
1473 this.trigger('afterCreateRuleFilters', rule);
1475 this.applyRuleFlags(rule);
1479 * Creates the operators for a rule and init the rule operator
1480 * @param {Rule} rule
1481 * @fires QueryBuilder.afterCreateRuleOperators
1484 QueryBuilder.prototype.createRuleOperators = function(rule) {
1485 var $operatorContainer = rule.$el.find(QueryBuilder.selectors.operator_container).empty();
1491 var operators = this.getOperators(rule.filter);
1492 var $operatorSelect = $(this.getRuleOperatorSelect(rule, operators));
1494 $operatorContainer.html($operatorSelect);
1496 // set the operator without triggering update event
1497 if (rule.filter.default_operator) {
1498 rule.__.operator = this.getOperatorByType(rule.filter.default_operator);
1501 rule.__.operator = operators[0];
1504 rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
1507 * After creating the dropdown for operators
1508 * @event afterCreateRuleOperators
1509 * @memberof QueryBuilder
1510 * @param {Rule} rule
1511 * @param {QueryBuilder.Operator[]} operators - allowed operators for this rule
1513 this.trigger('afterCreateRuleOperators', rule, operators);
1515 this.applyRuleFlags(rule);
1519 * Creates the main input for a rule
1520 * @param {Rule} rule
1521 * @fires QueryBuilder.afterCreateRuleInput
1524 QueryBuilder.prototype.createRuleInput = function(rule) {
1525 var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container).empty();
1527 rule.__.value = undefined;
1529 if (!rule.filter || !rule.operator || rule.operator.nb_inputs === 0) {
1535 var filter = rule.filter;
1537 if(filter.type === 'map') {
1538 for (var i = 0; i < 2; i++) {
1539 var $ruleInput = $(this.getRuleInput(rule, i));
1540 if (i > 0) $valueContainer.append('|');
1541 $valueContainer.append($ruleInput);
1542 $inputs = $inputs.add($ruleInput);
1545 for (var i = 0; i < rule.operator.nb_inputs; i++) {
1546 var $ruleInput = $(this.getRuleInput(rule, i));
1547 if (i > 0) $valueContainer.append(this.settings.inputs_separator);
1548 $valueContainer.append($ruleInput);
1549 $inputs = $inputs.add($ruleInput);
1553 $valueContainer.css('display', '');
1555 $inputs.on('change ' + (filter.input_event || ''), function() {
1556 if (!rule._updating_input) {
1557 rule._updating_value = true;
1558 rule.value = self.getRuleInputValue(rule);
1559 rule._updating_value = false;
1563 if (filter.plugin) {
1564 $inputs[filter.plugin](filter.plugin_config || {});
1568 * After creating the input for a rule and initializing optional plugin
1569 * @event afterCreateRuleInput
1570 * @memberof QueryBuilder
1571 * @param {Rule} rule
1573 this.trigger('afterCreateRuleInput', rule);
1575 if (filter.default_value !== undefined) {
1576 rule.value = filter.default_value;
1579 rule._updating_value = true;
1580 rule.value = self.getRuleInputValue(rule);
1581 rule._updating_value = false;
1584 this.applyRuleFlags(rule);
1588 * Performs action when a rule's filter changes
1589 * @param {Rule} rule
1590 * @param {object} previousFilter
1591 * @fires QueryBuilder.afterUpdateRuleFilter
1594 QueryBuilder.prototype.updateRuleFilter = function(rule, previousFilter) {
1595 this.createRuleOperators(rule);
1596 this.createRuleInput(rule);
1598 rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
1600 // clear rule data if the filter changed
1601 if (previousFilter && rule.filter && previousFilter.id !== rule.filter.id) {
1602 rule.data = undefined;
1606 * After the filter has been updated and the operators and input re-created
1607 * @event afterUpdateRuleFilter
1608 * @memberof QueryBuilder
1609 * @param {Rule} rule
1610 * @param {object} previousFilter
1612 this.trigger('afterUpdateRuleFilter', rule, previousFilter);
1614 this.trigger('rulesChanged');
1618 * Performs actions when a rule's operator changes
1619 * @param {Rule} rule
1620 * @param {object} previousOperator
1621 * @fires QueryBuilder.afterUpdateRuleOperator
1624 QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) {
1625 var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container);
1627 if (!rule.operator || rule.operator.nb_inputs === 0) {
1628 $valueContainer.hide();
1630 rule.__.value = undefined;
1633 $valueContainer.css('display', '');
1635 if ($valueContainer.is(':empty') || !previousOperator ||
1636 rule.operator.nb_inputs !== previousOperator.nb_inputs ||
1637 rule.operator.optgroup !== previousOperator.optgroup
1639 this.createRuleInput(rule);
1643 if (rule.operator) {
1644 rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
1646 // refresh value if the format changed for this operator
1647 rule.__.value = this.getRuleInputValue(rule);
1651 * After the operator has been updated and the input optionally re-created
1652 * @event afterUpdateRuleOperator
1653 * @memberof QueryBuilder
1654 * @param {Rule} rule
1655 * @param {object} previousOperator
1657 this.trigger('afterUpdateRuleOperator', rule, previousOperator);
1659 this.trigger('rulesChanged');
1663 * Performs actions when rule's value changes
1664 * @param {Rule} rule
1665 * @param {object} previousValue
1666 * @fires QueryBuilder.afterUpdateRuleValue
1669 QueryBuilder.prototype.updateRuleValue = function(rule, previousValue) {
1670 if (!rule._updating_value) {
1671 this.setRuleInputValue(rule, rule.value);
1675 * After the rule value has been modified
1676 * @event afterUpdateRuleValue
1677 * @memberof QueryBuilder
1678 * @param {Rule} rule
1679 * @param {*} previousValue
1681 this.trigger('afterUpdateRuleValue', rule, previousValue);
1683 this.trigger('rulesChanged');
1687 * Changes a rule's properties depending on its flags
1688 * @param {Rule} rule
1689 * @fires QueryBuilder.afterApplyRuleFlags
1692 QueryBuilder.prototype.applyRuleFlags = function(rule) {
1693 var flags = rule.flags;
1694 var Selectors = QueryBuilder.selectors;
1696 rule.$el.find(Selectors.rule_filter).prop('disabled', flags.filter_readonly);
1697 rule.$el.find(Selectors.rule_operator).prop('disabled', flags.operator_readonly);
1698 rule.$el.find(Selectors.rule_value).prop('disabled', flags.value_readonly);
1700 if (flags.no_delete) {
1701 rule.$el.find(Selectors.delete_rule).remove();
1705 * After rule's flags has been applied
1706 * @event afterApplyRuleFlags
1707 * @memberof QueryBuilder
1708 * @param {Rule} rule
1710 this.trigger('afterApplyRuleFlags', rule);
1714 * Changes group's properties depending on its flags
1715 * @param {Group} group
1716 * @fires QueryBuilder.afterApplyGroupFlags
1719 QueryBuilder.prototype.applyGroupFlags = function(group) {
1720 var flags = group.flags;
1721 var Selectors = QueryBuilder.selectors;
1723 group.$el.find('>' + Selectors.group_condition).prop('disabled', flags.condition_readonly)
1724 .parent().toggleClass('readonly', flags.condition_readonly);
1726 if (flags.no_add_rule) {
1727 group.$el.find(Selectors.add_rule).remove();
1729 if (flags.no_add_group) {
1730 group.$el.find(Selectors.add_group).remove();
1732 if (flags.no_delete) {
1733 group.$el.find(Selectors.delete_group).remove();
1737 * After group's flags has been applied
1738 * @event afterApplyGroupFlags
1739 * @memberof QueryBuilder
1740 * @param {Group} group
1742 this.trigger('afterApplyGroupFlags', group);
1746 * Clears all errors markers
1747 * @param {Node} [node] default is root Group
1749 QueryBuilder.prototype.clearErrors = function(node) {
1750 node = node || this.model.root;
1758 if (node instanceof Group) {
1759 node.each(function(rule) {
1761 }, function(group) {
1762 this.clearErrors(group);
1768 * Adds/Removes error on a Rule or Group
1769 * @param {Node} node
1770 * @fires QueryBuilder.changer:displayError
1773 QueryBuilder.prototype.updateError = function(node) {
1774 if (this.settings.display_errors) {
1775 if (node.error === null) {
1776 node.$el.removeClass('has-error');
1779 var errorMessage = this.translate('errors', node.error[0]);
1780 errorMessage = Utils.fmt(errorMessage, node.error.slice(1));
1783 * Modifies an error message before display
1784 * @event changer:displayError
1785 * @memberof QueryBuilder
1786 * @param {string} errorMessage - the error message (translated and formatted)
1787 * @param {array} error - the raw error array (error code and optional arguments)
1788 * @param {Node} node
1791 errorMessage = this.change('displayError', errorMessage, node.error, node);
1793 node.$el.addClass('has-error')
1794 .find(QueryBuilder.selectors.error_container).eq(0)
1795 .attr('title', errorMessage);
1801 * Triggers a validation error event
1802 * @param {Node} node
1803 * @param {string|array} error
1805 * @fires QueryBuilder.validationError
1808 QueryBuilder.prototype.triggerValidationError = function(node, error, value) {
1809 if (!$.isArray(error)) {
1814 * Fired when a validation error occurred, can be prevented
1815 * @event validationError
1816 * @memberof QueryBuilder
1817 * @param {Node} node
1818 * @param {string} error
1821 var e = this.trigger('validationError', node, error, value);
1822 if (!e.isDefaultPrevented()) {
1829 * Destroys the builder
1830 * @fires QueryBuilder.beforeDestroy
1832 QueryBuilder.prototype.destroy = function() {
1834 * Before the {@link QueryBuilder#destroy} method
1835 * @event beforeDestroy
1836 * @memberof QueryBuilder
1838 this.trigger('beforeDestroy');
1840 if (this.status.generated_id) {
1841 this.$el.removeAttr('id');
1848 .off('.queryBuilder')
1849 .removeClass('query-builder')
1850 .removeData('queryBuilder');
1852 delete this.$el[0].queryBuilder;
1856 * Clear all rules and resets the root group
1857 * @fires QueryBuilder.beforeReset
1858 * @fires QueryBuilder.afterReset
1860 QueryBuilder.prototype.reset = function() {
1862 * Before the {@link QueryBuilder#reset} method, can be prevented
1863 * @event beforeReset
1864 * @memberof QueryBuilder
1866 var e = this.trigger('beforeReset');
1867 if (e.isDefaultPrevented()) {
1871 this.status.group_id = 1;
1872 this.status.rule_id = 0;
1874 this.model.root.empty();
1876 this.model.root.data = undefined;
1877 this.model.root.flags = $.extend({}, this.settings.default_group_flags);
1878 this.model.root.condition = this.settings.default_condition;
1880 this.addRule(this.model.root);
1883 * After the {@link QueryBuilder#reset} method
1885 * @memberof QueryBuilder
1887 this.trigger('afterReset');
1889 this.trigger('rulesChanged');
1893 * Clears all rules and removes the root group
1894 * @fires QueryBuilder.beforeClear
1895 * @fires QueryBuilder.afterClear
1897 QueryBuilder.prototype.clear = function() {
1899 * Before the {@link QueryBuilder#clear} method, can be prevented
1900 * @event beforeClear
1901 * @memberof QueryBuilder
1903 var e = this.trigger('beforeClear');
1904 if (e.isDefaultPrevented()) {
1908 this.status.group_id = 0;
1909 this.status.rule_id = 0;
1911 if (this.model.root) {
1912 this.model.root.drop();
1913 this.model.root = null;
1917 * After the {@link QueryBuilder#clear} method
1919 * @memberof QueryBuilder
1921 this.trigger('afterClear');
1923 this.trigger('rulesChanged');
1927 * Modifies the builder configuration.<br>
1928 * Only options defined in QueryBuilder.modifiable_options are modifiable
1929 * @param {object} options
1931 QueryBuilder.prototype.setOptions = function(options) {
1932 $.each(options, function(opt, value) {
1933 if (QueryBuilder.modifiable_options.indexOf(opt) !== -1) {
1934 this.settings[opt] = value;
1940 * Returns the model associated to a DOM object, or the root model
1941 * @param {jQuery} [target]
1944 QueryBuilder.prototype.getModel = function(target) {
1946 return this.model.root;
1948 else if (target instanceof Node) {
1952 return $(target).data('queryBuilderModel');
1957 * Validates the whole builder
1958 * @param {object} [options]
1959 * @param {boolean} [options.skip_empty=false] - skips validating rules that have no filter selected
1960 * @returns {boolean}
1961 * @fires QueryBuilder.changer:validate
1963 QueryBuilder.prototype.validate = function(options) {
1964 options = $.extend({
1972 var valid = (function parse(group) {
1976 group.each(function(rule) {
1977 if (!rule.filter && options.skip_empty) {
1982 self.triggerValidationError(rule, 'no_filter', null);
1987 if (!rule.operator) {
1988 self.triggerValidationError(rule, 'no_operator', null);
1993 if (rule.operator.nb_inputs !== 0) {
1994 var valid = self.validateValue(rule, rule.value);
1996 if (valid !== true) {
1997 self.triggerValidationError(rule, valid, rule.value);
2005 }, function(group) {
2006 var res = parse(group);
2010 else if (res === false) {
2018 else if (done === 0 && !group.isRoot() && options.skip_empty) {
2021 else if (done === 0 && (!self.settings.allow_empty || !group.isRoot())) {
2022 self.triggerValidationError(group, 'empty_group', null);
2028 }(this.model.root));
2031 * Modifies the result of the {@link QueryBuilder#validate} method
2032 * @event changer:validate
2033 * @memberof QueryBuilder
2034 * @param {boolean} valid
2035 * @returns {boolean}
2037 return this.change('validate', valid);
2041 * Gets an object representing current rules
2042 * @param {object} [options]
2043 * @param {boolean|string} [options.get_flags=false] - export flags, true: only changes from default flags or 'all'
2044 * @param {boolean} [options.allow_invalid=false] - returns rules even if they are invalid
2045 * @param {boolean} [options.skip_empty=false] - remove rules that have no filter selected
2047 * @fires QueryBuilder.changer:ruleToJson
2048 * @fires QueryBuilder.changer:groupToJson
2049 * @fires QueryBuilder.changer:getRules
2051 QueryBuilder.prototype.getRules = function(options) {
2052 options = $.extend({
2054 allow_invalid: false,
2058 var valid = this.validate(options);
2059 if (!valid && !options.allow_invalid) {
2065 var out = (function parse(group) {
2067 condition: group.condition,
2072 groupData.data = $.extendext(true, 'replace', {}, group.data);
2075 if (options.get_flags) {
2076 var flags = self.getGroupFlags(group.flags, options.get_flags === 'all');
2077 if (!$.isEmptyObject(flags)) {
2078 groupData.flags = flags;
2082 group.each(function(rule) {
2083 if (!rule.filter && options.skip_empty) {
2088 if (!rule.operator || rule.operator.nb_inputs !== 0) {
2093 id: rule.filter ? rule.filter.id : null,
2094 field: rule.filter ? rule.filter.field : null,
2095 type: rule.filter ? rule.filter.type : null,
2096 input: rule.filter ? rule.filter.input : null,
2097 operator: rule.operator ? rule.operator.type : null,
2101 if (rule.filter && rule.filter.data || rule.data) {
2102 ruleData.data = $.extendext(true, 'replace', {}, rule.filter.data, rule.data);
2105 if (options.get_flags) {
2106 var flags = self.getRuleFlags(rule.flags, options.get_flags === 'all');
2107 if (!$.isEmptyObject(flags)) {
2108 ruleData.flags = flags;
2113 * Modifies the JSON generated from a Rule object
2114 * @event changer:ruleToJson
2115 * @memberof QueryBuilder
2116 * @param {object} json
2117 * @param {Rule} rule
2120 groupData.rules.push(self.change('ruleToJson', ruleData, rule));
2122 }, function(model) {
2123 var data = parse(model);
2124 if (data.rules.length !== 0 || !options.skip_empty) {
2125 groupData.rules.push(data);
2130 * Modifies the JSON generated from a Group object
2131 * @event changer:groupToJson
2132 * @memberof QueryBuilder
2133 * @param {object} json
2134 * @param {Group} group
2137 return self.change('groupToJson', groupData, group);
2139 }(this.model.root));
2144 * Modifies the result of the {@link QueryBuilder#getRules} method
2145 * @event changer:getRules
2146 * @memberof QueryBuilder
2147 * @param {object} json
2150 return this.change('getRules', out);
2154 * Sets rules from object
2155 * @param {object} data
2156 * @param {object} [options]
2157 * @param {boolean} [options.allow_invalid=false] - silent-fail if the data are invalid
2158 * @throws RulesError, UndefinedConditionError
2159 * @fires QueryBuilder.changer:setRules
2160 * @fires QueryBuilder.changer:jsonToRule
2161 * @fires QueryBuilder.changer:jsonToGroup
2162 * @fires QueryBuilder.afterSetRules
2164 QueryBuilder.prototype.setRules = function(data, options) {
2165 options = $.extend({
2166 allow_invalid: false
2169 if ($.isArray(data)) {
2171 condition: this.settings.default_condition,
2176 if (!data || !data.rules || (data.rules.length === 0 && !this.settings.allow_empty)) {
2177 Utils.error('RulesParse', 'Incorrect data object passed');
2181 this.setRoot(false, data.data, this.parseGroupFlags(data));
2184 * Modifies data before the {@link QueryBuilder#setRules} method
2185 * @event changer:setRules
2186 * @memberof QueryBuilder
2187 * @param {object} json
2188 * @param {object} options
2191 data = this.change('setRules', data, options);
2195 (function add(data, group) {
2196 if (group === null) {
2200 if (data.condition === undefined) {
2201 data.condition = self.settings.default_condition;
2203 else if (self.settings.conditions.indexOf(data.condition) == -1) {
2204 Utils.error(!options.allow_invalid, 'UndefinedCondition', 'Invalid condition "{0}"', data.condition);
2205 data.condition = self.settings.default_condition;
2208 group.condition = data.condition;
2210 data.rules.forEach(function(item) {
2213 if (item.rules !== undefined) {
2214 if (self.settings.allow_groups !== -1 && self.settings.allow_groups < group.level) {
2215 Utils.error(!options.allow_invalid, 'RulesParse', 'No more than {0} groups are allowed', self.settings.allow_groups);
2219 model = self.addGroup(group, false, item.data, self.parseGroupFlags(item));
2220 if (model === null) {
2229 if (item.id === undefined) {
2230 Utils.error(!options.allow_invalid, 'RulesParse', 'Missing rule field id');
2233 if (item.operator === undefined) {
2234 item.operator = 'equal';
2238 model = self.addRule(group, item.data, self.parseRuleFlags(item));
2239 if (model === null) {
2244 model.filter = self.getFilterById(item.id, !options.allow_invalid);
2248 model.operator = self.getOperatorByType(item.operator, !options.allow_invalid);
2250 if (!model.operator) {
2251 model.operator = self.getOperators(model.filter)[0];
2255 if (model.operator && model.operator.nb_inputs !== 0) {
2256 if (item.value !== undefined) {
2257 if(model.filter.type === 'map') {
2258 model.value = item.value.split('|')
2259 } else if (model.filter.type === 'datetime') {
2260 if (!('moment' in window)) {
2261 Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
2263 model.value = moment(item.value * 1000).format('YYYY/MM/DD HH:mm:ss');
2265 model.value = item.value;
2268 else if (model.filter.default_value !== undefined) {
2269 model.value = model.filter.default_value;
2274 * Modifies the Rule object generated from the JSON
2275 * @event changer:jsonToRule
2276 * @memberof QueryBuilder
2277 * @param {Rule} rule
2278 * @param {object} json
2279 * @returns {Rule} the same rule
2281 if (self.change('jsonToRule', model, item) != model) {
2282 Utils.error('RulesParse', 'Plugin tried to change rule reference');
2288 * Modifies the Group object generated from the JSON
2289 * @event changer:jsonToGroup
2290 * @memberof QueryBuilder
2291 * @param {Group} group
2292 * @param {object} json
2293 * @returns {Group} the same group
2295 if (self.change('jsonToGroup', group, data) != group) {
2296 Utils.error('RulesParse', 'Plugin tried to change group reference');
2299 }(data, this.model.root));
2302 * After the {@link QueryBuilder#setRules} method
2303 * @event afterSetRules
2304 * @memberof QueryBuilder
2306 this.trigger('afterSetRules');
2311 * Performs value validation
2312 * @param {Rule} rule
2313 * @param {string|string[]} value
2314 * @returns {array|boolean} true or error array
2315 * @fires QueryBuilder.changer:validateValue
2317 QueryBuilder.prototype.validateValue = function(rule, value) {
2318 var validation = rule.filter.validation || {};
2321 if (validation.callback) {
2322 result = validation.callback.call(this, value, rule);
2325 result = this._validateValue(rule, value);
2329 * Modifies the result of the rule validation method
2330 * @event changer:validateValue
2331 * @memberof QueryBuilder
2332 * @param {array|boolean} result - true or an error array
2334 * @param {Rule} rule
2335 * @returns {array|boolean}
2337 return this.change('validateValue', result, value, rule);
2341 * Default validation function
2342 * @param {Rule} rule
2343 * @param {string|string[]} value
2344 * @returns {array|boolean} true or error array
2345 * @throws ConfigError
2348 QueryBuilder.prototype._validateValue = function(rule, value) {
2349 var filter = rule.filter;
2350 var operator = rule.operator;
2351 var validation = filter.validation || {};
2354 var numOfInputs = operator.nb_inputs;
2355 if(filter.type === 'map') {
2359 if (numOfInputs === 1) {
2363 for (var i = 0; i < numOfInputs; i++) {
2364 if (!operator.multiple && $.isArray(value[i]) && value[i].length > 1) {
2365 result = ['operator_not_multiple', operator.type, this.translate('operators', operator.type)];
2369 switch (filter.input) {
2371 if (value[i] === undefined || value[i].length === 0) {
2372 if (!validation.allow_empty_value) {
2373 result = ['radio_empty'];
2380 if (value[i] === undefined || value[i].length === 0) {
2381 if (!validation.allow_empty_value) {
2382 result = ['checkbox_empty'];
2389 if (value[i] === undefined || value[i].length === 0 || (filter.placeholder && value[i] == filter.placeholder_value)) {
2390 if (!validation.allow_empty_value) {
2391 result = ['select_empty'];
2398 tempValue = $.isArray(value[i]) ? value[i] : [value[i]];
2400 for (var j = 0; j < tempValue.length; j++) {
2401 switch (QueryBuilder.types[filter.type]) {
2404 if (tempValue[j] === undefined || tempValue[j].length === 0) {
2405 if (!validation.allow_empty_value) {
2406 result = ['string_empty'];
2410 if (validation.min !== undefined) {
2411 if (tempValue[j].length < parseInt(validation.min)) {
2412 result = [this.getValidationMessage(validation, 'min', 'string_exceed_min_length'), validation.min];
2416 if (validation.max !== undefined) {
2417 if (tempValue[j].length > parseInt(validation.max)) {
2418 result = [this.getValidationMessage(validation, 'max', 'string_exceed_max_length'), validation.max];
2422 if (validation.format) {
2423 if (typeof validation.format == 'string') {
2424 validation.format = new RegExp(validation.format);
2426 if (!validation.format.test(tempValue[j])) {
2427 result = [this.getValidationMessage(validation, 'format', 'string_invalid_format'), validation.format];
2434 if (tempValue[j] === undefined || tempValue[j].length === 0) {
2435 if (!validation.allow_empty_value) {
2436 result = ['number_nan'];
2440 if (isNaN(tempValue[j])) {
2441 result = ['number_nan'];
2444 if (filter.type == 'integer') {
2445 if (parseInt(tempValue[j]) != tempValue[j]) {
2446 result = ['number_not_integer'];
2451 if (parseFloat(tempValue[j]) != tempValue[j]) {
2452 result = ['number_not_double'];
2456 if (validation.min !== undefined) {
2457 if (tempValue[j] < parseFloat(validation.min)) {
2458 result = [this.getValidationMessage(validation, 'min', 'number_exceed_min'), validation.min];
2462 if (validation.max !== undefined) {
2463 if (tempValue[j] > parseFloat(validation.max)) {
2464 result = [this.getValidationMessage(validation, 'max', 'number_exceed_max'), validation.max];
2468 if (validation.step !== undefined && validation.step !== 'any') {
2469 var v = (tempValue[j] / validation.step).toPrecision(14);
2470 if (parseInt(v) != v) {
2471 result = [this.getValidationMessage(validation, 'step', 'number_wrong_step'), validation.step];
2478 if (tempValue[j] === undefined || tempValue[j].length === 0) {
2479 if (!validation.allow_empty_value) {
2480 result = ['datetime_empty'];
2486 if (validation.format) {
2487 if (!('moment' in window)) {
2488 Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
2491 var datetime = moment.utc(tempValue[j], validation.format, true);
2492 if (!datetime.isValid()) {
2493 result = [this.getValidationMessage(validation, 'format', 'datetime_invalid'), validation.format];
2497 if (validation.min) {
2498 if (datetime < moment.utc(validation.min, validation.format, true)) {
2499 result = [this.getValidationMessage(validation, 'min', 'datetime_exceed_min'), validation.min];
2503 if (validation.max) {
2504 if (datetime > moment.utc(validation.max, validation.format, true)) {
2505 result = [this.getValidationMessage(validation, 'max', 'datetime_exceed_max'), validation.max];
2514 if (tempValue[j] === undefined || tempValue[j].length === 0) {
2515 if (!validation.allow_empty_value) {
2516 result = ['boolean_not_valid'];
2520 tmp = ('' + tempValue[j]).trim().toLowerCase();
2521 if (tmp !== 'true' && tmp !== 'false' && tmp !== '1' && tmp !== '0' && tempValue[j] !== 1 && tempValue[j] !== 0) {
2522 result = ['boolean_not_valid'];
2527 if (result !== true) {
2533 if (result !== true) {
2538 if ((rule.operator.type === 'between' || rule.operator.type === 'not_between') && value.length === 2) {
2539 switch (QueryBuilder.types[filter.type]) {
2541 if (value[0] > value[1]) {
2542 result = ['number_between_invalid', value[0], value[1]];
2548 if (validation.format) {
2549 if (!('moment' in window)) {
2550 Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
2553 if (moment.utc(value[0], validation.format, true).isAfter(moment.utc(value[1], validation.format, true))) {
2554 result = ['datetime_between_invalid', value[0], value[1]];
2565 * Returns an incremented group ID
2569 QueryBuilder.prototype.nextGroupId = function() {
2570 return this.status.id + '_group_' + (this.status.group_id++);
2574 * Returns an incremented rule ID
2578 QueryBuilder.prototype.nextRuleId = function() {
2579 return this.status.id + '_rule_' + (this.status.rule_id++);
2583 * Returns the operators for a filter
2584 * @param {string|object} filter - filter id or filter object
2585 * @returns {object[]}
2586 * @fires QueryBuilder.changer:getOperators
2589 QueryBuilder.prototype.getOperators = function(filter) {
2590 if (typeof filter == 'string') {
2591 filter = this.getFilterById(filter);
2596 for (var i = 0, l = this.operators.length; i < l; i++) {
2597 // filter operators check
2598 if (filter.operators) {
2599 if (filter.operators.indexOf(this.operators[i].type) == -1) {
2604 else if (this.operators[i].apply_to.indexOf(QueryBuilder.types[filter.type]) == -1) {
2608 result.push(this.operators[i]);
2611 // keep sort order defined for the filter
2612 if (filter.operators) {
2613 result.sort(function(a, b) {
2614 return filter.operators.indexOf(a.type) - filter.operators.indexOf(b.type);
2619 * Modifies the operators available for a filter
2620 * @event changer:getOperators
2621 * @memberof QueryBuilder
2622 * @param {QueryBuilder.Operator[]} operators
2623 * @param {QueryBuilder.Filter} filter
2624 * @returns {QueryBuilder.Operator[]}
2626 return this.change('getOperators', result, filter);
2630 * Returns a particular filter by its id
2631 * @param {string} id
2632 * @param {boolean} [doThrow=true]
2633 * @returns {object|null}
2634 * @throws UndefinedFilterError
2637 QueryBuilder.prototype.getFilterById = function(id, doThrow) {
2642 for (var i = 0, l = this.filters.length; i < l; i++) {
2643 if (this.filters[i].id == id) {
2644 return this.filters[i];
2648 Utils.error(doThrow !== false, 'UndefinedFilter', 'Undefined filter "{0}"', id);
2654 * Returns a particular operator by its type
2655 * @param {string} type
2656 * @param {boolean} [doThrow=true]
2657 * @returns {object|null}
2658 * @throws UndefinedOperatorError
2661 QueryBuilder.prototype.getOperatorByType = function(type, doThrow) {
2666 for (var i = 0, l = this.operators.length; i < l; i++) {
2667 if (this.operators[i].type == type) {
2668 return this.operators[i];
2672 Utils.error(doThrow !== false, 'UndefinedOperator', 'Undefined operator "{0}"', type);
2678 * Returns rule's current input value
2679 * @param {Rule} rule
2681 * @fires QueryBuilder.changer:getRuleValue
2684 QueryBuilder.prototype.getRuleInputValue = function(rule) {
2685 var filter = rule.filter;
2686 var operator = rule.operator;
2687 var numOfInputs = operator.nb_inputs;
2688 if(filter.type === 'map') {
2693 if (filter.valueGetter) {
2694 value = filter.valueGetter.call(this, rule);
2697 var $value = rule.$el.find(QueryBuilder.selectors.value_container);
2699 for (var i = 0; i < numOfInputs; i++) {
2700 var name = Utils.escapeElementId(rule.id + '_value_' + i);
2703 switch (filter.input) {
2705 value.push($value.find('[name=' + name + ']:checked').val());
2710 // jshint loopfunc:true
2711 $value.find('[name=' + name + ']:checked').each(function() {
2712 tmp.push($(this).val());
2714 // jshint loopfunc:false
2719 if (filter.multiple) {
2721 // jshint loopfunc:true
2722 $value.find('[name=' + name + '] option:selected').each(function() {
2723 tmp.push($(this).val());
2725 // jshint loopfunc:false
2729 value.push($value.find('[name=' + name + '] option:selected').val());
2734 value.push($value.find('[name=' + name + ']').val());
2738 value = value.map(function(val) {
2739 if (operator.multiple && filter.value_separator && typeof val == 'string') {
2740 val = val.split(filter.value_separator);
2743 if ($.isArray(val)) {
2744 return val.map(function(subval) {
2745 return Utils.changeType(subval, filter.type);
2749 return Utils.changeType(val, filter.type);
2753 if (numOfInputs === 1) {
2758 if (filter.valueParser) {
2759 value = filter.valueParser.call(this, rule, value);
2764 * Modifies the rule's value grabbed from the DOM
2765 * @event changer:getRuleValue
2766 * @memberof QueryBuilder
2768 * @param {Rule} rule
2771 return this.change('getRuleValue', value, rule);
2775 * Sets the value of a rule's input
2776 * @param {Rule} rule
2780 QueryBuilder.prototype.setRuleInputValue = function(rule, value) {
2781 var filter = rule.filter;
2782 var operator = rule.operator;
2783 var numOfInputs = operator.nb_inputs;
2784 if(filter.type === 'map') {
2788 if (!filter || !operator) {
2792 rule._updating_input = true;
2794 if (filter.valueSetter) {
2795 filter.valueSetter.call(this, rule, value);
2798 var $value = rule.$el.find(QueryBuilder.selectors.value_container);
2800 if (numOfInputs == 1) {
2804 for (var i = 0; i < numOfInputs; i++) {
2805 var name = Utils.escapeElementId(rule.id + '_value_' + i);
2807 switch (filter.input) {
2809 $value.find('[name=' + name + '][value="' + value[i] + '"]').prop('checked', true).trigger('change');
2813 if (!$.isArray(value[i])) {
2814 value[i] = [value[i]];
2816 // jshint loopfunc:true
2817 value[i].forEach(function(value) {
2818 $value.find('[name=' + name + '][value="' + value + '"]').prop('checked', true).trigger('change');
2820 // jshint loopfunc:false
2824 if (operator.multiple && filter.value_separator && $.isArray(value[i])) {
2825 value[i] = value[i].join(filter.value_separator);
2827 $value.find('[name=' + name + ']').val(value[i]).trigger('change');
2833 rule._updating_input = false;
2838 * @param {object} rule
2840 * @fires QueryBuilder.changer:parseRuleFlags
2843 QueryBuilder.prototype.parseRuleFlags = function(rule) {
2844 var flags = $.extend({}, this.settings.default_rule_flags);
2846 if (rule.readonly) {
2848 filter_readonly: true,
2849 operator_readonly: true,
2850 value_readonly: true,
2856 $.extend(flags, rule.flags);
2860 * Modifies the consolidated rule's flags
2861 * @event changer:parseRuleFlags
2862 * @memberof QueryBuilder
2863 * @param {object} flags
2864 * @param {object} rule - <b>not</b> a Rule object
2867 return this.change('parseRuleFlags', flags, rule);
2871 * Gets a copy of flags of a rule
2872 * @param {object} flags
2873 * @param {boolean} [all=false] - return all flags or only changes from default flags
2877 QueryBuilder.prototype.getRuleFlags = function(flags, all) {
2879 return $.extend({}, flags);
2883 $.each(this.settings.default_rule_flags, function(key, value) {
2884 if (flags[key] !== value) {
2885 ret[key] = flags[key];
2893 * Parses group flags
2894 * @param {object} group
2896 * @fires QueryBuilder.changer:parseGroupFlags
2899 QueryBuilder.prototype.parseGroupFlags = function(group) {
2900 var flags = $.extend({}, this.settings.default_group_flags);
2902 if (group.readonly) {
2904 condition_readonly: true,
2912 $.extend(flags, group.flags);
2916 * Modifies the consolidated group's flags
2917 * @event changer:parseGroupFlags
2918 * @memberof QueryBuilder
2919 * @param {object} flags
2920 * @param {object} group - <b>not</b> a Group object
2923 return this.change('parseGroupFlags', flags, group);
2927 * Gets a copy of flags of a group
2928 * @param {object} flags
2929 * @param {boolean} [all=false] - return all flags or only changes from default flags
2933 QueryBuilder.prototype.getGroupFlags = function(flags, all) {
2935 return $.extend({}, flags);
2939 $.each(this.settings.default_group_flags, function(key, value) {
2940 if (flags[key] !== value) {
2941 ret[key] = flags[key];
2949 * Translate a label either by looking in the `lang` object or in itself if it's an object where keys are language codes
2950 * @param {string} [category]
2951 * @param {string|object} key
2953 * @fires QueryBuilder.changer:translate
2955 QueryBuilder.prototype.translate = function(category, key) {
2958 category = undefined;
2962 if (typeof key === 'object') {
2963 translation = key[this.settings.lang_code] || key['en'];
2966 translation = (category ? this.lang[category] : this.lang)[key] || key;
2970 * Modifies the translated label
2971 * @event changer:translate
2972 * @memberof QueryBuilder
2973 * @param {string} translation
2974 * @param {string|object} key
2975 * @param {string} [category]
2978 return this.change('translate', translation, key, category);
2982 * Returns a validation message
2983 * @param {object} validation
2984 * @param {string} type
2985 * @param {string} def
2989 QueryBuilder.prototype.getValidationMessage = function(validation, type, def) {
2990 return validation.messages && validation.messages[type] || def;
2994 QueryBuilder.templates.group = '\
2995 <div id="{{= it.group_id }}" class="rules-group-container"> \
2996 <div class="rules-group-header"> \
2997 <div class="btn-group pull-right group-actions"> \
2998 <button type="button" class="btn btn-xs btn-clamp" data-add="rule"> \
2999 <i class="{{= it.icons.add_rule }}"></i> {{= it.translate("add_rule") }} \
3001 {{? it.settings.allow_groups===-1 || it.settings.allow_groups>=it.level }} \
3002 <button type="button" class="btn btn-xs btn-clamp" data-add="group"> \
3003 <i class="{{= it.icons.add_group }}"></i> {{= it.translate("add_group") }} \
3007 <button type="button" class="btn btn-xs btn-clamp" data-delete="group"> \
3008 <i class="{{= it.icons.remove_group }}"></i> {{= it.translate("delete_group") }} \
3012 <div class="btn-group group-conditions"> \
3013 {{~ it.conditions: condition }} \
3014 <label class="btn btn-xs btn-primary"> \
3015 <input type="radio" name="{{= it.group_id }}_cond" value="{{= condition }}"> {{= it.translate("conditions", condition) }} \
3019 {{? it.settings.display_errors }} \
3020 <div class="error-container"><i class="{{= it.icons.error }}"></i></div> \
3023 <div class=rules-group-body> \
3024 <div class=rules-list></div> \
3028 QueryBuilder.templates.rule = '\
3029 <div id="{{= it.rule_id }}" class="rule-container"> \
3030 <div class="rule-header"> \
3031 <div class="btn-group pull-right rule-actions"> \
3032 <button type="button" class="btn btn-xs btn-clamp" data-delete="rule"> \
3033 <i class="{{= it.icons.remove_rule }}"></i> {{= it.translate("delete_rule") }} \
3037 {{? it.settings.display_errors }} \
3038 <div class="error-container"><i class="{{= it.icons.error }}"></i></div> \
3040 <div class="rule-filter-container"></div> \
3041 <div class="rule-operator-container"></div> \
3042 <div class="rule-value-container"></div> \
3045 QueryBuilder.templates.filterSelect = '\
3046 {{ var optgroup = null; }} \
3047 <select class="form-control" name="{{= it.rule.id }}_filter"> \
3048 {{? it.settings.display_empty_filter }} \
3049 <option value="-1">{{= it.settings.select_placeholder }}</option> \
3051 {{~ it.filters: filter }} \
3052 {{? optgroup !== filter.optgroup }} \
3053 {{? optgroup !== null }}</optgroup>{{?}} \
3054 {{? (optgroup = filter.optgroup) !== null }} \
3055 <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
3058 <option value="{{= filter.id }}" {{? filter.icon}}data-icon="{{= filter.icon}}"{{?}}>{{= it.translate(filter.label) }}</option> \
3060 {{? optgroup !== null }}</optgroup>{{?}} \
3063 QueryBuilder.templates.operatorSelect = '\
3064 {{? it.operators.length === 1 }} \
3066 {{= it.translate("operators", it.operators[0].type) }} \
3069 {{ var optgroup = null; }} \
3070 <select class="form-control {{? it.operators.length === 1 }}hide{{?}}" name="{{= it.rule.id }}_operator"> \
3071 {{~ it.operators: operator }} \
3072 {{? optgroup !== operator.optgroup }} \
3073 {{? optgroup !== null }}</optgroup>{{?}} \
3074 {{? (optgroup = operator.optgroup) !== null }} \
3075 <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
3078 <option value="{{= operator.type }}" {{? operator.icon}}data-icon="{{= operator.icon}}"{{?}}>{{= it.translate("operators", operator.type) }}</option> \
3080 {{? optgroup !== null }}</optgroup>{{?}} \
3083 QueryBuilder.templates.ruleValueSelect = '\
3084 {{ var optgroup = null; }} \
3085 <select class="form-control" name="{{= it.name }}" {{? it.rule.filter.multiple }}multiple{{?}}> \
3086 {{? it.rule.filter.placeholder }} \
3087 <option value="{{= it.rule.filter.placeholder_value }}" disabled selected>{{= it.rule.filter.placeholder }}</option> \
3089 {{~ it.rule.filter.values: entry }} \
3090 {{? optgroup !== entry.optgroup }} \
3091 {{? optgroup !== null }}</optgroup>{{?}} \
3092 {{? (optgroup = entry.optgroup) !== null }} \
3093 <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
3096 <option value="{{= entry.value }}">{{= entry.label }}</option> \
3098 {{? optgroup !== null }}</optgroup>{{?}} \
3102 * Returns group's HTML
3103 * @param {string} group_id
3104 * @param {int} level
3106 * @fires QueryBuilder.changer:getGroupTemplate
3109 QueryBuilder.prototype.getGroupTemplate = function(group_id, level) {
3110 var h = this.templates.group({
3114 conditions: this.settings.conditions,
3116 settings: this.settings,
3117 translate: this.translate.bind(this)
3121 * Modifies the raw HTML of a group
3122 * @event changer:getGroupTemplate
3123 * @memberof QueryBuilder
3124 * @param {string} html
3125 * @param {int} level
3128 return this.change('getGroupTemplate', h, level);
3132 * Returns rule's HTML
3133 * @param {string} rule_id
3135 * @fires QueryBuilder.changer:getRuleTemplate
3138 QueryBuilder.prototype.getRuleTemplate = function(rule_id) {
3139 var h = this.templates.rule({
3143 settings: this.settings,
3144 translate: this.translate.bind(this)
3148 * Modifies the raw HTML of a rule
3149 * @event changer:getRuleTemplate
3150 * @memberof QueryBuilder
3151 * @param {string} html
3154 return this.change('getRuleTemplate', h);
3158 * Returns rule's filter HTML
3159 * @param {Rule} rule
3160 * @param {object[]} filters
3162 * @fires QueryBuilder.changer:getRuleFilterTemplate
3165 QueryBuilder.prototype.getRuleFilterSelect = function(rule, filters) {
3166 var h = this.templates.filterSelect({
3171 settings: this.settings,
3172 translate: this.translate.bind(this)
3176 * Modifies the raw HTML of the rule's filter dropdown
3177 * @event changer:getRuleFilterSelect
3178 * @memberof QueryBuilder
3179 * @param {string} html
3180 * @param {Rule} rule
3181 * @param {QueryBuilder.Filter[]} filters
3184 return this.change('getRuleFilterSelect', h, rule, filters);
3188 * Returns rule's operator HTML
3189 * @param {Rule} rule
3190 * @param {object[]} operators
3192 * @fires QueryBuilder.changer:getRuleOperatorTemplate
3195 QueryBuilder.prototype.getRuleOperatorSelect = function(rule, operators) {
3196 var h = this.templates.operatorSelect({
3199 operators: operators,
3201 settings: this.settings,
3202 translate: this.translate.bind(this)
3206 * Modifies the raw HTML of the rule's operator dropdown
3207 * @event changer:getRuleOperatorSelect
3208 * @memberof QueryBuilder
3209 * @param {string} html
3210 * @param {Rule} rule
3211 * @param {QueryBuilder.Operator[]} operators
3214 return this.change('getRuleOperatorSelect', h, rule, operators);
3218 * Returns the rule's value select HTML
3219 * @param {string} name
3220 * @param {Rule} rule
3222 * @fires QueryBuilder.changer:getRuleValueSelect
3225 QueryBuilder.prototype.getRuleValueSelect = function(name, rule) {
3226 var h = this.templates.ruleValueSelect({
3231 settings: this.settings,
3232 translate: this.translate.bind(this)
3236 * Modifies the raw HTML of the rule's value dropdown (in case of a "select filter)
3237 * @event changer:getRuleValueSelect
3238 * @memberof QueryBuilder
3239 * @param {string} html
3240 * @param [string} name
3241 * @param {Rule} rule
3244 return this.change('getRuleValueSelect', h, name, rule);
3248 * Returns the rule's value HTML
3249 * @param {Rule} rule
3250 * @param {int} value_id
3252 * @fires QueryBuilder.changer:getRuleInput
3255 QueryBuilder.prototype.getRuleInput = function(rule, value_id) {
3256 var filter = rule.filter;
3257 var validation = rule.filter.validation || {};
3258 var name = rule.id + '_value_' + value_id;
3259 var c = filter.vertical ? ' class=block' : '';
3262 if (typeof filter.input == 'function') {
3263 h = filter.input.call(this, rule, name);
3266 switch (filter.input) {
3269 Utils.iterateOptions(filter.values, function(key, val) {
3270 h += '<label' + c + '><input type="' + filter.input + '" name="' + name + '" value="' + key + '"> ' + val + '</label> ';
3275 h = this.getRuleValueSelect(name, rule);
3279 h += '<textarea class="form-control" name="' + name + '"';
3280 if (filter.size) h += ' cols="' + filter.size + '"';
3281 if (filter.rows) h += ' rows="' + filter.rows + '"';
3282 if (validation.min !== undefined) h += ' minlength="' + validation.min + '"';
3283 if (validation.max !== undefined) h += ' maxlength="' + validation.max + '"';
3284 if (filter.placeholder) h += ' placeholder="' + filter.placeholder + '"';
3285 h += '></textarea>';
3289 h += '<input class="form-control" type="number" name="' + name + '"';
3290 if (validation.step !== undefined) h += ' step="' + validation.step + '"';
3291 if (validation.min !== undefined) h += ' min="' + validation.min + '"';
3292 if (validation.max !== undefined) h += ' max="' + validation.max + '"';
3293 if (filter.placeholder) h += ' placeholder="' + filter.placeholder + '"';
3294 if (filter.size) h += ' size="' + filter.size + '"';
3299 h += '<input class="form-control" type="text" name="' + name + '"';
3300 if (filter.placeholder) h += ' placeholder="' + filter.placeholder + '"';
3301 if (filter.type === 'string' && validation.min !== undefined) h += ' minlength="' + validation.min + '"';
3302 if (filter.type === 'string' && validation.max !== undefined) h += ' maxlength="' + validation.max + '"';
3303 if (filter.size) h += ' size="' + filter.size + '"';
3309 * Modifies the raw HTML of the rule's input
3310 * @event changer:getRuleInput
3311 * @memberof QueryBuilder
3312 * @param {string} html
3313 * @param {Rule} rule
3314 * @param {string} name - the name that the input must have
3317 return this.change('getRuleInput', h, rule, name);
3328 * @memberof QueryBuilder
3331 QueryBuilder.utils = Utils;
3334 * @callback Utils#OptionsIteratee
3335 * @param {string} key
3336 * @param {string} value
3337 * @param {string} [optgroup]
3341 * Iterates over radio/checkbox/selection options, it accept four formats
3344 * // array of values
3345 * options = ['one', 'two', 'three']
3347 * // simple key-value map
3348 * options = {1: 'one', 2: 'two', 3: 'three'}
3350 * // array of 1-element maps
3351 * options = [{1: 'one'}, {2: 'two'}, {3: 'three'}]
3353 * // array of elements
3354 * options = [{value: 1, label: 'one', optgroup: 'group'}, {value: 2, label: 'two'}]
3356 * @param {object|array} options
3357 * @param {Utils#OptionsIteratee} tpl
3359 Utils.iterateOptions = function(options, tpl) {
3361 if ($.isArray(options)) {
3362 options.forEach(function(entry) {
3363 if ($.isPlainObject(entry)) {
3364 // array of elements
3365 if ('value' in entry) {
3366 tpl(entry.value, entry.label || entry.value, entry.optgroup);
3368 // array of one-element maps
3370 $.each(entry, function(key, val) {
3372 return false; // break after first entry
3384 $.each(options, function(key, val) {
3392 * Replaces {0}, {1}, ... in a string
3393 * @param {string} str
3394 * @param {...*} args
3397 Utils.fmt = function(str, args) {
3398 if (!Array.isArray(args)) {
3399 args = Array.prototype.slice.call(arguments, 1);
3402 return str.replace(/{([0-9]+)}/g, function(m, i) {
3403 return args[parseInt(i)];
3408 * Throws an Error object with custom name or logs an error
3409 * @param {boolean} [doThrow=true]
3410 * @param {string} type
3411 * @param {string} message
3412 * @param {...*} args
3414 Utils.error = function() {
3416 var doThrow = typeof arguments[i] === 'boolean' ? arguments[i++] : true;
3417 var type = arguments[i++];
3418 var message = arguments[i++];
3419 var args = Array.isArray(arguments[i]) ? arguments[i] : Array.prototype.slice.call(arguments, i);
3422 var err = new Error(Utils.fmt(message, args));
3423 err.name = type + 'Error';
3428 console.error(type + 'Error: ' + Utils.fmt(message, args));
3433 * Changes the type of a value to int, float or bool
3435 * @param {string} type - 'integer', 'double', 'boolean' or anything else (passthrough)
3438 Utils.changeType = function(value, type) {
3439 if (value === '' || value === undefined) {
3446 if (typeof value === 'string' && !/^-?\d+$/.test(value)) {
3449 return parseInt(value);
3451 if (typeof value === 'string' && !/^-?\d+\.?\d*$/.test(value)) {
3454 return parseFloat(value);
3456 if (typeof value === 'string' && !/^(0|1|true|false){1}$/i.test(value)) {
3459 return value === true || value === 1 || value.toLowerCase() === 'true' || value === '1';
3460 default: return value;
3466 * Escapes a string like PHP's mysql_real_escape_string does
3467 * @param {string} value
3470 Utils.escapeString = function(value) {
3471 if (typeof value != 'string') {
3476 .replace(/[\0\n\r\b\\\'\"]/g, function(s) {
3479 case '\0': return '\\0';
3480 case '\n': return '\\n';
3481 case '\r': return '\\r';
3482 case '\b': return '\\b';
3483 default: return '\\' + s;
3488 .replace(/\t/g, '\\t')
3489 .replace(/\x1a/g, '\\Z');
3493 * Escapes a string for use in regex
3494 * @param {string} str
3497 Utils.escapeRegExp = function(str) {
3498 return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
3502 * Escapes a string for use in HTML element id
3503 * @param {string} str
3506 Utils.escapeElementId = function(str) {
3507 // Regex based on that suggested by:
3508 // https://learn.jquery.com/using-jquery-core/faq/how-do-i-select-an-element-by-an-id-that-has-characters-used-in-css-notation/
3509 // - escapes : . [ ] ,
3510 // - avoids escaping already escaped values
3511 return (str) ? str.replace(/(\\)?([:.\[\],])/g,
3512 function( $0, $1, $2 ) { return $1 ? $0 : '\\' + $2; }) : str;
3516 * Sorts objects by grouping them by `key`, preserving initial order when possible
3517 * @param {object[]} items
3518 * @param {string} key
3519 * @returns {object[]}
3521 Utils.groupSort = function(items, key) {
3525 items.forEach(function(item) {
3529 idx = optgroups.lastIndexOf(item[key]);
3532 idx = optgroups.length;
3539 idx = optgroups.length;
3542 optgroups.splice(idx, 0, item[key]);
3543 newItems.splice(idx, 0, item);
3550 * Defines properties on an Node prototype with getter and setter.<br>
3551 * Update events are emitted in the setter through root Model (if any).<br>
3552 * The object must have a `__` object, non enumerable property to store values.
3553 * @param {function} obj
3554 * @param {string[]} fields
3556 Utils.defineModelProperties = function(obj, fields) {
3557 fields.forEach(function(field) {
3558 Object.defineProperty(obj.prototype, field, {
3561 return this.__[field];
3563 set: function(value) {
3564 var previousValue = (this.__[field] !== null && typeof this.__[field] == 'object') ?
3565 $.extend({}, this.__[field]) :
3568 this.__[field] = value;
3570 if (this.model !== null) {
3572 * After a value of the model changed
3573 * @event model:update
3575 * @param {Node} node
3576 * @param {string} field
3578 * @param {*} previousValue
3580 this.model.trigger('update', this, field, value, previousValue);
3589 * Main object storing data model and emitting model events
3600 * Base for event emitting
3608 $.extend(Model.prototype, /** @lends Model.prototype */ {
3610 * Triggers an event on the model
3611 * @param {string} type
3612 * @returns {$.Event}
3614 trigger: function(type) {
3615 var event = new $.Event(type);
3616 this.$.triggerHandler(event, Array.prototype.slice.call(arguments, 1));
3621 * Attaches an event listener on the model
3622 * @param {string} type
3623 * @param {function} cb
3627 this.$.on.apply(this.$, Array.prototype.slice.call(arguments));
3632 * Removes an event listener from the model
3633 * @param {string} type
3634 * @param {function} [cb]
3638 this.$.off.apply(this.$, Array.prototype.slice.call(arguments));
3643 * Attaches an event listener called once on the model
3644 * @param {string} type
3645 * @param {function} cb
3649 this.$.one.apply(this.$, Array.prototype.slice.call(arguments));
3656 * Root abstract object
3658 * @param {Node} [parent]
3659 * @param {jQuery} $el
3661 var Node = function(parent, $el) {
3662 if (!(this instanceof Node)) {
3663 return new Node(parent, $el);
3666 Object.defineProperty(this, '__', { value: {} });
3668 $el.data('queryBuilderModel', this);
3685 this.__.error = null;
3702 this.__.data = undefined;
3714 this.id = $el[0].id;
3726 this.parent = parent;
3729 Utils.defineModelProperties(Node, ['level', 'error', 'data', 'flags']);
3731 Object.defineProperty(Node.prototype, 'parent', {
3734 return this.__.parent;
3736 set: function(value) {
3737 this.__.parent = value;
3738 this.level = value === null ? 1 : value.level + 1;
3739 this.model = value === null ? null : value.model;
3744 * Checks if this Node is the root
3745 * @returns {boolean}
3747 Node.prototype.isRoot = function() {
3748 return (this.level === 1);
3752 * Returns the node position inside its parent
3755 Node.prototype.getPos = function() {
3756 if (this.isRoot()) {
3760 return this.parent.getNodePos(this);
3766 * @fires Model.model:drop
3768 Node.prototype.drop = function() {
3769 var model = this.model;
3771 if (!!this.parent) {
3772 this.parent.removeNode(this);
3775 this.$el.removeData('queryBuilderModel');
3777 if (model !== null) {
3779 * After a node of the model has been removed
3782 * @param {Node} node
3784 model.trigger('drop', this);
3789 * Moves itself after another Node
3790 * @param {Node} target
3791 * @fires Model.model:move
3793 Node.prototype.moveAfter = function(target) {
3794 if (!this.isRoot()) {
3795 this.move(target.parent, target.getPos() + 1);
3800 * Moves itself at the beginning of parent or another Group
3801 * @param {Group} [target]
3802 * @fires Model.model:move
3804 Node.prototype.moveAtBegin = function(target) {
3805 if (!this.isRoot()) {
3806 if (target === undefined) {
3807 target = this.parent;
3810 this.move(target, 0);
3815 * Moves itself at the end of parent or another Group
3816 * @param {Group} [target]
3817 * @fires Model.model:move
3819 Node.prototype.moveAtEnd = function(target) {
3820 if (!this.isRoot()) {
3821 if (target === undefined) {
3822 target = this.parent;
3825 this.move(target, target.length() === 0 ? 0 : target.length() - 1);
3830 * Moves itself at specific position of Group
3831 * @param {Group} target
3832 * @param {int} index
3833 * @fires Model.model:move
3835 Node.prototype.move = function(target, index) {
3836 if (!this.isRoot()) {
3837 if (typeof target === 'number') {
3839 target = this.parent;
3842 this.parent.removeNode(this);
3843 target.insertNode(this, index, false);
3845 if (this.model !== null) {
3847 * After a node of the model has been moved
3850 * @param {Node} node
3851 * @param {Node} target
3852 * @param {int} index
3854 this.model.trigger('move', this, target, index);
3864 * @param {Group} [parent]
3865 * @param {jQuery} $el
3867 var Group = function(parent, $el) {
3868 if (!(this instanceof Group)) {
3869 return new Group(parent, $el);
3872 Node.call(this, parent, $el);
3875 * @member {object[]}
3886 this.__.condition = null;
3889 Group.prototype = Object.create(Node.prototype);
3890 Group.prototype.constructor = Group;
3892 Utils.defineModelProperties(Group, ['condition']);
3895 * Removes group's content
3897 Group.prototype.empty = function() {
3898 this.each('reverse', function(rule) {
3900 }, function(group) {
3908 Group.prototype.drop = function() {
3910 Node.prototype.drop.call(this);
3914 * Returns the number of children
3917 Group.prototype.length = function() {
3918 return this.rules.length;
3922 * Adds a Node at specified index
3923 * @param {Node} node
3924 * @param {int} [index=end]
3925 * @param {boolean} [trigger=false] - fire 'add' event
3926 * @returns {Node} the inserted node
3927 * @fires Model.model:add
3929 Group.prototype.insertNode = function(node, index, trigger) {
3930 if (index === undefined) {
3931 index = this.length();
3934 this.rules.splice(index, 0, node);
3937 if (trigger && this.model !== null) {
3939 * After a node of the model has been added
3942 * @param {Node} parent
3943 * @param {Node} node
3944 * @param {int} index
3946 this.model.trigger('add', this, node, index);
3953 * Adds a new Group at specified index
3954 * @param {jQuery} $el
3955 * @param {int} [index=end]
3957 * @fires Model.model:add
3959 Group.prototype.addGroup = function($el, index) {
3960 return this.insertNode(new Group(this, $el), index, true);
3964 * Adds a new Rule at specified index
3965 * @param {jQuery} $el
3966 * @param {int} [index=end]
3968 * @fires Model.model:add
3970 Group.prototype.addRule = function($el, index) {
3971 return this.insertNode(new Rule(this, $el), index, true);
3975 * Deletes a specific Node
3976 * @param {Node} node
3978 Group.prototype.removeNode = function(node) {
3979 var index = this.getNodePos(node);
3982 this.rules.splice(index, 1);
3987 * Returns the position of a child Node
3988 * @param {Node} node
3991 Group.prototype.getNodePos = function(node) {
3992 return this.rules.indexOf(node);
3996 * @callback Model#GroupIteratee
3997 * @param {Node} node
3998 * @returns {boolean} stop the iteration
4002 * Iterate over all Nodes
4003 * @param {boolean} [reverse=false] - iterate in reverse order, required if you delete nodes
4004 * @param {Model#GroupIteratee} cbRule - callback for Rules (can be `null` but not omitted)
4005 * @param {Model#GroupIteratee} [cbGroup] - callback for Groups
4006 * @param {object} [context] - context for callbacks
4007 * @returns {boolean} if the iteration has been stopped by a callback
4009 Group.prototype.each = function(reverse, cbRule, cbGroup, context) {
4010 if (typeof reverse !== 'boolean' && typeof reverse !== 'string') {
4016 context = context === undefined ? null : context;
4018 var i = reverse ? this.rules.length - 1 : 0;
4019 var l = reverse ? 0 : this.rules.length - 1;
4020 var c = reverse ? -1 : 1;
4021 var next = function() {
4022 return reverse ? i >= l : i <= l;
4026 for (; next(); i += c) {
4027 if (this.rules[i] instanceof Group) {
4029 stop = cbGroup.call(context, this.rules[i]) === false;
4032 else if (!!cbRule) {
4033 stop = cbRule.call(context, this.rules[i]) === false;
4045 * Checks if the group contains a particular Node
4046 * @param {Node} node
4047 * @param {boolean} [recursive=false]
4048 * @returns {boolean}
4050 Group.prototype.contains = function(node, recursive) {
4051 if (this.getNodePos(node) !== -1) {
4054 else if (!recursive) {
4058 // the loop will return with false as soon as the Node is found
4059 return !this.each(function() {
4061 }, function(group) {
4062 return !group.contains(node, true);
4072 * @param {Group} parent
4073 * @param {jQuery} $el
4075 var Rule = function(parent, $el) {
4076 if (!(this instanceof Rule)) {
4077 return new Rule(parent, $el);
4080 Node.call(this, parent, $el);
4082 this._updating_value = false;
4083 this._updating_input = false;
4087 * @member {QueryBuilder.Filter}
4091 this.__.filter = null;
4095 * @member {QueryBuilder.Operator}
4099 this.__.operator = null;
4107 this.__.value = undefined;
4110 Rule.prototype = Object.create(Node.prototype);
4111 Rule.prototype.constructor = Rule;
4113 Utils.defineModelProperties(Rule, ['filter', 'operator', 'value']);
4116 * Checks if this Node is the root
4117 * @returns {boolean} always false
4119 Rule.prototype.isRoot = function() {
4125 * @member {function}
4126 * @memberof QueryBuilder
4129 QueryBuilder.Group = Group;
4132 * @member {function}
4133 * @memberof QueryBuilder
4136 QueryBuilder.Rule = Rule;
4140 * The {@link http://learn.jquery.com/plugins/|jQuery Plugins} namespace
4141 * @external "jQuery.fn"
4145 * Instanciates or accesses the {@link QueryBuilder} on an element
4147 * @memberof external:"jQuery.fn"
4148 * @param {*} option - initial configuration or method name
4149 * @param {...*} args - method arguments
4152 * $('#builder').queryBuilder({ /** configuration object *\/ });
4154 * $('#builder').queryBuilder('methodName', methodParam1, methodParam2);
4156 $.fn.queryBuilder = function(option) {
4157 if (this.length === 0) {
4158 Utils.error('Config', 'No target defined');
4160 if (this.length > 1) {
4161 Utils.error('Config', 'Unable to initialize on multiple target');
4164 var data = this.data('queryBuilder');
4165 var options = (typeof option == 'object' && option) || {};
4167 if (!data && option == 'destroy') {
4171 var builder = new QueryBuilder(this, options);
4172 this.data('queryBuilder', builder);
4173 builder.init(options.rules);
4175 if (typeof option == 'string') {
4176 return data[option].apply(data, Array.prototype.slice.call(arguments, 1));
4184 * @memberof external:"jQuery.fn"
4187 $.fn.queryBuilder.constructor = QueryBuilder;
4191 * @memberof external:"jQuery.fn"
4192 * @see QueryBuilder.defaults
4194 $.fn.queryBuilder.defaults = QueryBuilder.defaults;
4198 * @memberof external:"jQuery.fn"
4199 * @see QueryBuilder.defaults
4201 $.fn.queryBuilder.extend = QueryBuilder.extend;
4205 * @memberof external:"jQuery.fn"
4206 * @see QueryBuilder.define
4208 $.fn.queryBuilder.define = QueryBuilder.define;
4212 * @memberof external:"jQuery.fn"
4213 * @see QueryBuilder.regional
4215 $.fn.queryBuilder.regional = QueryBuilder.regional;
4220 * @memberof module:plugins
4221 * @description Applies Awesome Bootstrap Checkbox for checkbox and radio inputs.
4222 * @param {object} [options]
4223 * @param {string} [options.font='glyphicons']
4224 * @param {string} [options.color='default']
4226 QueryBuilder.define('bt-checkbox', function(options) {
4227 if (options.font == 'glyphicons') {
4228 this.$el.addClass('bt-checkbox-glyphicons');
4231 this.on('getRuleInput.filter', function(h, rule, name) {
4232 var filter = rule.filter;
4234 if ((filter.input === 'radio' || filter.input === 'checkbox') && !filter.plugin) {
4237 if (!filter.colors) {
4241 filter.colors._def_ = filter.color;
4244 var style = filter.vertical ? ' style="display:block"' : '';
4247 Utils.iterateOptions(filter.values, function(key, val) {
4248 var color = filter.colors[key] || filter.colors._def_ || options.color;
4249 var id = name + '_' + (i++);
4252 <div' + style + ' class="' + filter.input + ' ' + filter.input + '-' + color + '"> \
4253 <input type="' + filter.input + '" name="' + name + '" id="' + id + '" value="' + key + '"> \
4254 <label for="' + id + '">' + val + '</label> \
4266 * @class BtSelectpicker
4267 * @memberof module:plugins
4268 * @descriptioon Applies Bootstrap Select on filters and operators combo-boxes.
4269 * @param {object} [options]
4270 * @param {string} [options.container='body']
4271 * @param {string} [options.style='btn-inverse btn-xs']
4272 * @param {int|string} [options.width='auto']
4273 * @param {boolean} [options.showIcon=false]
4274 * @throws MissingLibraryError
4276 QueryBuilder.define('bt-selectpicker', function(options) {
4277 if (!$.fn.selectpicker || !$.fn.selectpicker.Constructor) {
4278 Utils.error('MissingLibrary', 'Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select');
4281 var Selectors = QueryBuilder.selectors;
4283 // init selectpicker
4284 this.on('afterCreateRuleFilters', function(e, rule) {
4285 rule.$el.find(Selectors.rule_filter).removeClass('form-control').selectpicker(options);
4288 this.on('afterCreateRuleOperators', function(e, rule) {
4289 rule.$el.find(Selectors.rule_operator).removeClass('form-control').selectpicker(options);
4292 // update selectpicker on change
4293 this.on('afterUpdateRuleFilter', function(e, rule) {
4294 rule.$el.find(Selectors.rule_filter).selectpicker('render');
4297 this.on('afterUpdateRuleOperator', function(e, rule) {
4298 rule.$el.find(Selectors.rule_operator).selectpicker('render');
4301 this.on('beforeDeleteRule', function(e, rule) {
4302 rule.$el.find(Selectors.rule_filter).selectpicker('destroy');
4303 rule.$el.find(Selectors.rule_operator).selectpicker('destroy');
4307 style: 'btn-inverse btn-xs',
4314 * @class BtTooltipErrors
4315 * @memberof module:plugins
4316 * @description Applies Bootstrap Tooltips on validation error messages.
4317 * @param {object} [options]
4318 * @param {string} [options.placement='right']
4319 * @throws MissingLibraryError
4321 QueryBuilder.define('bt-tooltip-errors', function(options) {
4322 if (!$.fn.tooltip || !$.fn.tooltip.Constructor || !$.fn.tooltip.Constructor.prototype.fixTitle) {
4323 Utils.error('MissingLibrary', 'Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com');
4328 // add BT Tooltip data
4329 this.on('getRuleTemplate.filter getGroupTemplate.filter', function(h) {
4330 var $h = $(h.value);
4331 $h.find(QueryBuilder.selectors.error_container).attr('data-toggle', 'tooltip');
4332 h.value = $h.prop('outerHTML');
4335 // init/refresh tooltip when title changes
4336 this.model.on('update', function(e, node, field) {
4337 if (field == 'error' && self.settings.display_errors) {
4338 node.$el.find(QueryBuilder.selectors.error_container).eq(0)
4341 .tooltip('fixTitle');
4350 * @class ChangeFilters
4351 * @memberof module:plugins
4352 * @description Allows to change available filters after plugin initialization.
4355 QueryBuilder.extend(/** @lends module:plugins.ChangeFilters.prototype */ {
4357 * Change the filters of the builder
4358 * @param {boolean} [deleteOrphans=false] - delete rules using old filters
4359 * @param {QueryBuilder[]} filters
4360 * @fires module:plugins.ChangeFilters.changer:setFilters
4361 * @fires module:plugins.ChangeFilters.afterSetFilters
4362 * @throws ChangeFilterError
4364 setFilters: function(deleteOrphans, filters) {
4367 if (filters === undefined) {
4368 filters = deleteOrphans;
4369 deleteOrphans = false;
4372 filters = this.checkFilters(filters);
4375 * Modifies the filters before {@link module:plugins.ChangeFilters.setFilters} method
4376 * @event changer:setFilters
4377 * @memberof module:plugins.ChangeFilters
4378 * @param {QueryBuilder.Filter[]} filters
4379 * @returns {QueryBuilder.Filter[]}
4381 filters = this.change('setFilters', filters);
4383 var filtersIds = filters.map(function(filter) {
4387 // check for orphans
4388 if (!deleteOrphans) {
4389 (function checkOrphans(node) {
4392 if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
4393 Utils.error('ChangeFilter', 'A rule is using filter "{0}"', rule.filter.id);
4398 }(this.model.root));
4402 this.filters = filters;
4404 // apply on existing DOM
4405 (function updateBuilder(node) {
4408 if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
4411 self.trigger('rulesChanged');
4414 self.createRuleFilters(rule);
4416 rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
4417 self.trigger('afterUpdateRuleFilter', rule);
4422 }(this.model.root));
4425 if (this.settings.plugins) {
4426 if (this.settings.plugins['unique-filter']) {
4427 this.updateDisabledFilters();
4429 if (this.settings.plugins['bt-selectpicker']) {
4430 this.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render');
4434 // reset the default_filter if does not exist anymore
4435 if (this.settings.default_filter) {
4437 this.getFilterById(this.settings.default_filter);
4440 this.settings.default_filter = null;
4445 * After {@link module:plugins.ChangeFilters.setFilters} method
4446 * @event afterSetFilters
4447 * @memberof module:plugins.ChangeFilters
4448 * @param {QueryBuilder.Filter[]} filters
4450 this.trigger('afterSetFilters', filters);
4454 * Adds a new filter to the builder
4455 * @param {QueryBuilder.Filter|Filter[]} newFilters
4456 * @param {int|string} [position=#end] - index or '#start' or '#end'
4457 * @fires module:plugins.ChangeFilters.changer:setFilters
4458 * @fires module:plugins.ChangeFilters.afterSetFilters
4459 * @throws ChangeFilterError
4461 addFilter: function(newFilters, position) {
4462 if (position === undefined || position == '#end') {
4463 position = this.filters.length;
4465 else if (position == '#start') {
4469 if (!$.isArray(newFilters)) {
4470 newFilters = [newFilters];
4473 var filters = $.extend(true, [], this.filters);
4476 if (parseInt(position) == position) {
4477 Array.prototype.splice.apply(filters, [position, 0].concat(newFilters));
4480 // after filter by its id
4481 if (this.filters.some(function(filter, index) {
4482 if (filter.id == position) {
4483 position = index + 1;
4488 Array.prototype.splice.apply(filters, [position, 0].concat(newFilters));
4490 // defaults to end of list
4492 Array.prototype.push.apply(filters, newFilters);
4496 this.setFilters(filters);
4500 * Removes a filter from the builder
4501 * @param {string|string[]} filterIds
4502 * @param {boolean} [deleteOrphans=false] delete rules using old filters
4503 * @fires module:plugins.ChangeFilters.changer:setFilters
4504 * @fires module:plugins.ChangeFilters.afterSetFilters
4505 * @throws ChangeFilterError
4507 removeFilter: function(filterIds, deleteOrphans) {
4508 var filters = $.extend(true, [], this.filters);
4509 if (typeof filterIds === 'string') {
4510 filterIds = [filterIds];
4513 filters = filters.filter(function(filter) {
4514 return filterIds.indexOf(filter.id) === -1;
4517 this.setFilters(deleteOrphans, filters);
4523 * @class ChosenSelectpicker
4524 * @memberof module:plugins
4525 * @descriptioon Applies chosen-js Select on filters and operators combo-boxes.
4526 * @param {object} [options] Supports all the options for chosen
4527 * @throws MissingLibraryError
4529 QueryBuilder.define('chosen-selectpicker', function(options) {
4532 Utils.error('MissingLibrary', 'chosen is required to use "chosen-selectpicker" plugin. Get it here: https://github.com/harvesthq/chosen');
4535 if (this.settings.plugins['bt-selectpicker']) {
4536 Utils.error('Conflict', 'bt-selectpicker is already selected as the dropdown plugin. Please remove chosen-selectpicker from the plugin list');
4539 var Selectors = QueryBuilder.selectors;
4541 // init selectpicker
4542 this.on('afterCreateRuleFilters', function(e, rule) {
4543 rule.$el.find(Selectors.rule_filter).removeClass('form-control').chosen(options);
4546 this.on('afterCreateRuleOperators', function(e, rule) {
4547 rule.$el.find(Selectors.rule_operator).removeClass('form-control').chosen(options);
4550 // update selectpicker on change
4551 this.on('afterUpdateRuleFilter', function(e, rule) {
4552 rule.$el.find(Selectors.rule_filter).trigger('chosen:updated');
4555 this.on('afterUpdateRuleOperator', function(e, rule) {
4556 rule.$el.find(Selectors.rule_operator).trigger('chosen:updated');
4559 this.on('beforeDeleteRule', function(e, rule) {
4560 rule.$el.find(Selectors.rule_filter).chosen('destroy');
4561 rule.$el.find(Selectors.rule_operator).chosen('destroy');
4567 * @class FilterDescription
4568 * @memberof module:plugins
4569 * @description Provides three ways to display a description about a filter: inline, Bootsrap Popover or Bootbox.
4570 * @param {object} [options]
4571 * @param {string} [options.icon='glyphicon glyphicon-info-sign']
4572 * @param {string} [options.mode='popover'] - inline, popover or bootbox
4573 * @throws ConfigError
4575 QueryBuilder.define('filter-description', function(options) {
4577 if (options.mode === 'inline') {
4578 this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
4579 var $p = rule.$el.find('p.filter-description');
4580 var description = e.builder.getFilterDescription(rule.filter, rule);
4586 if ($p.length === 0) {
4587 $p = $('<p class="filter-description"></p>');
4588 $p.appendTo(rule.$el);
4591 $p.css('display', '');
4594 $p.html('<i class="' + options.icon + '"></i> ' + description);
4599 else if (options.mode === 'popover') {
4600 if (!$.fn.popover || !$.fn.popover.Constructor || !$.fn.popover.Constructor.prototype.fixTitle) {
4601 Utils.error('MissingLibrary', 'Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com');
4604 this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
4605 var $b = rule.$el.find('button.filter-description');
4606 var description = e.builder.getFilterDescription(rule.filter, rule);
4611 if ($b.data('bs.popover')) {
4616 if ($b.length === 0) {
4617 $b = $('<button type="button" class="btn btn-xs btn-info filter-description" data-toggle="popover"><i class="' + options.icon + '"></i></button>');
4618 $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions));
4626 $b.on('mouseout', function() {
4631 $b.css('display', '');
4634 $b.data('bs.popover').options.content = description;
4636 if ($b.attr('aria-describedby')) {
4643 else if (options.mode === 'bootbox') {
4644 if (!('bootbox' in window)) {
4645 Utils.error('MissingLibrary', 'Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com');
4648 this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
4649 var $b = rule.$el.find('button.filter-description');
4650 var description = e.builder.getFilterDescription(rule.filter, rule);
4656 if ($b.length === 0) {
4657 $b = $('<button type="button" class="btn btn-xs btn-info filter-description" data-toggle="bootbox"><i class="' + options.icon + '"></i></button>');
4658 $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions));
4660 $b.on('click', function() {
4661 bootbox.alert($b.data('description'));
4665 $b.css('display', '');
4668 $b.data('description', description);
4673 icon: 'glyphicon glyphicon-info-sign',
4677 QueryBuilder.extend(/** @lends module:plugins.FilterDescription.prototype */ {
4679 * Returns the description of a filter for a particular rule (if present)
4680 * @param {object} filter
4681 * @param {Rule} [rule]
4685 getFilterDescription: function(filter, rule) {
4689 else if (typeof filter.description == 'function') {
4690 return filter.description.call(this, rule);
4693 return filter.description;
4701 * @memberof module:plugins
4702 * @description Allows to invert a rule operator, a group condition or the entire builder.
4703 * @param {object} [options]
4704 * @param {string} [options.icon='glyphicon glyphicon-random']
4705 * @param {boolean} [options.recursive=true]
4706 * @param {boolean} [options.invert_rules=true]
4707 * @param {boolean} [options.display_rules_button=false]
4708 * @param {boolean} [options.silent_fail=false]
4710 QueryBuilder.define('invert', function(options) {
4712 var Selectors = QueryBuilder.selectors;
4715 this.on('afterInit', function() {
4716 self.$el.on('click.queryBuilder', '[data-invert=group]', function() {
4717 var $group = $(this).closest(Selectors.group_container);
4718 self.invert(self.getModel($group), options);
4721 if (options.display_rules_button && options.invert_rules) {
4722 self.$el.on('click.queryBuilder', '[data-invert=rule]', function() {
4723 var $rule = $(this).closest(Selectors.rule_container);
4724 self.invert(self.getModel($rule), options);
4730 if (!options.disable_template) {
4731 this.on('getGroupTemplate.filter', function(h) {
4732 var $h = $(h.value);
4733 $h.find(Selectors.condition_container).after(
4734 '<button type="button" class="btn btn-xs btn-default" data-invert="group">' +
4735 '<i class="' + options.icon + '"></i> ' + self.translate('invert') +
4738 h.value = $h.prop('outerHTML');
4741 if (options.display_rules_button && options.invert_rules) {
4742 this.on('getRuleTemplate.filter', function(h) {
4743 var $h = $(h.value);
4744 $h.find(Selectors.rule_actions).prepend(
4745 '<button type="button" class="btn btn-xs btn-default" data-invert="rule">' +
4746 '<i class="' + options.icon + '"></i> ' + self.translate('invert') +
4749 h.value = $h.prop('outerHTML');
4754 icon: 'glyphicon glyphicon-random',
4757 display_rules_button: false,
4759 disable_template: false
4762 QueryBuilder.defaults({
4763 operatorOpposites: {
4764 'equal': 'not_equal',
4765 'not_equal': 'equal',
4768 'less': 'greater_or_equal',
4769 'less_or_equal': 'greater',
4770 'greater': 'less_or_equal',
4771 'greater_or_equal': 'less',
4772 'between': 'not_between',
4773 'not_between': 'between',
4774 'begins_with': 'not_begins_with',
4775 'not_begins_with': 'begins_with',
4776 'contains': 'not_contains',
4777 'not_contains': 'contains',
4778 'ends_with': 'not_ends_with',
4779 'not_ends_with': 'ends_with',
4780 'is_empty': 'is_not_empty',
4781 'is_not_empty': 'is_empty',
4782 'is_null': 'is_not_null',
4783 'is_not_null': 'is_null'
4786 conditionOpposites: {
4792 QueryBuilder.extend(/** @lends module:plugins.Invert.prototype */ {
4794 * Invert a Group, a Rule or the whole builder
4795 * @param {Node} [node]
4796 * @param {object} [options] {@link module:plugins.Invert}
4797 * @fires module:plugins.Invert.afterInvert
4798 * @throws InvertConditionError, InvertOperatorError
4800 invert: function(node, options) {
4801 if (!(node instanceof Node)) {
4802 if (!this.model.root) return;
4804 node = this.model.root;
4807 if (typeof options != 'object') options = {};
4808 if (options.recursive === undefined) options.recursive = true;
4809 if (options.invert_rules === undefined) options.invert_rules = true;
4810 if (options.silent_fail === undefined) options.silent_fail = false;
4811 if (options.trigger === undefined) options.trigger = true;
4813 if (node instanceof Group) {
4814 // invert group condition
4815 if (this.settings.conditionOpposites[node.condition]) {
4816 node.condition = this.settings.conditionOpposites[node.condition];
4818 else if (!options.silent_fail) {
4819 Utils.error('InvertCondition', 'Unknown inverse of condition "{0}"', node.condition);
4823 if (options.recursive) {
4824 var tempOpts = $.extend({}, options, { trigger: false });
4825 node.each(function(rule) {
4826 if (options.invert_rules) {
4827 this.invert(rule, tempOpts);
4829 }, function(group) {
4830 this.invert(group, tempOpts);
4834 else if (node instanceof Rule) {
4835 if (node.operator && !node.filter.no_invert) {
4836 // invert rule operator
4837 if (this.settings.operatorOpposites[node.operator.type]) {
4838 var invert = this.settings.operatorOpposites[node.operator.type];
4839 // check if the invert is "authorized"
4840 if (!node.filter.operators || node.filter.operators.indexOf(invert) != -1) {
4841 node.operator = this.getOperatorByType(invert);
4844 else if (!options.silent_fail) {
4845 Utils.error('InvertOperator', 'Unknown inverse of operator "{0}"', node.operator.type);
4850 if (options.trigger) {
4852 * After {@link module:plugins.Invert.invert} method
4853 * @event afterInvert
4854 * @memberof module:plugins.Invert
4855 * @param {Node} node - the main group or rule that has been modified
4856 * @param {object} options
4858 this.trigger('afterInvert', node, options);
4860 this.trigger('rulesChanged');
4867 * @class MongoDbSupport
4868 * @memberof module:plugins
4869 * @description Allows to export rules as a MongoDB find object as well as populating the builder from a MongoDB object.
4872 QueryBuilder.defaults({
4875 equal: function(v) { return v[0]; },
4876 not_equal: function(v) { return { '$ne': v[0] }; },
4877 in: function(v) { return { '$in': v }; },
4878 not_in: function(v) { return { '$nin': v }; },
4879 less: function(v) { return { '$lt': v[0] }; },
4880 less_or_equal: function(v) { return { '$lte': v[0] }; },
4881 greater: function(v) { return { '$gt': v[0] }; },
4882 greater_or_equal: function(v) { return { '$gte': v[0] }; },
4883 between: function(v) { return { '$gte': v[0], '$lte': v[1] }; },
4884 not_between: function(v) { return { '$lt': v[0], '$gt': v[1] }; },
4885 begins_with: function(v) { return { '$regex': '^' + Utils.escapeRegExp(v[0]) }; },
4886 not_begins_with: function(v) { return { '$regex': '^(?!' + Utils.escapeRegExp(v[0]) + ')' }; },
4887 contains: function(v) { return { '$regex': Utils.escapeRegExp(v[0]) }; },
4888 not_contains: function(v) { return { '$regex': '^((?!' + Utils.escapeRegExp(v[0]) + ').)*$', '$options': 's' }; },
4889 ends_with: function(v) { return { '$regex': Utils.escapeRegExp(v[0]) + '$' }; },
4890 not_ends_with: function(v) { return { '$regex': '(?<!' + Utils.escapeRegExp(v[0]) + ')$' }; },
4891 is_empty: function(v) { return ''; },
4892 is_not_empty: function(v) { return { '$ne': '' }; },
4893 is_null: function(v) { return null; },
4894 is_not_null: function(v) { return { '$ne': null }; }
4898 mongoRuleOperators: {
4902 'op': v === null ? 'is_null' : (v === '' ? 'is_empty' : 'equal')
4909 'op': v === null ? 'is_not_null' : (v === '' ? 'is_not_empty' : 'not_equal')
4912 $regex: function(v) {
4914 if (v.slice(0, 4) == '^(?!' && v.slice(-1) == ')') {
4915 return { 'val': v.slice(4, -1), 'op': 'not_begins_with' };
4917 else if (v.slice(0, 5) == '^((?!' && v.slice(-5) == ').)*$') {
4918 return { 'val': v.slice(5, -5), 'op': 'not_contains' };
4920 else if (v.slice(0, 4) == '(?<!' && v.slice(-2) == ')$') {
4921 return { 'val': v.slice(4, -2), 'op': 'not_ends_with' };
4923 else if (v.slice(-1) == '$') {
4924 return { 'val': v.slice(0, -1), 'op': 'ends_with' };
4926 else if (v.slice(0, 1) == '^') {
4927 return { 'val': v.slice(1), 'op': 'begins_with' };
4930 return { 'val': v, 'op': 'contains' };
4933 between: function(v) {
4934 return { 'val': [v.$gte, v.$lte], 'op': 'between' };
4936 not_between: function(v) {
4937 return { 'val': [v.$lt, v.$gt], 'op': 'not_between' };
4940 return { 'val': v.$in, 'op': 'in' };
4943 return { 'val': v.$nin, 'op': 'not_in' };
4946 return { 'val': v.$lt, 'op': 'less' };
4949 return { 'val': v.$lte, 'op': 'less_or_equal' };
4952 return { 'val': v.$gt, 'op': 'greater' };
4955 return { 'val': v.$gte, 'op': 'greater_or_equal' };
4960 QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ {
4962 * Returns rules as a MongoDB query
4963 * @param {object} [data] - current rules by default
4965 * @fires module:plugins.MongoDbSupport.changer:getMongoDBField
4966 * @fires module:plugins.MongoDbSupport.changer:ruleToMongo
4967 * @fires module:plugins.MongoDbSupport.changer:groupToMongo
4968 * @throws UndefinedMongoConditionError, UndefinedMongoOperatorError
4970 getMongo: function(data) {
4971 data = (data === undefined) ? this.getRules() : data;
4979 return (function parse(group) {
4980 if (!group.condition) {
4981 group.condition = self.settings.default_condition;
4983 if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) {
4984 Utils.error('UndefinedMongoCondition', 'Unable to build MongoDB query with condition "{0}"', group.condition);
4993 group.rules.forEach(function(rule) {
4994 if (rule.rules && rule.rules.length > 0) {
4995 parts.push(parse(rule));
4998 var mdb = self.settings.mongoOperators[rule.operator];
4999 var ope = self.getOperatorByType(rule.operator);
5001 if (mdb === undefined) {
5002 Utils.error('UndefinedMongoOperator', 'Unknown MongoDB operation for operator "{0}"', rule.operator);
5005 if (ope.nb_inputs !== 0) {
5006 if (!(rule.value instanceof Array)) {
5007 rule.value = [rule.value];
5012 * Modifies the MongoDB field used by a rule
5013 * @event changer:getMongoDBField
5014 * @memberof module:plugins.MongoDbSupport
5015 * @param {string} field
5016 * @param {Rule} rule
5019 var field = self.change('getMongoDBField', rule.field, rule);
5021 var ruleExpression = {};
5022 ruleExpression[field] = mdb.call(self, rule.value);
5025 * Modifies the MongoDB expression generated for a rul
5026 * @event changer:ruleToMongo
5027 * @memberof module:plugins.MongoDbSupport
5028 * @param {object} expression
5029 * @param {Rule} rule
5031 * @param {function} valueWrapper - function that takes the value and adds the operator
5034 parts.push(self.change('ruleToMongo', ruleExpression, rule, rule.value, mdb));
5038 var groupExpression = {};
5039 groupExpression['$' + group.condition.toLowerCase()] = parts;
5042 * Modifies the MongoDB expression generated for a group
5043 * @event changer:groupToMongo
5044 * @memberof module:plugins.MongoDbSupport
5045 * @param {object} expression
5046 * @param {Group} group
5049 return self.change('groupToMongo', groupExpression, group);
5054 * Converts a MongoDB query to rules
5055 * @param {object} query
5057 * @fires module:plugins.MongoDbSupport.changer:parseMongoNode
5058 * @fires module:plugins.MongoDbSupport.changer:getMongoDBFieldID
5059 * @fires module:plugins.MongoDbSupport.changer:mongoToRule
5060 * @fires module:plugins.MongoDbSupport.changer:mongoToGroup
5061 * @throws MongoParseError, UndefinedMongoConditionError, UndefinedMongoOperatorError
5063 getRulesFromMongo: function(query) {
5064 if (query === undefined || query === null) {
5071 * Custom parsing of a MongoDB expression, you can return a sub-part of the expression, or a well formed group or rule JSON
5072 * @event changer:parseMongoNode
5073 * @memberof module:plugins.MongoDbSupport
5074 * @param {object} expression
5075 * @returns {object} expression, rule or group
5077 query = self.change('parseMongoNode', query);
5079 // a plugin returned a group
5080 if ('rules' in query && 'condition' in query) {
5084 // a plugin returned a rule
5085 if ('id' in query && 'operator' in query && 'value' in query) {
5087 condition: this.settings.default_condition,
5092 var key = self.getMongoCondition(query);
5094 Utils.error('MongoParse', 'Invalid MongoDB query format');
5097 return (function parse(data, topKey) {
5098 var rules = data[topKey];
5101 rules.forEach(function(data) {
5102 // allow plugins to manually parse or handle special cases
5103 data = self.change('parseMongoNode', data);
5105 // a plugin returned a group
5106 if ('rules' in data && 'condition' in data) {
5111 // a plugin returned a rule
5112 if ('id' in data && 'operator' in data && 'value' in data) {
5117 var key = self.getMongoCondition(data);
5119 parts.push(parse(data, key));
5122 var field = Object.keys(data)[0];
5123 var value = data[field];
5125 var operator = self.getMongoOperator(value);
5126 if (operator === undefined) {
5127 Utils.error('MongoParse', 'Invalid MongoDB query format');
5130 var mdbrl = self.settings.mongoRuleOperators[operator];
5131 if (mdbrl === undefined) {
5132 Utils.error('UndefinedMongoOperator', 'JSON Rule operation unknown for operator "{0}"', operator);
5135 var opVal = mdbrl.call(self, value);
5137 var id = self.getMongoDBFieldID(field, value);
5140 * Modifies the rule generated from the MongoDB expression
5141 * @event changer:mongoToRule
5142 * @memberof module:plugins.MongoDbSupport
5143 * @param {object} rule
5144 * @param {object} expression
5147 var rule = self.change('mongoToRule', {
5159 * Modifies the group generated from the MongoDB expression
5160 * @event changer:mongoToGroup
5161 * @memberof module:plugins.MongoDbSupport
5162 * @param {object} group
5163 * @param {object} expression
5166 return self.change('mongoToGroup', {
5167 condition: topKey.replace('$', '').toUpperCase(),
5174 * Sets rules a from MongoDB query
5175 * @see module:plugins.MongoDbSupport.getRulesFromMongo
5177 setRulesFromMongo: function(query) {
5178 this.setRules(this.getRulesFromMongo(query));
5182 * Returns a filter identifier from the MongoDB field.
5183 * Automatically use the only one filter with a matching field, fires a changer otherwise.
5184 * @param {string} field
5186 * @fires module:plugins.MongoDbSupport:changer:getMongoDBFieldID
5190 getMongoDBFieldID: function(field, value) {
5191 var matchingFilters = this.filters.filter(function(filter) {
5192 return filter.field === field;
5196 if (matchingFilters.length === 1) {
5197 id = matchingFilters[0].id;
5201 * Returns a filter identifier from the MongoDB field
5202 * @event changer:getMongoDBFieldID
5203 * @memberof module:plugins.MongoDbSupport
5204 * @param {string} field
5208 id = this.change('getMongoDBFieldID', field, value);
5215 * Finds which operator is used in a MongoDB sub-object
5217 * @returns {string|undefined}
5220 getMongoOperator: function(data) {
5221 if (data !== null && typeof data === 'object') {
5222 if (data.$gte !== undefined && data.$lte !== undefined) {
5225 if (data.$lt !== undefined && data.$gt !== undefined) {
5226 return 'not_between';
5229 var knownKeys = Object.keys(data).filter(function(key) {
5230 return !!this.settings.mongoRuleOperators[key];
5233 if (knownKeys.length === 1) {
5234 return knownKeys[0];
5244 * Returns the key corresponding to "$or" or "$and"
5245 * @param {object} data
5246 * @returns {string|undefined}
5249 getMongoCondition: function(data) {
5250 var keys = Object.keys(data);
5252 for (var i = 0, l = keys.length; i < l; i++) {
5253 if (keys[i].toLowerCase() === '$or' || keys[i].toLowerCase() === '$and') {
5263 * @memberof module:plugins
5264 * @description Adds a "Not" checkbox in front of group conditions.
5265 * @param {object} [options]
5266 * @param {string} [options.icon_checked='glyphicon glyphicon-checked']
5267 * @param {string} [options.icon_unchecked='glyphicon glyphicon-unchecked']
5269 QueryBuilder.define('not-group', function(options) {
5273 this.on('afterInit', function() {
5274 self.$el.on('click.queryBuilder', '[data-not=group]', function() {
5275 var $group = $(this).closest(QueryBuilder.selectors.group_container);
5276 var group = self.getModel($group);
5277 group.not = !group.not;
5280 self.model.on('update', function(e, node, field) {
5281 if (node instanceof Group && field === 'not') {
5282 self.updateGroupNot(node);
5287 // Init "not" property
5288 this.on('afterAddGroup', function(e, group) {
5289 group.__.not = false;
5293 if (!options.disable_template) {
5294 this.on('getGroupTemplate.filter', function(h) {
5295 var $h = $(h.value);
5296 $h.find(QueryBuilder.selectors.condition_container).prepend(
5297 '<button type="button" class="btn btn-xs btn-default" data-not="group">' +
5298 '<i class="' + options.icon_unchecked + '"></i> ' + self.translate('NOT') +
5301 h.value = $h.prop('outerHTML');
5305 // Export "not" to JSON
5306 this.on('groupToJson.filter', function(e, group) {
5307 e.value.not = group.not;
5310 // Read "not" from JSON
5311 this.on('jsonToGroup.filter', function(e, json) {
5312 e.value.not = !!json.not;
5315 // Export "not" to SQL
5316 this.on('groupToSQL.filter', function(e, group) {
5318 e.value = 'NOT ( ' + e.value + ' )';
5322 // Parse "NOT" function from sqlparser
5323 this.on('parseSQLNode.filter', function(e) {
5324 if (e.value.name && e.value.name.toUpperCase() == 'NOT') {
5325 e.value = e.value.arguments.value[0];
5327 // if the there is no sub-group, create one
5328 if (['AND', 'OR'].indexOf(e.value.operation.toUpperCase()) === -1) {
5329 e.value = new SQLParser.nodes.Op(
5330 self.settings.default_condition,
5340 // Request to create sub-group if the "not" flag is set
5341 this.on('sqlGroupsDistinct.filter', function(e, group, data, i) {
5342 if (data.not && i > 0) {
5347 // Read "not" from parsed SQL
5348 this.on('sqlToGroup.filter', function(e, data) {
5349 e.value.not = !!data.not;
5352 // Export "not" to Mongo
5353 this.on('groupToMongo.filter', function(e, group) {
5354 var key = '$' + group.condition.toLowerCase();
5355 if (group.not && e.value[key]) {
5356 e.value = { '$nor': [e.value] };
5360 // Parse "$nor" operator from Mongo
5361 this.on('parseMongoNode.filter', function(e) {
5362 var keys = Object.keys(e.value);
5364 if (keys[0] == '$nor') {
5365 e.value = e.value[keys[0]][0];
5370 // Read "not" from parsed Mongo
5371 this.on('mongoToGroup.filter', function(e, data) {
5372 e.value.not = !!data.not;
5375 icon_unchecked: 'glyphicon glyphicon-unchecked',
5376 icon_checked: 'glyphicon glyphicon-check',
5377 disable_template: false
5381 * From {@link module:plugins.NotGroup}
5387 Utils.defineModelProperties(Group, ['not']);
5389 QueryBuilder.selectors.group_not = QueryBuilder.selectors.group_header + ' [data-not=group]';
5391 QueryBuilder.extend(/** @lends module:plugins.NotGroup.prototype */ {
5393 * Performs actions when a group's not changes
5394 * @param {Group} group
5395 * @fires module:plugins.NotGroup.afterUpdateGroupNot
5398 updateGroupNot: function(group) {
5399 var options = this.plugins['not-group'];
5400 group.$el.find('>' + QueryBuilder.selectors.group_not)
5401 .toggleClass('active', group.not)
5402 .find('i').attr('class', group.not ? options.icon_checked : options.icon_unchecked);
5405 * After the group's not flag has been modified
5406 * @event afterUpdateGroupNot
5407 * @memberof module:plugins.NotGroup
5408 * @param {Group} group
5410 this.trigger('afterUpdateGroupNot', group);
5412 this.trigger('rulesChanged');
5419 * @memberof module:plugins
5420 * @description Enables drag & drop sort of rules.
5421 * @param {object} [options]
5422 * @param {boolean} [options.inherit_no_drop=true]
5423 * @param {boolean} [options.inherit_no_sortable=true]
5424 * @param {string} [options.icon='glyphicon glyphicon-sort']
5425 * @throws MissingLibraryError, ConfigError
5427 QueryBuilder.define('sortable', function(options) {
5428 if (!('interact' in window)) {
5429 Utils.error('MissingLibrary', 'interact.js is required to use "sortable" plugin. Get it here: http://interactjs.io');
5432 if (options.default_no_sortable !== undefined) {
5433 Utils.error(false, 'Config', 'Sortable plugin : "default_no_sortable" options is deprecated, use standard "default_rule_flags" and "default_group_flags" instead');
5434 this.settings.default_rule_flags.no_sortable = this.settings.default_group_flags.no_sortable = options.default_no_sortable;
5437 // recompute drop-zones during drag (when a rule is hidden)
5438 interact.dynamicDrop(true);
5440 // set move threshold to 10px
5441 interact.pointerMoveTolerance(10);
5448 // Init drag and drop
5449 this.on('afterAddRule afterAddGroup', function(e, node) {
5450 if (node == placeholder) {
5454 var self = e.builder;
5457 if (options.inherit_no_sortable && node.parent && node.parent.flags.no_sortable) {
5458 node.flags.no_sortable = true;
5460 if (options.inherit_no_drop && node.parent && node.parent.flags.no_drop) {
5461 node.flags.no_drop = true;
5465 if (!node.flags.no_sortable) {
5466 interact(node.$el[0])
5468 allowFrom: QueryBuilder.selectors.drag_handle,
5469 onstart: function(event) {
5472 // get model of dragged element
5473 src = self.getModel(event.target);
5476 ghost = src.$el.clone()
5477 .appendTo(src.$el.parent())
5478 .width(src.$el.outerWidth())
5479 .addClass('dragging');
5481 // create drop placeholder
5482 var ph = $('<div class="rule-placeholder"> </div>')
5483 .height(src.$el.outerHeight());
5485 placeholder = src.parent.addRule(ph, src.getPos());
5487 // hide dragged element
5490 onmove: function(event) {
5491 // make the ghost follow the cursor
5492 ghost[0].style.top = event.clientY - 15 + 'px';
5493 ghost[0].style.left = event.clientX - 15 + 'px';
5495 onend: function(event) {
5496 // starting from Interact 1.3.3, onend is called before ondrop
5497 if (event.dropzone) {
5498 moveSortableToTarget(src, $(event.relatedTarget), self);
5506 // remove placeholder
5508 placeholder = undefined;
5511 src.$el.css('display', '');
5514 * After a node has been moved with {@link module:plugins.Sortable}
5516 * @memberof module:plugins.Sortable
5517 * @param {Node} node
5519 self.trigger('afterMove', src);
5521 self.trigger('rulesChanged');
5526 if (!node.flags.no_drop) {
5527 // Configure drop on groups and rules
5528 interact(node.$el[0])
5530 accept: QueryBuilder.selectors.rule_and_group_containers,
5531 ondragenter: function(event) {
5532 moveSortableToTarget(placeholder, $(event.target), self);
5534 ondrop: function(event) {
5536 moveSortableToTarget(src, $(event.target), self);
5541 // Configure drop on group headers
5542 if (node instanceof Group) {
5543 interact(node.$el.find(QueryBuilder.selectors.group_header)[0])
5545 accept: QueryBuilder.selectors.rule_and_group_containers,
5546 ondragenter: function(event) {
5547 moveSortableToTarget(placeholder, $(event.target), self);
5549 ondrop: function(event) {
5551 moveSortableToTarget(src, $(event.target), self);
5559 // Detach interactables
5560 this.on('beforeDeleteRule beforeDeleteGroup', function(e, node) {
5561 if (!e.isDefaultPrevented()) {
5562 interact(node.$el[0]).unset();
5564 if (node instanceof Group) {
5565 interact(node.$el.find(QueryBuilder.selectors.group_header)[0]).unset();
5570 // Remove drag handle from non-sortable items
5571 this.on('afterApplyRuleFlags afterApplyGroupFlags', function(e, node) {
5572 if (node.flags.no_sortable) {
5573 node.$el.find('.drag-handle').remove();
5578 if (!options.disable_template) {
5579 this.on('getGroupTemplate.filter', function(h, level) {
5581 var $h = $(h.value);
5582 $h.find(QueryBuilder.selectors.condition_container).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>');
5583 h.value = $h.prop('outerHTML');
5587 this.on('getRuleTemplate.filter', function(h) {
5588 var $h = $(h.value);
5589 $h.find(QueryBuilder.selectors.rule_header).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>');
5590 h.value = $h.prop('outerHTML');
5594 inherit_no_sortable: true,
5595 inherit_no_drop: true,
5596 icon: 'glyphicon glyphicon-sort',
5597 disable_template: false
5600 QueryBuilder.selectors.rule_and_group_containers = QueryBuilder.selectors.rule_container + ', ' + QueryBuilder.selectors.group_container;
5601 QueryBuilder.selectors.drag_handle = '.drag-handle';
5603 QueryBuilder.defaults({
5604 default_rule_flags: {
5608 default_group_flags: {
5615 * Moves an element (placeholder or actual object) depending on active target
5616 * @memberof module:plugins.Sortable
5617 * @param {Node} node
5618 * @param {jQuery} target
5619 * @param {QueryBuilder} [builder]
5622 function moveSortableToTarget(node, target, builder) {
5624 var Selectors = QueryBuilder.selectors;
5627 parent = target.closest(Selectors.rule_container);
5628 if (parent.length) {
5629 method = 'moveAfter';
5634 parent = target.closest(Selectors.group_header);
5635 if (parent.length) {
5636 parent = target.closest(Selectors.group_container);
5637 method = 'moveAtBegin';
5643 parent = target.closest(Selectors.group_container);
5644 if (parent.length) {
5645 method = 'moveAtEnd';
5650 node[method](builder.getModel(parent));
5652 // refresh radio value
5653 if (builder && node instanceof Rule) {
5654 builder.setRuleInputValue(node, node.value);
5662 * @memberof module:plugins
5663 * @description Allows to export rules as a SQL WHERE statement as well as populating the builder from an SQL query.
5664 * @param {object} [options]
5665 * @param {boolean} [options.boolean_as_integer=true] - `true` to convert boolean values to integer in the SQL output
5667 QueryBuilder.define('sql-support', function(options) {
5670 boolean_as_integer: true
5673 QueryBuilder.defaults({
5674 // operators for internal -> SQL conversion
5676 equal: { op: '= ?' },
5677 not_equal: { op: '!= ?' },
5678 in: { op: 'IN(?)', sep: ', ' },
5679 not_in: { op: 'NOT IN(?)', sep: ', ' },
5680 less: { op: '< ?' },
5681 less_or_equal: { op: '<= ?' },
5682 greater: { op: '> ?' },
5683 greater_or_equal: { op: '>= ?' },
5684 between: { op: 'BETWEEN ?', sep: ' AND ' },
5685 not_between: { op: 'NOT BETWEEN ?', sep: ' AND ' },
5686 begins_with: { op: 'LIKE(?)', mod: '{0}%' },
5687 not_begins_with: { op: 'NOT LIKE(?)', mod: '{0}%' },
5688 contains: { op: 'LIKE(?)', mod: '%{0}%' },
5689 not_contains: { op: 'NOT LIKE(?)', mod: '%{0}%' },
5690 ends_with: { op: 'LIKE(?)', mod: '%{0}' },
5691 not_ends_with: { op: 'NOT LIKE(?)', mod: '%{0}' },
5692 is_empty: { op: '= \'\'' },
5693 is_not_empty: { op: '!= \'\'' },
5694 is_null: { op: 'IS NULL' },
5695 is_not_null: { op: 'IS NOT NULL' }
5698 // operators for SQL -> internal conversion
5703 op: v === '' ? 'is_empty' : 'equal'
5709 op: v === '' ? 'is_not_empty' : 'not_equal'
5712 'LIKE': function(v) {
5713 if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
5715 val: v.slice(1, -1),
5719 else if (v.slice(0, 1) == '%') {
5725 else if (v.slice(-1) == '%') {
5727 val: v.slice(0, -1),
5732 Utils.error('SQLParse', 'Invalid value for LIKE operator "{0}"', v);
5735 'NOT LIKE': function(v) {
5736 if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
5738 val: v.slice(1, -1),
5742 else if (v.slice(0, 1) == '%') {
5748 else if (v.slice(-1) == '%') {
5750 val: v.slice(0, -1),
5751 op: 'not_begins_with'
5755 Utils.error('SQLParse', 'Invalid value for NOT LIKE operator "{0}"', v);
5759 return { val: v, op: 'in' };
5761 'NOT IN': function(v) {
5762 return { val: v, op: 'not_in' };
5765 return { val: v, op: 'less' };
5768 return { val: v, op: 'less_or_equal' };
5771 return { val: v, op: 'greater' };
5774 return { val: v, op: 'greater_or_equal' };
5776 'BETWEEN': function(v) {
5777 return { val: v, op: 'between' };
5779 'NOT BETWEEN': function(v) {
5780 return { val: v, op: 'not_between' };
5784 Utils.error('SQLParse', 'Invalid value for IS operator');
5786 return { val: null, op: 'is_null' };
5788 'IS NOT': function(v) {
5790 Utils.error('SQLParse', 'Invalid value for IS operator');
5792 return { val: null, op: 'is_not_null' };
5796 // statements for internal -> SQL conversion
5798 'question_mark': function() {
5801 add: function(rule, value) {
5811 'numbered': function(char) {
5812 if (!char || char.length > 1) char = '$';
5816 add: function(rule, value) {
5819 return char + index;
5827 'named': function(char) {
5828 if (!char || char.length > 1) char = ':';
5832 add: function(rule, value) {
5833 if (!indexes[rule.field]) indexes[rule.field] = 1;
5834 var key = rule.field + '_' + (indexes[rule.field]++);
5835 params[key] = value;
5845 // statements for SQL -> internal conversion
5847 'question_mark': function(values) {
5850 parse: function(v) {
5851 return v == '?' ? values[index++] : v;
5853 esc: function(sql) {
5854 return sql.replace(/\?/g, '\'?\'');
5859 'numbered': function(values, char) {
5860 if (!char || char.length > 1) char = '$';
5861 var regex1 = new RegExp('^\\' + char + '[0-9]+$');
5862 var regex2 = new RegExp('\\' + char + '([0-9]+)', 'g');
5864 parse: function(v) {
5865 return regex1.test(v) ? values[v.slice(1) - 1] : v;
5867 esc: function(sql) {
5868 return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\'');
5873 'named': function(values, char) {
5874 if (!char || char.length > 1) char = ':';
5875 var regex1 = new RegExp('^\\' + char);
5876 var regex2 = new RegExp('\\' + char + '(' + Object.keys(values).join('|') + ')', 'g');
5878 parse: function(v) {
5879 return regex1.test(v) ? values[v.slice(1)] : v;
5881 esc: function(sql) {
5882 return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\'');
5890 * @typedef {object} SqlQuery
5891 * @memberof module:plugins.SqlSupport
5892 * @property {string} sql
5893 * @property {object} params
5896 QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ {
5898 * Returns rules as a SQL query
5899 * @param {boolean|string} [stmt] - use prepared statements: false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)'
5900 * @param {boolean} [nl=false] output with new lines
5901 * @param {object} [data] - current rules by default
5902 * @returns {module:plugins.SqlSupport.SqlQuery}
5903 * @fires module:plugins.SqlSupport.changer:getSQLField
5904 * @fires module:plugins.SqlSupport.changer:ruleToSQL
5905 * @fires module:plugins.SqlSupport.changer:groupToSQL
5906 * @throws UndefinedSQLConditionError, UndefinedSQLOperatorError
5908 getSQL: function(stmt, nl, data) {
5909 data = (data === undefined) ? this.getRules() : data;
5915 nl = !!nl ? '\n' : ' ';
5916 var boolean_as_integer = this.getPluginOptions('sql-support', 'boolean_as_integer');
5918 if (stmt === true) {
5919 stmt = 'question_mark';
5921 if (typeof stmt == 'string') {
5922 var config = getStmtConfig(stmt);
5923 stmt = this.settings.sqlStatements[config[1]](config[2]);
5928 var sql = (function parse(group) {
5929 if (!group.condition) {
5930 group.condition = self.settings.default_condition;
5932 if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) {
5933 Utils.error('UndefinedSQLCondition', 'Unable to build SQL query with condition "{0}"', group.condition);
5942 group.rules.forEach(function(rule) {
5943 if (rule.rules && rule.rules.length > 0) {
5944 parts.push('(' + nl + parse(rule) + nl + ')' + nl);
5947 var sql = self.settings.sqlOperators[rule.operator];
5948 var ope = self.getOperatorByType(rule.operator);
5951 if (sql === undefined) {
5952 Utils.error('UndefinedSQLOperator', 'Unknown SQL operation for operator "{0}"', rule.operator);
5955 if (ope.nb_inputs !== 0) {
5956 if (!(rule.value instanceof Array)) {
5957 rule.value = [rule.value];
5960 rule.value.forEach(function(v, i) {
5962 if (rule.type === 'map') {
5969 if (rule.type == 'boolean' && boolean_as_integer) {
5972 else if (!stmt && rule.type !== 'integer' && rule.type !== 'double' && rule.type !== 'boolean') {
5973 v = Utils.escapeString(v);
5976 if (rule.type == 'datetime') {
5977 if (!('moment' in window)) {
5978 Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
5980 v = moment(v, 'YYYY/MM/DD HH:mm:ss').utc().unix();
5984 if ((rule.type !== 'map') || (rule.type === 'map' && i > 0)) {
5985 v = Utils.fmt(sql.mod, v);
5990 value += stmt.add(rule, v);
5993 if ( rule.type === 'map') {
5996 value = '\'' + value + '\'';
6001 if (typeof v == 'string') {
6002 v = '\'' + v + '\'';
6010 var sqlFn = function(v) {
6011 return sql.op.replace('?', function() {
6017 * Modifies the SQL field used by a rule
6018 * @event changer:getSQLField
6019 * @memberof module:plugins.SqlSupport
6020 * @param {string} field
6021 * @param {Rule} rule
6024 var field = self.change('getSQLField', rule.field, rule);
6026 var ruleExpression = field + ' ' + sqlFn(value);
6029 * Modifies the SQL generated for a rule
6030 * @event changer:ruleToSQL
6031 * @memberof module:plugins.SqlSupport
6032 * @param {string} expression
6033 * @param {Rule} rule
6035 * @param {function} valueWrapper - function that takes the value and adds the operator
6038 parts.push(self.change('ruleToSQL', ruleExpression, rule, value, sqlFn));
6042 var groupExpression = parts.join(' ' + group.condition + nl);
6045 * Modifies the SQL generated for a group
6046 * @event changer:groupToSQL
6047 * @memberof module:plugins.SqlSupport
6048 * @param {string} expression
6049 * @param {Group} group
6052 return self.change('groupToSQL', groupExpression, group);
6069 * Convert a SQL query to rules
6070 * @param {string|module:plugins.SqlSupport.SqlQuery} query
6071 * @param {boolean|string} stmt
6073 * @fires module:plugins.SqlSupport.changer:parseSQLNode
6074 * @fires module:plugins.SqlSupport.changer:getSQLFieldID
6075 * @fires module:plugins.SqlSupport.changer:sqlToRule
6076 * @fires module:plugins.SqlSupport.changer:sqlToGroup
6077 * @throws MissingLibraryError, SQLParseError, UndefinedSQLOperatorError
6079 getRulesFromSQL: function(query, stmt) {
6080 if (!('SQLParser' in window)) {
6081 Utils.error('MissingLibrary', 'SQLParser is required to parse SQL queries. Get it here https://github.com/mistic100/sql-parser');
6086 if (typeof query == 'string') {
6087 query = { sql: query };
6090 if (stmt === true) stmt = 'question_mark';
6091 if (typeof stmt == 'string') {
6092 var config = getStmtConfig(stmt);
6093 stmt = this.settings.sqlRuleStatement[config[1]](query.params, config[2]);
6097 query.sql = stmt.esc(query.sql);
6100 if (query.sql.toUpperCase().indexOf('SELECT') !== 0) {
6101 query.sql = 'SELECT * FROM table WHERE ' + query.sql;
6104 var parsed = SQLParser.parse(query.sql);
6106 if (!parsed.where) {
6107 Utils.error('SQLParse', 'No WHERE clause found');
6111 * 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
6112 * @event changer:parseSQLNode
6113 * @memberof module:plugins.SqlSupport
6114 * @param {object} AST node
6115 * @returns {object} tree, rule or group
6117 var data = self.change('parseSQLNode', parsed.where.conditions);
6119 // a plugin returned a group
6120 if ('rules' in data && 'condition' in data) {
6124 // a plugin returned a rule
6125 if ('id' in data && 'operator' in data && 'value' in data) {
6127 condition: this.settings.default_condition,
6132 // create root group
6133 var out = self.change('sqlToGroup', {
6134 condition: this.settings.default_condition,
6138 // keep track of current group
6141 (function flatten(data, i) {
6142 if (data === null) {
6146 // allow plugins to manually parse or handle special cases
6147 data = self.change('parseSQLNode', data);
6149 // a plugin returned a group
6150 if ('rules' in data && 'condition' in data) {
6151 curr.rules.push(data);
6155 // a plugin returned a rule
6156 if ('id' in data && 'operator' in data && 'value' in data) {
6157 curr.rules.push(data);
6161 // data must be a SQL parser node
6162 if (!('left' in data) || !('right' in data) || !('operation' in data)) {
6163 Utils.error('SQLParse', 'Unable to parse WHERE clause');
6167 if (['AND', 'OR'].indexOf(data.operation.toUpperCase()) !== -1) {
6168 // create a sub-group if the condition is not the same and it's not the first level
6171 * Given an existing group and an AST node, determines if a sub-group must be created
6172 * @event changer:sqlGroupsDistinct
6173 * @memberof module:plugins.SqlSupport
6174 * @param {boolean} create - true by default if the group condition is different
6175 * @param {object} group
6176 * @param {object} AST
6177 * @param {int} current group level
6178 * @returns {boolean}
6180 var createGroup = self.change('sqlGroupsDistinct', i > 0 && curr.condition != data.operation.toUpperCase(), curr, data, i);
6184 * Modifies the group generated from the SQL expression (this is called before the group is filled with rules)
6185 * @event changer:sqlToGroup
6186 * @memberof module:plugins.SqlSupport
6187 * @param {object} group
6188 * @param {object} AST
6191 var group = self.change('sqlToGroup', {
6192 condition: self.settings.default_condition,
6196 curr.rules.push(group);
6200 curr.condition = data.operation.toUpperCase();
6205 flatten(data.left, i);
6208 flatten(data.right, i);
6212 if ($.isPlainObject(data.right.value)) {
6213 Utils.error('SQLParse', 'Value format not supported for {0}.', data.left.value);
6218 if ($.isArray(data.right.value)) {
6219 value = data.right.value.map(function(v) {
6224 value = data.right.value;
6227 // get actual values
6229 if ($.isArray(value)) {
6230 value = value.map(stmt.parse);
6233 value = stmt.parse(value);
6238 var operator = data.operation.toUpperCase();
6239 if (operator == '<>') {
6243 var sqlrl = self.settings.sqlRuleOperator[operator];
6244 if (sqlrl === undefined) {
6245 Utils.error('UndefinedSQLOperator', 'Invalid SQL operation "{0}".', data.operation);
6250 if ('values' in data.left) {
6251 field = data.left.values.join('.');
6253 else if ('value' in data.left) {
6254 field = data.left.value;
6257 Utils.error('SQLParse', 'Cannot find field name in {0}', JSON.stringify(data.left));
6260 var matchingFilter = self.filters.filter(function(filter) {
6261 return filter.field.toLowerCase() === field.toLowerCase();
6265 if(matchingFilter && matchingFilter[0].type === 'map') {
6266 var tempVal = value.split('|')
6267 opVal = sqlrl.call(this, tempVal[1], data.operation);
6268 opVal.val = tempVal[0] + '|' + opVal.val;
6270 opVal = sqlrl.call(this, value, data.operation);
6273 var id = self.getSQLFieldID(field, value);
6276 * Modifies the rule generated from the SQL expression
6277 * @event changer:sqlToRule
6278 * @memberof module:plugins.SqlSupport
6279 * @param {object} rule
6280 * @param {object} AST
6283 var rule = self.change('sqlToRule', {
6290 curr.rules.push(rule);
6298 * Sets the builder's rules from a SQL query
6299 * @see module:plugins.SqlSupport.getRulesFromSQL
6301 setRulesFromSQL: function(query, stmt) {
6302 this.setRules(this.getRulesFromSQL(query, stmt));
6306 * Returns a filter identifier from the SQL field.
6307 * Automatically use the only one filter with a matching field, fires a changer otherwise.
6308 * @param {string} field
6310 * @fires module:plugins.SqlSupport:changer:getSQLFieldID
6314 getSQLFieldID: function(field, value) {
6315 var matchingFilters = this.filters.filter(function(filter) {
6316 return filter.field.toLowerCase() === field.toLowerCase();
6320 if (matchingFilters.length === 1) {
6321 id = matchingFilters[0].id;
6325 * Returns a filter identifier from the SQL field
6326 * @event changer:getSQLFieldID
6327 * @memberof module:plugins.SqlSupport
6328 * @param {string} field
6332 id = this.change('getSQLFieldID', field, value);
6340 * Parses the statement configuration
6341 * @memberof module:plugins.SqlSupport
6342 * @param {string} stmt
6343 * @returns {Array} null, mode, option
6346 function getStmtConfig(stmt) {
6347 var config = stmt.match(/(question_mark|numbered|named)(?:\((.)\))?/);
6348 if (!config) config = [null, 'question_mark', undefined];
6354 * @class UniqueFilter
6355 * @memberof module:plugins
6356 * @description Allows to define some filters as "unique": ie which can be used for only one rule, globally or in the same group.
6358 QueryBuilder.define('unique-filter', function() {
6359 this.status.used_filters = {};
6361 this.on('afterUpdateRuleFilter', this.updateDisabledFilters);
6362 this.on('afterDeleteRule', this.updateDisabledFilters);
6363 this.on('afterCreateRuleFilters', this.applyDisabledFilters);
6364 this.on('afterReset', this.clearDisabledFilters);
6365 this.on('afterClear', this.clearDisabledFilters);
6367 // Ensure that the default filter is not already used if unique
6368 this.on('getDefaultFilter.filter', function(e, model) {
6369 var self = e.builder;
6371 self.updateDisabledFilters();
6373 if (e.value.id in self.status.used_filters) {
6374 var found = self.filters.some(function(filter) {
6375 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) {
6382 Utils.error(false, 'UniqueFilter', 'No more non-unique filters available');
6383 e.value = undefined;
6389 QueryBuilder.extend(/** @lends module:plugins.UniqueFilter.prototype */ {
6391 * Updates the list of used filters
6392 * @param {$.Event} [e]
6395 updateDisabledFilters: function(e) {
6396 var self = e ? e.builder : this;
6398 self.status.used_filters = {};
6405 (function walk(group) {
6406 group.each(function(rule) {
6407 if (rule.filter && rule.filter.unique) {
6408 if (!self.status.used_filters[rule.filter.id]) {
6409 self.status.used_filters[rule.filter.id] = [];
6411 if (rule.filter.unique == 'group') {
6412 self.status.used_filters[rule.filter.id].push(rule.parent);
6415 }, function(group) {
6418 }(self.model.root));
6420 self.applyDisabledFilters(e);
6424 * Clear the list of used filters
6425 * @param {$.Event} [e]
6428 clearDisabledFilters: function(e) {
6429 var self = e ? e.builder : this;
6431 self.status.used_filters = {};
6433 self.applyDisabledFilters(e);
6437 * Disabled filters depending on the list of used ones
6438 * @param {$.Event} [e]
6441 applyDisabledFilters: function(e) {
6442 var self = e ? e.builder : this;
6444 // re-enable everything
6445 self.$el.find(QueryBuilder.selectors.filter_container + ' option').prop('disabled', false);
6448 $.each(self.status.used_filters, function(filterId, groups) {
6449 if (groups.length === 0) {
6450 self.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true);
6453 groups.forEach(function(group) {
6454 group.each(function(rule) {
6455 rule.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true);
6461 // update Selectpicker
6462 if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) {
6463 self.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render');
6470 * jQuery QueryBuilder 2.5.2
6471 * Locale: English (en)
6472 * Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr
6473 * Licensed under MIT (https://opensource.org/licenses/MIT)
6476 QueryBuilder.regional['en'] = {
6477 "__locale": "English (en)",
6478 "__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr",
6479 "add_rule": "Add rule",
6480 "add_group": "Add group",
6481 "delete_rule": "Delete",
6482 "delete_group": "Delete",
6489 "not_equal": "not equal",
6493 "less_or_equal": "less or equal",
6494 "greater": "greater",
6495 "greater_or_equal": "greater or equal",
6496 "between": "between",
6497 "not_between": "not between",
6498 "begins_with": "begins with",
6499 "not_begins_with": "doesn't begin with",
6500 "contains": "contains",
6501 "not_contains": "doesn't contain",
6502 "ends_with": "ends with",
6503 "not_ends_with": "doesn't end with",
6504 "is_empty": "is empty",
6505 "is_not_empty": "is not empty",
6506 "is_null": "is null",
6507 "is_not_null": "is not null"
6510 "no_filter": "No filter selected",
6511 "empty_group": "The group is empty",
6512 "radio_empty": "No value selected",
6513 "checkbox_empty": "No value selected",
6514 "select_empty": "No value selected",
6515 "string_empty": "Empty value",
6516 "string_exceed_min_length": "Must contain at least {0} characters",
6517 "string_exceed_max_length": "Must not contain more than {0} characters",
6518 "string_invalid_format": "Invalid format ({0})",
6519 "number_nan": "Not a number",
6520 "number_not_integer": "Not an integer",
6521 "number_not_double": "Not a real number",
6522 "number_exceed_min": "Must be greater than {0}",
6523 "number_exceed_max": "Must be lower than {0}",
6524 "number_wrong_step": "Must be a multiple of {0}",
6525 "number_between_invalid": "Invalid values, {0} is greater than {1}",
6526 "datetime_empty": "Empty value",
6527 "datetime_invalid": "Invalid date format ({0})",
6528 "datetime_exceed_min": "Must be after {0}",
6529 "datetime_exceed_max": "Must be before {0}",
6530 "datetime_between_invalid": "Invalid values, {0} is greater than {1}",
6531 "boolean_not_valid": "Not a boolean",
6532 "operator_not_multiple": "Operator \"{1}\" cannot accept multiple values"
6538 QueryBuilder.defaults({ lang_code: 'en' });
6539 return QueryBuilder;