Draft of React test
[clamp.git] / src / main / resources / META-INF / resources / designer / lib / query-builder.standalone.js
1 /*!
2  * jQuery.extendext 0.1.2
3  *
4  * Copyright 2014-2016 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
5  * Licensed under MIT (http://opensource.org/licenses/MIT)
6  * 
7  * Based on jQuery.extend by jQuery Foundation, Inc. and other contributors
8  */
9
10 (function (root, factory) {
11     if (typeof define === 'function' && define.amd) {
12         define('jQuery.extendext', ['jquery'], factory);
13     }
14     else if (typeof module === 'object' && module.exports) {
15         module.exports = factory(require('jquery'));
16     }
17     else {
18         factory(root.jQuery);
19     }
20 }(this, function ($) {
21     "use strict";
22
23     $.extendext = function () {
24         var options, name, src, copy, copyIsArray, clone,
25             target = arguments[0] || {},
26             i = 1,
27             length = arguments.length,
28             deep = false,
29             arrayMode = 'default';
30
31         // Handle a deep copy situation
32         if (typeof target === "boolean") {
33             deep = target;
34
35             // Skip the boolean and the target
36             target = arguments[i++] || {};
37         }
38
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';
44             }
45
46             // Skip the string param
47             target = arguments[i++] || {};
48         }
49
50         // Handle case when target is a string or something (possible in deep copy)
51         if (typeof target !== "object" && !$.isFunction(target)) {
52             target = {};
53         }
54
55         // Extend jQuery itself if only one argument is passed
56         if (i === length) {
57             target = this;
58             i--;
59         }
60
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 : [];
67
68                     switch (arrayMode) {
69                     case 'concat':
70                         target = clone.concat($.extend(deep, [], options));
71                         break;
72
73                     case 'replace':
74                         target = $.extend(deep, [], options);
75                         break;
76
77                     case 'extend':
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);
82
83                             } else if (clone.indexOf(e) === -1) {
84                                 clone.push(e);
85                             }
86                         });
87
88                         target = clone;
89                         break;
90                     }
91
92                 } else {
93                     // Extend the base object
94                     for (name in options) {
95                         src = target[name];
96                         copy = options[name];
97
98                         // Prevent never-ending loop
99                         if (target === copy) {
100                             continue;
101                         }
102
103                         // Recurse if we're merging plain objects or arrays
104                         if (deep && copy && ( $.isPlainObject(copy) ||
105                             (copyIsArray = $.isArray(copy)) )) {
106
107                             if (copyIsArray) {
108                                 copyIsArray = false;
109                                 clone = src && $.isArray(src) ? src : [];
110
111                             } else {
112                                 clone = src && $.isPlainObject(src) ? src : {};
113                             }
114
115                             // Never move original objects, clone them
116                             target[name] = $.extendext(deep, arrayMode, clone, copy);
117
118                             // Don't bring in undefined values
119                         } else if (copy !== undefined) {
120                             target[name] = copy;
121                         }
122                     }
123                 }
124             }
125         }
126
127         // Return the modified object
128         return target;
129     };
130 }));
131
132 // doT.js
133 // 2011-2014, Laura Doktorova, https://github.com/olado/doT
134 // Licensed under the MIT license.
135
136 (function () {
137         "use strict";
138
139         var doT = {
140                 name: "doT",
141                 version: "1.1.1",
142                 templateSettings: {
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,
152                         varname:        "it",
153                         strip:          true,
154                         append:         true,
155                         selfcontained: false,
156                         doNotSkipEncoded: false
157                 },
158                 template: undefined, //fn, compile template
159                 compile:  undefined, //fn, for express
160                 log: true
161         }, _globals;
162
163         doT.encodeHTMLSource = function(doNotSkipEncoded) {
164                 var encodeHTMLRules = { "&": "&#38;", "<": "&#60;", ">": "&#62;", '"': "&#34;", "'": "&#39;", "/": "&#47;" },
165                         matchHTML = doNotSkipEncoded ? /[&<>"'\/]/g : /&(?!#?\w+;)|<|>|"|'|\//g;
166                 return function(code) {
167                         return code ? code.toString().replace(matchHTML, function(m) {return encodeHTMLRules[m] || m;}) : "";
168                 };
169         };
170
171         _globals = (function(){ return this || (0,eval)("this"); }());
172
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;});
178         } else {
179                 _globals.doT = doT;
180         }
181
182         var startend = {
183                 append: { start: "'+(",      end: ")+'",      startencode: "'+encodeHTML(" },
184                 split:  { start: "';out+=(", end: ");out+='", startencode: "';out+=encodeHTML(" }
185         }, skip = /$^/;
186
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);
192                         }
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};
197                                         });
198                                         if (!(code in def)) def[code]= value;
199                                 } else {
200                                         new Function("def", "def['"+code+"']=" + value)(def);
201                                 }
202                         }
203                         return "";
204                 })
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+"']";
212                                 }
213                         });
214                         var v = new Function("def", "return " + code)(def);
215                         return v ? resolveDefs(c, v, def) : v;
216                 });
217         }
218
219         function unescape(code) {
220                 return code.replace(/\\('|\\)/g, "$1").replace(/[\r\t\n]/g, " ");
221         }
222
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;
227
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;
233                         })
234                         .replace(c.encode || skip, function(m, code) {
235                                 needhtmlencode = true;
236                                 return cse.startencode + unescape(code) + cse.end;
237                         })
238                         .replace(c.conditional || skip, function(m, elsecase, code) {
239                                 return elsecase ?
240                                         (code ? "';}else if(" + unescape(code) + "){out+='" : "';}else{out+='") :
241                                         (code ? "';if(" + unescape(code) + "){out+='" : "';}out+='");
242                         })
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+='";
248                         })
249                         .replace(c.evaluate || skip, function(m, code) {
250                                 return "';" + unescape(code) + "out+='";
251                         })
252                         + "';return 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+=');
256
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 || '') + "));"
261                                 + str;
262                 }
263                 try {
264                         return new Function(c.varname, str);
265                 } catch (e) {
266                         /* istanbul ignore else */
267                         if (typeof console !== "undefined") console.log("Could not create a template function: " + str);
268                         throw e;
269                 }
270         };
271
272         doT.compile = function(tmpl, def) {
273                 return doT.template(tmpl, null, def);
274         };
275 }());
276
277
278 /*!
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)
282  */
283 (function(root, factory) {
284     if (typeof define == 'function' && define.amd) {
285         define('query-builder', ['jquery', 'dot/doT', 'jquery-extendext'], factory);
286     }
287     else if (typeof module === 'object' && module.exports) {
288         module.exports = factory(require('jquery'), require('dot/doT'), require('jquery-extendext'));
289     }
290     else {
291         factory(root.jQuery, root.doT);
292     }
293 }(this, function($, doT) {
294 "use strict";
295
296 /**
297  * @typedef {object} Filter
298  * @memberof QueryBuilder
299  * @description See {@link http://querybuilder.js.org/index.html#filters}
300  */
301
302 /**
303  * @typedef {object} Operator
304  * @memberof QueryBuilder
305  * @description See {@link http://querybuilder.js.org/index.html#operators}
306  */
307
308 /**
309  * @param {jQuery} $el
310  * @param {object} options - see {@link http://querybuilder.js.org/#options}
311  * @constructor
312  */
313 var QueryBuilder = function($el, options) {
314     $el[0].queryBuilder = this;
315
316     /**
317      * Element container
318      * @member {jQuery}
319      * @readonly
320      */
321     this.$el = $el;
322
323     /**
324      * Configuration object
325      * @member {object}
326      * @readonly
327      */
328     this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options);
329
330     /**
331      * Internal model
332      * @member {Model}
333      * @readonly
334      */
335     this.model = new Model();
336
337     /**
338      * Internal status
339      * @member {object}
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
346      * @readonly
347      * @private
348      */
349     this.status = {
350         id: null,
351         generated_id: false,
352         group_id: 0,
353         rule_id: 0,
354         has_optgroup: false,
355         has_operator_optgroup: false
356     };
357
358     /**
359      * List of filters
360      * @member {QueryBuilder.Filter[]}
361      * @readonly
362      */
363     this.filters = this.settings.filters;
364
365     /**
366      * List of icons
367      * @member {object.<string, string>}
368      * @readonly
369      */
370     this.icons = this.settings.icons;
371
372     /**
373      * List of operators
374      * @member {QueryBuilder.Operator[]}
375      * @readonly
376      */
377     this.operators = this.settings.operators;
378
379     /**
380      * List of templates
381      * @member {object.<string, function>}
382      * @readonly
383      */
384     this.templates = this.settings.templates;
385
386     /**
387      * Plugins configuration
388      * @member {object.<string, object>}
389      * @readonly
390      */
391     this.plugins = this.settings.plugins;
392
393     /**
394      * Translations object
395      * @member {object}
396      * @readonly
397      */
398     this.lang = null;
399
400     // translations : english << 'lang_code' << custom
401     if (QueryBuilder.regional['en'] === undefined) {
402         Utils.error('Config', '"i18n/en.js" not loaded.');
403     }
404     this.lang = $.extendext(true, 'replace', {}, QueryBuilder.regional['en'], QueryBuilder.regional[this.settings.lang_code], this.settings.lang);
405
406     // "allow_groups" can be boolean or int
407     if (this.settings.allow_groups === false) {
408         this.settings.allow_groups = 0;
409     }
410     else if (this.settings.allow_groups === true) {
411         this.settings.allow_groups = -1;
412     }
413
414     // init templates
415     Object.keys(this.templates).forEach(function(tpl) {
416         if (!this.templates[tpl]) {
417             this.templates[tpl] = QueryBuilder.templates[tpl];
418         }
419         if (typeof this.templates[tpl] == 'string') {
420             this.templates[tpl] = doT.template(this.templates[tpl]);
421         }
422     }, this);
423
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;
428     }
429     this.status.id = this.$el.attr('id');
430
431     // INIT
432     this.$el.addClass('query-builder form-inline');
433
434     this.filters = this.checkFilters(this.filters);
435     this.operators = this.checkOperators(this.operators);
436     this.bindEvents();
437     this.initPlugins();
438 };
439
440 $.extend(QueryBuilder.prototype, /** @lends QueryBuilder.prototype */ {
441     /**
442      * Triggers an event on the builder container
443      * @param {string} type
444      * @returns {$.Event}
445      */
446     trigger: function(type) {
447         var event = new $.Event(this._tojQueryEvent(type), {
448             builder: this
449         });
450
451         this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 1));
452
453         return event;
454     },
455
456     /**
457      * Triggers an event on the builder container and returns the modified value
458      * @param {string} type
459      * @param {*} value
460      * @returns {*}
461      */
462     change: function(type, value) {
463         var event = new $.Event(this._tojQueryEvent(type, true), {
464             builder: this,
465             value: value
466         });
467
468         this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 2));
469
470         return event.value;
471     },
472
473     /**
474      * Attaches an event listener on the builder container
475      * @param {string} type
476      * @param {function} cb
477      * @returns {QueryBuilder}
478      */
479     on: function(type, cb) {
480         this.$el.on(this._tojQueryEvent(type), cb);
481         return this;
482     },
483
484     /**
485      * Removes an event listener from the builder container
486      * @param {string} type
487      * @param {function} [cb]
488      * @returns {QueryBuilder}
489      */
490     off: function(type, cb) {
491         this.$el.off(this._tojQueryEvent(type), cb);
492         return this;
493     },
494
495     /**
496      * Attaches an event listener called once on the builder container
497      * @param {string} type
498      * @param {function} cb
499      * @returns {QueryBuilder}
500      */
501     once: function(type, cb) {
502         this.$el.one(this._tojQueryEvent(type), cb);
503         return this;
504     },
505
506     /**
507      * Appends `.queryBuilder` and optionally `.filter` to the events names
508      * @param {string} name
509      * @param {boolean} [filter=false]
510      * @returns {string}
511      * @private
512      */
513     _tojQueryEvent: function(name, filter) {
514         return name.split(' ').map(function(type) {
515             return type + '.queryBuilder' + (filter ? '.filter' : '');
516         }).join(' ');
517     }
518 });
519
520
521 /**
522  * Allowed types and their internal representation
523  * @type {object.<string, string>}
524  * @readonly
525  * @private
526  */
527 QueryBuilder.types = {
528     'string':   'string',
529     'integer':  'number',
530     'double':   'number',
531     'date':     'datetime',
532     'time':     'datetime',
533     'datetime': 'datetime',
534     'boolean':  'boolean',
535     'map': 'map'
536 };
537
538 /**
539  * Allowed inputs
540  * @type {string[]}
541  * @readonly
542  * @private
543  */
544 QueryBuilder.inputs = [
545     'text',
546     'number',
547     'textarea',
548     'radio',
549     'checkbox',
550     'select'
551 ];
552
553 /**
554  * Runtime modifiable options with `setOptions` method
555  * @type {string[]}
556  * @readonly
557  * @private
558  */
559 QueryBuilder.modifiable_options = [
560     'display_errors',
561     'allow_groups',
562     'allow_empty',
563     'default_condition',
564     'default_filter'
565 ];
566
567 /**
568  * CSS selectors for common components
569  * @type {object.<string, string>}
570  * @readonly
571  */
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',
580
581     rule_header:          '.rule-header',
582     group_header:         '.rules-group-header',
583     group_actions:        '.group-actions',
584     rule_actions:         '.rule-actions',
585
586     rules_list:           '.rules-group-body>.rules-list',
587
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_]',
592
593     add_rule:             '[data-add=rule]',
594     delete_rule:          '[data-delete=rule]',
595     add_group:            '[data-add=group]',
596     delete_group:         '[data-delete=group]'
597 };
598
599 /**
600  * Template strings (see template.js)
601  * @type {object.<string, string>}
602  * @readonly
603  */
604 QueryBuilder.templates = {};
605
606 /**
607  * Localized strings (see i18n/)
608  * @type {object.<string, object>}
609  * @readonly
610  */
611 QueryBuilder.regional = {};
612
613 /**
614  * Default operators
615  * @type {object.<string, object>}
616  * @readonly
617  */
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'] }
639 };
640
641 /**
642  * Default configuration
643  * @type {object}
644  * @readonly
645  */
646 QueryBuilder.DEFAULTS = {
647     filters: [],
648     plugins: [],
649
650     sort_filters: false,
651     display_errors: true,
652     allow_groups: -1,
653     allow_empty: true,
654     conditions: ['AND', 'OR'],
655     default_condition: 'AND',
656     inputs_separator: ' , ',
657     select_placeholder: '------',
658     display_empty_filter: true,
659     default_filter: null,
660     optgroups: {},
661
662     default_rule_flags: {
663         filter_readonly: false,
664         operator_readonly: false,
665         value_readonly: false,
666         no_delete: false
667     },
668
669     default_group_flags: {
670         condition_readonly: false,
671         no_add_rule: false,
672         no_add_group: false,
673         no_delete: false
674     },
675
676     templates: {
677         group: null,
678         rule: null,
679         filterSelect: null,
680         operatorSelect: null,
681         ruleValueSelect: null
682     },
683
684     lang_code: 'en',
685     lang: {},
686
687     operators: [
688         'equal',
689         'not_equal',
690         'in',
691         'not_in',
692         'less',
693         'less_or_equal',
694         'greater',
695         'greater_or_equal',
696         'between',
697         'not_between',
698         'begins_with',
699         'not_begins_with',
700         'contains',
701         'not_contains',
702         'ends_with',
703         'not_ends_with',
704         'is_empty',
705         'is_not_empty',
706         'is_null',
707         'is_not_null'
708     ],
709
710     icons: {
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'
716     }
717 };
718
719
720 /**
721  * @module plugins
722  */
723
724 /**
725  * Definition of available plugins
726  * @type {object.<String, object>}
727  */
728 QueryBuilder.plugins = {};
729
730 /**
731  * Gets or extends the default configuration
732  * @param {object} [options] - new configuration
733  * @returns {undefined|object} nothing or configuration object (copy)
734  */
735 QueryBuilder.defaults = function(options) {
736     if (typeof options == 'object') {
737         $.extendext(true, 'replace', QueryBuilder.DEFAULTS, options);
738     }
739     else if (typeof options == 'string') {
740         if (typeof QueryBuilder.DEFAULTS[options] == 'object') {
741             return $.extend(true, {}, QueryBuilder.DEFAULTS[options]);
742         }
743         else {
744             return QueryBuilder.DEFAULTS[options];
745         }
746     }
747     else {
748         return $.extend(true, {}, QueryBuilder.DEFAULTS);
749     }
750 };
751
752 /**
753  * Registers a new plugin
754  * @param {string} name
755  * @param {function} fct - init function
756  * @param {object} [def] - default options
757  */
758 QueryBuilder.define = function(name, fct, def) {
759     QueryBuilder.plugins[name] = {
760         fct: fct,
761         def: def || {}
762     };
763 };
764
765 /**
766  * Adds new methods to QueryBuilder prototype
767  * @param {object.<string, function>} methods
768  */
769 QueryBuilder.extend = function(methods) {
770     $.extend(QueryBuilder.prototype, methods);
771 };
772
773 /**
774  * Initializes plugins for an instance
775  * @throws ConfigError
776  * @private
777  */
778 QueryBuilder.prototype.initPlugins = function() {
779     if (!this.plugins) {
780         return;
781     }
782
783     if ($.isArray(this.plugins)) {
784         var tmp = {};
785         this.plugins.forEach(function(plugin) {
786             tmp[plugin] = null;
787         });
788         this.plugins = tmp;
789     }
790
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] || {}
796             );
797
798             QueryBuilder.plugins[plugin].fct.call(this, this.plugins[plugin]);
799         }
800         else {
801             Utils.error('Config', 'Unable to find plugin "{0}"', plugin);
802         }
803     }, this);
804 };
805
806 /**
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
811  * @returns {*}
812  */
813 QueryBuilder.prototype.getPluginOptions = function(name, property) {
814     var plugin;
815     if (this.plugins && this.plugins[name]) {
816         plugin = this.plugins[name];
817     }
818     else if (QueryBuilder.plugins[name]) {
819         plugin = QueryBuilder.plugins[name].def;
820     }
821
822     if (plugin) {
823         if (property) {
824             return plugin[property];
825         }
826         else {
827             return plugin;
828         }
829     }
830     else {
831         Utils.error('Config', 'Unable to find plugin "{0}"', name);
832     }
833 };
834
835
836 /**
837  * Final initialisation of the builder
838  * @param {object} [rules]
839  * @fires QueryBuilder.afterInit
840  * @private
841  */
842 QueryBuilder.prototype.init = function(rules) {
843     /**
844      * When the initilization is done, just before creating the root group
845      * @event afterInit
846      * @memberof QueryBuilder
847      */
848     this.trigger('afterInit');
849
850     if (rules) {
851         this.setRules(rules);
852         delete this.settings.rules;
853     }
854     else {
855         this.setRoot(true);
856     }
857 };
858
859 /**
860  * Checks the configuration of each filter
861  * @param {QueryBuilder.Filter[]} filters
862  * @returns {QueryBuilder.Filter[]}
863  * @throws ConfigError
864  */
865 QueryBuilder.prototype.checkFilters = function(filters) {
866     var definedFilters = [];
867
868     if (!filters || filters.length === 0) {
869         Utils.error('Config', 'Missing filters list');
870     }
871
872     filters.forEach(function(filter, i) {
873         if (!filter.id) {
874             Utils.error('Config', 'Missing filter {0} id', i);
875         }
876         if (definedFilters.indexOf(filter.id) != -1) {
877             Utils.error('Config', 'Filter "{0}" already defined', filter.id);
878         }
879         definedFilters.push(filter.id);
880
881         if (!filter.type) {
882             filter.type = 'string';
883         }
884         else if (!QueryBuilder.types[filter.type]) {
885             Utils.error('Config', 'Invalid type "{0}"', filter.type);
886         }
887
888         if (!filter.input) {
889             filter.input = QueryBuilder.types[filter.type] === 'number' ? 'number' : 'text';
890         }
891         else if (typeof filter.input != 'function' && QueryBuilder.inputs.indexOf(filter.input) == -1) {
892             Utils.error('Config', 'Invalid input "{0}"', filter.input);
893         }
894
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)');
899                 }
900             });
901         }
902
903         if (!filter.field) {
904             filter.field = filter.id;
905         }
906         if (!filter.label) {
907             filter.label = filter.field;
908         }
909
910         if (!filter.optgroup) {
911             filter.optgroup = null;
912         }
913         else {
914             this.status.has_optgroup = true;
915
916             // register optgroup if needed
917             if (!this.settings.optgroups[filter.optgroup]) {
918                 this.settings.optgroups[filter.optgroup] = filter.optgroup;
919             }
920         }
921
922         switch (filter.input) {
923             case 'radio':
924             case 'checkbox':
925                 if (!filter.values || filter.values.length < 1) {
926                     Utils.error('Config', 'Missing filter "{0}" values', filter.id);
927                 }
928                 break;
929
930             case 'select':
931                 var cleanValues = [];
932                 filter.has_optgroup = false;
933
934                 Utils.iterateOptions(filter.values, function(value, label, optgroup) {
935                     cleanValues.push({
936                         value: value,
937                         label: label,
938                         optgroup: optgroup || null
939                     });
940
941                     if (optgroup) {
942                         filter.has_optgroup = true;
943
944                         // register optgroup if needed
945                         if (!this.settings.optgroups[optgroup]) {
946                             this.settings.optgroups[optgroup] = optgroup;
947                         }
948                     }
949                 }.bind(this));
950
951                 if (filter.has_optgroup) {
952                     filter.values = Utils.groupSort(cleanValues, 'optgroup');
953                 }
954                 else {
955                     filter.values = cleanValues;
956                 }
957
958                 if (filter.placeholder) {
959                     if (filter.placeholder_value === undefined) {
960                         filter.placeholder_value = -1;
961                     }
962
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);
966                         }
967                     });
968                 }
969                 break;
970         }
971     }, this);
972
973     if (this.settings.sort_filters) {
974         if (typeof this.settings.sort_filters == 'function') {
975             filters.sort(this.settings.sort_filters);
976         }
977         else {
978             var self = this;
979             filters.sort(function(a, b) {
980                 return self.translate(a.label).localeCompare(self.translate(b.label));
981             });
982         }
983     }
984
985     if (this.status.has_optgroup) {
986         filters = Utils.groupSort(filters, 'optgroup');
987     }
988
989     return filters;
990 };
991
992 /**
993  * Checks the configuration of each operator
994  * @param {QueryBuilder.Operator[]} operators
995  * @returns {QueryBuilder.Operator[]}
996  * @throws ConfigError
997  */
998 QueryBuilder.prototype.checkOperators = function(operators) {
999     var definedOperators = [];
1000
1001     operators.forEach(function(operator, i) {
1002         if (typeof operator == 'string') {
1003             if (!QueryBuilder.OPERATORS[operator]) {
1004                 Utils.error('Config', 'Unknown operator "{0}"', operator);
1005             }
1006
1007             operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator]);
1008         }
1009         else {
1010             if (!operator.type) {
1011                 Utils.error('Config', 'Missing "type" for operator {0}', i);
1012             }
1013
1014             if (QueryBuilder.OPERATORS[operator.type]) {
1015                 operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator.type], operator);
1016             }
1017
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);
1020             }
1021         }
1022
1023         if (definedOperators.indexOf(operator.type) != -1) {
1024             Utils.error('Config', 'Operator "{0}" already defined', operator.type);
1025         }
1026         definedOperators.push(operator.type);
1027
1028         if (!operator.optgroup) {
1029             operator.optgroup = null;
1030         }
1031         else {
1032             this.status.has_operator_optgroup = true;
1033
1034             // register optgroup if needed
1035             if (!this.settings.optgroups[operator.optgroup]) {
1036                 this.settings.optgroups[operator.optgroup] = operator.optgroup;
1037             }
1038         }
1039     }, this);
1040
1041     if (this.status.has_operator_optgroup) {
1042         operators = Utils.groupSort(operators, 'optgroup');
1043     }
1044
1045     return operators;
1046 };
1047
1048 /**
1049  * Adds all events listeners to the builder
1050  * @private
1051  */
1052 QueryBuilder.prototype.bindEvents = function() {
1053     var self = this;
1054     var Selectors = QueryBuilder.selectors;
1055
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();
1061         }
1062     });
1063
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());
1068     });
1069
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());
1074     });
1075
1076     // add rule button
1077     this.$el.on('click.queryBuilder', Selectors.add_rule, function() {
1078         var $group = $(this).closest(Selectors.group_container);
1079         self.addRule(self.getModel($group));
1080     });
1081
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));
1086     });
1087
1088     if (this.settings.allow_groups !== 0) {
1089         // add group button
1090         this.$el.on('click.queryBuilder', Selectors.add_group, function() {
1091             var $group = $(this).closest(Selectors.group_container);
1092             self.addGroup(self.getModel($group));
1093         });
1094
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));
1099         });
1100     }
1101
1102     // model events
1103     this.model.on({
1104         'drop': function(e, node) {
1105             node.$el.remove();
1106             self.refreshGroupsConditions();
1107         },
1108         'add': function(e, parent, node, index) {
1109             if (index === 0) {
1110                 node.$el.prependTo(parent.$el.find('>' + QueryBuilder.selectors.rules_list));
1111             }
1112             else {
1113                 node.$el.insertAfter(parent.rules[index - 1].$el);
1114             }
1115             self.refreshGroupsConditions();
1116         },
1117         'move': function(e, node, group, index) {
1118             node.$el.detach();
1119
1120             if (index === 0) {
1121                 node.$el.prependTo(group.$el.find('>' + QueryBuilder.selectors.rules_list));
1122             }
1123             else {
1124                 node.$el.insertAfter(group.rules[index - 1].$el);
1125             }
1126             self.refreshGroupsConditions();
1127         },
1128         'update': function(e, node, field, value, oldValue) {
1129             if (node instanceof Rule) {
1130                 switch (field) {
1131                     case 'error':
1132                         self.updateError(node);
1133                         break;
1134
1135                     case 'flags':
1136                         self.applyRuleFlags(node);
1137                         break;
1138
1139                     case 'filter':
1140                         self.updateRuleFilter(node, oldValue);
1141                         break;
1142
1143                     case 'operator':
1144                         self.updateRuleOperator(node, oldValue);
1145                         break;
1146
1147                     case 'value':
1148                         self.updateRuleValue(node, oldValue);
1149                         break;
1150                 }
1151             }
1152             else {
1153                 switch (field) {
1154                     case 'error':
1155                         self.updateError(node);
1156                         break;
1157
1158                     case 'flags':
1159                         self.applyGroupFlags(node);
1160                         break;
1161
1162                     case 'condition':
1163                         self.updateGroupCondition(node, oldValue);
1164                         break;
1165                 }
1166             }
1167         }
1168     });
1169 };
1170
1171 /**
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
1178  */
1179 QueryBuilder.prototype.setRoot = function(addRule, data, flags) {
1180     addRule = (addRule === undefined || addRule === true);
1181
1182     var group_id = this.nextGroupId();
1183     var $group = $(this.getGroupTemplate(group_id, 1));
1184
1185     this.$el.append($group);
1186     this.model.root = new Group(null, $group);
1187     this.model.root.model = this.model;
1188
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;
1192
1193     this.trigger('afterAddGroup', this.model.root);
1194
1195     if (addRule) {
1196         this.addRule(this.model.root);
1197     }
1198
1199     return this.model.root;
1200 };
1201
1202 /**
1203  * Adds a new group
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
1208  * @returns {Group}
1209  * @fires QueryBuilder.beforeAddGroup
1210  * @fires QueryBuilder.afterAddGroup
1211  */
1212 QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) {
1213     addRule = (addRule === undefined || addRule === true);
1214
1215     var level = parent.level + 1;
1216
1217     /**
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
1224      */
1225     var e = this.trigger('beforeAddGroup', parent, addRule, level);
1226     if (e.isDefaultPrevented()) {
1227         return null;
1228     }
1229
1230     var group_id = this.nextGroupId();
1231     var $group = $(this.getGroupTemplate(group_id, level));
1232     var model = parent.addGroup($group);
1233
1234     model.data = data;
1235     model.flags = $.extend({}, this.settings.default_group_flags, flags);
1236     model.condition = this.settings.default_condition;
1237
1238     /**
1239      * Just after adding a group
1240      * @event afterAddGroup
1241      * @memberof QueryBuilder
1242      * @param {Group} group
1243      */
1244     this.trigger('afterAddGroup', model);
1245
1246     /**
1247      * After any change in the rules
1248      * @event rulesChanged
1249      * @memberof QueryBuilder
1250      */
1251     this.trigger('rulesChanged');
1252
1253     if (addRule) {
1254         this.addRule(model);
1255     }
1256
1257     return model;
1258 };
1259
1260 /**
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
1266  */
1267 QueryBuilder.prototype.deleteGroup = function(group) {
1268     if (group.isRoot()) {
1269         return false;
1270     }
1271
1272     /**
1273      * Just before deleting a group, can be prevented
1274      * @event beforeDeleteGroup
1275      * @memberof QueryBuilder
1276      * @param {Group} parent
1277      */
1278     var e = this.trigger('beforeDeleteGroup', group);
1279     if (e.isDefaultPrevented()) {
1280         return false;
1281     }
1282
1283     var del = true;
1284
1285     group.each('reverse', function(rule) {
1286         del &= this.deleteRule(rule);
1287     }, function(group) {
1288         del &= this.deleteGroup(group);
1289     }, this);
1290
1291     if (del) {
1292         group.drop();
1293
1294         /**
1295          * Just after deleting a group
1296          * @event afterDeleteGroup
1297          * @memberof QueryBuilder
1298          */
1299         this.trigger('afterDeleteGroup');
1300
1301         this.trigger('rulesChanged');
1302     }
1303
1304     return del;
1305 };
1306
1307 /**
1308  * Performs actions when a group's condition changes
1309  * @param {Group} group
1310  * @param {object} previousCondition
1311  * @fires QueryBuilder.afterUpdateGroupCondition
1312  * @private
1313  */
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);
1319     });
1320
1321     /**
1322      * After the group condition has been modified
1323      * @event afterUpdateGroupCondition
1324      * @memberof QueryBuilder
1325      * @param {Group} group
1326      * @param {object} previousCondition
1327      */
1328     this.trigger('afterUpdateGroupCondition', group, previousCondition);
1329
1330     this.trigger('rulesChanged');
1331 };
1332
1333 /**
1334  * Updates the visibility of conditions based on number of rules inside each group
1335  * @private
1336  */
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);
1342         }
1343
1344         group.each(null, function(group) {
1345             walk(group);
1346         }, this);
1347     }(this.model.root));
1348 };
1349
1350 /**
1351  * Adds a new rule
1352  * @param {Group} parent
1353  * @param {object} [data] - rule custom data
1354  * @param {object} [flags] - flags to apply to the rule
1355  * @returns {Rule}
1356  * @fires QueryBuilder.beforeAddRule
1357  * @fires QueryBuilder.afterAddRule
1358  * @fires QueryBuilder.changer:getDefaultFilter
1359  */
1360 QueryBuilder.prototype.addRule = function(parent, data, flags) {
1361     /**
1362      * Just before adding a rule, can be prevented
1363      * @event beforeAddRule
1364      * @memberof QueryBuilder
1365      * @param {Group} parent
1366      */
1367     var e = this.trigger('beforeAddRule', parent);
1368     if (e.isDefaultPrevented()) {
1369         return null;
1370     }
1371
1372     var rule_id = this.nextRuleId();
1373     var $rule = $(this.getRuleTemplate(rule_id));
1374     var model = parent.addRule($rule);
1375
1376     model.data = data;
1377     model.flags = $.extend({}, this.settings.default_rule_flags, flags);
1378
1379     /**
1380      * Just after adding a rule
1381      * @event afterAddRule
1382      * @memberof QueryBuilder
1383      * @param {Rule} rule
1384      */
1385     this.trigger('afterAddRule', model);
1386
1387     this.trigger('rulesChanged');
1388
1389     this.createRuleFilters(model);
1390
1391     if (this.settings.default_filter || !this.settings.display_empty_filter) {
1392         /**
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}
1399          */
1400         model.filter = this.change('getDefaultFilter',
1401             this.getFilterById(this.settings.default_filter || this.filters[0].id),
1402             model
1403         );
1404     }
1405
1406     return model;
1407 };
1408
1409 /**
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
1415  */
1416 QueryBuilder.prototype.deleteRule = function(rule) {
1417     if (rule.flags.no_delete) {
1418         return false;
1419     }
1420
1421     /**
1422      * Just before deleting a rule, can be prevented
1423      * @event beforeDeleteRule
1424      * @memberof QueryBuilder
1425      * @param {Rule} rule
1426      */
1427     var e = this.trigger('beforeDeleteRule', rule);
1428     if (e.isDefaultPrevented()) {
1429         return false;
1430     }
1431
1432     rule.drop();
1433
1434     /**
1435      * Just after deleting a rule
1436      * @event afterDeleteRule
1437      * @memberof QueryBuilder
1438      */
1439     this.trigger('afterDeleteRule');
1440
1441     this.trigger('rulesChanged');
1442
1443     return true;
1444 };
1445
1446 /**
1447  * Creates the filters for a rule
1448  * @param {Rule} rule
1449  * @fires QueryBuilder.changer:getRuleFilters
1450  * @fires QueryBuilder.afterCreateRuleFilters
1451  * @private
1452  */
1453 QueryBuilder.prototype.createRuleFilters = function(rule) {
1454     /**
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[]}
1461      */
1462     var filters = this.change('getRuleFilters', this.filters, rule);
1463     var $filterSelect = $(this.getRuleFilterSelect(rule, filters));
1464
1465     rule.$el.find(QueryBuilder.selectors.filter_container).html($filterSelect);
1466
1467     /**
1468      * After creating the dropdown for filters
1469      * @event afterCreateRuleFilters
1470      * @memberof QueryBuilder
1471      * @param {Rule} rule
1472      */
1473     this.trigger('afterCreateRuleFilters', rule);
1474
1475     this.applyRuleFlags(rule);
1476 };
1477
1478 /**
1479  * Creates the operators for a rule and init the rule operator
1480  * @param {Rule} rule
1481  * @fires QueryBuilder.afterCreateRuleOperators
1482  * @private
1483  */
1484 QueryBuilder.prototype.createRuleOperators = function(rule) {
1485     var $operatorContainer = rule.$el.find(QueryBuilder.selectors.operator_container).empty();
1486
1487     if (!rule.filter) {
1488         return;
1489     }
1490
1491     var operators = this.getOperators(rule.filter);
1492     var $operatorSelect = $(this.getRuleOperatorSelect(rule, operators));
1493
1494     $operatorContainer.html($operatorSelect);
1495
1496     // set the operator without triggering update event
1497     if (rule.filter.default_operator) {
1498         rule.__.operator = this.getOperatorByType(rule.filter.default_operator);
1499     }
1500     else {
1501         rule.__.operator = operators[0];
1502     }
1503
1504     rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
1505
1506     /**
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
1512      */
1513     this.trigger('afterCreateRuleOperators', rule, operators);
1514
1515     this.applyRuleFlags(rule);
1516 };
1517
1518 /**
1519  * Creates the main input for a rule
1520  * @param {Rule} rule
1521  * @fires QueryBuilder.afterCreateRuleInput
1522  * @private
1523  */
1524 QueryBuilder.prototype.createRuleInput = function(rule) {
1525     var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container).empty();
1526
1527     rule.__.value = undefined;
1528
1529     if (!rule.filter || !rule.operator || rule.operator.nb_inputs === 0) {
1530         return;
1531     }
1532
1533     var self = this;
1534     var $inputs = $();
1535     var filter = rule.filter;
1536
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);
1543         }
1544     } else {
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);
1550         }
1551     }
1552
1553     $valueContainer.css('display', '');
1554
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;
1560         }
1561     });
1562
1563     if (filter.plugin) {
1564         $inputs[filter.plugin](filter.plugin_config || {});
1565     }
1566
1567     /**
1568      * After creating the input for a rule and initializing optional plugin
1569      * @event afterCreateRuleInput
1570      * @memberof QueryBuilder
1571      * @param {Rule} rule
1572      */
1573     this.trigger('afterCreateRuleInput', rule);
1574
1575     if (filter.default_value !== undefined) {
1576         rule.value = filter.default_value;
1577     }
1578     else {
1579         rule._updating_value = true;
1580         rule.value = self.getRuleInputValue(rule);
1581         rule._updating_value = false;
1582     }
1583
1584     this.applyRuleFlags(rule);
1585 };
1586
1587 /**
1588  * Performs action when a rule's filter changes
1589  * @param {Rule} rule
1590  * @param {object} previousFilter
1591  * @fires QueryBuilder.afterUpdateRuleFilter
1592  * @private
1593  */
1594 QueryBuilder.prototype.updateRuleFilter = function(rule, previousFilter) {
1595     this.createRuleOperators(rule);
1596     this.createRuleInput(rule);
1597
1598     rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
1599
1600     // clear rule data if the filter changed
1601     if (previousFilter && rule.filter && previousFilter.id !== rule.filter.id) {
1602         rule.data = undefined;
1603     }
1604
1605     /**
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
1611      */
1612     this.trigger('afterUpdateRuleFilter', rule, previousFilter);
1613
1614     this.trigger('rulesChanged');
1615 };
1616
1617 /**
1618  * Performs actions when a rule's operator changes
1619  * @param {Rule} rule
1620  * @param {object} previousOperator
1621  * @fires QueryBuilder.afterUpdateRuleOperator
1622  * @private
1623  */
1624 QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) {
1625     var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container);
1626
1627     if (!rule.operator || rule.operator.nb_inputs === 0) {
1628         $valueContainer.hide();
1629
1630         rule.__.value = undefined;
1631     }
1632     else {
1633         $valueContainer.css('display', '');
1634
1635         if ($valueContainer.is(':empty') || !previousOperator ||
1636             rule.operator.nb_inputs !== previousOperator.nb_inputs ||
1637             rule.operator.optgroup !== previousOperator.optgroup
1638         ) {
1639             this.createRuleInput(rule);
1640         }
1641     }
1642
1643     if (rule.operator) {
1644         rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
1645
1646         // refresh value if the format changed for this operator
1647         rule.__.value = this.getRuleInputValue(rule);
1648     }
1649
1650     /**
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
1656      */
1657     this.trigger('afterUpdateRuleOperator', rule, previousOperator);
1658
1659     this.trigger('rulesChanged');
1660 };
1661
1662 /**
1663  * Performs actions when rule's value changes
1664  * @param {Rule} rule
1665  * @param {object} previousValue
1666  * @fires QueryBuilder.afterUpdateRuleValue
1667  * @private
1668  */
1669 QueryBuilder.prototype.updateRuleValue = function(rule, previousValue) {
1670     if (!rule._updating_value) {
1671         this.setRuleInputValue(rule, rule.value);
1672     }
1673
1674     /**
1675      * After the rule value has been modified
1676      * @event afterUpdateRuleValue
1677      * @memberof QueryBuilder
1678      * @param {Rule} rule
1679      * @param {*} previousValue
1680      */
1681     this.trigger('afterUpdateRuleValue', rule, previousValue);
1682
1683     this.trigger('rulesChanged');
1684 };
1685
1686 /**
1687  * Changes a rule's properties depending on its flags
1688  * @param {Rule} rule
1689  * @fires QueryBuilder.afterApplyRuleFlags
1690  * @private
1691  */
1692 QueryBuilder.prototype.applyRuleFlags = function(rule) {
1693     var flags = rule.flags;
1694     var Selectors = QueryBuilder.selectors;
1695
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);
1699
1700     if (flags.no_delete) {
1701         rule.$el.find(Selectors.delete_rule).remove();
1702     }
1703
1704     /**
1705      * After rule's flags has been applied
1706      * @event afterApplyRuleFlags
1707      * @memberof QueryBuilder
1708      * @param {Rule} rule
1709      */
1710     this.trigger('afterApplyRuleFlags', rule);
1711 };
1712
1713 /**
1714  * Changes group's properties depending on its flags
1715  * @param {Group} group
1716  * @fires QueryBuilder.afterApplyGroupFlags
1717  * @private
1718  */
1719 QueryBuilder.prototype.applyGroupFlags = function(group) {
1720     var flags = group.flags;
1721     var Selectors = QueryBuilder.selectors;
1722
1723     group.$el.find('>' + Selectors.group_condition).prop('disabled', flags.condition_readonly)
1724         .parent().toggleClass('readonly', flags.condition_readonly);
1725
1726     if (flags.no_add_rule) {
1727         group.$el.find(Selectors.add_rule).remove();
1728     }
1729     if (flags.no_add_group) {
1730         group.$el.find(Selectors.add_group).remove();
1731     }
1732     if (flags.no_delete) {
1733         group.$el.find(Selectors.delete_group).remove();
1734     }
1735
1736     /**
1737      * After group's flags has been applied
1738      * @event afterApplyGroupFlags
1739      * @memberof QueryBuilder
1740      * @param {Group} group
1741      */
1742     this.trigger('afterApplyGroupFlags', group);
1743 };
1744
1745 /**
1746  * Clears all errors markers
1747  * @param {Node} [node] default is root Group
1748  */
1749 QueryBuilder.prototype.clearErrors = function(node) {
1750     node = node || this.model.root;
1751
1752     if (!node) {
1753         return;
1754     }
1755
1756     node.error = null;
1757
1758     if (node instanceof Group) {
1759         node.each(function(rule) {
1760             rule.error = null;
1761         }, function(group) {
1762             this.clearErrors(group);
1763         }, this);
1764     }
1765 };
1766
1767 /**
1768  * Adds/Removes error on a Rule or Group
1769  * @param {Node} node
1770  * @fires QueryBuilder.changer:displayError
1771  * @private
1772  */
1773 QueryBuilder.prototype.updateError = function(node) {
1774     if (this.settings.display_errors) {
1775         if (node.error === null) {
1776             node.$el.removeClass('has-error');
1777         }
1778         else {
1779             var errorMessage = this.translate('errors', node.error[0]);
1780             errorMessage = Utils.fmt(errorMessage, node.error.slice(1));
1781
1782             /**
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
1789              * @returns {string}
1790              */
1791             errorMessage = this.change('displayError', errorMessage, node.error, node);
1792
1793             node.$el.addClass('has-error')
1794                 .find(QueryBuilder.selectors.error_container).eq(0)
1795                 .attr('title', errorMessage);
1796         }
1797     }
1798 };
1799
1800 /**
1801  * Triggers a validation error event
1802  * @param {Node} node
1803  * @param {string|array} error
1804  * @param {*} value
1805  * @fires QueryBuilder.validationError
1806  * @private
1807  */
1808 QueryBuilder.prototype.triggerValidationError = function(node, error, value) {
1809     if (!$.isArray(error)) {
1810         error = [error];
1811     }
1812
1813     /**
1814      * Fired when a validation error occurred, can be prevented
1815      * @event validationError
1816      * @memberof QueryBuilder
1817      * @param {Node} node
1818      * @param {string} error
1819      * @param {*} value
1820      */
1821     var e = this.trigger('validationError', node, error, value);
1822     if (!e.isDefaultPrevented()) {
1823         node.error = error;
1824     }
1825 };
1826
1827
1828 /**
1829  * Destroys the builder
1830  * @fires QueryBuilder.beforeDestroy
1831  */
1832 QueryBuilder.prototype.destroy = function() {
1833     /**
1834      * Before the {@link QueryBuilder#destroy} method
1835      * @event beforeDestroy
1836      * @memberof QueryBuilder
1837      */
1838     this.trigger('beforeDestroy');
1839
1840     if (this.status.generated_id) {
1841         this.$el.removeAttr('id');
1842     }
1843
1844     this.clear();
1845     this.model = null;
1846
1847     this.$el
1848         .off('.queryBuilder')
1849         .removeClass('query-builder')
1850         .removeData('queryBuilder');
1851
1852     delete this.$el[0].queryBuilder;
1853 };
1854
1855 /**
1856  * Clear all rules and resets the root group
1857  * @fires QueryBuilder.beforeReset
1858  * @fires QueryBuilder.afterReset
1859  */
1860 QueryBuilder.prototype.reset = function() {
1861     /**
1862      * Before the {@link QueryBuilder#reset} method, can be prevented
1863      * @event beforeReset
1864      * @memberof QueryBuilder
1865      */
1866     var e = this.trigger('beforeReset');
1867     if (e.isDefaultPrevented()) {
1868         return;
1869     }
1870
1871     this.status.group_id = 1;
1872     this.status.rule_id = 0;
1873
1874     this.model.root.empty();
1875
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;
1879
1880     this.addRule(this.model.root);
1881
1882     /**
1883      * After the {@link QueryBuilder#reset} method
1884      * @event afterReset
1885      * @memberof QueryBuilder
1886      */
1887     this.trigger('afterReset');
1888
1889     this.trigger('rulesChanged');
1890 };
1891
1892 /**
1893  * Clears all rules and removes the root group
1894  * @fires QueryBuilder.beforeClear
1895  * @fires QueryBuilder.afterClear
1896  */
1897 QueryBuilder.prototype.clear = function() {
1898     /**
1899      * Before the {@link QueryBuilder#clear} method, can be prevented
1900      * @event beforeClear
1901      * @memberof QueryBuilder
1902      */
1903     var e = this.trigger('beforeClear');
1904     if (e.isDefaultPrevented()) {
1905         return;
1906     }
1907
1908     this.status.group_id = 0;
1909     this.status.rule_id = 0;
1910
1911     if (this.model.root) {
1912         this.model.root.drop();
1913         this.model.root = null;
1914     }
1915
1916     /**
1917      * After the {@link QueryBuilder#clear} method
1918      * @event afterClear
1919      * @memberof QueryBuilder
1920      */
1921     this.trigger('afterClear');
1922
1923     this.trigger('rulesChanged');
1924 };
1925
1926 /**
1927  * Modifies the builder configuration.<br>
1928  * Only options defined in QueryBuilder.modifiable_options are modifiable
1929  * @param {object} options
1930  */
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;
1935         }
1936     }.bind(this));
1937 };
1938
1939 /**
1940  * Returns the model associated to a DOM object, or the root model
1941  * @param {jQuery} [target]
1942  * @returns {Node}
1943  */
1944 QueryBuilder.prototype.getModel = function(target) {
1945     if (!target) {
1946         return this.model.root;
1947     }
1948     else if (target instanceof Node) {
1949         return target;
1950     }
1951     else {
1952         return $(target).data('queryBuilderModel');
1953     }
1954 };
1955
1956 /**
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
1962  */
1963 QueryBuilder.prototype.validate = function(options) {
1964     options = $.extend({
1965         skip_empty: false
1966     }, options);
1967
1968     this.clearErrors();
1969
1970     var self = this;
1971
1972     var valid = (function parse(group) {
1973         var done = 0;
1974         var errors = 0;
1975
1976         group.each(function(rule) {
1977             if (!rule.filter && options.skip_empty) {
1978                 return;
1979             }
1980
1981             if (!rule.filter) {
1982                 self.triggerValidationError(rule, 'no_filter', null);
1983                 errors++;
1984                 return;
1985             }
1986
1987             if (!rule.operator) {
1988                 self.triggerValidationError(rule, 'no_operator', null);
1989                 errors++;
1990                 return;
1991             }
1992
1993             if (rule.operator.nb_inputs !== 0) {
1994                 var valid = self.validateValue(rule, rule.value);
1995
1996                 if (valid !== true) {
1997                     self.triggerValidationError(rule, valid, rule.value);
1998                     errors++;
1999                     return;
2000                 }
2001             }
2002
2003             done++;
2004
2005         }, function(group) {
2006             var res = parse(group);
2007             if (res === true) {
2008                 done++;
2009             }
2010             else if (res === false) {
2011                 errors++;
2012             }
2013         });
2014
2015         if (errors > 0) {
2016             return false;
2017         }
2018         else if (done === 0 && !group.isRoot() && options.skip_empty) {
2019             return null;
2020         }
2021         else if (done === 0 && (!self.settings.allow_empty || !group.isRoot())) {
2022             self.triggerValidationError(group, 'empty_group', null);
2023             return false;
2024         }
2025
2026         return true;
2027
2028     }(this.model.root));
2029
2030     /**
2031      * Modifies the result of the {@link QueryBuilder#validate} method
2032      * @event changer:validate
2033      * @memberof QueryBuilder
2034      * @param {boolean} valid
2035      * @returns {boolean}
2036      */
2037     return this.change('validate', valid);
2038 };
2039
2040 /**
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
2046  * @returns {object}
2047  * @fires QueryBuilder.changer:ruleToJson
2048  * @fires QueryBuilder.changer:groupToJson
2049  * @fires QueryBuilder.changer:getRules
2050  */
2051 QueryBuilder.prototype.getRules = function(options) {
2052     options = $.extend({
2053         get_flags: false,
2054         allow_invalid: false,
2055         skip_empty: false
2056     }, options);
2057
2058     var valid = this.validate(options);
2059     if (!valid && !options.allow_invalid) {
2060         return null;
2061     }
2062
2063     var self = this;
2064
2065     var out = (function parse(group) {
2066         var groupData = {
2067             condition: group.condition,
2068             rules: []
2069         };
2070
2071         if (group.data) {
2072             groupData.data = $.extendext(true, 'replace', {}, group.data);
2073         }
2074
2075         if (options.get_flags) {
2076             var flags = self.getGroupFlags(group.flags, options.get_flags === 'all');
2077             if (!$.isEmptyObject(flags)) {
2078                 groupData.flags = flags;
2079             }
2080         }
2081
2082         group.each(function(rule) {
2083             if (!rule.filter && options.skip_empty) {
2084                 return;
2085             }
2086
2087             var value = null;
2088             if (!rule.operator || rule.operator.nb_inputs !== 0) {
2089                 value = rule.value;
2090             }
2091
2092             var ruleData = {
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,
2098                 value: value
2099             };
2100
2101             if (rule.filter && rule.filter.data || rule.data) {
2102                 ruleData.data = $.extendext(true, 'replace', {}, rule.filter.data, rule.data);
2103             }
2104
2105             if (options.get_flags) {
2106                 var flags = self.getRuleFlags(rule.flags, options.get_flags === 'all');
2107                 if (!$.isEmptyObject(flags)) {
2108                     ruleData.flags = flags;
2109                 }
2110             }
2111
2112             /**
2113              * Modifies the JSON generated from a Rule object
2114              * @event changer:ruleToJson
2115              * @memberof QueryBuilder
2116              * @param {object} json
2117              * @param {Rule} rule
2118              * @returns {object}
2119              */
2120             groupData.rules.push(self.change('ruleToJson', ruleData, rule));
2121
2122         }, function(model) {
2123             var data = parse(model);
2124             if (data.rules.length !== 0 || !options.skip_empty) {
2125                 groupData.rules.push(data);
2126             }
2127         }, this);
2128
2129         /**
2130          * Modifies the JSON generated from a Group object
2131          * @event changer:groupToJson
2132          * @memberof QueryBuilder
2133          * @param {object} json
2134          * @param {Group} group
2135          * @returns {object}
2136          */
2137         return self.change('groupToJson', groupData, group);
2138
2139     }(this.model.root));
2140
2141     out.valid = valid;
2142
2143     /**
2144      * Modifies the result of the {@link QueryBuilder#getRules} method
2145      * @event changer:getRules
2146      * @memberof QueryBuilder
2147      * @param {object} json
2148      * @returns {object}
2149      */
2150     return this.change('getRules', out);
2151 };
2152
2153 /**
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
2163  */
2164 QueryBuilder.prototype.setRules = function(data, options) {
2165     options = $.extend({
2166         allow_invalid: false
2167     }, options);
2168
2169     if ($.isArray(data)) {
2170         data = {
2171             condition: this.settings.default_condition,
2172             rules: data
2173         };
2174     }
2175
2176     if (!data || !data.rules || (data.rules.length === 0 && !this.settings.allow_empty)) {
2177         Utils.error('RulesParse', 'Incorrect data object passed');
2178     }
2179
2180     this.clear();
2181     this.setRoot(false, data.data, this.parseGroupFlags(data));
2182
2183     /**
2184      * Modifies data before the {@link QueryBuilder#setRules} method
2185      * @event changer:setRules
2186      * @memberof QueryBuilder
2187      * @param {object} json
2188      * @param {object} options
2189      * @returns {object}
2190      */
2191     data = this.change('setRules', data, options);
2192
2193     var self = this;
2194
2195     (function add(data, group) {
2196         if (group === null) {
2197             return;
2198         }
2199
2200         if (data.condition === undefined) {
2201             data.condition = self.settings.default_condition;
2202         }
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;
2206         }
2207
2208         group.condition = data.condition;
2209
2210         data.rules.forEach(function(item) {
2211             var model;
2212
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);
2216                     self.reset();
2217                 }
2218                 else {
2219                     model = self.addGroup(group, false, item.data, self.parseGroupFlags(item));
2220                     if (model === null) {
2221                         return;
2222                     }
2223
2224                     add(item, model);
2225                 }
2226             }
2227             else {
2228                 if (!item.empty) {
2229                     if (item.id === undefined) {
2230                         Utils.error(!options.allow_invalid, 'RulesParse', 'Missing rule field id');
2231                         item.empty = true;
2232                     }
2233                     if (item.operator === undefined) {
2234                         item.operator = 'equal';
2235                     }
2236                 }
2237
2238                 model = self.addRule(group, item.data, self.parseRuleFlags(item));
2239                 if (model === null) {
2240                     return;
2241                 }
2242
2243                 if (!item.empty) {
2244                     model.filter = self.getFilterById(item.id, !options.allow_invalid);
2245                 }
2246
2247                 if (model.filter) {
2248                     model.operator = self.getOperatorByType(item.operator, !options.allow_invalid);
2249
2250                     if (!model.operator) {
2251                         model.operator = self.getOperators(model.filter)[0];
2252                     }
2253                 }
2254
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');
2262                                 }
2263                                 model.value = moment(item.value * 1000).format('YYYY/MM/DD HH:mm:ss');
2264                         } else {
2265                                 model.value = item.value;
2266                         }
2267                     }
2268                     else if (model.filter.default_value !== undefined) {
2269                         model.value = model.filter.default_value;
2270                     }
2271                 }
2272
2273                 /**
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
2280                  */
2281                 if (self.change('jsonToRule', model, item) != model) {
2282                     Utils.error('RulesParse', 'Plugin tried to change rule reference');
2283                 }
2284             }
2285         });
2286
2287         /**
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
2294          */
2295         if (self.change('jsonToGroup', group, data) != group) {
2296             Utils.error('RulesParse', 'Plugin tried to change group reference');
2297         }
2298
2299     }(data, this.model.root));
2300
2301     /**
2302      * After the {@link QueryBuilder#setRules} method
2303      * @event afterSetRules
2304      * @memberof QueryBuilder
2305      */
2306     this.trigger('afterSetRules');
2307 };
2308
2309
2310 /**
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
2316  */
2317 QueryBuilder.prototype.validateValue = function(rule, value) {
2318     var validation = rule.filter.validation || {};
2319     var result = true;
2320
2321     if (validation.callback) {
2322         result = validation.callback.call(this, value, rule);
2323     }
2324     else {
2325         result = this._validateValue(rule, value);
2326     }
2327
2328     /**
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
2333      * @param {*} value
2334      * @param {Rule} rule
2335      * @returns {array|boolean}
2336      */
2337     return this.change('validateValue', result, value, rule);
2338 };
2339
2340 /**
2341  * Default validation function
2342  * @param {Rule} rule
2343  * @param {string|string[]} value
2344  * @returns {array|boolean} true or error array
2345  * @throws ConfigError
2346  * @private
2347  */
2348 QueryBuilder.prototype._validateValue = function(rule, value) {
2349     var filter = rule.filter;
2350     var operator = rule.operator;
2351     var validation = filter.validation || {};
2352     var result = true;
2353     var tmp, tempValue;
2354     var numOfInputs = operator.nb_inputs;
2355     if(filter.type === 'map') {
2356         numOfInputs = 2;
2357     }
2358
2359     if (numOfInputs === 1) {
2360         value = [value];
2361     }
2362
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)];
2366             break;
2367         }
2368
2369         switch (filter.input) {
2370             case 'radio':
2371                 if (value[i] === undefined || value[i].length === 0) {
2372                     if (!validation.allow_empty_value) {
2373                         result = ['radio_empty'];
2374                     }
2375                     break;
2376                 }
2377                 break;
2378
2379             case 'checkbox':
2380                 if (value[i] === undefined || value[i].length === 0) {
2381                     if (!validation.allow_empty_value) {
2382                         result = ['checkbox_empty'];
2383                     }
2384                     break;
2385                 }
2386                 break;
2387
2388             case 'select':
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'];
2392                     }
2393                     break;
2394                 }
2395                 break;
2396
2397             default:
2398                 tempValue = $.isArray(value[i]) ? value[i] : [value[i]];
2399
2400                 for (var j = 0; j < tempValue.length; j++) {
2401                     switch (QueryBuilder.types[filter.type]) {
2402                         case 'string':
2403                         case 'map':
2404                             if (tempValue[j] === undefined || tempValue[j].length === 0) {
2405                                 if (!validation.allow_empty_value) {
2406                                     result = ['string_empty'];
2407                                 }
2408                                 break;
2409                             }
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];
2413                                     break;
2414                                 }
2415                             }
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];
2419                                     break;
2420                                 }
2421                             }
2422                             if (validation.format) {
2423                                 if (typeof validation.format == 'string') {
2424                                     validation.format = new RegExp(validation.format);
2425                                 }
2426                                 if (!validation.format.test(tempValue[j])) {
2427                                     result = [this.getValidationMessage(validation, 'format', 'string_invalid_format'), validation.format];
2428                                     break;
2429                                 }
2430                             }
2431                             break;
2432
2433                         case 'number':
2434                             if (tempValue[j] === undefined || tempValue[j].length === 0) {
2435                                 if (!validation.allow_empty_value) {
2436                                     result = ['number_nan'];
2437                                 }
2438                                 break;
2439                             }
2440                             if (isNaN(tempValue[j])) {
2441                                 result = ['number_nan'];
2442                                 break;
2443                             }
2444                             if (filter.type == 'integer') {
2445                                 if (parseInt(tempValue[j]) != tempValue[j]) {
2446                                     result = ['number_not_integer'];
2447                                     break;
2448                                 }
2449                             }
2450                             else {
2451                                 if (parseFloat(tempValue[j]) != tempValue[j]) {
2452                                     result = ['number_not_double'];
2453                                     break;
2454                                 }
2455                             }
2456                             if (validation.min !== undefined) {
2457                                 if (tempValue[j] < parseFloat(validation.min)) {
2458                                     result = [this.getValidationMessage(validation, 'min', 'number_exceed_min'), validation.min];
2459                                     break;
2460                                 }
2461                             }
2462                             if (validation.max !== undefined) {
2463                                 if (tempValue[j] > parseFloat(validation.max)) {
2464                                     result = [this.getValidationMessage(validation, 'max', 'number_exceed_max'), validation.max];
2465                                     break;
2466                                 }
2467                             }
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];
2472                                     break;
2473                                 }
2474                             }
2475                             break;
2476
2477                         case 'datetime':
2478                             if (tempValue[j] === undefined || tempValue[j].length === 0) {
2479                                 if (!validation.allow_empty_value) {
2480                                     result = ['datetime_empty'];
2481                                 }
2482                                 break;
2483                             }
2484
2485                             // we need MomentJS
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');
2489                                 }
2490
2491                                 var datetime = moment.utc(tempValue[j], validation.format, true);
2492                                 if (!datetime.isValid()) {
2493                                     result = [this.getValidationMessage(validation, 'format', 'datetime_invalid'), validation.format];
2494                                     break;
2495                                 }
2496                                 else {
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];
2500                                             break;
2501                                         }
2502                                     }
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];
2506                                             break;
2507                                         }
2508                                     }
2509                                 }
2510                             }
2511                             break;
2512
2513                         case 'boolean':
2514                             if (tempValue[j] === undefined || tempValue[j].length === 0) {
2515                                 if (!validation.allow_empty_value) {
2516                                     result = ['boolean_not_valid'];
2517                                 }
2518                                 break;
2519                             }
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'];
2523                                 break;
2524                             }
2525                     }
2526
2527                     if (result !== true) {
2528                         break;
2529                     }
2530                 }
2531         }
2532
2533         if (result !== true) {
2534             break;
2535         }
2536     }
2537
2538     if ((rule.operator.type === 'between' || rule.operator.type === 'not_between') && value.length === 2) {
2539         switch (QueryBuilder.types[filter.type]) {
2540             case 'number':
2541                 if (value[0] > value[1]) {
2542                     result = ['number_between_invalid', value[0], value[1]];
2543                 }
2544                 break;
2545
2546             case 'datetime':
2547                 // we need MomentJS
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');
2551                     }
2552
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]];
2555                     }
2556                 }
2557                 break;
2558         }
2559     }
2560
2561     return result;
2562 };
2563
2564 /**
2565  * Returns an incremented group ID
2566  * @returns {string}
2567  * @private
2568  */
2569 QueryBuilder.prototype.nextGroupId = function() {
2570     return this.status.id + '_group_' + (this.status.group_id++);
2571 };
2572
2573 /**
2574  * Returns an incremented rule ID
2575  * @returns {string}
2576  * @private
2577  */
2578 QueryBuilder.prototype.nextRuleId = function() {
2579     return this.status.id + '_rule_' + (this.status.rule_id++);
2580 };
2581
2582 /**
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
2587  * @private
2588  */
2589 QueryBuilder.prototype.getOperators = function(filter) {
2590     if (typeof filter == 'string') {
2591         filter = this.getFilterById(filter);
2592     }
2593
2594     var result = [];
2595
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) {
2600                 continue;
2601             }
2602         }
2603         // type check
2604         else if (this.operators[i].apply_to.indexOf(QueryBuilder.types[filter.type]) == -1) {
2605             continue;
2606         }
2607
2608         result.push(this.operators[i]);
2609     }
2610
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);
2615         });
2616     }
2617
2618     /**
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[]}
2625      */
2626     return this.change('getOperators', result, filter);
2627 };
2628
2629 /**
2630  * Returns a particular filter by its id
2631  * @param {string} id
2632  * @param {boolean} [doThrow=true]
2633  * @returns {object|null}
2634  * @throws UndefinedFilterError
2635  * @private
2636  */
2637 QueryBuilder.prototype.getFilterById = function(id, doThrow) {
2638     if (id == '-1') {
2639         return null;
2640     }
2641
2642     for (var i = 0, l = this.filters.length; i < l; i++) {
2643         if (this.filters[i].id == id) {
2644             return this.filters[i];
2645         }
2646     }
2647
2648     Utils.error(doThrow !== false, 'UndefinedFilter', 'Undefined filter "{0}"', id);
2649
2650     return null;
2651 };
2652
2653 /**
2654  * Returns a particular operator by its type
2655  * @param {string} type
2656  * @param {boolean} [doThrow=true]
2657  * @returns {object|null}
2658  * @throws UndefinedOperatorError
2659  * @private
2660  */
2661 QueryBuilder.prototype.getOperatorByType = function(type, doThrow) {
2662     if (type == '-1') {
2663         return null;
2664     }
2665
2666     for (var i = 0, l = this.operators.length; i < l; i++) {
2667         if (this.operators[i].type == type) {
2668             return this.operators[i];
2669         }
2670     }
2671
2672     Utils.error(doThrow !== false, 'UndefinedOperator', 'Undefined operator "{0}"', type);
2673
2674     return null;
2675 };
2676
2677 /**
2678  * Returns rule's current input value
2679  * @param {Rule} rule
2680  * @returns {*}
2681  * @fires QueryBuilder.changer:getRuleValue
2682  * @private
2683  */
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') {
2689         numOfInputs = 2;
2690     }
2691     var value = [];
2692     
2693     if (filter.valueGetter) {
2694         value = filter.valueGetter.call(this, rule);
2695     }
2696     else {
2697         var $value = rule.$el.find(QueryBuilder.selectors.value_container);
2698
2699         for (var i = 0; i < numOfInputs; i++) {
2700             var name = Utils.escapeElementId(rule.id + '_value_' + i);
2701             var tmp;
2702
2703             switch (filter.input) {
2704                 case 'radio':
2705                     value.push($value.find('[name=' + name + ']:checked').val());
2706                     break;
2707
2708                 case 'checkbox':
2709                     tmp = [];
2710                     // jshint loopfunc:true
2711                     $value.find('[name=' + name + ']:checked').each(function() {
2712                         tmp.push($(this).val());
2713                     });
2714                     // jshint loopfunc:false
2715                     value.push(tmp);
2716                     break;
2717
2718                 case 'select':
2719                     if (filter.multiple) {
2720                         tmp = [];
2721                         // jshint loopfunc:true
2722                         $value.find('[name=' + name + '] option:selected').each(function() {
2723                             tmp.push($(this).val());
2724                         });
2725                         // jshint loopfunc:false
2726                         value.push(tmp);
2727                     }
2728                     else {
2729                         value.push($value.find('[name=' + name + '] option:selected').val());
2730                     }
2731                     break;
2732
2733                 default:
2734                     value.push($value.find('[name=' + name + ']').val());
2735             }
2736         }
2737
2738         value = value.map(function(val) {
2739             if (operator.multiple && filter.value_separator && typeof val == 'string') {
2740                 val = val.split(filter.value_separator);
2741             }
2742             
2743             if ($.isArray(val)) {
2744                 return val.map(function(subval) {
2745                     return Utils.changeType(subval, filter.type);
2746                 });
2747             }
2748             else {
2749                 return Utils.changeType(val, filter.type);
2750             }
2751         });
2752
2753         if (numOfInputs === 1) {
2754             value = value[0];
2755         }
2756
2757         // @deprecated
2758         if (filter.valueParser) {
2759             value = filter.valueParser.call(this, rule, value);
2760         }
2761     }
2762
2763     /**
2764      * Modifies the rule's value grabbed from the DOM
2765      * @event changer:getRuleValue
2766      * @memberof QueryBuilder
2767      * @param {*} value
2768      * @param {Rule} rule
2769      * @returns {*}
2770      */
2771     return this.change('getRuleValue', value, rule);
2772 };
2773
2774 /**
2775  * Sets the value of a rule's input
2776  * @param {Rule} rule
2777  * @param {*} value
2778  * @private
2779  */
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') {
2785         numOfInputs = 2;
2786     }
2787
2788     if (!filter || !operator) {
2789         return;
2790     }
2791
2792     rule._updating_input = true;
2793
2794     if (filter.valueSetter) {
2795         filter.valueSetter.call(this, rule, value);
2796     }
2797     else {
2798         var $value = rule.$el.find(QueryBuilder.selectors.value_container);
2799
2800         if (numOfInputs == 1) {
2801             value = [value];
2802         }
2803
2804         for (var i = 0; i < numOfInputs; i++) {
2805             var name = Utils.escapeElementId(rule.id + '_value_' + i);
2806
2807             switch (filter.input) {
2808                 case 'radio':
2809                     $value.find('[name=' + name + '][value="' + value[i] + '"]').prop('checked', true).trigger('change');
2810                     break;
2811
2812                 case 'checkbox':
2813                     if (!$.isArray(value[i])) {
2814                         value[i] = [value[i]];
2815                     }
2816                     // jshint loopfunc:true
2817                     value[i].forEach(function(value) {
2818                         $value.find('[name=' + name + '][value="' + value + '"]').prop('checked', true).trigger('change');
2819                     });
2820                     // jshint loopfunc:false
2821                     break;
2822
2823                 default:
2824                     if (operator.multiple && filter.value_separator && $.isArray(value[i])) {
2825                         value[i] = value[i].join(filter.value_separator);
2826                     }
2827                     $value.find('[name=' + name + ']').val(value[i]).trigger('change');
2828                     break;
2829             }
2830         }
2831     }
2832
2833     rule._updating_input = false;
2834 };
2835
2836 /**
2837  * Parses rule flags
2838  * @param {object} rule
2839  * @returns {object}
2840  * @fires QueryBuilder.changer:parseRuleFlags
2841  * @private
2842  */
2843 QueryBuilder.prototype.parseRuleFlags = function(rule) {
2844     var flags = $.extend({}, this.settings.default_rule_flags);
2845
2846     if (rule.readonly) {
2847         $.extend(flags, {
2848             filter_readonly: true,
2849             operator_readonly: true,
2850             value_readonly: true,
2851             no_delete: true
2852         });
2853     }
2854
2855     if (rule.flags) {
2856         $.extend(flags, rule.flags);
2857     }
2858
2859     /**
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
2865      * @returns {object}
2866      */
2867     return this.change('parseRuleFlags', flags, rule);
2868 };
2869
2870 /**
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
2874  * @returns {object}
2875  * @private
2876  */
2877 QueryBuilder.prototype.getRuleFlags = function(flags, all) {
2878     if (all) {
2879         return $.extend({}, flags);
2880     }
2881     else {
2882         var ret = {};
2883         $.each(this.settings.default_rule_flags, function(key, value) {
2884             if (flags[key] !== value) {
2885                 ret[key] = flags[key];
2886             }
2887         });
2888         return ret;
2889     }
2890 };
2891
2892 /**
2893  * Parses group flags
2894  * @param {object} group
2895  * @returns {object}
2896  * @fires QueryBuilder.changer:parseGroupFlags
2897  * @private
2898  */
2899 QueryBuilder.prototype.parseGroupFlags = function(group) {
2900     var flags = $.extend({}, this.settings.default_group_flags);
2901
2902     if (group.readonly) {
2903         $.extend(flags, {
2904             condition_readonly: true,
2905             no_add_rule: true,
2906             no_add_group: true,
2907             no_delete: true
2908         });
2909     }
2910
2911     if (group.flags) {
2912         $.extend(flags, group.flags);
2913     }
2914
2915     /**
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
2921      * @returns {object}
2922      */
2923     return this.change('parseGroupFlags', flags, group);
2924 };
2925
2926 /**
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
2930  * @returns {object}
2931  * @private
2932  */
2933 QueryBuilder.prototype.getGroupFlags = function(flags, all) {
2934     if (all) {
2935         return $.extend({}, flags);
2936     }
2937     else {
2938         var ret = {};
2939         $.each(this.settings.default_group_flags, function(key, value) {
2940             if (flags[key] !== value) {
2941                 ret[key] = flags[key];
2942             }
2943         });
2944         return ret;
2945     }
2946 };
2947
2948 /**
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
2952  * @returns {string}
2953  * @fires QueryBuilder.changer:translate
2954  */
2955 QueryBuilder.prototype.translate = function(category, key) {
2956     if (!key) {
2957         key = category;
2958         category = undefined;
2959     }
2960
2961     var translation;
2962     if (typeof key === 'object') {
2963         translation = key[this.settings.lang_code] || key['en'];
2964     }
2965     else {
2966         translation = (category ? this.lang[category] : this.lang)[key] || key;
2967     }
2968
2969     /**
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]
2976      * @returns {string}
2977      */
2978     return this.change('translate', translation, key, category);
2979 };
2980
2981 /**
2982  * Returns a validation message
2983  * @param {object} validation
2984  * @param {string} type
2985  * @param {string} def
2986  * @returns {string}
2987  * @private
2988  */
2989 QueryBuilder.prototype.getValidationMessage = function(validation, type, def) {
2990     return validation.messages && validation.messages[type] || def;
2991 };
2992
2993
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") }} \
3000       </button> \
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") }} \
3004         </button> \
3005       {{?}} \
3006       {{? it.level>1 }} \
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") }} \
3009         </button> \
3010       {{?}} \
3011     </div> \
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) }} \
3016         </label> \
3017       {{~}} \
3018     </div> \
3019     {{? it.settings.display_errors }} \
3020       <div class="error-container"><i class="{{= it.icons.error }}"></i></div> \
3021     {{?}} \
3022   </div> \
3023   <div class=rules-group-body> \
3024     <div class=rules-list></div> \
3025   </div> \
3026 </div>';
3027
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") }} \
3034       </button> \
3035     </div> \
3036   </div> \
3037   {{? it.settings.display_errors }} \
3038     <div class="error-container"><i class="{{= it.icons.error }}"></i></div> \
3039   {{?}} \
3040   <div class="rule-filter-container"></div> \
3041   <div class="rule-operator-container"></div> \
3042   <div class="rule-value-container"></div> \
3043 </div>';
3044
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> \
3050   {{?}} \
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]) }}"> \
3056       {{?}} \
3057     {{?}} \
3058     <option value="{{= filter.id }}" {{? filter.icon}}data-icon="{{= filter.icon}}"{{?}}>{{= it.translate(filter.label) }}</option> \
3059   {{~}} \
3060   {{? optgroup !== null }}</optgroup>{{?}} \
3061 </select>';
3062
3063 QueryBuilder.templates.operatorSelect = '\
3064 {{? it.operators.length === 1 }} \
3065 <span> \
3066 {{= it.translate("operators", it.operators[0].type) }} \
3067 </span> \
3068 {{?}} \
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]) }}"> \
3076       {{?}} \
3077     {{?}} \
3078     <option value="{{= operator.type }}" {{? operator.icon}}data-icon="{{= operator.icon}}"{{?}}>{{= it.translate("operators", operator.type) }}</option> \
3079   {{~}} \
3080   {{? optgroup !== null }}</optgroup>{{?}} \
3081 </select>';
3082
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> \
3088   {{?}} \
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]) }}"> \
3094       {{?}} \
3095     {{?}} \
3096     <option value="{{= entry.value }}">{{= entry.label }}</option> \
3097   {{~}} \
3098   {{? optgroup !== null }}</optgroup>{{?}} \
3099 </select>';
3100
3101 /**
3102  * Returns group's HTML
3103  * @param {string} group_id
3104  * @param {int} level
3105  * @returns {string}
3106  * @fires QueryBuilder.changer:getGroupTemplate
3107  * @private
3108  */
3109 QueryBuilder.prototype.getGroupTemplate = function(group_id, level) {
3110     var h = this.templates.group({
3111         builder: this,
3112         group_id: group_id,
3113         level: level,
3114         conditions: this.settings.conditions,
3115         icons: this.icons,
3116         settings: this.settings,
3117         translate: this.translate.bind(this)
3118     });
3119
3120     /**
3121      * Modifies the raw HTML of a group
3122      * @event changer:getGroupTemplate
3123      * @memberof QueryBuilder
3124      * @param {string} html
3125      * @param {int} level
3126      * @returns {string}
3127      */
3128     return this.change('getGroupTemplate', h, level);
3129 };
3130
3131 /**
3132  * Returns rule's HTML
3133  * @param {string} rule_id
3134  * @returns {string}
3135  * @fires QueryBuilder.changer:getRuleTemplate
3136  * @private
3137  */
3138 QueryBuilder.prototype.getRuleTemplate = function(rule_id) {
3139     var h = this.templates.rule({
3140         builder: this,
3141         rule_id: rule_id,
3142         icons: this.icons,
3143         settings: this.settings,
3144         translate: this.translate.bind(this)
3145     });
3146
3147     /**
3148      * Modifies the raw HTML of a rule
3149      * @event changer:getRuleTemplate
3150      * @memberof QueryBuilder
3151      * @param {string} html
3152      * @returns {string}
3153      */
3154     return this.change('getRuleTemplate', h);
3155 };
3156
3157 /**
3158  * Returns rule's filter HTML
3159  * @param {Rule} rule
3160  * @param {object[]} filters
3161  * @returns {string}
3162  * @fires QueryBuilder.changer:getRuleFilterTemplate
3163  * @private
3164  */
3165 QueryBuilder.prototype.getRuleFilterSelect = function(rule, filters) {
3166     var h = this.templates.filterSelect({
3167         builder: this,
3168         rule: rule,
3169         filters: filters,
3170         icons: this.icons,
3171         settings: this.settings,
3172         translate: this.translate.bind(this)
3173     });
3174
3175     /**
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
3182      * @returns {string}
3183      */
3184     return this.change('getRuleFilterSelect', h, rule, filters);
3185 };
3186
3187 /**
3188  * Returns rule's operator HTML
3189  * @param {Rule} rule
3190  * @param {object[]} operators
3191  * @returns {string}
3192  * @fires QueryBuilder.changer:getRuleOperatorTemplate
3193  * @private
3194  */
3195 QueryBuilder.prototype.getRuleOperatorSelect = function(rule, operators) {
3196     var h = this.templates.operatorSelect({
3197         builder: this,
3198         rule: rule,
3199         operators: operators,
3200         icons: this.icons,
3201         settings: this.settings,
3202         translate: this.translate.bind(this)
3203     });
3204
3205     /**
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
3212      * @returns {string}
3213      */
3214     return this.change('getRuleOperatorSelect', h, rule, operators);
3215 };
3216
3217 /**
3218  * Returns the rule's value select HTML
3219  * @param {string} name
3220  * @param {Rule} rule
3221  * @returns {string}
3222  * @fires QueryBuilder.changer:getRuleValueSelect
3223  * @private
3224  */
3225 QueryBuilder.prototype.getRuleValueSelect = function(name, rule) {
3226     var h = this.templates.ruleValueSelect({
3227         builder: this,
3228         name: name,
3229         rule: rule,
3230         icons: this.icons,
3231         settings: this.settings,
3232         translate: this.translate.bind(this)
3233     });
3234
3235     /**
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
3242      * @returns {string}
3243      */
3244     return this.change('getRuleValueSelect', h, name, rule);
3245 };
3246
3247 /**
3248  * Returns the rule's value HTML
3249  * @param {Rule} rule
3250  * @param {int} value_id
3251  * @returns {string}
3252  * @fires QueryBuilder.changer:getRuleInput
3253  * @private
3254  */
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' : '';
3260     var h = '';
3261
3262     if (typeof filter.input == 'function') {
3263         h = filter.input.call(this, rule, name);
3264     }
3265     else {
3266         switch (filter.input) {
3267             case 'radio':
3268             case 'checkbox':
3269                 Utils.iterateOptions(filter.values, function(key, val) {
3270                     h += '<label' + c + '><input type="' + filter.input + '" name="' + name + '" value="' + key + '"> ' + val + '</label> ';
3271                 });
3272                 break;
3273
3274             case 'select':
3275                 h = this.getRuleValueSelect(name, rule);
3276                 break;
3277
3278             case 'textarea':
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>';
3286                 break;
3287
3288             case 'number':
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 + '"';
3295                 h += '>';
3296                 break;
3297
3298             default:
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 + '"';
3304                 h += '>';
3305         }
3306     }
3307
3308     /**
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
3315      * @returns {string}
3316      */
3317     return this.change('getRuleInput', h, rule, name);
3318 };
3319
3320
3321 /**
3322  * @namespace
3323  */
3324 var Utils = {};
3325
3326 /**
3327  * @member {object}
3328  * @memberof QueryBuilder
3329  * @see Utils
3330  */
3331 QueryBuilder.utils = Utils;
3332
3333 /**
3334  * @callback Utils#OptionsIteratee
3335  * @param {string} key
3336  * @param {string} value
3337  * @param {string} [optgroup]
3338  */
3339
3340 /**
3341  * Iterates over radio/checkbox/selection options, it accept four formats
3342  *
3343  * @example
3344  * // array of values
3345  * options = ['one', 'two', 'three']
3346  * @example
3347  * // simple key-value map
3348  * options = {1: 'one', 2: 'two', 3: 'three'}
3349  * @example
3350  * // array of 1-element maps
3351  * options = [{1: 'one'}, {2: 'two'}, {3: 'three'}]
3352  * @example
3353  * // array of elements
3354  * options = [{value: 1, label: 'one', optgroup: 'group'}, {value: 2, label: 'two'}]
3355  *
3356  * @param {object|array} options
3357  * @param {Utils#OptionsIteratee} tpl
3358  */
3359 Utils.iterateOptions = function(options, tpl) {
3360     if (options) {
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);
3367                     }
3368                     // array of one-element maps
3369                     else {
3370                         $.each(entry, function(key, val) {
3371                             tpl(key, val);
3372                             return false; // break after first entry
3373                         });
3374                     }
3375                 }
3376                 // array of values
3377                 else {
3378                     tpl(entry, entry);
3379                 }
3380             });
3381         }
3382         // unordered map
3383         else {
3384             $.each(options, function(key, val) {
3385                 tpl(key, val);
3386             });
3387         }
3388     }
3389 };
3390
3391 /**
3392  * Replaces {0}, {1}, ... in a string
3393  * @param {string} str
3394  * @param {...*} args
3395  * @returns {string}
3396  */
3397 Utils.fmt = function(str, args) {
3398     if (!Array.isArray(args)) {
3399         args = Array.prototype.slice.call(arguments, 1);
3400     }
3401
3402     return str.replace(/{([0-9]+)}/g, function(m, i) {
3403         return args[parseInt(i)];
3404     });
3405 };
3406
3407 /**
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
3413  */
3414 Utils.error = function() {
3415     var i = 0;
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);
3420
3421     if (doThrow) {
3422         var err = new Error(Utils.fmt(message, args));
3423         err.name = type + 'Error';
3424         err.args = args;
3425         throw err;
3426     }
3427     else {
3428         console.error(type + 'Error: ' + Utils.fmt(message, args));
3429     }
3430 };
3431
3432 /**
3433  * Changes the type of a value to int, float or bool
3434  * @param {*} value
3435  * @param {string} type - 'integer', 'double', 'boolean' or anything else (passthrough)
3436  * @returns {*}
3437  */
3438 Utils.changeType = function(value, type) {
3439     if (value === '' || value === undefined) {
3440         return undefined;
3441     }
3442
3443     switch (type) {
3444         // @formatter:off
3445         case 'integer':
3446             if (typeof value === 'string' && !/^-?\d+$/.test(value)) {
3447                 return value;
3448             }
3449             return parseInt(value);
3450         case 'double':
3451             if (typeof value === 'string' && !/^-?\d+\.?\d*$/.test(value)) {
3452                 return value;
3453             }
3454             return parseFloat(value);
3455         case 'boolean':
3456             if (typeof value === 'string' && !/^(0|1|true|false){1}$/i.test(value)) {
3457                 return value;
3458             }
3459             return value === true || value === 1 || value.toLowerCase() === 'true' || value === '1';
3460         default: return value;
3461         // @formatter:on
3462     }
3463 };
3464
3465 /**
3466  * Escapes a string like PHP's mysql_real_escape_string does
3467  * @param {string} value
3468  * @returns {string}
3469  */
3470 Utils.escapeString = function(value) {
3471     if (typeof value != 'string') {
3472         return value;
3473     }
3474
3475     return value
3476         .replace(/[\0\n\r\b\\\'\"]/g, function(s) {
3477             switch (s) {
3478                 // @formatter:off
3479                 case '\0': return '\\0';
3480                 case '\n': return '\\n';
3481                 case '\r': return '\\r';
3482                 case '\b': return '\\b';
3483                 default:   return '\\' + s;
3484                 // @formatter:off
3485             }
3486         })
3487         // uglify compliant
3488         .replace(/\t/g, '\\t')
3489         .replace(/\x1a/g, '\\Z');
3490 };
3491
3492 /**
3493  * Escapes a string for use in regex
3494  * @param {string} str
3495  * @returns {string}
3496  */
3497 Utils.escapeRegExp = function(str) {
3498     return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
3499 };
3500
3501 /**
3502  * Escapes a string for use in HTML element id
3503  * @param {string} str
3504  * @returns {string}
3505  */
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;
3513 };
3514
3515 /**
3516  * Sorts objects by grouping them by `key`, preserving initial order when possible
3517  * @param {object[]} items
3518  * @param {string} key
3519  * @returns {object[]}
3520  */
3521 Utils.groupSort = function(items, key) {
3522     var optgroups = [];
3523     var newItems = [];
3524
3525     items.forEach(function(item) {
3526         var idx;
3527
3528         if (item[key]) {
3529             idx = optgroups.lastIndexOf(item[key]);
3530
3531             if (idx == -1) {
3532                 idx = optgroups.length;
3533             }
3534             else {
3535                 idx++;
3536             }
3537         }
3538         else {
3539             idx = optgroups.length;
3540         }
3541
3542         optgroups.splice(idx, 0, item[key]);
3543         newItems.splice(idx, 0, item);
3544     });
3545
3546     return newItems;
3547 };
3548
3549 /**
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
3555  */
3556 Utils.defineModelProperties = function(obj, fields) {
3557     fields.forEach(function(field) {
3558         Object.defineProperty(obj.prototype, field, {
3559             enumerable: true,
3560             get: function() {
3561                 return this.__[field];
3562             },
3563             set: function(value) {
3564                 var previousValue = (this.__[field] !== null && typeof this.__[field] == 'object') ?
3565                     $.extend({}, this.__[field]) :
3566                     this.__[field];
3567
3568                 this.__[field] = value;
3569
3570                 if (this.model !== null) {
3571                     /**
3572                      * After a value of the model changed
3573                      * @event model:update
3574                      * @memberof Model
3575                      * @param {Node} node
3576                      * @param {string} field
3577                      * @param {*} value
3578                      * @param {*} previousValue
3579                      */
3580                     this.model.trigger('update', this, field, value, previousValue);
3581                 }
3582             }
3583         });
3584     });
3585 };
3586
3587
3588 /**
3589  * Main object storing data model and emitting model events
3590  * @constructor
3591  */
3592 function Model() {
3593     /**
3594      * @member {Group}
3595      * @readonly
3596      */
3597     this.root = null;
3598
3599     /**
3600      * Base for event emitting
3601      * @member {jQuery}
3602      * @readonly
3603      * @private
3604      */
3605     this.$ = $(this);
3606 }
3607
3608 $.extend(Model.prototype, /** @lends Model.prototype */ {
3609     /**
3610      * Triggers an event on the model
3611      * @param {string} type
3612      * @returns {$.Event}
3613      */
3614     trigger: function(type) {
3615         var event = new $.Event(type);
3616         this.$.triggerHandler(event, Array.prototype.slice.call(arguments, 1));
3617         return event;
3618     },
3619
3620     /**
3621      * Attaches an event listener on the model
3622      * @param {string} type
3623      * @param {function} cb
3624      * @returns {Model}
3625      */
3626     on: function() {
3627         this.$.on.apply(this.$, Array.prototype.slice.call(arguments));
3628         return this;
3629     },
3630
3631     /**
3632      * Removes an event listener from the model
3633      * @param {string} type
3634      * @param {function} [cb]
3635      * @returns {Model}
3636      */
3637     off: function() {
3638         this.$.off.apply(this.$, Array.prototype.slice.call(arguments));
3639         return this;
3640     },
3641
3642     /**
3643      * Attaches an event listener called once on the model
3644      * @param {string} type
3645      * @param {function} cb
3646      * @returns {Model}
3647      */
3648     once: function() {
3649         this.$.one.apply(this.$, Array.prototype.slice.call(arguments));
3650         return this;
3651     }
3652 });
3653
3654
3655 /**
3656  * Root abstract object
3657  * @constructor
3658  * @param {Node} [parent]
3659  * @param {jQuery} $el
3660  */
3661 var Node = function(parent, $el) {
3662     if (!(this instanceof Node)) {
3663         return new Node(parent, $el);
3664     }
3665
3666     Object.defineProperty(this, '__', { value: {} });
3667
3668     $el.data('queryBuilderModel', this);
3669
3670     /**
3671      * @name level
3672      * @member {int}
3673      * @memberof Node
3674      * @instance
3675      * @readonly
3676      */
3677     this.__.level = 1;
3678
3679     /**
3680      * @name error
3681      * @member {string}
3682      * @memberof Node
3683      * @instance
3684      */
3685     this.__.error = null;
3686
3687     /**
3688      * @name flags
3689      * @member {object}
3690      * @memberof Node
3691      * @instance
3692      * @readonly
3693      */
3694     this.__.flags = {};
3695
3696     /**
3697      * @name data
3698      * @member {object}
3699      * @memberof Node
3700      * @instance
3701      */
3702     this.__.data = undefined;
3703
3704     /**
3705      * @member {jQuery}
3706      * @readonly
3707      */
3708     this.$el = $el;
3709
3710     /**
3711      * @member {string}
3712      * @readonly
3713      */
3714     this.id = $el[0].id;
3715
3716     /**
3717      * @member {Model}
3718      * @readonly
3719      */
3720     this.model = null;
3721
3722     /**
3723      * @member {Group}
3724      * @readonly
3725      */
3726     this.parent = parent;
3727 };
3728
3729 Utils.defineModelProperties(Node, ['level', 'error', 'data', 'flags']);
3730
3731 Object.defineProperty(Node.prototype, 'parent', {
3732     enumerable: true,
3733     get: function() {
3734         return this.__.parent;
3735     },
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;
3740     }
3741 });
3742
3743 /**
3744  * Checks if this Node is the root
3745  * @returns {boolean}
3746  */
3747 Node.prototype.isRoot = function() {
3748     return (this.level === 1);
3749 };
3750
3751 /**
3752  * Returns the node position inside its parent
3753  * @returns {int}
3754  */
3755 Node.prototype.getPos = function() {
3756     if (this.isRoot()) {
3757         return -1;
3758     }
3759     else {
3760         return this.parent.getNodePos(this);
3761     }
3762 };
3763
3764 /**
3765  * Deletes self
3766  * @fires Model.model:drop
3767  */
3768 Node.prototype.drop = function() {
3769     var model = this.model;
3770
3771     if (!!this.parent) {
3772         this.parent.removeNode(this);
3773     }
3774
3775     this.$el.removeData('queryBuilderModel');
3776
3777     if (model !== null) {
3778         /**
3779          * After a node of the model has been removed
3780          * @event model:drop
3781          * @memberof Model
3782          * @param {Node} node
3783          */
3784         model.trigger('drop', this);
3785     }
3786 };
3787
3788 /**
3789  * Moves itself after another Node
3790  * @param {Node} target
3791  * @fires Model.model:move
3792  */
3793 Node.prototype.moveAfter = function(target) {
3794     if (!this.isRoot()) {
3795         this.move(target.parent, target.getPos() + 1);
3796     }
3797 };
3798
3799 /**
3800  * Moves itself at the beginning of parent or another Group
3801  * @param {Group} [target]
3802  * @fires Model.model:move
3803  */
3804 Node.prototype.moveAtBegin = function(target) {
3805     if (!this.isRoot()) {
3806         if (target === undefined) {
3807             target = this.parent;
3808         }
3809
3810         this.move(target, 0);
3811     }
3812 };
3813
3814 /**
3815  * Moves itself at the end of parent or another Group
3816  * @param {Group} [target]
3817  * @fires Model.model:move
3818  */
3819 Node.prototype.moveAtEnd = function(target) {
3820     if (!this.isRoot()) {
3821         if (target === undefined) {
3822             target = this.parent;
3823         }
3824
3825         this.move(target, target.length() === 0 ? 0 : target.length() - 1);
3826     }
3827 };
3828
3829 /**
3830  * Moves itself at specific position of Group
3831  * @param {Group} target
3832  * @param {int} index
3833  * @fires Model.model:move
3834  */
3835 Node.prototype.move = function(target, index) {
3836     if (!this.isRoot()) {
3837         if (typeof target === 'number') {
3838             index = target;
3839             target = this.parent;
3840         }
3841
3842         this.parent.removeNode(this);
3843         target.insertNode(this, index, false);
3844
3845         if (this.model !== null) {
3846             /**
3847              * After a node of the model has been moved
3848              * @event model:move
3849              * @memberof Model
3850              * @param {Node} node
3851              * @param {Node} target
3852              * @param {int} index
3853              */
3854             this.model.trigger('move', this, target, index);
3855         }
3856     }
3857 };
3858
3859
3860 /**
3861  * Group object
3862  * @constructor
3863  * @extends Node
3864  * @param {Group} [parent]
3865  * @param {jQuery} $el
3866  */
3867 var Group = function(parent, $el) {
3868     if (!(this instanceof Group)) {
3869         return new Group(parent, $el);
3870     }
3871
3872     Node.call(this, parent, $el);
3873
3874     /**
3875      * @member {object[]}
3876      * @readonly
3877      */
3878     this.rules = [];
3879
3880     /**
3881      * @name condition
3882      * @member {string}
3883      * @memberof Group
3884      * @instance
3885      */
3886     this.__.condition = null;
3887 };
3888
3889 Group.prototype = Object.create(Node.prototype);
3890 Group.prototype.constructor = Group;
3891
3892 Utils.defineModelProperties(Group, ['condition']);
3893
3894 /**
3895  * Removes group's content
3896  */
3897 Group.prototype.empty = function() {
3898     this.each('reverse', function(rule) {
3899         rule.drop();
3900     }, function(group) {
3901         group.drop();
3902     });
3903 };
3904
3905 /**
3906  * Deletes self
3907  */
3908 Group.prototype.drop = function() {
3909     this.empty();
3910     Node.prototype.drop.call(this);
3911 };
3912
3913 /**
3914  * Returns the number of children
3915  * @returns {int}
3916  */
3917 Group.prototype.length = function() {
3918     return this.rules.length;
3919 };
3920
3921 /**
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
3928  */
3929 Group.prototype.insertNode = function(node, index, trigger) {
3930     if (index === undefined) {
3931         index = this.length();
3932     }
3933
3934     this.rules.splice(index, 0, node);
3935     node.parent = this;
3936
3937     if (trigger && this.model !== null) {
3938         /**
3939          * After a node of the model has been added
3940          * @event model:add
3941          * @memberof Model
3942          * @param {Node} parent
3943          * @param {Node} node
3944          * @param {int} index
3945          */
3946         this.model.trigger('add', this, node, index);
3947     }
3948
3949     return node;
3950 };
3951
3952 /**
3953  * Adds a new Group at specified index
3954  * @param {jQuery} $el
3955  * @param {int} [index=end]
3956  * @returns {Group}
3957  * @fires Model.model:add
3958  */
3959 Group.prototype.addGroup = function($el, index) {
3960     return this.insertNode(new Group(this, $el), index, true);
3961 };
3962
3963 /**
3964  * Adds a new Rule at specified index
3965  * @param {jQuery} $el
3966  * @param {int} [index=end]
3967  * @returns {Rule}
3968  * @fires Model.model:add
3969  */
3970 Group.prototype.addRule = function($el, index) {
3971     return this.insertNode(new Rule(this, $el), index, true);
3972 };
3973
3974 /**
3975  * Deletes a specific Node
3976  * @param {Node} node
3977  */
3978 Group.prototype.removeNode = function(node) {
3979     var index = this.getNodePos(node);
3980     if (index !== -1) {
3981         node.parent = null;
3982         this.rules.splice(index, 1);
3983     }
3984 };
3985
3986 /**
3987  * Returns the position of a child Node
3988  * @param {Node} node
3989  * @returns {int}
3990  */
3991 Group.prototype.getNodePos = function(node) {
3992     return this.rules.indexOf(node);
3993 };
3994
3995 /**
3996  * @callback Model#GroupIteratee
3997  * @param {Node} node
3998  * @returns {boolean} stop the iteration
3999  */
4000
4001 /**
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
4008  */
4009 Group.prototype.each = function(reverse, cbRule, cbGroup, context) {
4010     if (typeof reverse !== 'boolean' && typeof reverse !== 'string') {
4011         context = cbGroup;
4012         cbGroup = cbRule;
4013         cbRule = reverse;
4014         reverse = false;
4015     }
4016     context = context === undefined ? null : context;
4017
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;
4023     };
4024     var stop = false;
4025
4026     for (; next(); i += c) {
4027         if (this.rules[i] instanceof Group) {
4028             if (!!cbGroup) {
4029                 stop = cbGroup.call(context, this.rules[i]) === false;
4030             }
4031         }
4032         else if (!!cbRule) {
4033             stop = cbRule.call(context, this.rules[i]) === false;
4034         }
4035
4036         if (stop) {
4037             break;
4038         }
4039     }
4040
4041     return !stop;
4042 };
4043
4044 /**
4045  * Checks if the group contains a particular Node
4046  * @param {Node} node
4047  * @param {boolean} [recursive=false]
4048  * @returns {boolean}
4049  */
4050 Group.prototype.contains = function(node, recursive) {
4051     if (this.getNodePos(node) !== -1) {
4052         return true;
4053     }
4054     else if (!recursive) {
4055         return false;
4056     }
4057     else {
4058         // the loop will return with false as soon as the Node is found
4059         return !this.each(function() {
4060             return true;
4061         }, function(group) {
4062             return !group.contains(node, true);
4063         });
4064     }
4065 };
4066
4067
4068 /**
4069  * Rule object
4070  * @constructor
4071  * @extends Node
4072  * @param {Group} parent
4073  * @param {jQuery} $el
4074  */
4075 var Rule = function(parent, $el) {
4076     if (!(this instanceof Rule)) {
4077         return new Rule(parent, $el);
4078     }
4079
4080     Node.call(this, parent, $el);
4081
4082     this._updating_value = false;
4083     this._updating_input = false;
4084
4085     /**
4086      * @name filter
4087      * @member {QueryBuilder.Filter}
4088      * @memberof Rule
4089      * @instance
4090      */
4091     this.__.filter = null;
4092
4093     /**
4094      * @name operator
4095      * @member {QueryBuilder.Operator}
4096      * @memberof Rule
4097      * @instance
4098      */
4099     this.__.operator = null;
4100
4101     /**
4102      * @name value
4103      * @member {*}
4104      * @memberof Rule
4105      * @instance
4106      */
4107     this.__.value = undefined;
4108 };
4109
4110 Rule.prototype = Object.create(Node.prototype);
4111 Rule.prototype.constructor = Rule;
4112
4113 Utils.defineModelProperties(Rule, ['filter', 'operator', 'value']);
4114
4115 /**
4116  * Checks if this Node is the root
4117  * @returns {boolean} always false
4118  */
4119 Rule.prototype.isRoot = function() {
4120     return false;
4121 };
4122
4123
4124 /**
4125  * @member {function}
4126  * @memberof QueryBuilder
4127  * @see Group
4128  */
4129 QueryBuilder.Group = Group;
4130
4131 /**
4132  * @member {function}
4133  * @memberof QueryBuilder
4134  * @see Rule
4135  */
4136 QueryBuilder.Rule = Rule;
4137
4138
4139 /**
4140  * The {@link http://learn.jquery.com/plugins/|jQuery Plugins} namespace
4141  * @external "jQuery.fn"
4142  */
4143
4144 /**
4145  * Instanciates or accesses the {@link QueryBuilder} on an element
4146  * @function
4147  * @memberof external:"jQuery.fn"
4148  * @param {*} option - initial configuration or method name
4149  * @param {...*} args - method arguments
4150  *
4151  * @example
4152  * $('#builder').queryBuilder({ /** configuration object *\/ });
4153  * @example
4154  * $('#builder').queryBuilder('methodName', methodParam1, methodParam2);
4155  */
4156 $.fn.queryBuilder = function(option) {
4157     if (this.length === 0) {
4158         Utils.error('Config', 'No target defined');
4159     }
4160     if (this.length > 1) {
4161         Utils.error('Config', 'Unable to initialize on multiple target');
4162     }
4163
4164     var data = this.data('queryBuilder');
4165     var options = (typeof option == 'object' && option) || {};
4166
4167     if (!data && option == 'destroy') {
4168         return this;
4169     }
4170     if (!data) {
4171         var builder = new QueryBuilder(this, options);
4172         this.data('queryBuilder', builder);
4173         builder.init(options.rules);
4174     }
4175     if (typeof option == 'string') {
4176         return data[option].apply(data, Array.prototype.slice.call(arguments, 1));
4177     }
4178
4179     return this;
4180 };
4181
4182 /**
4183  * @function
4184  * @memberof external:"jQuery.fn"
4185  * @see QueryBuilder
4186  */
4187 $.fn.queryBuilder.constructor = QueryBuilder;
4188
4189 /**
4190  * @function
4191  * @memberof external:"jQuery.fn"
4192  * @see QueryBuilder.defaults
4193  */
4194 $.fn.queryBuilder.defaults = QueryBuilder.defaults;
4195
4196 /**
4197  * @function
4198  * @memberof external:"jQuery.fn"
4199  * @see QueryBuilder.defaults
4200  */
4201 $.fn.queryBuilder.extend = QueryBuilder.extend;
4202
4203 /**
4204  * @function
4205  * @memberof external:"jQuery.fn"
4206  * @see QueryBuilder.define
4207  */
4208 $.fn.queryBuilder.define = QueryBuilder.define;
4209
4210 /**
4211  * @function
4212  * @memberof external:"jQuery.fn"
4213  * @see QueryBuilder.regional
4214  */
4215 $.fn.queryBuilder.regional = QueryBuilder.regional;
4216
4217
4218 /**
4219  * @class BtCheckbox
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']
4225  */
4226 QueryBuilder.define('bt-checkbox', function(options) {
4227     if (options.font == 'glyphicons') {
4228         this.$el.addClass('bt-checkbox-glyphicons');
4229     }
4230
4231     this.on('getRuleInput.filter', function(h, rule, name) {
4232         var filter = rule.filter;
4233
4234         if ((filter.input === 'radio' || filter.input === 'checkbox') && !filter.plugin) {
4235             h.value = '';
4236
4237             if (!filter.colors) {
4238                 filter.colors = {};
4239             }
4240             if (filter.color) {
4241                 filter.colors._def_ = filter.color;
4242             }
4243
4244             var style = filter.vertical ? ' style="display:block"' : '';
4245             var i = 0;
4246
4247             Utils.iterateOptions(filter.values, function(key, val) {
4248                 var color = filter.colors[key] || filter.colors._def_ || options.color;
4249                 var id = name + '_' + (i++);
4250
4251                 h.value+= '\
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> \
4255 </div>';
4256             });
4257         }
4258     });
4259 }, {
4260     font: 'glyphicons',
4261     color: 'default'
4262 });
4263
4264
4265 /**
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
4275  */
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');
4279     }
4280
4281     var Selectors = QueryBuilder.selectors;
4282
4283     // init selectpicker
4284     this.on('afterCreateRuleFilters', function(e, rule) {
4285         rule.$el.find(Selectors.rule_filter).removeClass('form-control').selectpicker(options);
4286     });
4287
4288     this.on('afterCreateRuleOperators', function(e, rule) {
4289         rule.$el.find(Selectors.rule_operator).removeClass('form-control').selectpicker(options);
4290     });
4291
4292     // update selectpicker on change
4293     this.on('afterUpdateRuleFilter', function(e, rule) {
4294         rule.$el.find(Selectors.rule_filter).selectpicker('render');
4295     });
4296
4297     this.on('afterUpdateRuleOperator', function(e, rule) {
4298         rule.$el.find(Selectors.rule_operator).selectpicker('render');
4299     });
4300
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');
4304     });
4305 }, {
4306     container: 'body',
4307     style: 'btn-inverse btn-xs',
4308     width: 'auto',
4309     showIcon: false
4310 });
4311
4312
4313 /**
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
4320  */
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');
4324     }
4325
4326     var self = this;
4327
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');
4333     });
4334
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)
4339                 .tooltip(options)
4340                 .tooltip('hide')
4341                 .tooltip('fixTitle');
4342         }
4343     });
4344 }, {
4345     placement: 'right'
4346 });
4347
4348
4349 /**
4350  * @class ChangeFilters
4351  * @memberof module:plugins
4352  * @description Allows to change available filters after plugin initialization.
4353  */
4354
4355 QueryBuilder.extend(/** @lends module:plugins.ChangeFilters.prototype */ {
4356     /**
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
4363      */
4364     setFilters: function(deleteOrphans, filters) {
4365         var self = this;
4366
4367         if (filters === undefined) {
4368             filters = deleteOrphans;
4369             deleteOrphans = false;
4370         }
4371
4372         filters = this.checkFilters(filters);
4373
4374         /**
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[]}
4380          */
4381         filters = this.change('setFilters', filters);
4382
4383         var filtersIds = filters.map(function(filter) {
4384             return filter.id;
4385         });
4386
4387         // check for orphans
4388         if (!deleteOrphans) {
4389             (function checkOrphans(node) {
4390                 node.each(
4391                     function(rule) {
4392                         if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
4393                             Utils.error('ChangeFilter', 'A rule is using filter "{0}"', rule.filter.id);
4394                         }
4395                     },
4396                     checkOrphans
4397                 );
4398             }(this.model.root));
4399         }
4400
4401         // replace filters
4402         this.filters = filters;
4403
4404         // apply on existing DOM
4405         (function updateBuilder(node) {
4406             node.each(true,
4407                 function(rule) {
4408                     if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
4409                         rule.drop();
4410
4411                         self.trigger('rulesChanged');
4412                     }
4413                     else {
4414                         self.createRuleFilters(rule);
4415
4416                         rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
4417                         self.trigger('afterUpdateRuleFilter', rule);
4418                     }
4419                 },
4420                 updateBuilder
4421             );
4422         }(this.model.root));
4423
4424         // update plugins
4425         if (this.settings.plugins) {
4426             if (this.settings.plugins['unique-filter']) {
4427                 this.updateDisabledFilters();
4428             }
4429             if (this.settings.plugins['bt-selectpicker']) {
4430                 this.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render');
4431             }
4432         }
4433
4434         // reset the default_filter if does not exist anymore
4435         if (this.settings.default_filter) {
4436             try {
4437                 this.getFilterById(this.settings.default_filter);
4438             }
4439             catch (e) {
4440                 this.settings.default_filter = null;
4441             }
4442         }
4443
4444         /**
4445          * After {@link module:plugins.ChangeFilters.setFilters} method
4446          * @event afterSetFilters
4447          * @memberof module:plugins.ChangeFilters
4448          * @param {QueryBuilder.Filter[]} filters
4449          */
4450         this.trigger('afterSetFilters', filters);
4451     },
4452
4453     /**
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
4460      */
4461     addFilter: function(newFilters, position) {
4462         if (position === undefined || position == '#end') {
4463             position = this.filters.length;
4464         }
4465         else if (position == '#start') {
4466             position = 0;
4467         }
4468
4469         if (!$.isArray(newFilters)) {
4470             newFilters = [newFilters];
4471         }
4472
4473         var filters = $.extend(true, [], this.filters);
4474
4475         // numeric position
4476         if (parseInt(position) == position) {
4477             Array.prototype.splice.apply(filters, [position, 0].concat(newFilters));
4478         }
4479         else {
4480             // after filter by its id
4481             if (this.filters.some(function(filter, index) {
4482                     if (filter.id == position) {
4483                         position = index + 1;
4484                         return true;
4485                     }
4486                 })
4487             ) {
4488                 Array.prototype.splice.apply(filters, [position, 0].concat(newFilters));
4489             }
4490             // defaults to end of list
4491             else {
4492                 Array.prototype.push.apply(filters, newFilters);
4493             }
4494         }
4495
4496         this.setFilters(filters);
4497     },
4498
4499     /**
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
4506      */
4507     removeFilter: function(filterIds, deleteOrphans) {
4508         var filters = $.extend(true, [], this.filters);
4509         if (typeof filterIds === 'string') {
4510             filterIds = [filterIds];
4511         }
4512
4513         filters = filters.filter(function(filter) {
4514             return filterIds.indexOf(filter.id) === -1;
4515         });
4516
4517         this.setFilters(deleteOrphans, filters);
4518     }
4519 });
4520
4521
4522 /**
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
4528  */
4529 QueryBuilder.define('chosen-selectpicker', function(options) {
4530
4531     if (!$.fn.chosen) {
4532         Utils.error('MissingLibrary', 'chosen is required to use "chosen-selectpicker" plugin. Get it here: https://github.com/harvesthq/chosen');
4533     }
4534
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');
4537     }
4538
4539     var Selectors = QueryBuilder.selectors;
4540
4541     // init selectpicker
4542     this.on('afterCreateRuleFilters', function(e, rule) {
4543         rule.$el.find(Selectors.rule_filter).removeClass('form-control').chosen(options);
4544     });
4545
4546     this.on('afterCreateRuleOperators', function(e, rule) {
4547         rule.$el.find(Selectors.rule_operator).removeClass('form-control').chosen(options);
4548     });
4549
4550     // update selectpicker on change
4551     this.on('afterUpdateRuleFilter', function(e, rule) {
4552         rule.$el.find(Selectors.rule_filter).trigger('chosen:updated');
4553     });
4554
4555     this.on('afterUpdateRuleOperator', function(e, rule) {
4556         rule.$el.find(Selectors.rule_operator).trigger('chosen:updated');
4557     });
4558
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');
4562     });
4563 });
4564
4565
4566 /**
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
4574  */
4575 QueryBuilder.define('filter-description', function(options) {
4576     // INLINE
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);
4581
4582             if (!description) {
4583                 $p.hide();
4584             }
4585             else {
4586                 if ($p.length === 0) {
4587                     $p = $('<p class="filter-description"></p>');
4588                     $p.appendTo(rule.$el);
4589                 }
4590                 else {
4591                     $p.css('display', '');
4592                 }
4593
4594                 $p.html('<i class="' + options.icon + '"></i> ' + description);
4595             }
4596         });
4597     }
4598     // POPOVER
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');
4602         }
4603
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);
4607
4608             if (!description) {
4609                 $b.hide();
4610
4611                 if ($b.data('bs.popover')) {
4612                     $b.popover('hide');
4613                 }
4614             }
4615             else {
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));
4619
4620                     $b.popover({
4621                         placement: 'left',
4622                         container: 'body',
4623                         html: true
4624                     });
4625
4626                     $b.on('mouseout', function() {
4627                         $b.popover('hide');
4628                     });
4629                 }
4630                 else {
4631                     $b.css('display', '');
4632                 }
4633
4634                 $b.data('bs.popover').options.content = description;
4635
4636                 if ($b.attr('aria-describedby')) {
4637                     $b.popover('show');
4638                 }
4639             }
4640         });
4641     }
4642     // BOOTBOX
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');
4646         }
4647
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);
4651
4652             if (!description) {
4653                 $b.hide();
4654             }
4655             else {
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));
4659
4660                     $b.on('click', function() {
4661                         bootbox.alert($b.data('description'));
4662                     });
4663                 }
4664                 else {
4665                     $b.css('display', '');
4666                 }
4667
4668                 $b.data('description', description);
4669             }
4670         });
4671     }
4672 }, {
4673     icon: 'glyphicon glyphicon-info-sign',
4674     mode: 'popover'
4675 });
4676
4677 QueryBuilder.extend(/** @lends module:plugins.FilterDescription.prototype */ {
4678     /**
4679      * Returns the description of a filter for a particular rule (if present)
4680      * @param {object} filter
4681      * @param {Rule} [rule]
4682      * @returns {string}
4683      * @private
4684      */
4685     getFilterDescription: function(filter, rule) {
4686         if (!filter) {
4687             return undefined;
4688         }
4689         else if (typeof filter.description == 'function') {
4690             return filter.description.call(this, rule);
4691         }
4692         else {
4693             return filter.description;
4694         }
4695     }
4696 });
4697
4698
4699 /**
4700  * @class Invert
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]
4709  */
4710 QueryBuilder.define('invert', function(options) {
4711     var self = this;
4712     var Selectors = QueryBuilder.selectors;
4713
4714     // Bind events
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);
4719         });
4720
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);
4725             });
4726         }
4727     });
4728
4729     // Modify templates
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') +
4736                 '</button>'
4737             );
4738             h.value = $h.prop('outerHTML');
4739         });
4740
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') +
4747                     '</button>'
4748                 );
4749                 h.value = $h.prop('outerHTML');
4750             });
4751         }
4752     }
4753 }, {
4754     icon: 'glyphicon glyphicon-random',
4755     recursive: true,
4756     invert_rules: true,
4757     display_rules_button: false,
4758     silent_fail: false,
4759     disable_template: false
4760 });
4761
4762 QueryBuilder.defaults({
4763     operatorOpposites: {
4764         'equal':            'not_equal',
4765         'not_equal':        'equal',
4766         'in':               'not_in',
4767         'not_in':           'in',
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'
4784     },
4785
4786     conditionOpposites: {
4787         'AND': 'OR',
4788         'OR': 'AND'
4789     }
4790 });
4791
4792 QueryBuilder.extend(/** @lends module:plugins.Invert.prototype */ {
4793     /**
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
4799      */
4800     invert: function(node, options) {
4801         if (!(node instanceof Node)) {
4802             if (!this.model.root) return;
4803             options = node;
4804             node = this.model.root;
4805         }
4806
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;
4812
4813         if (node instanceof Group) {
4814             // invert group condition
4815             if (this.settings.conditionOpposites[node.condition]) {
4816                 node.condition = this.settings.conditionOpposites[node.condition];
4817             }
4818             else if (!options.silent_fail) {
4819                 Utils.error('InvertCondition', 'Unknown inverse of condition "{0}"', node.condition);
4820             }
4821
4822             // recursive call
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);
4828                     }
4829                 }, function(group) {
4830                     this.invert(group, tempOpts);
4831                 }, this);
4832             }
4833         }
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);
4842                     }
4843                 }
4844                 else if (!options.silent_fail) {
4845                     Utils.error('InvertOperator', 'Unknown inverse of operator "{0}"', node.operator.type);
4846                 }
4847             }
4848         }
4849
4850         if (options.trigger) {
4851             /**
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
4857              */
4858             this.trigger('afterInvert', node, options);
4859
4860             this.trigger('rulesChanged');
4861         }
4862     }
4863 });
4864
4865
4866 /**
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.
4870  */
4871
4872 QueryBuilder.defaults({
4873     mongoOperators: {
4874         // @formatter:off
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 }; }
4895         // @formatter:on
4896     },
4897
4898     mongoRuleOperators: {
4899         $eq: function(v) {
4900             return {
4901                 'val': v,
4902                 'op': v === null ? 'is_null' : (v === '' ? 'is_empty' : 'equal')
4903             };
4904         },
4905         $ne: function(v) {
4906             v = v.$ne;
4907             return {
4908                 'val': v,
4909                 'op': v === null ? 'is_not_null' : (v === '' ? 'is_not_empty' : 'not_equal')
4910             };
4911         },
4912         $regex: function(v) {
4913             v = v.$regex;
4914             if (v.slice(0, 4) == '^(?!' && v.slice(-1) == ')') {
4915                 return { 'val': v.slice(4, -1), 'op': 'not_begins_with' };
4916             }
4917             else if (v.slice(0, 5) == '^((?!' && v.slice(-5) == ').)*$') {
4918                 return { 'val': v.slice(5, -5), 'op': 'not_contains' };
4919             }
4920             else if (v.slice(0, 4) == '(?<!' && v.slice(-2) == ')$') {
4921                 return { 'val': v.slice(4, -2), 'op': 'not_ends_with' };
4922             }
4923             else if (v.slice(-1) == '$') {
4924                 return { 'val': v.slice(0, -1), 'op': 'ends_with' };
4925             }
4926             else if (v.slice(0, 1) == '^') {
4927                 return { 'val': v.slice(1), 'op': 'begins_with' };
4928             }
4929             else {
4930                 return { 'val': v, 'op': 'contains' };
4931             }
4932         },
4933         between: function(v) {
4934             return { 'val': [v.$gte, v.$lte], 'op': 'between' };
4935         },
4936         not_between: function(v) {
4937             return { 'val': [v.$lt, v.$gt], 'op': 'not_between' };
4938         },
4939         $in: function(v) {
4940             return { 'val': v.$in, 'op': 'in' };
4941         },
4942         $nin: function(v) {
4943             return { 'val': v.$nin, 'op': 'not_in' };
4944         },
4945         $lt: function(v) {
4946             return { 'val': v.$lt, 'op': 'less' };
4947         },
4948         $lte: function(v) {
4949             return { 'val': v.$lte, 'op': 'less_or_equal' };
4950         },
4951         $gt: function(v) {
4952             return { 'val': v.$gt, 'op': 'greater' };
4953         },
4954         $gte: function(v) {
4955             return { 'val': v.$gte, 'op': 'greater_or_equal' };
4956         }
4957     }
4958 });
4959
4960 QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ {
4961     /**
4962      * Returns rules as a MongoDB query
4963      * @param {object} [data] - current rules by default
4964      * @returns {object}
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
4969      */
4970     getMongo: function(data) {
4971         data = (data === undefined) ? this.getRules() : data;
4972
4973         if (!data) {
4974             return null;
4975         }
4976
4977         var self = this;
4978
4979         return (function parse(group) {
4980             if (!group.condition) {
4981                 group.condition = self.settings.default_condition;
4982             }
4983             if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) {
4984                 Utils.error('UndefinedMongoCondition', 'Unable to build MongoDB query with condition "{0}"', group.condition);
4985             }
4986
4987             if (!group.rules) {
4988                 return {};
4989             }
4990
4991             var parts = [];
4992
4993             group.rules.forEach(function(rule) {
4994                 if (rule.rules && rule.rules.length > 0) {
4995                     parts.push(parse(rule));
4996                 }
4997                 else {
4998                     var mdb = self.settings.mongoOperators[rule.operator];
4999                     var ope = self.getOperatorByType(rule.operator);
5000
5001                     if (mdb === undefined) {
5002                         Utils.error('UndefinedMongoOperator', 'Unknown MongoDB operation for operator "{0}"', rule.operator);
5003                     }
5004
5005                     if (ope.nb_inputs !== 0) {
5006                         if (!(rule.value instanceof Array)) {
5007                             rule.value = [rule.value];
5008                         }
5009                     }
5010
5011                     /**
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
5017                      * @returns {string}
5018                      */
5019                     var field = self.change('getMongoDBField', rule.field, rule);
5020
5021                     var ruleExpression = {};
5022                     ruleExpression[field] = mdb.call(self, rule.value);
5023
5024                     /**
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
5030                      * @param {*} value
5031                      * @param {function} valueWrapper - function that takes the value and adds the operator
5032                      * @returns {object}
5033                      */
5034                     parts.push(self.change('ruleToMongo', ruleExpression, rule, rule.value, mdb));
5035                 }
5036             });
5037
5038             var groupExpression = {};
5039             groupExpression['$' + group.condition.toLowerCase()] = parts;
5040
5041             /**
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
5047              * @returns {object}
5048              */
5049             return self.change('groupToMongo', groupExpression, group);
5050         }(data));
5051     },
5052
5053     /**
5054      * Converts a MongoDB query to rules
5055      * @param {object} query
5056      * @returns {object}
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
5062      */
5063     getRulesFromMongo: function(query) {
5064         if (query === undefined || query === null) {
5065             return null;
5066         }
5067
5068         var self = this;
5069
5070         /**
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
5076          */
5077         query = self.change('parseMongoNode', query);
5078
5079         // a plugin returned a group
5080         if ('rules' in query && 'condition' in query) {
5081             return query;
5082         }
5083
5084         // a plugin returned a rule
5085         if ('id' in query && 'operator' in query && 'value' in query) {
5086             return {
5087                 condition: this.settings.default_condition,
5088                 rules: [query]
5089             };
5090         }
5091
5092         var key = self.getMongoCondition(query);
5093         if (!key) {
5094             Utils.error('MongoParse', 'Invalid MongoDB query format');
5095         }
5096
5097         return (function parse(data, topKey) {
5098             var rules = data[topKey];
5099             var parts = [];
5100
5101             rules.forEach(function(data) {
5102                 // allow plugins to manually parse or handle special cases
5103                 data = self.change('parseMongoNode', data);
5104
5105                 // a plugin returned a group
5106                 if ('rules' in data && 'condition' in data) {
5107                     parts.push(data);
5108                     return;
5109                 }
5110
5111                 // a plugin returned a rule
5112                 if ('id' in data && 'operator' in data && 'value' in data) {
5113                     parts.push(data);
5114                     return;
5115                 }
5116
5117                 var key = self.getMongoCondition(data);
5118                 if (key) {
5119                     parts.push(parse(data, key));
5120                 }
5121                 else {
5122                     var field = Object.keys(data)[0];
5123                     var value = data[field];
5124
5125                     var operator = self.getMongoOperator(value);
5126                     if (operator === undefined) {
5127                         Utils.error('MongoParse', 'Invalid MongoDB query format');
5128                     }
5129
5130                     var mdbrl = self.settings.mongoRuleOperators[operator];
5131                     if (mdbrl === undefined) {
5132                         Utils.error('UndefinedMongoOperator', 'JSON Rule operation unknown for operator "{0}"', operator);
5133                     }
5134
5135                     var opVal = mdbrl.call(self, value);
5136
5137                     var id = self.getMongoDBFieldID(field, value);
5138
5139                     /**
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
5145                      * @returns {object}
5146                      */
5147                     var rule = self.change('mongoToRule', {
5148                         id: id,
5149                         field: field,
5150                         operator: opVal.op,
5151                         value: opVal.val
5152                     }, data);
5153
5154                     parts.push(rule);
5155                 }
5156             });
5157
5158             /**
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
5164              * @returns {object}
5165              */
5166             return self.change('mongoToGroup', {
5167                 condition: topKey.replace('$', '').toUpperCase(),
5168                 rules: parts
5169             }, data);
5170         }(query, key));
5171     },
5172
5173     /**
5174      * Sets rules a from MongoDB query
5175      * @see module:plugins.MongoDbSupport.getRulesFromMongo
5176      */
5177     setRulesFromMongo: function(query) {
5178         this.setRules(this.getRulesFromMongo(query));
5179     },
5180
5181     /**
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
5185      * @param {*} value
5186      * @fires module:plugins.MongoDbSupport:changer:getMongoDBFieldID
5187      * @returns {string}
5188      * @private
5189      */
5190     getMongoDBFieldID: function(field, value) {
5191         var matchingFilters = this.filters.filter(function(filter) {
5192             return filter.field === field;
5193         });
5194
5195         var id;
5196         if (matchingFilters.length === 1) {
5197             id = matchingFilters[0].id;
5198         }
5199         else {
5200             /**
5201              * Returns a filter identifier from the MongoDB field
5202              * @event changer:getMongoDBFieldID
5203              * @memberof module:plugins.MongoDbSupport
5204              * @param {string} field
5205              * @param {*} value
5206              * @returns {string}
5207              */
5208             id = this.change('getMongoDBFieldID', field, value);
5209         }
5210
5211         return id;
5212     },
5213
5214     /**
5215      * Finds which operator is used in a MongoDB sub-object
5216      * @param {*} data
5217      * @returns {string|undefined}
5218      * @private
5219      */
5220     getMongoOperator: function(data) {
5221         if (data !== null && typeof data === 'object') {
5222             if (data.$gte !== undefined && data.$lte !== undefined) {
5223                 return 'between';
5224             }
5225             if (data.$lt !== undefined && data.$gt !== undefined) {
5226                 return 'not_between';
5227             }
5228
5229             var knownKeys = Object.keys(data).filter(function(key) {
5230                 return !!this.settings.mongoRuleOperators[key];
5231             }.bind(this));
5232
5233             if (knownKeys.length === 1) {
5234                 return knownKeys[0];
5235             }
5236         }
5237         else {
5238             return '$eq';
5239         }
5240     },
5241
5242
5243     /**
5244      * Returns the key corresponding to "$or" or "$and"
5245      * @param {object} data
5246      * @returns {string|undefined}
5247      * @private
5248      */
5249     getMongoCondition: function(data) {
5250         var keys = Object.keys(data);
5251
5252         for (var i = 0, l = keys.length; i < l; i++) {
5253             if (keys[i].toLowerCase() === '$or' || keys[i].toLowerCase() === '$and') {
5254                 return keys[i];
5255             }
5256         }
5257     }
5258 });
5259
5260
5261 /**
5262  * @class NotGroup
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']
5268  */
5269 QueryBuilder.define('not-group', function(options) {
5270     var self = this;
5271
5272     // Bind events
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;
5278         });
5279
5280         self.model.on('update', function(e, node, field) {
5281             if (node instanceof Group && field === 'not') {
5282                 self.updateGroupNot(node);
5283             }
5284         });
5285     });
5286
5287     // Init "not" property
5288     this.on('afterAddGroup', function(e, group) {
5289         group.__.not = false;
5290     });
5291
5292     // Modify templates
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') +
5299                 '</button>'
5300             );
5301             h.value = $h.prop('outerHTML');
5302         });
5303     }
5304
5305     // Export "not" to JSON
5306     this.on('groupToJson.filter', function(e, group) {
5307         e.value.not = group.not;
5308     });
5309
5310     // Read "not" from JSON
5311     this.on('jsonToGroup.filter', function(e, json) {
5312         e.value.not = !!json.not;
5313     });
5314
5315     // Export "not" to SQL
5316     this.on('groupToSQL.filter', function(e, group) {
5317         if (group.not) {
5318             e.value = 'NOT ( ' + e.value + ' )';
5319         }
5320     });
5321
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];
5326
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,
5331                     e.value,
5332                     null
5333                 );
5334             }
5335
5336             e.value.not = true;
5337         }
5338     });
5339
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) {
5343             e.value = true;
5344         }
5345     });
5346
5347     // Read "not" from parsed SQL
5348     this.on('sqlToGroup.filter', function(e, data) {
5349         e.value.not = !!data.not;
5350     });
5351
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] };
5357         }
5358     });
5359
5360     // Parse "$nor" operator from Mongo
5361     this.on('parseMongoNode.filter', function(e) {
5362         var keys = Object.keys(e.value);
5363
5364         if (keys[0] == '$nor') {
5365             e.value = e.value[keys[0]][0];
5366             e.value.not = true;
5367         }
5368     });
5369
5370     // Read "not" from parsed Mongo
5371     this.on('mongoToGroup.filter', function(e, data) {
5372         e.value.not = !!data.not;
5373     });
5374 }, {
5375     icon_unchecked: 'glyphicon glyphicon-unchecked',
5376     icon_checked: 'glyphicon glyphicon-check',
5377     disable_template: false
5378 });
5379
5380 /**
5381  * From {@link module:plugins.NotGroup}
5382  * @name not
5383  * @member {boolean}
5384  * @memberof Group
5385  * @instance
5386  */
5387 Utils.defineModelProperties(Group, ['not']);
5388
5389 QueryBuilder.selectors.group_not = QueryBuilder.selectors.group_header + ' [data-not=group]';
5390
5391 QueryBuilder.extend(/** @lends module:plugins.NotGroup.prototype */ {
5392     /**
5393      * Performs actions when a group's not changes
5394      * @param {Group} group
5395      * @fires module:plugins.NotGroup.afterUpdateGroupNot
5396      * @private
5397      */
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);
5403
5404         /**
5405          * After the group's not flag has been modified
5406          * @event afterUpdateGroupNot
5407          * @memberof module:plugins.NotGroup
5408          * @param {Group} group
5409          */
5410         this.trigger('afterUpdateGroupNot', group);
5411
5412         this.trigger('rulesChanged');
5413     }
5414 });
5415
5416
5417 /**
5418  * @class Sortable
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
5426  */
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');
5430     }
5431
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;
5435     }
5436
5437     // recompute drop-zones during drag (when a rule is hidden)
5438     interact.dynamicDrop(true);
5439
5440     // set move threshold to 10px
5441     interact.pointerMoveTolerance(10);
5442
5443     var placeholder;
5444     var ghost;
5445     var src;
5446     var moved;
5447
5448     // Init drag and drop
5449     this.on('afterAddRule afterAddGroup', function(e, node) {
5450         if (node == placeholder) {
5451             return;
5452         }
5453
5454         var self = e.builder;
5455
5456         // Inherit flags
5457         if (options.inherit_no_sortable && node.parent && node.parent.flags.no_sortable) {
5458             node.flags.no_sortable = true;
5459         }
5460         if (options.inherit_no_drop && node.parent && node.parent.flags.no_drop) {
5461             node.flags.no_drop = true;
5462         }
5463
5464         // Configure drag
5465         if (!node.flags.no_sortable) {
5466             interact(node.$el[0])
5467                 .draggable({
5468                     allowFrom: QueryBuilder.selectors.drag_handle,
5469                     onstart: function(event) {
5470                         moved = false;
5471
5472                         // get model of dragged element
5473                         src = self.getModel(event.target);
5474
5475                         // create ghost
5476                         ghost = src.$el.clone()
5477                             .appendTo(src.$el.parent())
5478                             .width(src.$el.outerWidth())
5479                             .addClass('dragging');
5480
5481                         // create drop placeholder
5482                         var ph = $('<div class="rule-placeholder">&nbsp;</div>')
5483                             .height(src.$el.outerHeight());
5484
5485                         placeholder = src.parent.addRule(ph, src.getPos());
5486
5487                         // hide dragged element
5488                         src.$el.hide();
5489                     },
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';
5494                     },
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);
5499                             moved = true;
5500                         }
5501
5502                         // remove ghost
5503                         ghost.remove();
5504                         ghost = undefined;
5505
5506                         // remove placeholder
5507                         placeholder.drop();
5508                         placeholder = undefined;
5509
5510                         // show element
5511                         src.$el.css('display', '');
5512
5513                         /**
5514                          * After a node has been moved with {@link module:plugins.Sortable}
5515                          * @event afterMove
5516                          * @memberof module:plugins.Sortable
5517                          * @param {Node} node
5518                          */
5519                         self.trigger('afterMove', src);
5520
5521                         self.trigger('rulesChanged');
5522                     }
5523                 });
5524         }
5525
5526         if (!node.flags.no_drop) {
5527             //  Configure drop on groups and rules
5528             interact(node.$el[0])
5529                 .dropzone({
5530                     accept: QueryBuilder.selectors.rule_and_group_containers,
5531                     ondragenter: function(event) {
5532                         moveSortableToTarget(placeholder, $(event.target), self);
5533                     },
5534                     ondrop: function(event) {
5535                         if (!moved) {
5536                             moveSortableToTarget(src, $(event.target), self);
5537                         }
5538                     }
5539                 });
5540
5541             // Configure drop on group headers
5542             if (node instanceof Group) {
5543                 interact(node.$el.find(QueryBuilder.selectors.group_header)[0])
5544                     .dropzone({
5545                         accept: QueryBuilder.selectors.rule_and_group_containers,
5546                         ondragenter: function(event) {
5547                             moveSortableToTarget(placeholder, $(event.target), self);
5548                         },
5549                         ondrop: function(event) {
5550                             if (!moved) {
5551                                 moveSortableToTarget(src, $(event.target), self);
5552                             }
5553                         }
5554                     });
5555             }
5556         }
5557     });
5558
5559     // Detach interactables
5560     this.on('beforeDeleteRule beforeDeleteGroup', function(e, node) {
5561         if (!e.isDefaultPrevented()) {
5562             interact(node.$el[0]).unset();
5563
5564             if (node instanceof Group) {
5565                 interact(node.$el.find(QueryBuilder.selectors.group_header)[0]).unset();
5566             }
5567         }
5568     });
5569
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();
5574         }
5575     });
5576
5577     // Modify templates
5578     if (!options.disable_template) {
5579         this.on('getGroupTemplate.filter', function(h, level) {
5580             if (level > 1) {
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');
5584             }
5585         });
5586
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');
5591         });
5592     }
5593 }, {
5594     inherit_no_sortable: true,
5595     inherit_no_drop: true,
5596     icon: 'glyphicon glyphicon-sort',
5597     disable_template: false
5598 });
5599
5600 QueryBuilder.selectors.rule_and_group_containers = QueryBuilder.selectors.rule_container + ', ' + QueryBuilder.selectors.group_container;
5601 QueryBuilder.selectors.drag_handle = '.drag-handle';
5602
5603 QueryBuilder.defaults({
5604     default_rule_flags: {
5605         no_sortable: false,
5606         no_drop: false
5607     },
5608     default_group_flags: {
5609         no_sortable: false,
5610         no_drop: false
5611     }
5612 });
5613
5614 /**
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]
5620  * @private
5621  */
5622 function moveSortableToTarget(node, target, builder) {
5623     var parent, method;
5624     var Selectors = QueryBuilder.selectors;
5625
5626     // on rule
5627     parent = target.closest(Selectors.rule_container);
5628     if (parent.length) {
5629         method = 'moveAfter';
5630     }
5631
5632     // on group header
5633     if (!method) {
5634         parent = target.closest(Selectors.group_header);
5635         if (parent.length) {
5636             parent = target.closest(Selectors.group_container);
5637             method = 'moveAtBegin';
5638         }
5639     }
5640
5641     // on group
5642     if (!method) {
5643         parent = target.closest(Selectors.group_container);
5644         if (parent.length) {
5645             method = 'moveAtEnd';
5646         }
5647     }
5648
5649     if (method) {
5650         node[method](builder.getModel(parent));
5651
5652         // refresh radio value
5653         if (builder && node instanceof Rule) {
5654             builder.setRuleInputValue(node, node.value);
5655         }
5656     }
5657 }
5658
5659
5660 /**
5661  * @class SqlSupport
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
5666  */
5667 QueryBuilder.define('sql-support', function(options) {
5668
5669 }, {
5670     boolean_as_integer: true
5671 });
5672
5673 QueryBuilder.defaults({
5674     // operators for internal -> SQL conversion
5675     sqlOperators: {
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' }
5696     },
5697
5698     // operators for SQL -> internal conversion
5699     sqlRuleOperator: {
5700         '=': function(v) {
5701             return {
5702                 val: v,
5703                 op: v === '' ? 'is_empty' : 'equal'
5704             };
5705         },
5706         '!=': function(v) {
5707             return {
5708                 val: v,
5709                 op: v === '' ? 'is_not_empty' : 'not_equal'
5710             };
5711         },
5712         'LIKE': function(v) {
5713             if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
5714                 return {
5715                     val: v.slice(1, -1),
5716                     op: 'contains'
5717                 };
5718             }
5719             else if (v.slice(0, 1) == '%') {
5720                 return {
5721                     val: v.slice(1),
5722                     op: 'ends_with'
5723                 };
5724             }
5725             else if (v.slice(-1) == '%') {
5726                 return {
5727                     val: v.slice(0, -1),
5728                     op: 'begins_with'
5729                 };
5730             }
5731             else {
5732                 Utils.error('SQLParse', 'Invalid value for LIKE operator "{0}"', v);
5733             }
5734         },
5735         'NOT LIKE': function(v) {
5736             if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
5737                 return {
5738                     val: v.slice(1, -1),
5739                     op: 'not_contains'
5740                 };
5741             }
5742             else if (v.slice(0, 1) == '%') {
5743                 return {
5744                     val: v.slice(1),
5745                     op: 'not_ends_with'
5746                 };
5747             }
5748             else if (v.slice(-1) == '%') {
5749                 return {
5750                     val: v.slice(0, -1),
5751                     op: 'not_begins_with'
5752                 };
5753             }
5754             else {
5755                 Utils.error('SQLParse', 'Invalid value for NOT LIKE operator "{0}"', v);
5756             }
5757         },
5758         'IN': function(v) {
5759             return { val: v, op: 'in' };
5760         },
5761         'NOT IN': function(v) {
5762             return { val: v, op: 'not_in' };
5763         },
5764         '<': function(v) {
5765             return { val: v, op: 'less' };
5766         },
5767         '<=': function(v) {
5768             return { val: v, op: 'less_or_equal' };
5769         },
5770         '>': function(v) {
5771             return { val: v, op: 'greater' };
5772         },
5773         '>=': function(v) {
5774             return { val: v, op: 'greater_or_equal' };
5775         },
5776         'BETWEEN': function(v) {
5777             return { val: v, op: 'between' };
5778         },
5779         'NOT BETWEEN': function(v) {
5780             return { val: v, op: 'not_between' };
5781         },
5782         'IS': function(v) {
5783             if (v !== null) {
5784                 Utils.error('SQLParse', 'Invalid value for IS operator');
5785             }
5786             return { val: null, op: 'is_null' };
5787         },
5788         'IS NOT': function(v) {
5789             if (v !== null) {
5790                 Utils.error('SQLParse', 'Invalid value for IS operator');
5791             }
5792             return { val: null, op: 'is_not_null' };
5793         }
5794     },
5795
5796     // statements for internal -> SQL conversion
5797     sqlStatements: {
5798         'question_mark': function() {
5799             var params = [];
5800             return {
5801                 add: function(rule, value) {
5802                     params.push(value);
5803                     return '?';
5804                 },
5805                 run: function() {
5806                     return params;
5807                 }
5808             };
5809         },
5810
5811         'numbered': function(char) {
5812             if (!char || char.length > 1) char = '$';
5813             var index = 0;
5814             var params = [];
5815             return {
5816                 add: function(rule, value) {
5817                     params.push(value);
5818                     index++;
5819                     return char + index;
5820                 },
5821                 run: function() {
5822                     return params;
5823                 }
5824             };
5825         },
5826
5827         'named': function(char) {
5828             if (!char || char.length > 1) char = ':';
5829             var indexes = {};
5830             var params = {};
5831             return {
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;
5836                     return char + key;
5837                 },
5838                 run: function() {
5839                     return params;
5840                 }
5841             };
5842         }
5843     },
5844
5845     // statements for SQL -> internal conversion
5846     sqlRuleStatement: {
5847         'question_mark': function(values) {
5848             var index = 0;
5849             return {
5850                 parse: function(v) {
5851                     return v == '?' ? values[index++] : v;
5852                 },
5853                 esc: function(sql) {
5854                     return sql.replace(/\?/g, '\'?\'');
5855                 }
5856             };
5857         },
5858
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');
5863             return {
5864                 parse: function(v) {
5865                     return regex1.test(v) ? values[v.slice(1) - 1] : v;
5866                 },
5867                 esc: function(sql) {
5868                     return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\'');
5869                 }
5870             };
5871         },
5872
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');
5877             return {
5878                 parse: function(v) {
5879                     return regex1.test(v) ? values[v.slice(1)] : v;
5880                 },
5881                 esc: function(sql) {
5882                     return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\'');
5883                 }
5884             };
5885         }
5886     }
5887 });
5888
5889 /**
5890  * @typedef {object} SqlQuery
5891  * @memberof module:plugins.SqlSupport
5892  * @property {string} sql
5893  * @property {object} params
5894  */
5895
5896 QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ {
5897     /**
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
5907      */
5908     getSQL: function(stmt, nl, data) {
5909         data = (data === undefined) ? this.getRules() : data;
5910
5911         if (!data) {
5912             return null;
5913         }
5914
5915         nl = !!nl ? '\n' : ' ';
5916         var boolean_as_integer = this.getPluginOptions('sql-support', 'boolean_as_integer');
5917
5918         if (stmt === true) {
5919             stmt = 'question_mark';
5920         }
5921         if (typeof stmt == 'string') {
5922             var config = getStmtConfig(stmt);
5923             stmt = this.settings.sqlStatements[config[1]](config[2]);
5924         }
5925
5926         var self = this;
5927
5928         var sql = (function parse(group) {
5929             if (!group.condition) {
5930                 group.condition = self.settings.default_condition;
5931             }
5932             if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) {
5933                 Utils.error('UndefinedSQLCondition', 'Unable to build SQL query with condition "{0}"', group.condition);
5934             }
5935
5936             if (!group.rules) {
5937                 return '';
5938             }
5939
5940             var parts = [];
5941
5942             group.rules.forEach(function(rule) {
5943                 if (rule.rules && rule.rules.length > 0) {
5944                     parts.push('(' + nl + parse(rule) + nl + ')' + nl);
5945                 }
5946                 else {
5947                     var sql = self.settings.sqlOperators[rule.operator];
5948                     var ope = self.getOperatorByType(rule.operator);
5949                     var value = '';
5950
5951                     if (sql === undefined) {
5952                         Utils.error('UndefinedSQLOperator', 'Unknown SQL operation for operator "{0}"', rule.operator);
5953                     }
5954
5955                     if (ope.nb_inputs !== 0) {
5956                         if (!(rule.value instanceof Array)) {
5957                             rule.value = [rule.value];
5958                         }
5959
5960                         rule.value.forEach(function(v, i) {
5961                             if (i > 0) {
5962                                 if (rule.type === 'map') {
5963                                         value += "|";
5964                                 } else {
5965                                         value += sql.sep;
5966                                 }
5967                             }
5968
5969                             if (rule.type == 'boolean' && boolean_as_integer) {
5970                                 v = v ? 1 : 0;
5971                             }
5972                             else if (!stmt && rule.type !== 'integer' && rule.type !== 'double' && rule.type !== 'boolean') {
5973                                 v = Utils.escapeString(v);
5974                             }
5975                             
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');
5979                                 }
5980                                 v = moment(v, 'YYYY/MM/DD HH:mm:ss').utc().unix();
5981                             }
5982                             
5983                             if (sql.mod) {
5984                                 if ((rule.type !== 'map') || (rule.type === 'map' && i > 0)) {
5985                                         v = Utils.fmt(sql.mod, v);
5986                                 }
5987                             }
5988
5989                             if (stmt) {
5990                                 value += stmt.add(rule, v);
5991                             }
5992                             else {
5993                                 if ( rule.type === 'map') {
5994                                         if(i > 0) {
5995                                                 value += v;
5996                                                 value = '\'' + value + '\'';
5997                                         } else{
5998                                                 value += v;
5999                                         }
6000                                 } else {
6001                                         if (typeof v == 'string') {
6002                                                 v = '\'' + v + '\'';
6003                                         }
6004                                         value += v;
6005                                 }
6006                             }
6007                         });
6008                     }
6009
6010                     var sqlFn = function(v) {
6011                         return sql.op.replace('?', function() {
6012                             return v;
6013                         });
6014                     };
6015
6016                     /**
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
6022                      * @returns {string}
6023                      */
6024                     var field = self.change('getSQLField', rule.field, rule);
6025
6026                     var ruleExpression = field + ' ' + sqlFn(value);
6027
6028                     /**
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
6034                      * @param {*} value
6035                      * @param {function} valueWrapper - function that takes the value and adds the operator
6036                      * @returns {string}
6037                      */
6038                     parts.push(self.change('ruleToSQL', ruleExpression, rule, value, sqlFn));
6039                 }
6040             });
6041
6042             var groupExpression = parts.join(' ' + group.condition + nl);
6043
6044             /**
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
6050              * @returns {string}
6051              */
6052             return self.change('groupToSQL', groupExpression, group);
6053         }(data));
6054
6055         if (stmt) {
6056             return {
6057                 sql: sql,
6058                 params: stmt.run()
6059             };
6060         }
6061         else {
6062             return {
6063                 sql: sql
6064             };
6065         }
6066     },
6067
6068     /**
6069      * Convert a SQL query to rules
6070      * @param {string|module:plugins.SqlSupport.SqlQuery} query
6071      * @param {boolean|string} stmt
6072      * @returns {object}
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
6078      */
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');
6082         }
6083
6084         var self = this;
6085
6086         if (typeof query == 'string') {
6087             query = { sql: query };
6088         }
6089
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]);
6094         }
6095
6096         if (stmt) {
6097             query.sql = stmt.esc(query.sql);
6098         }
6099
6100         if (query.sql.toUpperCase().indexOf('SELECT') !== 0) {
6101             query.sql = 'SELECT * FROM table WHERE ' + query.sql;
6102         }
6103
6104         var parsed = SQLParser.parse(query.sql);
6105
6106         if (!parsed.where) {
6107             Utils.error('SQLParse', 'No WHERE clause found');
6108         }
6109
6110         /**
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
6116          */
6117         var data = self.change('parseSQLNode', parsed.where.conditions);
6118
6119         // a plugin returned a group
6120         if ('rules' in data && 'condition' in data) {
6121             return data;
6122         }
6123
6124         // a plugin returned a rule
6125         if ('id' in data && 'operator' in data && 'value' in data) {
6126             return {
6127                 condition: this.settings.default_condition,
6128                 rules: [data]
6129             };
6130         }
6131
6132         // create root group
6133         var out = self.change('sqlToGroup', {
6134             condition: this.settings.default_condition,
6135             rules: []
6136         }, data);
6137
6138         // keep track of current group
6139         var curr = out;
6140
6141         (function flatten(data, i) {
6142             if (data === null) {
6143                 return;
6144             }
6145
6146             // allow plugins to manually parse or handle special cases
6147             data = self.change('parseSQLNode', data);
6148
6149             // a plugin returned a group
6150             if ('rules' in data && 'condition' in data) {
6151                 curr.rules.push(data);
6152                 return;
6153             }
6154
6155             // a plugin returned a rule
6156             if ('id' in data && 'operator' in data && 'value' in data) {
6157                 curr.rules.push(data);
6158                 return;
6159             }
6160
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');
6164             }
6165
6166             // it's a node
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
6169
6170                 /**
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}
6179                  */
6180                 var createGroup = self.change('sqlGroupsDistinct', i > 0 && curr.condition != data.operation.toUpperCase(), curr, data, i);
6181
6182                 if (createGroup) {
6183                     /**
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
6189                      * @returns {object}
6190                      */
6191                     var group = self.change('sqlToGroup', {
6192                         condition: self.settings.default_condition,
6193                         rules: []
6194                     }, data);
6195
6196                     curr.rules.push(group);
6197                     curr = group;
6198                 }
6199
6200                 curr.condition = data.operation.toUpperCase();
6201                 i++;
6202
6203                 // some magic !
6204                 var next = curr;
6205                 flatten(data.left, i);
6206
6207                 curr = next;
6208                 flatten(data.right, i);
6209             }
6210             // it's a leaf
6211             else {
6212                 if ($.isPlainObject(data.right.value)) {
6213                     Utils.error('SQLParse', 'Value format not supported for {0}.', data.left.value);
6214                 }
6215
6216                 // convert array
6217                 var value;
6218                 if ($.isArray(data.right.value)) {
6219                     value = data.right.value.map(function(v) {
6220                         return v.value;
6221                     });
6222                 }
6223                 else {
6224                     value = data.right.value;
6225                 }
6226
6227                 // get actual values
6228                 if (stmt) {
6229                     if ($.isArray(value)) {
6230                         value = value.map(stmt.parse);
6231                     }
6232                     else {
6233                         value = stmt.parse(value);
6234                     }
6235                 }
6236
6237                 // convert operator
6238                 var operator = data.operation.toUpperCase();
6239                 if (operator == '<>') {
6240                     operator = '!=';
6241                 }
6242
6243                 var sqlrl = self.settings.sqlRuleOperator[operator];
6244                 if (sqlrl === undefined) {
6245                     Utils.error('UndefinedSQLOperator', 'Invalid SQL operation "{0}".', data.operation);
6246                 }
6247                 
6248                 // find field name
6249                 var field;
6250                 if ('values' in data.left) {
6251                     field = data.left.values.join('.');
6252                 }
6253                 else if ('value' in data.left) {
6254                     field = data.left.value;
6255                 }
6256                 else {
6257                     Utils.error('SQLParse', 'Cannot find field name in {0}', JSON.stringify(data.left));
6258                 }
6259
6260                 var matchingFilter = self.filters.filter(function(filter) {
6261                     return filter.field.toLowerCase() === field.toLowerCase();
6262                 });
6263                 
6264                 var opVal;
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;
6269                 } else {
6270                     opVal = sqlrl.call(this, value, data.operation);
6271                 }
6272                 
6273                 var id = self.getSQLFieldID(field, value);
6274
6275                 /**
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
6281                  * @returns {object}
6282                  */
6283                 var rule = self.change('sqlToRule', {
6284                     id: id,
6285                     field: field,
6286                     operator: opVal.op,
6287                     value: opVal.val
6288                 }, data);
6289
6290                 curr.rules.push(rule);
6291             }
6292         }(data, 0));
6293
6294         return out;
6295     },
6296
6297     /**
6298      * Sets the builder's rules from a SQL query
6299      * @see module:plugins.SqlSupport.getRulesFromSQL
6300      */
6301     setRulesFromSQL: function(query, stmt) {
6302         this.setRules(this.getRulesFromSQL(query, stmt));
6303     },
6304
6305     /**
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
6309      * @param {*} value
6310      * @fires module:plugins.SqlSupport:changer:getSQLFieldID
6311      * @returns {string}
6312      * @private
6313      */
6314     getSQLFieldID: function(field, value) {
6315         var matchingFilters = this.filters.filter(function(filter) {
6316             return filter.field.toLowerCase() === field.toLowerCase();
6317         });
6318
6319         var id;
6320         if (matchingFilters.length === 1) {
6321             id = matchingFilters[0].id;
6322         }
6323         else {
6324             /**
6325              * Returns a filter identifier from the SQL field
6326              * @event changer:getSQLFieldID
6327              * @memberof module:plugins.SqlSupport
6328              * @param {string} field
6329              * @param {*} value
6330              * @returns {string}
6331              */
6332             id = this.change('getSQLFieldID', field, value);
6333         }
6334
6335         return id;
6336     }
6337 });
6338
6339 /**
6340  * Parses the statement configuration
6341  * @memberof module:plugins.SqlSupport
6342  * @param {string} stmt
6343  * @returns {Array} null, mode, option
6344  * @private
6345  */
6346 function getStmtConfig(stmt) {
6347     var config = stmt.match(/(question_mark|numbered|named)(?:\((.)\))?/);
6348     if (!config) config = [null, 'question_mark', undefined];
6349     return config;
6350 }
6351
6352
6353 /**
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.
6357  */
6358 QueryBuilder.define('unique-filter', function() {
6359     this.status.used_filters = {};
6360
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);
6366
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;
6370
6371         self.updateDisabledFilters();
6372
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) {
6376                     e.value = filter;
6377                     return true;
6378                 }
6379             });
6380
6381             if (!found) {
6382                 Utils.error(false, 'UniqueFilter', 'No more non-unique filters available');
6383                 e.value = undefined;
6384             }
6385         }
6386     });
6387 });
6388
6389 QueryBuilder.extend(/** @lends module:plugins.UniqueFilter.prototype */ {
6390     /**
6391      * Updates the list of used filters
6392      * @param {$.Event} [e]
6393      * @private
6394      */
6395     updateDisabledFilters: function(e) {
6396         var self = e ? e.builder : this;
6397
6398         self.status.used_filters = {};
6399
6400         if (!self.model) {
6401             return;
6402         }
6403
6404         // get 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] = [];
6410                     }
6411                     if (rule.filter.unique == 'group') {
6412                         self.status.used_filters[rule.filter.id].push(rule.parent);
6413                     }
6414                 }
6415             }, function(group) {
6416                 walk(group);
6417             });
6418         }(self.model.root));
6419
6420         self.applyDisabledFilters(e);
6421     },
6422
6423     /**
6424      * Clear the list of used filters
6425      * @param {$.Event} [e]
6426      * @private
6427      */
6428     clearDisabledFilters: function(e) {
6429         var self = e ? e.builder : this;
6430
6431         self.status.used_filters = {};
6432
6433         self.applyDisabledFilters(e);
6434     },
6435
6436     /**
6437      * Disabled filters depending on the list of used ones
6438      * @param {$.Event} [e]
6439      * @private
6440      */
6441     applyDisabledFilters: function(e) {
6442         var self = e ? e.builder : this;
6443
6444         // re-enable everything
6445         self.$el.find(QueryBuilder.selectors.filter_container + ' option').prop('disabled', false);
6446
6447         // disable some
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);
6451             }
6452             else {
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);
6456                     });
6457                 });
6458             }
6459         });
6460
6461         // update Selectpicker
6462         if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) {
6463             self.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render');
6464         }
6465     }
6466 });
6467
6468
6469 /*!
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)
6474  */
6475
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",
6483   "conditions": {
6484     "AND": "AND",
6485     "OR": "OR"
6486   },
6487   "operators": {
6488     "equal": "equal",
6489     "not_equal": "not equal",
6490     "in": "in",
6491     "not_in": "not in",
6492     "less": "less",
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"
6508   },
6509   "errors": {
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"
6533   },
6534   "invert": "Invert",
6535   "NOT": "NOT"
6536 };
6537
6538 QueryBuilder.defaults({ lang_code: 'en' });
6539 return QueryBuilder;
6540
6541 }));