PageSourceSearch

https://quorumapp.com/node_modules/jQuery-QueryBuilder/dist/js/query-builder.standalone.js

js quorumapp.com collected 2026-09-26 04:38:20 UTC 196,596 bytes, 6,363 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.