<div id="builder"></div>
.form-control {min-width: 60px}
.select2 {min-width: 60px;}
.rule-header {text-align:right;}
.pull-right {float: right;}
/*!
 * jQuery.extendext 1.0.0
 *
 * Copyright 2014-2019 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
 * Licensed under MIT (http://opensource.org/licenses/MIT)
 *
 * Based on jQuery.extend by jQuery Foundation, Inc. and other contributors
 */

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    define('jquery-extendext', ['jquery'], factory);
  } else if (typeof module === 'object' && module.exports) {
    module.exports = factory(require('jquery'));
  } else {
    factory(root.jQuery);
  }
})(this, function ($) {
  'use strict';

  $.extendext = function () {
    var options,
      name,
      src,
      copy,
      copyIsArray,
      clone,
      target = arguments[0] || {},
      i = 1,
      length = arguments.length,
      deep = false,
      arrayMode = 'default';

    // Handle a deep copy situation
    if (typeof target === 'boolean') {
      deep = target;

      // Skip the boolean and the target
      target = arguments[i++] || {};
    }

    // Handle array mode parameter
    if (typeof target === 'string') {
      arrayMode = target.toLowerCase();
      if (
        arrayMode !== 'concat' &&
        arrayMode !== 'replace' &&
        arrayMode !== 'extend'
      ) {
        arrayMode = 'default';
      }

      // Skip the string param
      target = arguments[i++] || {};
    }

    // Handle case when target is a string or something (possible in deep copy)
    if (typeof target !== 'object' && !$.isFunction(target)) {
      target = {};
    }

    // Extend jQuery itself if only one argument is passed
    if (i === length) {
      target = this;
      i--;
    }

    for (; i < length; i++) {
      // Only deal with non-null/undefined values
      if ((options = arguments[i]) !== null) {
        // Special operations for arrays
        if ($.isArray(options) && arrayMode !== 'default') {
          clone = target && $.isArray(target) ? target : [];

          switch (arrayMode) {
            case 'concat':
              target = clone.concat($.extend(deep, [], options));
              break;

            case 'replace':
              target = $.extend(deep, [], options);
              break;

            case 'extend':
              options.forEach(function (e, i) {
                if (typeof e === 'object') {
                  var type = $.isArray(e) ? [] : {};
                  clone[i] = $.extendext(deep, arrayMode, clone[i] || type, e);
                } else if (clone.indexOf(e) === -1) {
                  clone.push(e);
                }
              });

              target = clone;
              break;
          }
        } else {
          // Extend the base object
          for (name in options) {
            copy = options[name];

            // Prevent never-ending loop
            if (name === '__proto__' || target === copy) {
              continue;
            }

            // Recurse if we're merging plain objects or arrays
            if (
              deep &&
              copy &&
              ($.isPlainObject(copy) || (copyIsArray = $.isArray(copy)))
            ) {
              src = target[name];

              // Ensure proper type for the source value
              if (copyIsArray && !Array.isArray(src)) {
                clone = [];
              } else if (!copyIsArray && !$.isPlainObject(src)) {
                clone = {};
              } else {
                clone = src;
              }
              copyIsArray = false;

              // Never move original objects, clone them
              target[name] = $.extendext(deep, arrayMode, clone, copy);

              // Don't bring in undefined values
            } else if (copy !== undefined) {
              target[name] = copy;
            }
          }
        }
      }
    }

    // Return the modified object
    return target;
  };
});

// doT.js
// 2011-2014, Laura Doktorova, https://github.com/olado/doT
// Licensed under the MIT license.

(function () {
  'use strict';

  var doT = {
      name: 'doT',
      version: '1.1.1',
      templateSettings: {
        evaluate: /\{\{([\s\S]+?(\}?)+)\}\}/g,
        interpolate: /\{\{=([\s\S]+?)\}\}/g,
        encode: /\{\{!([\s\S]+?)\}\}/g,
        use: /\{\{#([\s\S]+?)\}\}/g,
        useParams:
          /(^|[^\w$])def(?:\.|\[[\'\"])([\w$\.]+)(?:[\'\"]\])?\s*\:\s*([\w$\.]+|\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})/g,
        define: /\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g,
        defineParams: /^\s*([\w$]+):([\s\S]+)/,
        conditional: /\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g,
        iterate:
          /\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/g,
        varname: 'it',
        strip: true,
        append: true,
        selfcontained: false,
        doNotSkipEncoded: false,
      },
      template: undefined, //fn, compile template
      compile: undefined, //fn, for express
      log: true,
    },
    _globals;

  doT.encodeHTMLSource = function (doNotSkipEncoded) {
    var encodeHTMLRules = {
        '&': '&#38;',
        '<': '&#60;',
        '>': '&#62;',
        '"': '&#34;',
        "'": '&#39;',
        '/': '&#47;',
      },
      matchHTML = doNotSkipEncoded ? /[&<>"'\/]/g : /&(?!#?\w+;)|<|>|"|'|\//g;
    return function (code) {
      return code
        ? code.toString().replace(matchHTML, function (m) {
            return encodeHTMLRules[m] || m;
          })
        : '';
    };
  };

  _globals = (function () {
    return this || (0, eval)('this');
  })();

  /* istanbul ignore else */
  if (typeof module !== 'undefined' && module.exports) {
    module.exports = doT;
  } else if (typeof define === 'function' && define.amd) {
    define('doT', function () {
      return doT;
    });
  } else {
    _globals.doT = doT;
  }

  var startend = {
      append: { start: "'+(", end: ")+'", startencode: "'+encodeHTML(" },
      split: {
        start: "';out+=(",
        end: ");out+='",
        startencode: "';out+=encodeHTML(",
      },
    },
    skip = /$^/;

  function resolveDefs(c, block, def) {
    return (typeof block === 'string' ? block : block.toString())
      .replace(c.define || skip, function (m, code, assign, value) {
        if (code.indexOf('def.') === 0) {
          code = code.substring(4);
        }
        if (!(code in def)) {
          if (assign === ':') {
            if (c.defineParams)
              value.replace(c.defineParams, function (m, param, v) {
                def[code] = { arg: param, text: v };
              });
            if (!(code in def)) def[code] = value;
          } else {
            new Function('def', "def['" + code + "']=" + value)(def);
          }
        }
        return '';
      })
      .replace(c.use || skip, function (m, code) {
        if (c.useParams)
          code = code.replace(c.useParams, function (m, s, d, param) {
            if (def[d] && def[d].arg && param) {
              var rw = (d + ':' + param).replace(/'|\\/g, '_');
              def.__exp = def.__exp || {};
              def.__exp[rw] = def[d].text.replace(
                new RegExp('(^|[^\\w$])' + def[d].arg + '([^\\w$])', 'g'),
                '$1' + param + '$2'
              );
              return s + "def.__exp['" + rw + "']";
            }
          });
        var v = new Function('def', 'return ' + code)(def);
        return v ? resolveDefs(c, v, def) : v;
      });
  }

  function unescape(code) {
    return code.replace(/\\('|\\)/g, '$1').replace(/[\r\t\n]/g, ' ');
  }

  doT.template = function (tmpl, c, def) {
    c = c || doT.templateSettings;
    var cse = c.append ? startend.append : startend.split,
      needhtmlencode,
      sid = 0,
      indv,
      str = c.use || c.define ? resolveDefs(c, tmpl, def || {}) : tmpl;

    str = (
      "var out='" +
      (c.strip
        ? str
            .replace(/(^|\r|\n)\t* +| +\t*(\r|\n|$)/g, ' ')
            .replace(/\r|\n|\t|\/\*[\s\S]*?\*\//g, '')
        : str
      )
        .replace(/'|\\/g, '\\$&')
        .replace(c.interpolate || skip, function (m, code) {
          return cse.start + unescape(code) + cse.end;
        })
        .replace(c.encode || skip, function (m, code) {
          needhtmlencode = true;
          return cse.startencode + unescape(code) + cse.end;
        })
        .replace(c.conditional || skip, function (m, elsecase, code) {
          return elsecase
            ? code
              ? "';}else if(" + unescape(code) + "){out+='"
              : "';}else{out+='"
            : code
            ? "';if(" + unescape(code) + "){out+='"
            : "';}out+='";
        })
        .replace(c.iterate || skip, function (m, iterate, vname, iname) {
          if (!iterate) return "';} } out+='";
          sid += 1;
          indv = iname || 'i' + sid;
          iterate = unescape(iterate);
          return (
            "';var arr" +
            sid +
            '=' +
            iterate +
            ';if(arr' +
            sid +
            '){var ' +
            vname +
            ',' +
            indv +
            '=-1,l' +
            sid +
            '=arr' +
            sid +
            '.length-1;while(' +
            indv +
            '<l' +
            sid +
            '){' +
            vname +
            '=arr' +
            sid +
            '[' +
            indv +
            "+=1];out+='"
          );
        })
        .replace(c.evaluate || skip, function (m, code) {
          return "';" + unescape(code) + "out+='";
        }) +
      "';return out;"
    )
      .replace(/\n/g, '\\n')
      .replace(/\t/g, '\\t')
      .replace(/\r/g, '\\r')
      .replace(/(\s|;|\}|^|\{)out\+='';/g, '$1')
      .replace(/\+''/g, '');
    //.replace(/(\s|;|\}|^|\{)out\+=''\+/g,'$1out+=');

    if (needhtmlencode) {
      if (!c.selfcontained && _globals && !_globals._encodeHTML)
        _globals._encodeHTML = doT.encodeHTMLSource(c.doNotSkipEncoded);
      str =
        "var encodeHTML = typeof _encodeHTML !== 'undefined' ? _encodeHTML : (" +
        doT.encodeHTMLSource.toString() +
        '(' +
        (c.doNotSkipEncoded || '') +
        '));' +
        str;
    }
    try {
      return new Function(c.varname, str);
    } catch (e) {
      /* istanbul ignore else */
      if (typeof console !== 'undefined')
        console.log('Could not create a template function: ' + str);
      throw e;
    }
  };

  doT.compile = function (tmpl, def) {
    return doT.template(tmpl, null, def);
  };
})();

/*!
 * jQuery QueryBuilder 2.6.2
 * Copyright 2014-2021 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
 * Licensed under MIT (https://opensource.org/licenses/MIT)
 */
(function (root, factory) {
  if (typeof define == 'function' && define.amd) {
    define('query-builder', ['jquery', 'dot/doT', 'jquery-extendext'], factory);
  } else if (typeof module === 'object' && module.exports) {
    module.exports = factory(
      require('jquery'),
      require('dot/doT'),
      require('jquery-extendext')
    );
  } else {
    factory(root.jQuery, root.doT);
  }
})(this, function ($, doT) {
  'use strict';

  /**
   * @typedef {object} Filter
   * @memberof QueryBuilder
   * @description See {@link http://querybuilder.js.org/index.html#filters}
   */

  /**
   * @typedef {object} Operator
   * @memberof QueryBuilder
   * @description See {@link http://querybuilder.js.org/index.html#operators}
   */

  /**
   * @param {jQuery} $el
   * @param {object} options - see {@link http://querybuilder.js.org/#options}
   * @constructor
   */
  var QueryBuilder = function ($el, options) {
    $el[0].queryBuilder = this;

    /**
     * Element container
     * @member {jQuery}
     * @readonly
     */
    this.$el = $el;

    /**
     * Configuration object
     * @member {object}
     * @readonly
     */
    this.settings = $.extendext(
      true,
      'replace',
      {},
      QueryBuilder.DEFAULTS,
      options
    );

    /**
     * Internal model
     * @member {Model}
     * @readonly
     */
    this.model = new Model();

    /**
     * Internal status
     * @member {object}
     * @property {string} id - id of the container
     * @property {boolean} generated_id - if the container id has been generated
     * @property {int} group_id - current group id
     * @property {int} rule_id - current rule id
     * @property {boolean} has_optgroup - if filters have optgroups
     * @property {boolean} has_operator_optgroup - if operators have optgroups
     * @readonly
     * @private
     */
    this.status = {
      id: null,
      generated_id: false,
      group_id: 0,
      rule_id: 0,
      has_optgroup: false,
      has_operator_optgroup: false,
    };

    /**
     * List of filters
     * @member {QueryBuilder.Filter[]}
     * @readonly
     */
    this.filters = this.settings.filters;

    /**
     * List of icons
     * @member {object.<string, string>}
     * @readonly
     */
    this.icons = this.settings.icons;

    /**
     * List of operators
     * @member {QueryBuilder.Operator[]}
     * @readonly
     */
    this.operators = this.settings.operators;

    /**
     * List of templates
     * @member {object.<string, function>}
     * @readonly
     */
    this.templates = this.settings.templates;

    /**
     * Plugins configuration
     * @member {object.<string, object>}
     * @readonly
     */
    this.plugins = this.settings.plugins;

    /**
     * Translations object
     * @member {object}
     * @readonly
     */
    this.lang = null;

    // translations : english << 'lang_code' << custom
    if (QueryBuilder.regional['en'] === undefined) {
      Utils.error('Config', '"i18n/en.js" not loaded.');
    }
    this.lang = $.extendext(
      true,
      'replace',
      {},
      QueryBuilder.regional['en'],
      QueryBuilder.regional[this.settings.lang_code],
      this.settings.lang
    );

    // "allow_groups" can be boolean or int
    if (this.settings.allow_groups === false) {
      this.settings.allow_groups = 0;
    } else if (this.settings.allow_groups === true) {
      this.settings.allow_groups = -1;
    }

    // init templates
    Object.keys(this.templates).forEach(function (tpl) {
      if (!this.templates[tpl]) {
        this.templates[tpl] = QueryBuilder.templates[tpl];
      }
      if (typeof this.templates[tpl] == 'string') {
        this.templates[tpl] = doT.template(this.templates[tpl]);
      }
    }, this);

    // ensure we have a container id
    if (!this.$el.attr('id')) {
      this.$el.attr('id', 'qb_' + Math.floor(Math.random() * 99999));
      this.status.generated_id = true;
    }
    this.status.id = this.$el.attr('id');

    // INIT
    this.$el.addClass('query-builder form-inline');

    this.filters = this.checkFilters(this.filters);
    this.operators = this.checkOperators(this.operators);
    this.bindEvents();
    this.initPlugins();
  };

  $.extend(
    QueryBuilder.prototype,
    /** @lends QueryBuilder.prototype */ {
      /**
       * Triggers an event on the builder container
       * @param {string} type
       * @returns {$.Event}
       */
      trigger: function (type) {
        var event = new $.Event(this._tojQueryEvent(type), {
          builder: this,
        });

        this.$el.triggerHandler(
          event,
          Array.prototype.slice.call(arguments, 1)
        );

        return event;
      },

      /**
       * Triggers an event on the builder container and returns the modified value
       * @param {string} type
       * @param {*} value
       * @returns {*}
       */
      change: function (type, value) {
        var event = new $.Event(this._tojQueryEvent(type, true), {
          builder: this,
          value: value,
        });

        this.$el.triggerHandler(
          event,
          Array.prototype.slice.call(arguments, 2)
        );

        return event.value;
      },

      /**
       * Attaches an event listener on the builder container
       * @param {string} type
       * @param {function} cb
       * @returns {QueryBuilder}
       */
      on: function (type, cb) {
        this.$el.on(this._tojQueryEvent(type), cb);
        return this;
      },

      /**
       * Removes an event listener from the builder container
       * @param {string} type
       * @param {function} [cb]
       * @returns {QueryBuilder}
       */
      off: function (type, cb) {
        this.$el.off(this._tojQueryEvent(type), cb);
        return this;
      },

      /**
       * Attaches an event listener called once on the builder container
       * @param {string} type
       * @param {function} cb
       * @returns {QueryBuilder}
       */
      once: function (type, cb) {
        this.$el.one(this._tojQueryEvent(type), cb);
        return this;
      },

      /**
       * Appends `.queryBuilder` and optionally `.filter` to the events names
       * @param {string} name
       * @param {boolean} [filter=false]
       * @returns {string}
       * @private
       */
      _tojQueryEvent: function (name, filter) {
        return name
          .split(' ')
          .map(function (type) {
            return type + '.queryBuilder' + (filter ? '.filter' : '');
          })
          .join(' ');
      },
    }
  );

  /**
   * Allowed types and their internal representation
   * @type {object.<string, string>}
   * @readonly
   * @private
   */
  QueryBuilder.types = {
    string: 'string',
    integer: 'number',
    double: 'number',
    date: 'datetime',
    time: 'datetime',
    datetime: 'datetime',
    boolean: 'boolean',
  };

  /**
   * Allowed inputs
   * @type {string[]}
   * @readonly
   * @private
   */
  QueryBuilder.inputs = [
    'text',
    'number',
    'textarea',
    'radio',
    'checkbox',
    'select',
  ];

  /**
   * Runtime modifiable options with `setOptions` method
   * @type {string[]}
   * @readonly
   * @private
   */
  QueryBuilder.modifiable_options = [
    'display_errors',
    'allow_groups',
    'allow_empty',
    'default_condition',
    'default_filter',
  ];

  /**
   * CSS selectors for common components
   * @type {object.<string, string>}
   * @readonly
   */
  QueryBuilder.selectors = {
    group_container: '.rules-group-container',
    rule_container: '.rule-container',
    filter_container: '.rule-filter-container',
    operator_container: '.rule-operator-container',
    value_container: '.rule-value-container',
    error_container: '.error-container',
    condition_container: '.rules-group-header .group-conditions',

    rule_header: '.rule-header',
    group_header: '.rules-group-header',
    group_actions: '.group-actions',
    rule_actions: '.rule-actions',

    rules_list: '.rules-group-body>.rules-list',

    group_condition: '.rules-group-header [name$=_cond]',
    rule_filter: '.rule-filter-container [name$=_filter]',
    rule_operator: '.rule-operator-container [name$=_operator]',
    rule_value: '.rule-value-container [name*=_value_]',

    add_rule: '[data-add=rule]',
    delete_rule: '[data-delete=rule]',
    add_group: '[data-add=group]',
    delete_group: '[data-delete=group]',
  };

  /**
   * Template strings (see template.js)
   * @type {object.<string, string>}
   * @readonly
   */
  QueryBuilder.templates = {};

  /**
   * Localized strings (see i18n/)
   * @type {object.<string, object>}
   * @readonly
   */
  QueryBuilder.regional = {};

  /**
   * Default operators
   * @type {object.<string, object>}
   * @readonly
   */
  QueryBuilder.OPERATORS = {
    equal: {
      type: 'equal',
      nb_inputs: 1,
      multiple: false,
      apply_to: ['string', 'number', 'datetime', 'boolean'],
    },
    not_equal: {
      type: 'not_equal',
      nb_inputs: 1,
      multiple: false,
      apply_to: ['string', 'number', 'datetime', 'boolean'],
    },
    in: {
      type: 'in',
      nb_inputs: 1,
      multiple: true,
      apply_to: ['string', 'number', 'datetime'],
    },
    not_in: {
      type: 'not_in',
      nb_inputs: 1,
      multiple: true,
      apply_to: ['string', 'number', 'datetime'],
    },
    less: {
      type: 'less',
      nb_inputs: 1,
      multiple: false,
      apply_to: ['number', 'datetime'],
    },
    less_or_equal: {
      type: 'less_or_equal',
      nb_inputs: 1,
      multiple: false,
      apply_to: ['number', 'datetime'],
    },
    greater: {
      type: 'greater',
      nb_inputs: 1,
      multiple: false,
      apply_to: ['number', 'datetime'],
    },
    greater_or_equal: {
      type: 'greater_or_equal',
      nb_inputs: 1,
      multiple: false,
      apply_to: ['number', 'datetime'],
    },
    between: {
      type: 'between',
      nb_inputs: 2,
      multiple: false,
      apply_to: ['number', 'datetime'],
    },
    not_between: {
      type: 'not_between',
      nb_inputs: 2,
      multiple: false,
      apply_to: ['number', 'datetime'],
    },
    begins_with: {
      type: 'begins_with',
      nb_inputs: 1,
      multiple: false,
      apply_to: ['string'],
    },
    not_begins_with: {
      type: 'not_begins_with',
      nb_inputs: 1,
      multiple: false,
      apply_to: ['string'],
    },
    contains: {
      type: 'contains',
      nb_inputs: 1,
      multiple: false,
      apply_to: ['string'],
    },
    not_contains: {
      type: 'not_contains',
      nb_inputs: 1,
      multiple: false,
      apply_to: ['string'],
    },
    ends_with: {
      type: 'ends_with',
      nb_inputs: 1,
      multiple: false,
      apply_to: ['string'],
    },
    not_ends_with: {
      type: 'not_ends_with',
      nb_inputs: 1,
      multiple: false,
      apply_to: ['string'],
    },
    is_empty: {
      type: 'is_empty',
      nb_inputs: 0,
      multiple: false,
      apply_to: ['string'],
    },
    is_not_empty: {
      type: 'is_not_empty',
      nb_inputs: 0,
      multiple: false,
      apply_to: ['string'],
    },
    is_null: {
      type: 'is_null',
      nb_inputs: 0,
      multiple: false,
      apply_to: ['string', 'number', 'datetime', 'boolean'],
    },
    is_not_null: {
      type: 'is_not_null',
      nb_inputs: 0,
      multiple: false,
      apply_to: ['string', 'number', 'datetime', 'boolean'],
    },
  };

  /**
   * Default configuration
   * @type {object}
   * @readonly
   */
  QueryBuilder.DEFAULTS = {
    filters: [],
    plugins: [],

    sort_filters: false,
    display_errors: true,
    allow_groups: -1,
    allow_empty: false,
    conditions: ['AND', 'OR'],
    default_condition: 'AND',
    inputs_separator: ' , ',
    select_placeholder: '------',
    display_empty_filter: false,
    default_filter: null,
    optgroups: {},

    default_rule_flags: {
      filter_readonly: false,
      operator_readonly: false,
      value_readonly: false,
      no_delete: false,
    },

    default_group_flags: {
      condition_readonly: false,
      no_add_rule: false,
      no_add_group: false,
      no_delete: false,
    },

    templates: {
      group: null,
      rule: null,
      filterSelect: null,
      operatorSelect: null,
      ruleValueSelect: null,
    },

    lang_code: 'en',
    lang: {},

    operators: [
      'equal',
      'not_equal',
      'in',
      'not_in',
      'less',
      'less_or_equal',
      'greater',
      'greater_or_equal',
      'between',
      'not_between',
      'begins_with',
      'not_begins_with',
      'contains',
      'not_contains',
      'ends_with',
      'not_ends_with',
      'is_empty',
      'is_not_empty',
      'is_null',
      'is_not_null',
    ],

    icons: {
      add_group: 'glyphicon glyphicon-plus-sign',
      add_rule: 'glyphicon glyphicon-plus',
      remove_group: 'glyphicon glyphicon-remove',
      remove_rule: 'glyphicon glyphicon-remove',
      error: 'glyphicon glyphicon-warning-sign',
    },
  };

  /**
   * @module plugins
   */

  /**
   * Definition of available plugins
   * @type {object.<String, object>}
   */
  QueryBuilder.plugins = {};

  /**
   * Gets or extends the default configuration
   * @param {object} [options] - new configuration
   * @returns {undefined|object} nothing or configuration object (copy)
   */
  QueryBuilder.defaults = function (options) {
    if (typeof options == 'object') {
      $.extendext(true, 'replace', QueryBuilder.DEFAULTS, options);
    } else if (typeof options == 'string') {
      if (typeof QueryBuilder.DEFAULTS[options] == 'object') {
        return $.extend(true, {}, QueryBuilder.DEFAULTS[options]);
      } else {
        return QueryBuilder.DEFAULTS[options];
      }
    } else {
      return $.extend(true, {}, QueryBuilder.DEFAULTS);
    }
  };

  /**
   * Registers a new plugin
   * @param {string} name
   * @param {function} fct - init function
   * @param {object} [def] - default options
   */
  QueryBuilder.define = function (name, fct, def) {
    QueryBuilder.plugins[name] = {
      fct: fct,
      def: def || {},
    };
  };

  /**
   * Adds new methods to QueryBuilder prototype
   * @param {object.<string, function>} methods
   */
  QueryBuilder.extend = function (methods) {
    $.extend(QueryBuilder.prototype, methods);
  };

  /**
   * Initializes plugins for an instance
   * @throws ConfigError
   * @private
   */
  QueryBuilder.prototype.initPlugins = function () {
    if (!this.plugins) {
      return;
    }

    if ($.isArray(this.plugins)) {
      var tmp = {};
      this.plugins.forEach(function (plugin) {
        tmp[plugin] = null;
      });
      this.plugins = tmp;
    }

    Object.keys(this.plugins).forEach(function (plugin) {
      if (plugin in QueryBuilder.plugins) {
        this.plugins[plugin] = $.extend(
          true,
          {},
          QueryBuilder.plugins[plugin].def,
          this.plugins[plugin] || {}
        );

        QueryBuilder.plugins[plugin].fct.call(this, this.plugins[plugin]);
      } else {
        Utils.error('Config', 'Unable to find plugin "{0}"', plugin);
      }
    }, this);
  };

  /**
   * Returns the config of a plugin, if the plugin is not loaded, returns the default config.
   * @param {string} name
   * @param {string} [property]
   * @throws ConfigError
   * @returns {*}
   */
  QueryBuilder.prototype.getPluginOptions = function (name, property) {
    var plugin;
    if (this.plugins && this.plugins[name]) {
      plugin = this.plugins[name];
    } else if (QueryBuilder.plugins[name]) {
      plugin = QueryBuilder.plugins[name].def;
    }

    if (plugin) {
      if (property) {
        return plugin[property];
      } else {
        return plugin;
      }
    } else {
      Utils.error('Config', 'Unable to find plugin "{0}"', name);
    }
  };

  /**
   * Final initialisation of the builder
   * @param {object} [rules]
   * @fires QueryBuilder.afterInit
   * @private
   */
  QueryBuilder.prototype.init = function (rules) {
    /**
     * When the initilization is done, just before creating the root group
     * @event afterInit
     * @memberof QueryBuilder
     */
    this.trigger('afterInit');

    if (rules) {
      this.setRules(rules);
      delete this.settings.rules;
    } else {
      this.setRoot(true);
    }
  };

  /**
   * Checks the configuration of each filter
   * @param {QueryBuilder.Filter[]} filters
   * @returns {QueryBuilder.Filter[]}
   * @throws ConfigError
   */
  QueryBuilder.prototype.checkFilters = function (filters) {
    var definedFilters = [];

    if (!filters || filters.length === 0) {
      Utils.error('Config', 'Missing filters list');
    }

    filters.forEach(function (filter, i) {
      if (!filter.id) {
        Utils.error('Config', 'Missing filter {0} id', i);
      }
      if (definedFilters.indexOf(filter.id) != -1) {
        Utils.error('Config', 'Filter "{0}" already defined', filter.id);
      }
      definedFilters.push(filter.id);

      if (!filter.type) {
        filter.type = 'string';
      } else if (!QueryBuilder.types[filter.type]) {
        Utils.error('Config', 'Invalid type "{0}"', filter.type);
      }

      if (!filter.input) {
        filter.input =
          QueryBuilder.types[filter.type] === 'number' ? 'number' : 'text';
      } else if (
        typeof filter.input != 'function' &&
        QueryBuilder.inputs.indexOf(filter.input) == -1
      ) {
        Utils.error('Config', 'Invalid input "{0}"', filter.input);
      }

      if (filter.operators) {
        filter.operators.forEach(function (operator) {
          if (typeof operator != 'string') {
            Utils.error(
              'Config',
              'Filter operators must be global operators types (string)'
            );
          }
        });
      }

      if (!filter.field) {
        filter.field = filter.id;
      }
      if (!filter.label) {
        filter.label = filter.field;
      }

      if (!filter.optgroup) {
        filter.optgroup = null;
      } else {
        this.status.has_optgroup = true;

        // register optgroup if needed
        if (!this.settings.optgroups[filter.optgroup]) {
          this.settings.optgroups[filter.optgroup] = filter.optgroup;
        }
      }

      switch (filter.input) {
        case 'radio':
        case 'checkbox':
          if (!filter.values || filter.values.length < 1) {
            Utils.error('Config', 'Missing filter "{0}" values', filter.id);
          }
          break;

        case 'select':
          var cleanValues = [];
          filter.has_optgroup = false;

          Utils.iterateOptions(
            filter.values,
            function (value, label, optgroup) {
              cleanValues.push({
                value: value,
                label: label,
                optgroup: optgroup || null,
              });

              if (optgroup) {
                filter.has_optgroup = true;

                // register optgroup if needed
                if (!this.settings.optgroups[optgroup]) {
                  this.settings.optgroups[optgroup] = optgroup;
                }
              }
            }.bind(this)
          );

          if (filter.has_optgroup) {
            filter.values = Utils.groupSort(cleanValues, 'optgroup');
          } else {
            filter.values = cleanValues;
          }

          if (filter.placeholder) {
            if (filter.placeholder_value === undefined) {
              filter.placeholder_value = -1;
            }

            filter.values.forEach(function (entry) {
              if (entry.value == filter.placeholder_value) {
                Utils.error(
                  'Config',
                  'Placeholder of filter "{0}" overlaps with one of its values',
                  filter.id
                );
              }
            });
          }
          break;
      }
    }, this);

    if (this.settings.sort_filters) {
      if (typeof this.settings.sort_filters == 'function') {
        filters.sort(this.settings.sort_filters);
      } else {
        var self = this;
        filters.sort(function (a, b) {
          return self.translate(a.label).localeCompare(self.translate(b.label));
        });
      }
    }

    if (this.status.has_optgroup) {
      filters = Utils.groupSort(filters, 'optgroup');
    }

    return filters;
  };

  /**
   * Checks the configuration of each operator
   * @param {QueryBuilder.Operator[]} operators
   * @returns {QueryBuilder.Operator[]}
   * @throws ConfigError
   */
  QueryBuilder.prototype.checkOperators = function (operators) {
    var definedOperators = [];

    operators.forEach(function (operator, i) {
      if (typeof operator == 'string') {
        if (!QueryBuilder.OPERATORS[operator]) {
          Utils.error('Config', 'Unknown operator "{0}"', operator);
        }

        operators[i] = operator = $.extendext(
          true,
          'replace',
          {},
          QueryBuilder.OPERATORS[operator]
        );
      } else {
        if (!operator.type) {
          Utils.error('Config', 'Missing "type" for operator {0}', i);
        }

        if (QueryBuilder.OPERATORS[operator.type]) {
          operators[i] = operator = $.extendext(
            true,
            'replace',
            {},
            QueryBuilder.OPERATORS[operator.type],
            operator
          );
        }

        if (
          operator.nb_inputs === undefined ||
          operator.apply_to === undefined
        ) {
          Utils.error(
            'Config',
            'Missing "nb_inputs" and/or "apply_to" for operator "{0}"',
            operator.type
          );
        }
      }

      if (definedOperators.indexOf(operator.type) != -1) {
        Utils.error('Config', 'Operator "{0}" already defined', operator.type);
      }
      definedOperators.push(operator.type);

      if (!operator.optgroup) {
        operator.optgroup = null;
      } else {
        this.status.has_operator_optgroup = true;

        // register optgroup if needed
        if (!this.settings.optgroups[operator.optgroup]) {
          this.settings.optgroups[operator.optgroup] = operator.optgroup;
        }
      }
    }, this);

    if (this.status.has_operator_optgroup) {
      operators = Utils.groupSort(operators, 'optgroup');
    }

    return operators;
  };

  /**
   * Adds all events listeners to the builder
   * @private
   */
  QueryBuilder.prototype.bindEvents = function () {
    var self = this;
    var Selectors = QueryBuilder.selectors;

    // group condition change
    this.$el.on('change.queryBuilder', Selectors.group_condition, function () {
      if ($(this).is(':checked')) {
        var $group = $(this).closest(Selectors.group_container);
        self.getModel($group).condition = $(this).val();
      }
    });

    // rule filter change
    this.$el.on('change.queryBuilder', Selectors.rule_filter, function () {
      var $rule = $(this).closest(Selectors.rule_container);
      self.getModel($rule).filter = self.getFilterById($(this).val());
    });

    // rule operator change
    this.$el.on('change.queryBuilder', Selectors.rule_operator, function () {
      var $rule = $(this).closest(Selectors.rule_container);
      self.getModel($rule).operator = self.getOperatorByType($(this).val());
    });

    // add rule button
    this.$el.on('click.queryBuilder', Selectors.add_rule, function () {
      var $group = $(this).closest(Selectors.group_container);
      self.addRule(self.getModel($group));
    });

    // delete rule button
    this.$el.on('click.queryBuilder', Selectors.delete_rule, function () {
      var $rule = $(this).closest(Selectors.rule_container);
      self.deleteRule(self.getModel($rule));
    });

    if (this.settings.allow_groups !== 0) {
      // add group button
      this.$el.on('click.queryBuilder', Selectors.add_group, function () {
        var $group = $(this).closest(Selectors.group_container);
        self.addGroup(self.getModel($group));
      });

      // delete group button
      this.$el.on('click.queryBuilder', Selectors.delete_group, function () {
        var $group = $(this).closest(Selectors.group_container);
        self.deleteGroup(self.getModel($group));
      });
    }

    // model events
    this.model.on({
      drop: function (e, node) {
        node.$el.remove();
        self.refreshGroupsConditions();
      },
      add: function (e, parent, node, index) {
        if (index === 0) {
          node.$el.prependTo(
            parent.$el.find('>' + QueryBuilder.selectors.rules_list)
          );
        } else {
          node.$el.insertAfter(parent.rules[index - 1].$el);
        }
        self.refreshGroupsConditions();
      },
      move: function (e, node, group, index) {
        node.$el.detach();

        if (index === 0) {
          node.$el.prependTo(
            group.$el.find('>' + QueryBuilder.selectors.rules_list)
          );
        } else {
          node.$el.insertAfter(group.rules[index - 1].$el);
        }
        self.refreshGroupsConditions();
      },
      update: function (e, node, field, value, oldValue) {
        if (node instanceof Rule) {
          switch (field) {
            case 'error':
              self.updateError(node);
              break;

            case 'flags':
              self.applyRuleFlags(node);
              break;

            case 'filter':
              self.updateRuleFilter(node, oldValue);
              break;

            case 'operator':
              self.updateRuleOperator(node, oldValue);
              break;

            case 'value':
              self.updateRuleValue(node, oldValue);
              break;
          }
        } else {
          switch (field) {
            case 'error':
              self.updateError(node);
              break;

            case 'flags':
              self.applyGroupFlags(node);
              break;

            case 'condition':
              self.updateGroupCondition(node, oldValue);
              break;
          }
        }
      },
    });
  };

  /**
   * Creates the root group
   * @param {boolean} [addRule=true] - adds a default empty rule
   * @param {object} [data] - group custom data
   * @param {object} [flags] - flags to apply to the group
   * @returns {Group} root group
   * @fires QueryBuilder.afterAddGroup
   */
  QueryBuilder.prototype.setRoot = function (addRule, data, flags) {
    addRule = addRule === undefined || addRule === true;

    var group_id = this.nextGroupId();
    var $group = $($.parseHTML(this.getGroupTemplate(group_id, 1)));

    this.$el.append($group);
    this.model.root = new Group(null, $group);
    this.model.root.model = this.model;

    this.model.root.data = data;
    this.model.root.flags = $.extend(
      {},
      this.settings.default_group_flags,
      flags
    );
    this.model.root.condition = this.settings.default_condition;

    this.trigger('afterAddGroup', this.model.root);

    if (addRule) {
      this.addRule(this.model.root);
    }

    return this.model.root;
  };

  /**
   * Adds a new group
   * @param {Group} parent
   * @param {boolean} [addRule=true] - adds a default empty rule
   * @param {object} [data] - group custom data
   * @param {object} [flags] - flags to apply to the group
   * @returns {Group}
   * @fires QueryBuilder.beforeAddGroup
   * @fires QueryBuilder.afterAddGroup
   */
  QueryBuilder.prototype.addGroup = function (parent, addRule, data, flags) {
    addRule = addRule === undefined || addRule === true;

    var level = parent.level + 1;

    /**
     * Just before adding a group, can be prevented.
     * @event beforeAddGroup
     * @memberof QueryBuilder
     * @param {Group} parent
     * @param {boolean} addRule - if an empty rule will be added in the group
     * @param {int} level - nesting level of the group, 1 is the root group
     */
    var e = this.trigger('beforeAddGroup', parent, addRule, level);
    if (e.isDefaultPrevented()) {
      return null;
    }

    var group_id = this.nextGroupId();
    var $group = $(this.getGroupTemplate(group_id, level));
    var model = parent.addGroup($group);

    model.data = data;
    model.flags = $.extend({}, this.settings.default_group_flags, flags);
    model.condition = this.settings.default_condition;

    /**
     * Just after adding a group
     * @event afterAddGroup
     * @memberof QueryBuilder
     * @param {Group} group
     */
    this.trigger('afterAddGroup', model);

    /**
     * After any change in the rules
     * @event rulesChanged
     * @memberof QueryBuilder
     */
    this.trigger('rulesChanged');

    if (addRule) {
      this.addRule(model);
    }

    return model;
  };

  /**
   * Tries to delete a group. The group is not deleted if at least one rule is flagged `no_delete`.
   * @param {Group} group
   * @returns {boolean} if the group has been deleted
   * @fires QueryBuilder.beforeDeleteGroup
   * @fires QueryBuilder.afterDeleteGroup
   */
  QueryBuilder.prototype.deleteGroup = function (group) {
    if (group.isRoot()) {
      return false;
    }

    /**
     * Just before deleting a group, can be prevented
     * @event beforeDeleteGroup
     * @memberof QueryBuilder
     * @param {Group} parent
     */
    var e = this.trigger('beforeDeleteGroup', group);
    if (e.isDefaultPrevented()) {
      return false;
    }

    var del = true;

    group.each(
      'reverse',
      function (rule) {
        del &= this.deleteRule(rule);
      },
      function (group) {
        del &= this.deleteGroup(group);
      },
      this
    );

    if (del) {
      group.drop();

      /**
       * Just after deleting a group
       * @event afterDeleteGroup
       * @memberof QueryBuilder
       */
      this.trigger('afterDeleteGroup');

      this.trigger('rulesChanged');
    }

    return del;
  };

  /**
   * Performs actions when a group's condition changes
   * @param {Group} group
   * @param {object} previousCondition
   * @fires QueryBuilder.afterUpdateGroupCondition
   * @private
   */
  QueryBuilder.prototype.updateGroupCondition = function (
    group,
    previousCondition
  ) {
    group.$el
      .find('>' + QueryBuilder.selectors.group_condition)
      .each(function () {
        var $this = $(this);
        $this.prop('checked', $this.val() === group.condition);
        $this.parent().toggleClass('active', $this.val() === group.condition);
      });

    /**
     * After the group condition has been modified
     * @event afterUpdateGroupCondition
     * @memberof QueryBuilder
     * @param {Group} group
     * @param {object} previousCondition
     */
    this.trigger('afterUpdateGroupCondition', group, previousCondition);

    this.trigger('rulesChanged');
  };

  /**
   * Updates the visibility of conditions based on number of rules inside each group
   * @private
   */
  QueryBuilder.prototype.refreshGroupsConditions = function () {
    (function walk(group) {
      if (!group.flags || (group.flags && !group.flags.condition_readonly)) {
        group.$el
          .find('>' + QueryBuilder.selectors.group_condition)
          .prop('disabled', group.rules.length <= 1)
          .parent()
          .toggleClass('disabled', group.rules.length <= 1);
      }

      group.each(
        null,
        function (group) {
          walk(group);
        },
        this
      );
    })(this.model.root);
  };

  /**
   * Adds a new rule
   * @param {Group} parent
   * @param {object} [data] - rule custom data
   * @param {object} [flags] - flags to apply to the rule
   * @returns {Rule}
   * @fires QueryBuilder.beforeAddRule
   * @fires QueryBuilder.afterAddRule
   * @fires QueryBuilder.changer:getDefaultFilter
   */
  QueryBuilder.prototype.addRule = function (parent, data, flags) {
    /**
     * Just before adding a rule, can be prevented
     * @event beforeAddRule
     * @memberof QueryBuilder
     * @param {Group} parent
     */
    var e = this.trigger('beforeAddRule', parent);
    if (e.isDefaultPrevented()) {
      return null;
    }

    var rule_id = this.nextRuleId();
    var $rule = $($.parseHTML(this.getRuleTemplate(rule_id)));
    var model = parent.addRule($rule);

    model.data = data;
    model.flags = $.extend({}, this.settings.default_rule_flags, flags);

    /**
     * Just after adding a rule
     * @event afterAddRule
     * @memberof QueryBuilder
     * @param {Rule} rule
     */
    this.trigger('afterAddRule', model);

    this.trigger('rulesChanged');

    this.createRuleFilters(model);

    if (this.settings.default_filter || !this.settings.display_empty_filter) {
      /**
       * Modifies the default filter for a rule
       * @event changer:getDefaultFilter
       * @memberof QueryBuilder
       * @param {QueryBuilder.Filter} filter
       * @param {Rule} rule
       * @returns {QueryBuilder.Filter}
       */
      model.filter = this.change(
        'getDefaultFilter',
        this.getFilterById(this.settings.default_filter || this.filters[0].id),
        model
      );
    }

    return model;
  };

  /**
   * Tries to delete a rule
   * @param {Rule} rule
   * @returns {boolean} if the rule has been deleted
   * @fires QueryBuilder.beforeDeleteRule
   * @fires QueryBuilder.afterDeleteRule
   */
  QueryBuilder.prototype.deleteRule = function (rule) {
    if (rule.flags.no_delete) {
      return false;
    }

    /**
     * Just before deleting a rule, can be prevented
     * @event beforeDeleteRule
     * @memberof QueryBuilder
     * @param {Rule} rule
     */
    var e = this.trigger('beforeDeleteRule', rule);
    if (e.isDefaultPrevented()) {
      return false;
    }

    rule.drop();

    /**
     * Just after deleting a rule
     * @event afterDeleteRule
     * @memberof QueryBuilder
     */
    this.trigger('afterDeleteRule');

    this.trigger('rulesChanged');

    return true;
  };

  /**
   * Creates the filters for a rule
   * @param {Rule} rule
   * @fires QueryBuilder.changer:getRuleFilters
   * @fires QueryBuilder.afterCreateRuleFilters
   * @private
   */
  QueryBuilder.prototype.createRuleFilters = function (rule) {
    /**
     * Modifies the list a filters available for a rule
     * @event changer:getRuleFilters
     * @memberof QueryBuilder
     * @param {QueryBuilder.Filter[]} filters
     * @param {Rule} rule
     * @returns {QueryBuilder.Filter[]}
     */
    var filters = this.change('getRuleFilters', this.filters, rule);
    var $filterSelect = $($.parseHTML(this.getRuleFilterSelect(rule, filters)));

    rule.$el.find(QueryBuilder.selectors.filter_container).html($filterSelect);

    /**
     * After creating the dropdown for filters
     * @event afterCreateRuleFilters
     * @memberof QueryBuilder
     * @param {Rule} rule
     */
    this.trigger('afterCreateRuleFilters', rule);

    this.applyRuleFlags(rule);
  };

  /**
   * Creates the operators for a rule and init the rule operator
   * @param {Rule} rule
   * @fires QueryBuilder.afterCreateRuleOperators
   * @private
   */
  QueryBuilder.prototype.createRuleOperators = function (rule) {
    var $operatorContainer = rule.$el
      .find(QueryBuilder.selectors.operator_container)
      .empty();

    if (!rule.filter) {
      return;
    }

    var operators = this.getOperators(rule.filter);
    var $operatorSelect = $(
      $.parseHTML(this.getRuleOperatorSelect(rule, operators))
    );

    $operatorContainer.html($operatorSelect);

    // set the operator without triggering update event
    if (rule.filter.default_operator) {
      rule.__.operator = this.getOperatorByType(rule.filter.default_operator);
    } else {
      rule.__.operator = operators[0];
    }

    rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);

    /**
     * After creating the dropdown for operators
     * @event afterCreateRuleOperators
     * @memberof QueryBuilder
     * @param {Rule} rule
     * @param {QueryBuilder.Operator[]} operators - allowed operators for this rule
     */
    this.trigger('afterCreateRuleOperators', rule, operators);

    this.applyRuleFlags(rule);
  };

  /**
   * Creates the main input for a rule
   * @param {Rule} rule
   * @fires QueryBuilder.afterCreateRuleInput
   * @private
   */
  QueryBuilder.prototype.createRuleInput = function (rule) {
    var $valueContainer = rule.$el
      .find(QueryBuilder.selectors.value_container)
      .empty();

    rule.__.value = undefined;

    if (!rule.filter || !rule.operator || rule.operator.nb_inputs === 0) {
      return;
    }

    var self = this;
    var $inputs = $();
    var filter = rule.filter;

    for (var i = 0; i < rule.operator.nb_inputs; i++) {
      var $ruleInput = $($.parseHTML($.trim(this.getRuleInput(rule, i))));
      if (i > 0) $valueContainer.append(this.settings.inputs_separator);
      $valueContainer.append($ruleInput);
      $inputs = $inputs.add($ruleInput);
    }

    $valueContainer.css('display', '');

    $inputs.on('change ' + (filter.input_event || ''), function () {
      if (!rule._updating_input) {
        rule._updating_value = true;
        rule.value = self.getRuleInputValue(rule);
        rule._updating_value = false;
      }
    });

    if (filter.plugin) {
      $inputs[filter.plugin](filter.plugin_config || {});
    }

    /**
     * After creating the input for a rule and initializing optional plugin
     * @event afterCreateRuleInput
     * @memberof QueryBuilder
     * @param {Rule} rule
     */
    this.trigger('afterCreateRuleInput', rule);

    if (filter.default_value !== undefined) {
      rule.value = filter.default_value;
    } else {
      rule._updating_value = true;
      rule.value = self.getRuleInputValue(rule);
      rule._updating_value = false;
    }

    this.applyRuleFlags(rule);
  };

  /**
   * Performs action when a rule's filter changes
   * @param {Rule} rule
   * @param {object} previousFilter
   * @fires QueryBuilder.afterUpdateRuleFilter
   * @private
   */
  QueryBuilder.prototype.updateRuleFilter = function (rule, previousFilter) {
    this.createRuleOperators(rule);
    this.createRuleInput(rule);

    rule.$el
      .find(QueryBuilder.selectors.rule_filter)
      .val(rule.filter ? rule.filter.id : '-1');

    // clear rule data if the filter changed
    if (previousFilter && rule.filter && previousFilter.id !== rule.filter.id) {
      rule.data = undefined;
    }

    /**
     * After the filter has been updated and the operators and input re-created
     * @event afterUpdateRuleFilter
     * @memberof QueryBuilder
     * @param {Rule} rule
     * @param {object} previousFilter
     */
    this.trigger('afterUpdateRuleFilter', rule, previousFilter);

    this.trigger('rulesChanged');
  };

  /**
   * Performs actions when a rule's operator changes
   * @param {Rule} rule
   * @param {object} previousOperator
   * @fires QueryBuilder.afterUpdateRuleOperator
   * @private
   */
  QueryBuilder.prototype.updateRuleOperator = function (
    rule,
    previousOperator
  ) {
    var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container);

    if (!rule.operator || rule.operator.nb_inputs === 0) {
      $valueContainer.hide();

      rule.__.value = undefined;
    } else {
      $valueContainer.css('display', '');

      if (
        $valueContainer.is(':empty') ||
        !previousOperator ||
        rule.operator.nb_inputs !== previousOperator.nb_inputs ||
        rule.operator.optgroup !== previousOperator.optgroup
      ) {
        this.createRuleInput(rule);
      }
    }

    if (rule.operator) {
      rule.$el
        .find(QueryBuilder.selectors.rule_operator)
        .val(rule.operator.type);

      // refresh value if the format changed for this operator
      rule.__.value = this.getRuleInputValue(rule);
    }

    /**
     *  After the operator has been updated and the input optionally re-created
     * @event afterUpdateRuleOperator
     * @memberof QueryBuilder
     * @param {Rule} rule
     * @param {object} previousOperator
     */
    this.trigger('afterUpdateRuleOperator', rule, previousOperator);

    this.trigger('rulesChanged');
  };

  /**
   * Performs actions when rule's value changes
   * @param {Rule} rule
   * @param {object} previousValue
   * @fires QueryBuilder.afterUpdateRuleValue
   * @private
   */
  QueryBuilder.prototype.updateRuleValue = function (rule, previousValue) {
    if (!rule._updating_value) {
      this.setRuleInputValue(rule, rule.value);
    }

    /**
     * After the rule value has been modified
     * @event afterUpdateRuleValue
     * @memberof QueryBuilder
     * @param {Rule} rule
     * @param {*} previousValue
     */
    this.trigger('afterUpdateRuleValue', rule, previousValue);

    this.trigger('rulesChanged');
  };

  /**
   * Changes a rule's properties depending on its flags
   * @param {Rule} rule
   * @fires QueryBuilder.afterApplyRuleFlags
   * @private
   */
  QueryBuilder.prototype.applyRuleFlags = function (rule) {
    var flags = rule.flags;
    var Selectors = QueryBuilder.selectors;

    rule.$el
      .find(Selectors.rule_filter)
      .prop('disabled', flags.filter_readonly);
    rule.$el
      .find(Selectors.rule_operator)
      .prop('disabled', flags.operator_readonly);
    rule.$el.find(Selectors.rule_value).prop('disabled', flags.value_readonly);

    if (flags.no_delete) {
      rule.$el.find(Selectors.delete_rule).remove();
    }

    /**
     * After rule's flags has been applied
     * @event afterApplyRuleFlags
     * @memberof QueryBuilder
     * @param {Rule} rule
     */
    this.trigger('afterApplyRuleFlags', rule);
  };

  /**
   * Changes group's properties depending on its flags
   * @param {Group} group
   * @fires QueryBuilder.afterApplyGroupFlags
   * @private
   */
  QueryBuilder.prototype.applyGroupFlags = function (group) {
    var flags = group.flags;
    var Selectors = QueryBuilder.selectors;

    group.$el
      .find('>' + Selectors.group_condition)
      .prop('disabled', flags.condition_readonly)
      .parent()
      .toggleClass('readonly', flags.condition_readonly);

    if (flags.no_add_rule) {
      group.$el.find(Selectors.add_rule).remove();
    }
    if (flags.no_add_group) {
      group.$el.find(Selectors.add_group).remove();
    }
    if (flags.no_delete) {
      group.$el.find(Selectors.delete_group).remove();
    }

    /**
     * After group's flags has been applied
     * @event afterApplyGroupFlags
     * @memberof QueryBuilder
     * @param {Group} group
     */
    this.trigger('afterApplyGroupFlags', group);
  };

  /**
   * Clears all errors markers
   * @param {Node} [node] default is root Group
   */
  QueryBuilder.prototype.clearErrors = function (node) {
    node = node || this.model.root;

    if (!node) {
      return;
    }

    node.error = null;

    if (node instanceof Group) {
      node.each(
        function (rule) {
          rule.error = null;
        },
        function (group) {
          this.clearErrors(group);
        },
        this
      );
    }
  };

  /**
   * Adds/Removes error on a Rule or Group
   * @param {Node} node
   * @fires QueryBuilder.changer:displayError
   * @private
   */
  QueryBuilder.prototype.updateError = function (node) {
    if (this.settings.display_errors) {
      if (node.error === null) {
        node.$el.removeClass('has-error');
      } else {
        var errorMessage = this.translate('errors', node.error[0]);
        errorMessage = Utils.fmt(errorMessage, node.error.slice(1));

        /**
         * Modifies an error message before display
         * @event changer:displayError
         * @memberof QueryBuilder
         * @param {string} errorMessage - the error message (translated and formatted)
         * @param {array} error - the raw error array (error code and optional arguments)
         * @param {Node} node
         * @returns {string}
         */
        errorMessage = this.change(
          'displayError',
          errorMessage,
          node.error,
          node
        );

        node.$el
          .addClass('has-error')
          .find(QueryBuilder.selectors.error_container)
          .eq(0)
          .attr('title', errorMessage);
      }
    }
  };

  /**
   * Triggers a validation error event
   * @param {Node} node
   * @param {string|array} error
   * @param {*} value
   * @fires QueryBuilder.validationError
   * @private
   */
  QueryBuilder.prototype.triggerValidationError = function (
    node,
    error,
    value
  ) {
    if (!$.isArray(error)) {
      error = [error];
    }

    /**
     * Fired when a validation error occurred, can be prevented
     * @event validationError
     * @memberof QueryBuilder
     * @param {Node} node
     * @param {string} error
     * @param {*} value
     */
    var e = this.trigger('validationError', node, error, value);
    if (!e.isDefaultPrevented()) {
      node.error = error;
    }
  };

  /**
   * Destroys the builder
   * @fires QueryBuilder.beforeDestroy
   */
  QueryBuilder.prototype.destroy = function () {
    /**
     * Before the {@link QueryBuilder#destroy} method
     * @event beforeDestroy
     * @memberof QueryBuilder
     */
    this.trigger('beforeDestroy');

    if (this.status.generated_id) {
      this.$el.removeAttr('id');
    }

    this.clear();
    this.model = null;

    this.$el
      .off('.queryBuilder')
      .removeClass('query-builder')
      .removeData('queryBuilder');

    delete this.$el[0].queryBuilder;
  };

  /**
   * Clear all rules and resets the root group
   * @fires QueryBuilder.beforeReset
   * @fires QueryBuilder.afterReset
   */
  QueryBuilder.prototype.reset = function () {
    /**
     * Before the {@link QueryBuilder#reset} method, can be prevented
     * @event beforeReset
     * @memberof QueryBuilder
     */
    var e = this.trigger('beforeReset');
    if (e.isDefaultPrevented()) {
      return;
    }

    this.status.group_id = 1;
    this.status.rule_id = 0;

    this.model.root.empty();

    this.model.root.data = undefined;
    this.model.root.flags = $.extend({}, this.settings.default_group_flags);
    this.model.root.condition = this.settings.default_condition;

    this.addRule(this.model.root);

    /**
     * After the {@link QueryBuilder#reset} method
     * @event afterReset
     * @memberof QueryBuilder
     */
    this.trigger('afterReset');

    this.trigger('rulesChanged');
  };

  /**
   * Clears all rules and removes the root group
   * @fires QueryBuilder.beforeClear
   * @fires QueryBuilder.afterClear
   */
  QueryBuilder.prototype.clear = function () {
    /**
     * Before the {@link QueryBuilder#clear} method, can be prevented
     * @event beforeClear
     * @memberof QueryBuilder
     */
    var e = this.trigger('beforeClear');
    if (e.isDefaultPrevented()) {
      return;
    }

    this.status.group_id = 0;
    this.status.rule_id = 0;

    if (this.model.root) {
      this.model.root.drop();
      this.model.root = null;
    }

    /**
     * After the {@link QueryBuilder#clear} method
     * @event afterClear
     * @memberof QueryBuilder
     */
    this.trigger('afterClear');

    this.trigger('rulesChanged');
  };

  /**
   * Modifies the builder configuration.<br>
   * Only options defined in QueryBuilder.modifiable_options are modifiable
   * @param {object} options
   */
  QueryBuilder.prototype.setOptions = function (options) {
    $.each(
      options,
      function (opt, value) {
        if (QueryBuilder.modifiable_options.indexOf(opt) !== -1) {
          this.settings[opt] = value;
        }
      }.bind(this)
    );
  };

  /**
   * Returns the model associated to a DOM object, or the root model
   * @param {jQuery} [target]
   * @returns {Node}
   */
  QueryBuilder.prototype.getModel = function (target) {
    if (!target) {
      return this.model.root;
    } else if (target instanceof Node) {
      return target;
    } else {
      return $(target).data('queryBuilderModel');
    }
  };

  /**
   * Validates the whole builder
   * @param {object} [options]
   * @param {boolean} [options.skip_empty=false] - skips validating rules that have no filter selected
   * @returns {boolean}
   * @fires QueryBuilder.changer:validate
   */
  QueryBuilder.prototype.validate = function (options) {
    options = $.extend(
      {
        skip_empty: false,
      },
      options
    );

    this.clearErrors();

    var self = this;

    var valid = (function parse(group) {
      var done = 0;
      var errors = 0;

      group.each(
        function (rule) {
          if (!rule.filter && options.skip_empty) {
            return;
          }

          if (!rule.filter) {
            self.triggerValidationError(rule, 'no_filter', null);
            errors++;
            return;
          }

          if (!rule.operator) {
            self.triggerValidationError(rule, 'no_operator', null);
            errors++;
            return;
          }

          if (rule.operator.nb_inputs !== 0) {
            var valid = self.validateValue(rule, rule.value);

            if (valid !== true) {
              self.triggerValidationError(rule, valid, rule.value);
              errors++;
              return;
            }
          }

          done++;
        },
        function (group) {
          var res = parse(group);
          if (res === true) {
            done++;
          } else if (res === false) {
            errors++;
          }
        }
      );

      if (errors > 0) {
        return false;
      } else if (done === 0 && !group.isRoot() && options.skip_empty) {
        return null;
      } else if (
        done === 0 &&
        (!self.settings.allow_empty || !group.isRoot())
      ) {
        self.triggerValidationError(group, 'empty_group', null);
        return false;
      }

      return true;
    })(this.model.root);

    /**
     * Modifies the result of the {@link QueryBuilder#validate} method
     * @event changer:validate
     * @memberof QueryBuilder
     * @param {boolean} valid
     * @returns {boolean}
     */
    return this.change('validate', valid);
  };

  /**
   * Gets an object representing current rules
   * @param {object} [options]
   * @param {boolean|string} [options.get_flags=false] - export flags, true: only changes from default flags or 'all'
   * @param {boolean} [options.allow_invalid=false] - returns rules even if they are invalid
   * @param {boolean} [options.skip_empty=false] - remove rules that have no filter selected
   * @returns {object}
   * @fires QueryBuilder.changer:ruleToJson
   * @fires QueryBuilder.changer:groupToJson
   * @fires QueryBuilder.changer:getRules
   */
  QueryBuilder.prototype.getRules = function (options) {
    options = $.extend(
      {
        get_flags: false,
        allow_invalid: false,
        skip_empty: false,
      },
      options
    );

    var valid = this.validate(options);
    if (!valid && !options.allow_invalid) {
      return null;
    }

    var self = this;

    var out = (function parse(group) {
      var groupData = {
        condition: group.condition,
        rules: [],
      };

      if (group.data) {
        groupData.data = $.extendext(true, 'replace', {}, group.data);
      }

      if (options.get_flags) {
        var flags = self.getGroupFlags(
          group.flags,
          options.get_flags === 'all'
        );
        if (!$.isEmptyObject(flags)) {
          groupData.flags = flags;
        }
      }

      group.each(
        function (rule) {
          if (!rule.filter && options.skip_empty) {
            return;
          }

          var value = null;
          if (!rule.operator || rule.operator.nb_inputs !== 0) {
            value = rule.value;
          }

          var ruleData = {
            id: rule.filter ? rule.filter.id : null,
            field: rule.filter ? rule.filter.field : null,
            type: rule.filter ? rule.filter.type : null,
            input: rule.filter ? rule.filter.input : null,
            operator: rule.operator ? rule.operator.type : null,
            value: value,
          };

          if ((rule.filter && rule.filter.data) || rule.data) {
            ruleData.data = $.extendext(
              true,
              'replace',
              {},
              rule.filter ? rule.filter.data : {},
              rule.data
            );
          }

          if (options.get_flags) {
            var flags = self.getRuleFlags(
              rule.flags,
              options.get_flags === 'all'
            );
            if (!$.isEmptyObject(flags)) {
              ruleData.flags = flags;
            }
          }

          /**
           * Modifies the JSON generated from a Rule object
           * @event changer:ruleToJson
           * @memberof QueryBuilder
           * @param {object} json
           * @param {Rule} rule
           * @returns {object}
           */
          groupData.rules.push(self.change('ruleToJson', ruleData, rule));
        },
        function (model) {
          var data = parse(model);
          if (data.rules.length !== 0 || !options.skip_empty) {
            groupData.rules.push(data);
          }
        },
        this
      );

      /**
       * Modifies the JSON generated from a Group object
       * @event changer:groupToJson
       * @memberof QueryBuilder
       * @param {object} json
       * @param {Group} group
       * @returns {object}
       */
      return self.change('groupToJson', groupData, group);
    })(this.model.root);

    out.valid = valid;

    /**
     * Modifies the result of the {@link QueryBuilder#getRules} method
     * @event changer:getRules
     * @memberof QueryBuilder
     * @param {object} json
     * @returns {object}
     */
    return this.change('getRules', out);
  };

  /**
   * Sets rules from object
   * @param {object} data
   * @param {object} [options]
   * @param {boolean} [options.allow_invalid=false] - silent-fail if the data are invalid
   * @throws RulesError, UndefinedConditionError
   * @fires QueryBuilder.changer:setRules
   * @fires QueryBuilder.changer:jsonToRule
   * @fires QueryBuilder.changer:jsonToGroup
   * @fires QueryBuilder.afterSetRules
   */
  QueryBuilder.prototype.setRules = function (data, options) {
    options = $.extend(
      {
        allow_invalid: false,
      },
      options
    );

    if ($.isArray(data)) {
      data = {
        condition: this.settings.default_condition,
        rules: data,
      };
    }

    if (
      !data ||
      !data.rules ||
      (data.rules.length === 0 && !this.settings.allow_empty)
    ) {
      Utils.error('RulesParse', 'Incorrect data object passed');
    }

    this.clear();
    this.setRoot(false, data.data, this.parseGroupFlags(data));

    /**
     * Modifies data before the {@link QueryBuilder#setRules} method
     * @event changer:setRules
     * @memberof QueryBuilder
     * @param {object} json
     * @param {object} options
     * @returns {object}
     */
    data = this.change('setRules', data, options);

    var self = this;

    (function add(data, group) {
      if (group === null) {
        return;
      }

      if (data.condition === undefined) {
        data.condition = self.settings.default_condition;
      } else if (self.settings.conditions.indexOf(data.condition) == -1) {
        Utils.error(
          !options.allow_invalid,
          'UndefinedCondition',
          'Invalid condition "{0}"',
          data.condition
        );
        data.condition = self.settings.default_condition;
      }

      group.condition = data.condition;

      data.rules.forEach(function (item) {
        var model;

        if (item.rules !== undefined) {
          if (
            self.settings.allow_groups !== -1 &&
            self.settings.allow_groups < group.level
          ) {
            Utils.error(
              !options.allow_invalid,
              'RulesParse',
              'No more than {0} groups are allowed',
              self.settings.allow_groups
            );
            self.reset();
          } else {
            model = self.addGroup(
              group,
              false,
              item.data,
              self.parseGroupFlags(item)
            );
            if (model === null) {
              return;
            }

            add(item, model);
          }
        } else {
          if (!item.empty) {
            if (item.id === undefined) {
              Utils.error(
                !options.allow_invalid,
                'RulesParse',
                'Missing rule field id'
              );
              item.empty = true;
            }
            if (item.operator === undefined) {
              item.operator = 'equal';
            }
          }

          model = self.addRule(group, item.data, self.parseRuleFlags(item));
          if (model === null) {
            return;
          }

          if (!item.empty) {
            model.filter = self.getFilterById(item.id, !options.allow_invalid);
          }

          if (model.filter) {
            model.operator = self.getOperatorByType(
              item.operator,
              !options.allow_invalid
            );

            if (!model.operator) {
              model.operator = self.getOperators(model.filter)[0];
            }
          }

          if (model.operator && model.operator.nb_inputs !== 0) {
            if (item.value !== undefined) {
              model.value = item.value;
            } else if (model.filter.default_value !== undefined) {
              model.value = model.filter.default_value;
            }
          }

          /**
           * Modifies the Rule object generated from the JSON
           * @event changer:jsonToRule
           * @memberof QueryBuilder
           * @param {Rule} rule
           * @param {object} json
           * @returns {Rule} the same rule
           */
          if (self.change('jsonToRule', model, item) != model) {
            Utils.error('RulesParse', 'Plugin tried to change rule reference');
          }
        }
      });

      /**
       * Modifies the Group object generated from the JSON
       * @event changer:jsonToGroup
       * @memberof QueryBuilder
       * @param {Group} group
       * @param {object} json
       * @returns {Group} the same group
       */
      if (self.change('jsonToGroup', group, data) != group) {
        Utils.error('RulesParse', 'Plugin tried to change group reference');
      }
    })(data, this.model.root);

    /**
     * After the {@link QueryBuilder#setRules} method
     * @event afterSetRules
     * @memberof QueryBuilder
     */
    this.trigger('afterSetRules');
  };

  /**
   * Performs value validation
   * @param {Rule} rule
   * @param {string|string[]} value
   * @returns {array|boolean} true or error array
   * @fires QueryBuilder.changer:validateValue
   */
  QueryBuilder.prototype.validateValue = function (rule, value) {
    var validation = rule.filter.validation || {};
    var result = true;

    if (validation.callback) {
      result = validation.callback.call(this, value, rule);
    } else {
      result = this._validateValue(rule, value);
    }

    /**
     * Modifies the result of the rule validation method
     * @event changer:validateValue
     * @memberof QueryBuilder
     * @param {array|boolean} result - true or an error array
     * @param {*} value
     * @param {Rule} rule
     * @returns {array|boolean}
     */
    return this.change('validateValue', result, value, rule);
  };

  /**
   * Default validation function
   * @param {Rule} rule
   * @param {string|string[]} value
   * @returns {array|boolean} true or error array
   * @throws ConfigError
   * @private
   */
  QueryBuilder.prototype._validateValue = function (rule, value) {
    var filter = rule.filter;
    var operator = rule.operator;
    var validation = filter.validation || {};
    var result = true;
    var tmp, tempValue;

    if (rule.operator.nb_inputs === 1) {
      value = [value];
    }

    for (var i = 0; i < operator.nb_inputs; i++) {
      if (!operator.multiple && $.isArray(value[i]) && value[i].length > 1) {
        result = [
          'operator_not_multiple',
          operator.type,
          this.translate('operators', operator.type),
        ];
        break;
      }

      switch (filter.input) {
        case 'radio':
          if (value[i] === undefined || value[i].length === 0) {
            if (!validation.allow_empty_value) {
              result = ['radio_empty'];
            }
            break;
          }
          break;

        case 'checkbox':
          if (value[i] === undefined || value[i].length === 0) {
            if (!validation.allow_empty_value) {
              result = ['checkbox_empty'];
            }
            break;
          }
          break;

        case 'select':
          if (
            value[i] === undefined ||
            value[i].length === 0 ||
            (filter.placeholder && value[i] == filter.placeholder_value)
          ) {
            if (!validation.allow_empty_value) {
              result = ['select_empty'];
            }
            break;
          }
          break;

        default:
          tempValue = $.isArray(value[i]) ? value[i] : [value[i]];

          for (var j = 0; j < tempValue.length; j++) {
            switch (QueryBuilder.types[filter.type]) {
              case 'string':
                if (tempValue[j] === undefined || tempValue[j].length === 0) {
                  if (!validation.allow_empty_value) {
                    result = ['string_empty'];
                  }
                  break;
                }
                if (validation.min !== undefined) {
                  if (tempValue[j].length < parseInt(validation.min)) {
                    result = [
                      this.getValidationMessage(
                        validation,
                        'min',
                        'string_exceed_min_length'
                      ),
                      validation.min,
                    ];
                    break;
                  }
                }
                if (validation.max !== undefined) {
                  if (tempValue[j].length > parseInt(validation.max)) {
                    result = [
                      this.getValidationMessage(
                        validation,
                        'max',
                        'string_exceed_max_length'
                      ),
                      validation.max,
                    ];
                    break;
                  }
                }
                if (validation.format) {
                  if (typeof validation.format == 'string') {
                    validation.format = new RegExp(validation.format);
                  }
                  if (!validation.format.test(tempValue[j])) {
                    result = [
                      this.getValidationMessage(
                        validation,
                        'format',
                        'string_invalid_format'
                      ),
                      validation.format,
                    ];
                    break;
                  }
                }
                break;

              case 'number':
                if (tempValue[j] === undefined || tempValue[j].length === 0) {
                  if (!validation.allow_empty_value) {
                    result = ['number_nan'];
                  }
                  break;
                }
                if (isNaN(tempValue[j])) {
                  result = ['number_nan'];
                  break;
                }
                if (filter.type == 'integer') {
                  if (parseInt(tempValue[j]) != tempValue[j]) {
                    result = ['number_not_integer'];
                    break;
                  }
                } else {
                  if (parseFloat(tempValue[j]) != tempValue[j]) {
                    result = ['number_not_double'];
                    break;
                  }
                }
                if (validation.min !== undefined) {
                  if (tempValue[j] < parseFloat(validation.min)) {
                    result = [
                      this.getValidationMessage(
                        validation,
                        'min',
                        'number_exceed_min'
                      ),
                      validation.min,
                    ];
                    break;
                  }
                }
                if (validation.max !== undefined) {
                  if (tempValue[j] > parseFloat(validation.max)) {
                    result = [
                      this.getValidationMessage(
                        validation,
                        'max',
                        'number_exceed_max'
                      ),
                      validation.max,
                    ];
                    break;
                  }
                }
                if (
                  validation.step !== undefined &&
                  validation.step !== 'any'
                ) {
                  var v = (tempValue[j] / validation.step).toPrecision(14);
                  if (parseInt(v) != v) {
                    result = [
                      this.getValidationMessage(
                        validation,
                        'step',
                        'number_wrong_step'
                      ),
                      validation.step,
                    ];
                    break;
                  }
                }
                break;

              case 'datetime':
                if (tempValue[j] === undefined || tempValue[j].length === 0) {
                  if (!validation.allow_empty_value) {
                    result = ['datetime_empty'];
                  }
                  break;
                }

                // we need MomentJS
                if (validation.format) {
                  if (!('moment' in window)) {
                    Utils.error(
                      'MissingLibrary',
                      'MomentJS is required for Date/Time validation. Get it here http://momentjs.com'
                    );
                  }

                  var datetime = moment(tempValue[j], validation.format);
                  if (!datetime.isValid()) {
                    result = [
                      this.getValidationMessage(
                        validation,
                        'format',
                        'datetime_invalid'
                      ),
                      validation.format,
                    ];
                    break;
                  } else {
                    if (validation.min) {
                      if (
                        datetime < moment(validation.min, validation.format)
                      ) {
                        result = [
                          this.getValidationMessage(
                            validation,
                            'min',
                            'datetime_exceed_min'
                          ),
                          validation.min,
                        ];
                        break;
                      }
                    }
                    if (validation.max) {
                      if (
                        datetime > moment(validation.max, validation.format)
                      ) {
                        result = [
                          this.getValidationMessage(
                            validation,
                            'max',
                            'datetime_exceed_max'
                          ),
                          validation.max,
                        ];
                        break;
                      }
                    }
                  }
                }
                break;

              case 'boolean':
                if (tempValue[j] === undefined || tempValue[j].length === 0) {
                  if (!validation.allow_empty_value) {
                    result = ['boolean_not_valid'];
                  }
                  break;
                }
                tmp = ('' + tempValue[j]).trim().toLowerCase();
                if (
                  tmp !== 'true' &&
                  tmp !== 'false' &&
                  tmp !== '1' &&
                  tmp !== '0' &&
                  tempValue[j] !== 1 &&
                  tempValue[j] !== 0
                ) {
                  result = ['boolean_not_valid'];
                  break;
                }
            }

            if (result !== true) {
              break;
            }
          }
      }

      if (result !== true) {
        break;
      }
    }

    if (
      (rule.operator.type === 'between' ||
        rule.operator.type === 'not_between') &&
      value.length === 2
    ) {
      switch (QueryBuilder.types[filter.type]) {
        case 'number':
          if (value[0] > value[1]) {
            result = ['number_between_invalid', value[0], value[1]];
          }
          break;

        case 'datetime':
          // we need MomentJS
          if (validation.format) {
            if (!('moment' in window)) {
              Utils.error(
                'MissingLibrary',
                'MomentJS is required for Date/Time validation. Get it here http://momentjs.com'
              );
            }

            if (
              moment(value[0], validation.format).isAfter(
                moment(value[1], validation.format)
              )
            ) {
              result = ['datetime_between_invalid', value[0], value[1]];
            }
          }
          break;
      }
    }

    return result;
  };

  /**
   * Returns an incremented group ID
   * @returns {string}
   * @private
   */
  QueryBuilder.prototype.nextGroupId = function () {
    return this.status.id + '_group_' + this.status.group_id++;
  };

  /**
   * Returns an incremented rule ID
   * @returns {string}
   * @private
   */
  QueryBuilder.prototype.nextRuleId = function () {
    return this.status.id + '_rule_' + this.status.rule_id++;
  };

  /**
   * Returns the operators for a filter
   * @param {string|object} filter - filter id or filter object
   * @returns {object[]}
   * @fires QueryBuilder.changer:getOperators
   */
  QueryBuilder.prototype.getOperators = function (filter) {
    if (typeof filter == 'string') {
      filter = this.getFilterById(filter);
    }

    var result = [];

    for (var i = 0, l = this.operators.length; i < l; i++) {
      // filter operators check
      if (filter.operators) {
        if (filter.operators.indexOf(this.operators[i].type) == -1) {
          continue;
        }
      }
      // type check
      else if (
        this.operators[i].apply_to.indexOf(QueryBuilder.types[filter.type]) ==
        -1
      ) {
        continue;
      }

      result.push(this.operators[i]);
    }

    // keep sort order defined for the filter
    if (filter.operators) {
      result.sort(function (a, b) {
        return (
          filter.operators.indexOf(a.type) - filter.operators.indexOf(b.type)
        );
      });
    }

    /**
     * Modifies the operators available for a filter
     * @event changer:getOperators
     * @memberof QueryBuilder
     * @param {QueryBuilder.Operator[]} operators
     * @param {QueryBuilder.Filter} filter
     * @returns {QueryBuilder.Operator[]}
     */
    return this.change('getOperators', result, filter);
  };

  /**
   * Returns a particular filter by its id
   * @param {string} id
   * @param {boolean} [doThrow=true]
   * @returns {object|null}
   * @throws UndefinedFilterError
   */
  QueryBuilder.prototype.getFilterById = function (id, doThrow) {
    if (id == '-1') {
      return null;
    }

    for (var i = 0, l = this.filters.length; i < l; i++) {
      if (this.filters[i].id == id) {
        return this.filters[i];
      }
    }

    Utils.error(
      doThrow !== false,
      'UndefinedFilter',
      'Undefined filter "{0}"',
      id
    );

    return null;
  };

  /**
   * Returns a particular operator by its type
   * @param {string} type
   * @param {boolean} [doThrow=true]
   * @returns {object|null}
   * @throws UndefinedOperatorError
   */
  QueryBuilder.prototype.getOperatorByType = function (type, doThrow) {
    if (type == '-1') {
      return null;
    }

    for (var i = 0, l = this.operators.length; i < l; i++) {
      if (this.operators[i].type == type) {
        return this.operators[i];
      }
    }

    Utils.error(
      doThrow !== false,
      'UndefinedOperator',
      'Undefined operator "{0}"',
      type
    );

    return null;
  };

  /**
   * Returns rule's current input value
   * @param {Rule} rule
   * @returns {*}
   * @fires QueryBuilder.changer:getRuleValue
   * @private
   */
  QueryBuilder.prototype.getRuleInputValue = function (rule) {
    var filter = rule.filter;
    var operator = rule.operator;
    var value = [];

    if (filter.valueGetter) {
      value = filter.valueGetter.call(this, rule);
    } else {
      var $value = rule.$el.find(QueryBuilder.selectors.value_container);

      for (var i = 0; i < operator.nb_inputs; i++) {
        var name = Utils.escapeElementId(rule.id + '_value_' + i);
        var tmp;

        switch (filter.input) {
          case 'radio':
            value.push($value.find('[name=' + name + ']:checked').val());
            break;

          case 'checkbox':
            tmp = [];
            // jshint loopfunc:true
            $value.find('[name=' + name + ']:checked').each(function () {
              tmp.push($(this).val());
            });
            // jshint loopfunc:false
            value.push(tmp);
            break;

          case 'select':
            if (filter.multiple) {
              tmp = [];
              // jshint loopfunc:true
              $value
                .find('[name=' + name + '] option:selected')
                .each(function () {
                  tmp.push($(this).val());
                });
              // jshint loopfunc:false
              value.push(tmp);
            } else {
              value.push(
                $value.find('[name=' + name + '] option:selected').val()
              );
            }
            break;

          default:
            value.push($value.find('[name=' + name + ']').val());
        }
      }

      value = value.map(function (val) {
        if (
          operator.multiple &&
          filter.value_separator &&
          typeof val == 'string'
        ) {
          val = val.split(filter.value_separator);
        }

        if ($.isArray(val)) {
          return val.map(function (subval) {
            return Utils.changeType(subval, filter.type);
          });
        } else {
          return Utils.changeType(val, filter.type);
        }
      });

      if (operator.nb_inputs === 1) {
        value = value[0];
      }

      // @deprecated
      if (filter.valueParser) {
        value = filter.valueParser.call(this, rule, value);
      }
    }

    /**
     * Modifies the rule's value grabbed from the DOM
     * @event changer:getRuleValue
     * @memberof QueryBuilder
     * @param {*} value
     * @param {Rule} rule
     * @returns {*}
     */
    return this.change('getRuleValue', value, rule);
  };

  /**
   * Sets the value of a rule's input
   * @param {Rule} rule
   * @param {*} value
   * @private
   */
  QueryBuilder.prototype.setRuleInputValue = function (rule, value) {
    var filter = rule.filter;
    var operator = rule.operator;

    if (!filter || !operator) {
      return;
    }

    rule._updating_input = true;

    if (filter.valueSetter) {
      filter.valueSetter.call(this, rule, value);
    } else {
      var $value = rule.$el.find(QueryBuilder.selectors.value_container);

      if (operator.nb_inputs == 1) {
        value = [value];
      }

      for (var i = 0; i < operator.nb_inputs; i++) {
        var name = Utils.escapeElementId(rule.id + '_value_' + i);

        switch (filter.input) {
          case 'radio':
            $value
              .find('[name=' + name + '][value="' + value[i] + '"]')
              .prop('checked', true)
              .trigger('change');
            break;

          case 'checkbox':
            if (!$.isArray(value[i])) {
              value[i] = [value[i]];
            }
            // jshint loopfunc:true
            value[i].forEach(function (value) {
              $value
                .find('[name=' + name + '][value="' + value + '"]')
                .prop('checked', true)
                .trigger('change');
            });
            // jshint loopfunc:false
            break;

          default:
            if (
              operator.multiple &&
              filter.value_separator &&
              $.isArray(value[i])
            ) {
              value[i] = value[i].join(filter.value_separator);
            }
            $value
              .find('[name=' + name + ']')
              .val(value[i])
              .trigger('change');
            break;
        }
      }
    }

    rule._updating_input = false;
  };

  /**
   * Parses rule flags
   * @param {object} rule
   * @returns {object}
   * @fires QueryBuilder.changer:parseRuleFlags
   * @private
   */
  QueryBuilder.prototype.parseRuleFlags = function (rule) {
    var flags = $.extend({}, this.settings.default_rule_flags);

    if (rule.readonly) {
      $.extend(flags, {
        filter_readonly: true,
        operator_readonly: true,
        value_readonly: true,
        no_delete: true,
      });
    }

    if (rule.flags) {
      $.extend(flags, rule.flags);
    }

    /**
     * Modifies the consolidated rule's flags
     * @event changer:parseRuleFlags
     * @memberof QueryBuilder
     * @param {object} flags
     * @param {object} rule - <b>not</b> a Rule object
     * @returns {object}
     */
    return this.change('parseRuleFlags', flags, rule);
  };

  /**
   * Gets a copy of flags of a rule
   * @param {object} flags
   * @param {boolean} [all=false] - return all flags or only changes from default flags
   * @returns {object}
   * @private
   */
  QueryBuilder.prototype.getRuleFlags = function (flags, all) {
    if (all) {
      return $.extend({}, flags);
    } else {
      var ret = {};
      $.each(this.settings.default_rule_flags, function (key, value) {
        if (flags[key] !== value) {
          ret[key] = flags[key];
        }
      });
      return ret;
    }
  };

  /**
   * Parses group flags
   * @param {object} group
   * @returns {object}
   * @fires QueryBuilder.changer:parseGroupFlags
   * @private
   */
  QueryBuilder.prototype.parseGroupFlags = function (group) {
    var flags = $.extend({}, this.settings.default_group_flags);

    if (group.readonly) {
      $.extend(flags, {
        condition_readonly: true,
        no_add_rule: true,
        no_add_group: true,
        no_delete: true,
      });
    }

    if (group.flags) {
      $.extend(flags, group.flags);
    }

    /**
     * Modifies the consolidated group's flags
     * @event changer:parseGroupFlags
     * @memberof QueryBuilder
     * @param {object} flags
     * @param {object} group - <b>not</b> a Group object
     * @returns {object}
     */
    return this.change('parseGroupFlags', flags, group);
  };

  /**
   * Gets a copy of flags of a group
   * @param {object} flags
   * @param {boolean} [all=false] - return all flags or only changes from default flags
   * @returns {object}
   * @private
   */
  QueryBuilder.prototype.getGroupFlags = function (flags, all) {
    if (all) {
      return $.extend({}, flags);
    } else {
      var ret = {};
      $.each(this.settings.default_group_flags, function (key, value) {
        if (flags[key] !== value) {
          ret[key] = flags[key];
        }
      });
      return ret;
    }
  };

  /**
   * Translate a label either by looking in the `lang` object or in itself if it's an object where keys are language codes
   * @param {string} [category]
   * @param {string|object} key
   * @returns {string}
   * @fires QueryBuilder.changer:translate
   */
  QueryBuilder.prototype.translate = function (category, key) {
    if (!key) {
      key = category;
      category = undefined;
    }

    var translation;
    if (typeof key === 'object') {
      translation = key[this.settings.lang_code] || key['en'];
    } else {
      translation = (category ? this.lang[category] : this.lang)[key] || key;
    }

    /**
     * Modifies the translated label
     * @event changer:translate
     * @memberof QueryBuilder
     * @param {string} translation
     * @param {string|object} key
     * @param {string} [category]
     * @returns {string}
     */
    return this.change('translate', translation, key, category);
  };

  /**
   * Returns a validation message
   * @param {object} validation
   * @param {string} type
   * @param {string} def
   * @returns {string}
   * @private
   */
  QueryBuilder.prototype.getValidationMessage = function (
    validation,
    type,
    def
  ) {
    return (validation.messages && validation.messages[type]) || def;
  };

  QueryBuilder.templates.group =
    '\
<div id="{{= it.group_id }}" class="rules-group-container"> \
<div class="rules-group-header"> \
  <div class="btn-group pull-right group-actions"> \
    <button type="button" class="btn btn-xs btn-success" data-add="rule"> \
      <i class="{{= it.icons.add_rule }}"></i> {{= it.translate("add_rule") }} \
    </button> \
    {{? it.settings.allow_groups===-1 || it.settings.allow_groups>=it.level }} \
      <button type="button" class="btn btn-xs btn-success" data-add="group"> \
        <i class="{{= it.icons.add_group }}"></i> {{= it.translate("add_group") }} \
      </button> \
    {{?}} \
    {{? it.level>1 }} \
      <button type="button" class="btn btn-xs btn-danger" data-delete="group"> \
        <i class="{{= it.icons.remove_group }}"></i> {{= it.translate("delete_group") }} \
      </button> \
    {{?}} \
  </div> \
  <div class="btn-group group-conditions"> \
    {{~ it.conditions: condition }} \
      <label class="btn btn-xs btn-primary"> \
        <input type="radio" name="{{= it.group_id }}_cond" value="{{= condition }}"> {{= it.translate("conditions", condition) }} \
      </label> \
    {{~}} \
  </div> \
  {{? it.settings.display_errors }} \
    <div class="error-container"><i class="{{= it.icons.error }}"></i></div> \
  {{?}} \
</div> \
<div class=rules-group-body> \
  <div class=rules-list></div> \
</div> \
</div>';

  QueryBuilder.templates.rule =
    '\
<div id="{{= it.rule_id }}" class="rule-container"> \
    <div class="rule-header"> \
          <div class="btn-group pull-right rule-actions"> \
              <button type="button" class="btn btn-xs btn-danger" data-delete="rule"> \
                  <i class="{{= it.icons.remove_rule }}"></i> {{= it.translate("delete_rule") }} \
              </button> \
          </div> \
      </div> \
      {{? it.settings.display_errors }} \
          <div class="error-container">111<i class="{{= it.icons.error }}"></i></div> \
      {{?}} \
      <div class="rule-filter-container"></div> \
      <div class="rule-operator-container"></div> \
      <div class="rule-value-container"></div> \
</div>';

  QueryBuilder.templates.filterSelect =
    '\
{{ var optgroup = null; }} \
<select class="form-control" name="{{= it.rule.id }}_filter"> \
{{? it.settings.display_empty_filter }} \
  <option value="-1">{{= it.settings.select_placeholder }}</option> \
{{?}} \
{{~ it.filters: filter }} \
  {{? optgroup !== filter.optgroup }} \
    {{? optgroup !== null }}</optgroup>{{?}} \
    {{? (optgroup = filter.optgroup) !== null }} \
      <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
    {{?}} \
  {{?}} \
  <option value="{{= filter.id }}" {{? filter.icon}}data-icon="{{= filter.icon}}"{{?}}>{{= it.translate(filter.label) }}</option> \
{{~}} \
{{? optgroup !== null }}</optgroup>{{?}} \
</select>';

  QueryBuilder.templates.operatorSelect =
    '\
{{? it.operators.length === 1 }} \
<span> \
{{= it.translate("operators", it.operators[0].type) }} \
</span> \
{{?}} \
{{ var optgroup = null; }} \
<select class="form-control {{? it.operators.length === 1 }}hide{{?}}" name="{{= it.rule.id }}_operator"> \
{{~ it.operators: operator }} \
  {{? optgroup !== operator.optgroup }} \
    {{? optgroup !== null }}</optgroup>{{?}} \
    {{? (optgroup = operator.optgroup) !== null }} \
      <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
    {{?}} \
  {{?}} \
  <option value="{{= operator.type }}" {{? operator.icon}}data-icon="{{= operator.icon}}"{{?}}>{{= it.translate("operators", operator.type) }}</option> \
{{~}} \
{{? optgroup !== null }}</optgroup>{{?}} \
</select>';

  QueryBuilder.templates.ruleValueSelect =
    '\
{{ var optgroup = null; }} \
<select class="form-control" name="{{= it.name }}" {{? it.rule.filter.multiple }}multiple{{?}}> \
{{? it.rule.filter.placeholder }} \
  <option value="{{= it.rule.filter.placeholder_value }}" disabled selected>{{= it.rule.filter.placeholder }}</option> \
{{?}} \
{{~ it.rule.filter.values: entry }} \
  {{? optgroup !== entry.optgroup }} \
    {{? optgroup !== null }}</optgroup>{{?}} \
    {{? (optgroup = entry.optgroup) !== null }} \
      <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
    {{?}} \
  {{?}} \
  <option value="{{= entry.value }}">{{= entry.label }}</option> \
{{~}} \
{{? optgroup !== null }}</optgroup>{{?}} \
</select>';

  /**
   * Returns group's HTML
   * @param {string} group_id
   * @param {int} level
   * @returns {string}
   * @fires QueryBuilder.changer:getGroupTemplate
   * @private
   */
  QueryBuilder.prototype.getGroupTemplate = function (group_id, level) {
    var h = this.templates.group({
      builder: this,
      group_id: group_id,
      level: level,
      conditions: this.settings.conditions,
      icons: this.icons,
      settings: this.settings,
      translate: this.translate.bind(this),
    });

    /**
     * Modifies the raw HTML of a group
     * @event changer:getGroupTemplate
     * @memberof QueryBuilder
     * @param {string} html
     * @param {int} level
     * @returns {string}
     */
    return this.change('getGroupTemplate', h, level);
  };

  /**
   * Returns rule's HTML
   * @param {string} rule_id
   * @returns {string}
   * @fires QueryBuilder.changer:getRuleTemplate
   * @private
   */
  QueryBuilder.prototype.getRuleTemplate = function (rule_id) {
    var h = this.templates.rule({
      builder: this,
      rule_id: rule_id,
      icons: this.icons,
      settings: this.settings,
      translate: this.translate.bind(this),
    });

    /**
     * Modifies the raw HTML of a rule
     * @event changer:getRuleTemplate
     * @memberof QueryBuilder
     * @param {string} html
     * @returns {string}
     */
    return this.change('getRuleTemplate', h);
  };

  /**
   * Returns rule's filter HTML
   * @param {Rule} rule
   * @param {object[]} filters
   * @returns {string}
   * @fires QueryBuilder.changer:getRuleFilterTemplate
   * @private
   */
  QueryBuilder.prototype.getRuleFilterSelect = function (rule, filters) {
    var h = this.templates.filterSelect({
      builder: this,
      rule: rule,
      filters: filters,
      icons: this.icons,
      settings: this.settings,
      translate: this.translate.bind(this),
    });

    /**
     * Modifies the raw HTML of the rule's filter dropdown
     * @event changer:getRuleFilterSelect
     * @memberof QueryBuilder
     * @param {string} html
     * @param {Rule} rule
     * @param {QueryBuilder.Filter[]} filters
     * @returns {string}
     */
    return this.change('getRuleFilterSelect', h, rule, filters);
  };

  /**
   * Returns rule's operator HTML
   * @param {Rule} rule
   * @param {object[]} operators
   * @returns {string}
   * @fires QueryBuilder.changer:getRuleOperatorTemplate
   * @private
   */
  QueryBuilder.prototype.getRuleOperatorSelect = function (rule, operators) {
    var h = this.templates.operatorSelect({
      builder: this,
      rule: rule,
      operators: operators,
      icons: this.icons,
      settings: this.settings,
      translate: this.translate.bind(this),
    });

    /**
     * Modifies the raw HTML of the rule's operator dropdown
     * @event changer:getRuleOperatorSelect
     * @memberof QueryBuilder
     * @param {string} html
     * @param {Rule} rule
     * @param {QueryBuilder.Operator[]} operators
     * @returns {string}
     */
    return this.change('getRuleOperatorSelect', h, rule, operators);
  };

  /**
   * Returns the rule's value select HTML
   * @param {string} name
   * @param {Rule} rule
   * @returns {string}
   * @fires QueryBuilder.changer:getRuleValueSelect
   * @private
   */
  QueryBuilder.prototype.getRuleValueSelect = function (name, rule) {
    var h = this.templates.ruleValueSelect({
      builder: this,
      name: name,
      rule: rule,
      icons: this.icons,
      settings: this.settings,
      translate: this.translate.bind(this),
    });

    /**
     * Modifies the raw HTML of the rule's value dropdown (in case of a "select filter)
     * @event changer:getRuleValueSelect
     * @memberof QueryBuilder
     * @param {string} html
     * @param [string} name
     * @param {Rule} rule
     * @returns {string}
     */
    return this.change('getRuleValueSelect', h, name, rule);
  };

  /**
   * Returns the rule's value HTML
   * @param {Rule} rule
   * @param {int} value_id
   * @returns {string}
   * @fires QueryBuilder.changer:getRuleInput
   * @private
   */
  QueryBuilder.prototype.getRuleInput = function (rule, value_id) {
    var filter = rule.filter;
    var validation = rule.filter.validation || {};
    var name = rule.id + '_value_' + value_id;
    var c = filter.vertical ? ' class=block' : '';
    var h = '';
    var placeholder = Array.isArray(filter.placeholder)
      ? filter.placeholder[value_id]
      : filter.placeholder;

    if (typeof filter.input == 'function') {
      h = filter.input.call(this, rule, name);
    } else {
      switch (filter.input) {
        case 'radio':
        case 'checkbox':
          Utils.iterateOptions(filter.values, function (key, val) {
            h +=
              '<label' +
              c +
              '><input type="' +
              filter.input +
              '" name="' +
              name +
              '" value="' +
              key +
              '"> ' +
              val +
              '</label> ';
          });
          break;

        case 'select':
          h = this.getRuleValueSelect(name, rule);
          break;

        case 'textarea':
          h += '<textarea class="form-control" name="' + name + '"';
          if (filter.size) h += ' cols="' + filter.size + '"';
          if (filter.rows) h += ' rows="' + filter.rows + '"';
          if (validation.min !== undefined)
            h += ' minlength="' + validation.min + '"';
          if (validation.max !== undefined)
            h += ' maxlength="' + validation.max + '"';
          if (placeholder) h += ' placeholder="' + placeholder + '"';
          h += '></textarea>';
          break;

        case 'number':
          h += '<input class="form-control" type="number" name="' + name + '"';
          if (validation.step !== undefined)
            h += ' step="' + validation.step + '"';
          if (validation.min !== undefined)
            h += ' min="' + validation.min + '"';
          if (validation.max !== undefined)
            h += ' max="' + validation.max + '"';
          if (placeholder) h += ' placeholder="' + placeholder + '"';
          if (filter.size) h += ' size="' + filter.size + '"';
          h += '>';
          break;

        default:
          h += '<input class="form-control" type="text" name="' + name + '"';
          if (placeholder) h += ' placeholder="' + placeholder + '"';
          if (filter.type === 'string' && validation.min !== undefined)
            h += ' minlength="' + validation.min + '"';
          if (filter.type === 'string' && validation.max !== undefined)
            h += ' maxlength="' + validation.max + '"';
          if (filter.size) h += ' size="' + filter.size + '"';
          h += '>';
      }
    }

    /**
     * Modifies the raw HTML of the rule's input
     * @event changer:getRuleInput
     * @memberof QueryBuilder
     * @param {string} html
     * @param {Rule} rule
     * @param {string} name - the name that the input must have
     * @returns {string}
     */
    return this.change('getRuleInput', h, rule, name);
  };

  /**
   * @namespace
   */
  var Utils = {};

  /**
   * @member {object}
   * @memberof QueryBuilder
   * @see Utils
   */
  QueryBuilder.utils = Utils;

  /**
   * @callback Utils#OptionsIteratee
   * @param {string} key
   * @param {string} value
   * @param {string} [optgroup]
   */

  /**
   * Iterates over radio/checkbox/selection options, it accept four formats
   *
   * @example
   * // array of values
   * options = ['one', 'two', 'three']
   * @example
   * // simple key-value map
   * options = {1: 'one', 2: 'two', 3: 'three'}
   * @example
   * // array of 1-element maps
   * options = [{1: 'one'}, {2: 'two'}, {3: 'three'}]
   * @example
   * // array of elements
   * options = [{value: 1, label: 'one', optgroup: 'group'}, {value: 2, label: 'two'}]
   *
   * @param {object|array} options
   * @param {Utils#OptionsIteratee} tpl
   */
  Utils.iterateOptions = function (options, tpl) {
    if (options) {
      if ($.isArray(options)) {
        options.forEach(function (entry) {
          if ($.isPlainObject(entry)) {
            // array of elements
            if ('value' in entry) {
              tpl(entry.value, entry.label || entry.value, entry.optgroup);
            }
            // array of one-element maps
            else {
              $.each(entry, function (key, val) {
                tpl(key, val);
                return false; // break after first entry
              });
            }
          }
          // array of values
          else {
            tpl(entry, entry);
          }
        });
      }
      // unordered map
      else {
        $.each(options, function (key, val) {
          tpl(key, val);
        });
      }
    }
  };

  /**
   * Replaces {0}, {1}, ... in a string
   * @param {string} str
   * @param {...*} args
   * @returns {string}
   */
  Utils.fmt = function (str, args) {
    if (!Array.isArray(args)) {
      args = Array.prototype.slice.call(arguments, 1);
    }

    return str.replace(/{([0-9]+)}/g, function (m, i) {
      return args[parseInt(i)];
    });
  };

  /**
   * Throws an Error object with custom name or logs an error
   * @param {boolean} [doThrow=true]
   * @param {string} type
   * @param {string} message
   * @param {...*} args
   */
  Utils.error = function () {
    var i = 0;
    var doThrow = typeof arguments[i] === 'boolean' ? arguments[i++] : true;
    var type = arguments[i++];
    var message = arguments[i++];
    var args = Array.isArray(arguments[i])
      ? arguments[i]
      : Array.prototype.slice.call(arguments, i);

    if (doThrow) {
      var err = new Error(Utils.fmt(message, args));
      err.name = type + 'Error';
      err.args = args;
      throw err;
    } else {
      console.error(type + 'Error: ' + Utils.fmt(message, args));
    }
  };

  /**
   * Changes the type of a value to int, float or bool
   * @param {*} value
   * @param {string} type - 'integer', 'double', 'boolean' or anything else (passthrough)
   * @returns {*}
   */
  Utils.changeType = function (value, type) {
    if (value === '' || value === undefined) {
      return undefined;
    }

    switch (type) {
      // @formatter:off
      case 'integer':
        if (typeof value === 'string' && !/^-?\d+$/.test(value)) {
          return value;
        }
        return parseInt(value);
      case 'double':
        if (typeof value === 'string' && !/^-?\d+\.?\d*$/.test(value)) {
          return value;
        }
        return parseFloat(value);
      case 'boolean':
        if (
          typeof value === 'string' &&
          !/^(0|1|true|false){1}$/i.test(value)
        ) {
          return value;
        }
        return (
          value === true ||
          value === 1 ||
          value.toLowerCase() === 'true' ||
          value === '1'
        );
      default:
        return value;
      // @formatter:on
    }
  };

  /**
   * Escapes a string like PHP's mysql_real_escape_string does
   * @param {string} value
   * @param {string} [additionalEscape] additionnal chars to escape
   * @returns {string}
   */
  Utils.escapeString = function (value, additionalEscape) {
    if (typeof value != 'string') {
      return value;
    }

    var escaped = value
      .replace(/[\0\n\r\b\\\'\"]/g, function (s) {
        switch (s) {
          // @formatter:off
          case '\0':
            return '\\0';
          case '\n':
            return '\\n';
          case '\r':
            return '\\r';
          case '\b':
            return '\\b';
          case "'":
            return "''";
          default:
            return '\\' + s;
          // @formatter:off
        }
      })
      // uglify compliant
      .replace(/\t/g, '\\t')
      .replace(/\x1a/g, '\\Z');

    if (additionalEscape) {
      escaped = escaped.replace(
        new RegExp('[' + additionalEscape + ']', 'g'),
        function (s) {
          return '\\' + s;
        }
      );
    }

    return escaped;
  };

  /**
   * Escapes a string for use in regex
   * @param {string} str
   * @returns {string}
   */
  Utils.escapeRegExp = function (str) {
    return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
  };

  /**
   * Escapes a string for use in HTML element id
   * @param {string} str
   * @returns {string}
   */
  Utils.escapeElementId = function (str) {
    // Regex based on that suggested by:
    // https://learn.jquery.com/using-jquery-core/faq/how-do-i-select-an-element-by-an-id-that-has-characters-used-in-css-notation/
    // - escapes : . [ ] ,
    // - avoids escaping already escaped values
    return str
      ? str.replace(/(\\)?([:.\[\],])/g, function ($0, $1, $2) {
          return $1 ? $0 : '\\' + $2;
        })
      : str;
  };

  /**
   * Sorts objects by grouping them by `key`, preserving initial order when possible
   * @param {object[]} items
   * @param {string} key
   * @returns {object[]}
   */
  Utils.groupSort = function (items, key) {
    var optgroups = [];
    var newItems = [];

    items.forEach(function (item) {
      var idx;

      if (item[key]) {
        idx = optgroups.lastIndexOf(item[key]);

        if (idx == -1) {
          idx = optgroups.length;
        } else {
          idx++;
        }
      } else {
        idx = optgroups.length;
      }

      optgroups.splice(idx, 0, item[key]);
      newItems.splice(idx, 0, item);
    });

    return newItems;
  };

  /**
   * Defines properties on an Node prototype with getter and setter.<br>
   *     Update events are emitted in the setter through root Model (if any).<br>
   *     The object must have a `__` object, non enumerable property to store values.
   * @param {function} obj
   * @param {string[]} fields
   */
  Utils.defineModelProperties = function (obj, fields) {
    fields.forEach(function (field) {
      Object.defineProperty(obj.prototype, field, {
        enumerable: true,
        get: function () {
          return this.__[field];
        },
        set: function (value) {
          var previousValue =
            this.__[field] !== null && typeof this.__[field] == 'object'
              ? $.extend({}, this.__[field])
              : this.__[field];

          this.__[field] = value;

          if (this.model !== null) {
            /**
             * After a value of the model changed
             * @event model:update
             * @memberof Model
             * @param {Node} node
             * @param {string} field
             * @param {*} value
             * @param {*} previousValue
             */
            this.model.trigger('update', this, field, value, previousValue);
          }
        },
      });
    });
  };

  /**
   * Main object storing data model and emitting model events
   * @constructor
   */
  function Model() {
    /**
     * @member {Group}
     * @readonly
     */
    this.root = null;

    /**
     * Base for event emitting
     * @member {jQuery}
     * @readonly
     * @private
     */
    this.$ = $(this);
  }

  $.extend(
    Model.prototype,
    /** @lends Model.prototype */ {
      /**
       * Triggers an event on the model
       * @param {string} type
       * @returns {$.Event}
       */
      trigger: function (type) {
        var event = new $.Event(type);
        this.$.triggerHandler(event, Array.prototype.slice.call(arguments, 1));
        return event;
      },

      /**
       * Attaches an event listener on the model
       * @param {string} type
       * @param {function} cb
       * @returns {Model}
       */
      on: function () {
        this.$.on.apply(this.$, Array.prototype.slice.call(arguments));
        return this;
      },

      /**
       * Removes an event listener from the model
       * @param {string} type
       * @param {function} [cb]
       * @returns {Model}
       */
      off: function () {
        this.$.off.apply(this.$, Array.prototype.slice.call(arguments));
        return this;
      },

      /**
       * Attaches an event listener called once on the model
       * @param {string} type
       * @param {function} cb
       * @returns {Model}
       */
      once: function () {
        this.$.one.apply(this.$, Array.prototype.slice.call(arguments));
        return this;
      },
    }
  );

  /**
   * Root abstract object
   * @constructor
   * @param {Node} [parent]
   * @param {jQuery} $el
   */
  var Node = function (parent, $el) {
    if (!(this instanceof Node)) {
      return new Node(parent, $el);
    }

    Object.defineProperty(this, '__', { value: {} });

    $el.data('queryBuilderModel', this);

    /**
     * @name level
     * @member {int}
     * @memberof Node
     * @instance
     * @readonly
     */
    this.__.level = 1;

    /**
     * @name error
     * @member {string}
     * @memberof Node
     * @instance
     */
    this.__.error = null;

    /**
     * @name flags
     * @member {object}
     * @memberof Node
     * @instance
     * @readonly
     */
    this.__.flags = {};

    /**
     * @name data
     * @member {object}
     * @memberof Node
     * @instance
     */
    this.__.data = undefined;

    /**
     * @member {jQuery}
     * @readonly
     */
    this.$el = $el;

    /**
     * @member {string}
     * @readonly
     */
    this.id = $el[0].id;

    /**
     * @member {Model}
     * @readonly
     */
    this.model = null;

    /**
     * @member {Group}
     * @readonly
     */
    this.parent = parent;
  };

  Utils.defineModelProperties(Node, ['level', 'error', 'data', 'flags']);

  Object.defineProperty(Node.prototype, 'parent', {
    enumerable: true,
    get: function () {
      return this.__.parent;
    },
    set: function (value) {
      this.__.parent = value;
      this.level = value === null ? 1 : value.level + 1;
      this.model = value === null ? null : value.model;
    },
  });

  /**
   * Checks if this Node is the root
   * @returns {boolean}
   */
  Node.prototype.isRoot = function () {
    return this.level === 1;
  };

  /**
   * Returns the node position inside its parent
   * @returns {int}
   */
  Node.prototype.getPos = function () {
    if (this.isRoot()) {
      return -1;
    } else {
      return this.parent.getNodePos(this);
    }
  };

  /**
   * Deletes self
   * @fires Model.model:drop
   */
  Node.prototype.drop = function () {
    var model = this.model;

    if (!!this.parent) {
      this.parent.removeNode(this);
    }

    this.$el.removeData('queryBuilderModel');

    if (model !== null) {
      /**
       * After a node of the model has been removed
       * @event model:drop
       * @memberof Model
       * @param {Node} node
       */
      model.trigger('drop', this);
    }
  };

  /**
   * Moves itself after another Node
   * @param {Node} target
   * @fires Model.model:move
   */
  Node.prototype.moveAfter = function (target) {
    if (!this.isRoot()) {
      this.move(target.parent, target.getPos() + 1);
    }
  };

  /**
   * Moves itself at the beginning of parent or another Group
   * @param {Group} [target]
   * @fires Model.model:move
   */
  Node.prototype.moveAtBegin = function (target) {
    if (!this.isRoot()) {
      if (target === undefined) {
        target = this.parent;
      }

      this.move(target, 0);
    }
  };

  /**
   * Moves itself at the end of parent or another Group
   * @param {Group} [target]
   * @fires Model.model:move
   */
  Node.prototype.moveAtEnd = function (target) {
    if (!this.isRoot()) {
      if (target === undefined) {
        target = this.parent;
      }

      this.move(target, target.length() === 0 ? 0 : target.length() - 1);
    }
  };

  /**
   * Moves itself at specific position of Group
   * @param {Group} target
   * @param {int} index
   * @fires Model.model:move
   */
  Node.prototype.move = function (target, index) {
    if (!this.isRoot()) {
      if (typeof target === 'number') {
        index = target;
        target = this.parent;
      }

      this.parent.removeNode(this);
      target.insertNode(this, index, false);

      if (this.model !== null) {
        /**
         * After a node of the model has been moved
         * @event model:move
         * @memberof Model
         * @param {Node} node
         * @param {Node} target
         * @param {int} index
         */
        this.model.trigger('move', this, target, index);
      }
    }
  };

  /**
   * Group object
   * @constructor
   * @extends Node
   * @param {Group} [parent]
   * @param {jQuery} $el
   */
  var Group = function (parent, $el) {
    if (!(this instanceof Group)) {
      return new Group(parent, $el);
    }

    Node.call(this, parent, $el);

    /**
     * @member {object[]}
     * @readonly
     */
    this.rules = [];

    /**
     * @name condition
     * @member {string}
     * @memberof Group
     * @instance
     */
    this.__.condition = null;
  };

  Group.prototype = Object.create(Node.prototype);
  Group.prototype.constructor = Group;

  Utils.defineModelProperties(Group, ['condition']);

  /**
   * Removes group's content
   */
  Group.prototype.empty = function () {
    this.each(
      'reverse',
      function (rule) {
        rule.drop();
      },
      function (group) {
        group.drop();
      }
    );
  };

  /**
   * Deletes self
   */
  Group.prototype.drop = function () {
    this.empty();
    Node.prototype.drop.call(this);
  };

  /**
   * Returns the number of children
   * @returns {int}
   */
  Group.prototype.length = function () {
    return this.rules.length;
  };

  /**
   * Adds a Node at specified index
   * @param {Node} node
   * @param {int} [index=end]
   * @param {boolean} [trigger=false] - fire 'add' event
   * @returns {Node} the inserted node
   * @fires Model.model:add
   */
  Group.prototype.insertNode = function (node, index, trigger) {
    if (index === undefined) {
      index = this.length();
    }

    this.rules.splice(index, 0, node);
    node.parent = this;

    if (trigger && this.model !== null) {
      /**
       * After a node of the model has been added
       * @event model:add
       * @memberof Model
       * @param {Node} parent
       * @param {Node} node
       * @param {int} index
       */
      this.model.trigger('add', this, node, index);
    }

    return node;
  };

  /**
   * Adds a new Group at specified index
   * @param {jQuery} $el
   * @param {int} [index=end]
   * @returns {Group}
   * @fires Model.model:add
   */
  Group.prototype.addGroup = function ($el, index) {
    return this.insertNode(new Group(this, $el), index, true);
  };

  /**
   * Adds a new Rule at specified index
   * @param {jQuery} $el
   * @param {int} [index=end]
   * @returns {Rule}
   * @fires Model.model:add
   */
  Group.prototype.addRule = function ($el, index) {
    return this.insertNode(new Rule(this, $el), index, true);
  };

  /**
   * Deletes a specific Node
   * @param {Node} node
   */
  Group.prototype.removeNode = function (node) {
    var index = this.getNodePos(node);
    if (index !== -1) {
      node.parent = null;
      this.rules.splice(index, 1);
    }
  };

  /**
   * Returns the position of a child Node
   * @param {Node} node
   * @returns {int}
   */
  Group.prototype.getNodePos = function (node) {
    return this.rules.indexOf(node);
  };

  /**
   * @callback Model#GroupIteratee
   * @param {Node} node
   * @returns {boolean} stop the iteration
   */

  /**
   * Iterate over all Nodes
   * @param {boolean} [reverse=false] - iterate in reverse order, required if you delete nodes
   * @param {Model#GroupIteratee} cbRule - callback for Rules (can be `null` but not omitted)
   * @param {Model#GroupIteratee} [cbGroup] - callback for Groups
   * @param {object} [context] - context for callbacks
   * @returns {boolean} if the iteration has been stopped by a callback
   */
  Group.prototype.each = function (reverse, cbRule, cbGroup, context) {
    if (typeof reverse !== 'boolean' && typeof reverse !== 'string') {
      context = cbGroup;
      cbGroup = cbRule;
      cbRule = reverse;
      reverse = false;
    }
    context = context === undefined ? null : context;

    var i = reverse ? this.rules.length - 1 : 0;
    var l = reverse ? 0 : this.rules.length - 1;
    var c = reverse ? -1 : 1;
    var next = function () {
      return reverse ? i >= l : i <= l;
    };
    var stop = false;

    for (; next(); i += c) {
      if (this.rules[i] instanceof Group) {
        if (!!cbGroup) {
          stop = cbGroup.call(context, this.rules[i]) === false;
        }
      } else if (!!cbRule) {
        stop = cbRule.call(context, this.rules[i]) === false;
      }

      if (stop) {
        break;
      }
    }

    return !stop;
  };

  /**
   * Checks if the group contains a particular Node
   * @param {Node} node
   * @param {boolean} [recursive=false]
   * @returns {boolean}
   */
  Group.prototype.contains = function (node, recursive) {
    if (this.getNodePos(node) !== -1) {
      return true;
    } else if (!recursive) {
      return false;
    } else {
      // the loop will return with false as soon as the Node is found
      return !this.each(
        function () {
          return true;
        },
        function (group) {
          return !group.contains(node, true);
        }
      );
    }
  };

  /**
   * Rule object
   * @constructor
   * @extends Node
   * @param {Group} parent
   * @param {jQuery} $el
   */
  var Rule = function (parent, $el) {
    if (!(this instanceof Rule)) {
      return new Rule(parent, $el);
    }

    Node.call(this, parent, $el);

    this._updating_value = false;
    this._updating_input = false;

    /**
     * @name filter
     * @member {QueryBuilder.Filter}
     * @memberof Rule
     * @instance
     */
    this.__.filter = null;

    /**
     * @name operator
     * @member {QueryBuilder.Operator}
     * @memberof Rule
     * @instance
     */
    this.__.operator = null;

    /**
     * @name value
     * @member {*}
     * @memberof Rule
     * @instance
     */
    this.__.value = undefined;
  };

  Rule.prototype = Object.create(Node.prototype);
  Rule.prototype.constructor = Rule;

  Utils.defineModelProperties(Rule, ['filter', 'operator', 'value']);

  /**
   * Checks if this Node is the root
   * @returns {boolean} always false
   */
  Rule.prototype.isRoot = function () {
    return false;
  };

  /**
   * @member {function}
   * @memberof QueryBuilder
   * @see Group
   */
  QueryBuilder.Group = Group;

  /**
   * @member {function}
   * @memberof QueryBuilder
   * @see Rule
   */
  QueryBuilder.Rule = Rule;

  /**
   * The {@link http://learn.jquery.com/plugins/|jQuery Plugins} namespace
   * @external "jQuery.fn"
   */

  /**
   * Instanciates or accesses the {@link QueryBuilder} on an element
   * @function
   * @memberof external:"jQuery.fn"
   * @param {*} option - initial configuration or method name
   * @param {...*} args - method arguments
   *
   * @example
   * $('#builder').queryBuilder({ /** configuration object *\/ });
   * @example
   * $('#builder').queryBuilder('methodName', methodParam1, methodParam2);
   */
  $.fn.queryBuilder = function (option) {
    if (this.length === 0) {
      Utils.error('Config', 'No target defined');
    }
    if (this.length > 1) {
      Utils.error('Config', 'Unable to initialize on multiple target');
    }

    var data = this.data('queryBuilder');
    var options = (typeof option == 'object' && option) || {};

    if (!data && option == 'destroy') {
      return this;
    }
    if (!data) {
      var builder = new QueryBuilder(this, options);
      this.data('queryBuilder', builder);
      builder.init(options.rules);
    }
    if (typeof option == 'string') {
      return data[option].apply(data, Array.prototype.slice.call(arguments, 1));
    }

    return this;
  };

  /**
   * @function
   * @memberof external:"jQuery.fn"
   * @see QueryBuilder
   */
  $.fn.queryBuilder.constructor = QueryBuilder;

  /**
   * @function
   * @memberof external:"jQuery.fn"
   * @see QueryBuilder.defaults
   */
  $.fn.queryBuilder.defaults = QueryBuilder.defaults;

  /**
   * @function
   * @memberof external:"jQuery.fn"
   * @see QueryBuilder.defaults
   */
  $.fn.queryBuilder.extend = QueryBuilder.extend;

  /**
   * @function
   * @memberof external:"jQuery.fn"
   * @see QueryBuilder.define
   */
  $.fn.queryBuilder.define = QueryBuilder.define;

  /**
   * @function
   * @memberof external:"jQuery.fn"
   * @see QueryBuilder.regional
   */
  $.fn.queryBuilder.regional = QueryBuilder.regional;

  /**
   * @class BtCheckbox
   * @memberof module:plugins
   * @description Applies Awesome Bootstrap Checkbox for checkbox and radio inputs.
   * @param {object} [options]
   * @param {string} [options.font='glyphicons']
   * @param {string} [options.color='default']
   */
  QueryBuilder.define(
    'bt-checkbox',
    function (options) {
      if (options.font == 'glyphicons') {
        this.$el.addClass('bt-checkbox-glyphicons');
      }

      this.on('getRuleInput.filter', function (h, rule, name) {
        var filter = rule.filter;

        if (
          (filter.input === 'radio' || filter.input === 'checkbox') &&
          !filter.plugin
        ) {
          h.value = '';

          if (!filter.colors) {
            filter.colors = {};
          }
          if (filter.color) {
            filter.colors._def_ = filter.color;
          }

          var style = filter.vertical ? ' style="display:block"' : '';
          var i = 0;

          Utils.iterateOptions(filter.values, function (key, val) {
            var color =
              filter.colors[key] || filter.colors._def_ || options.color;
            var id = name + '_' + i++;

            h.value +=
              '\
<div' +
              style +
              ' class="' +
              filter.input +
              ' ' +
              filter.input +
              '-' +
              color +
              '"> \
<input type="' +
              filter.input +
              '" name="' +
              name +
              '" id="' +
              id +
              '" value="' +
              key +
              '"> \
<label for="' +
              id +
              '">' +
              val +
              '</label> \
</div>';
          });
        }
      });
    },
    {
      font: 'glyphicons',
      color: 'default',
    }
  );

  /**
   * @class BtSelectpicker
   * @memberof module:plugins
   * @descriptioon Applies Bootstrap Select on filters and operators combo-boxes.
   * @param {object} [options]
   * @param {string} [options.container='body']
   * @param {string} [options.style='btn-inverse btn-xs']
   * @param {int|string} [options.width='auto']
   * @param {boolean} [options.showIcon=false]
   * @throws MissingLibraryError
   */
  QueryBuilder.define(
    'bt-selectpicker',
    function (options) {
      if (!$.fn.selectpicker || !$.fn.selectpicker.Constructor) {
        Utils.error(
          'MissingLibrary',
          'Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select'
        );
      }

      var Selectors = QueryBuilder.selectors;

      // init selectpicker
      this.on('afterCreateRuleFilters', function (e, rule) {
        rule.$el
          .find(Selectors.rule_filter)
          .removeClass('form-control')
          .selectpicker(options);
      });

      this.on('afterCreateRuleOperators', function (e, rule) {
        rule.$el
          .find(Selectors.rule_operator)
          .removeClass('form-control')
          .selectpicker(options);
      });

      // update selectpicker on change
      this.on('afterUpdateRuleFilter', function (e, rule) {
        rule.$el.find(Selectors.rule_filter).selectpicker('render');
      });

      this.on('afterUpdateRuleOperator', function (e, rule) {
        rule.$el.find(Selectors.rule_operator).selectpicker('render');
      });

      this.on('beforeDeleteRule', function (e, rule) {
        rule.$el.find(Selectors.rule_filter).selectpicker('destroy');
        rule.$el.find(Selectors.rule_operator).selectpicker('destroy');
      });
    },
    {
      container: 'body',
      style: 'btn-inverse btn-xs',
      width: 'auto',
      showIcon: false,
    }
  );

  /**
   * @class BtTooltipErrors
   * @memberof module:plugins
   * @description Applies Bootstrap Tooltips on validation error messages.
   * @param {object} [options]
   * @param {string} [options.placement='right']
   * @throws MissingLibraryError
   */
  QueryBuilder.define(
    'bt-tooltip-errors',
    function (options) {
      if (
        !$.fn.tooltip ||
        !$.fn.tooltip.Constructor ||
        !$.fn.tooltip.Constructor.prototype.fixTitle
      ) {
        Utils.error(
          'MissingLibrary',
          'Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com'
        );
      }

      var self = this;

      // add BT Tooltip data
      this.on('getRuleTemplate.filter getGroupTemplate.filter', function (h) {
        var $h = $($.parseHTML(h.value));
        $h.find(QueryBuilder.selectors.error_container).attr(
          'data-toggle',
          'tooltip'
        );
        h.value = $h.prop('outerHTML');
      });

      // init/refresh tooltip when title changes
      this.model.on('update', function (e, node, field) {
        if (field == 'error' && self.settings.display_errors) {
          node.$el
            .find(QueryBuilder.selectors.error_container)
            .eq(0)
            .tooltip(options)
            .tooltip('hide')
            .tooltip('fixTitle');
        }
      });
    },
    {
      placement: 'right',
    }
  );

  /**
   * @class ChangeFilters
   * @memberof module:plugins
   * @description Allows to change available filters after plugin initialization.
   */

  QueryBuilder.extend(
    /** @lends module:plugins.ChangeFilters.prototype */ {
      /**
       * Change the filters of the builder
       * @param {boolean} [deleteOrphans=false] - delete rules using old filters
       * @param {QueryBuilder[]} filters
       * @fires module:plugins.ChangeFilters.changer:setFilters
       * @fires module:plugins.ChangeFilters.afterSetFilters
       * @throws ChangeFilterError
       */
      setFilters: function (deleteOrphans, filters) {
        var self = this;

        if (filters === undefined) {
          filters = deleteOrphans;
          deleteOrphans = false;
        }

        filters = this.checkFilters(filters);

        /**
         * Modifies the filters before {@link module:plugins.ChangeFilters.setFilters} method
         * @event changer:setFilters
         * @memberof module:plugins.ChangeFilters
         * @param {QueryBuilder.Filter[]} filters
         * @returns {QueryBuilder.Filter[]}
         */
        filters = this.change('setFilters', filters);

        var filtersIds = filters.map(function (filter) {
          return filter.id;
        });

        // check for orphans
        if (!deleteOrphans) {
          (function checkOrphans(node) {
            node.each(function (rule) {
              if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
                Utils.error(
                  'ChangeFilter',
                  'A rule is using filter "{0}"',
                  rule.filter.id
                );
              }
            }, checkOrphans);
          })(this.model.root);
        }

        // replace filters
        this.filters = filters;

        // apply on existing DOM
        (function updateBuilder(node) {
          node.each(
            true,
            function (rule) {
              if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
                rule.drop();

                self.trigger('rulesChanged');
              } else {
                self.createRuleFilters(rule);

                rule.$el
                  .find(QueryBuilder.selectors.rule_filter)
                  .val(rule.filter ? rule.filter.id : '-1');
                self.trigger('afterUpdateRuleFilter', rule);
              }
            },
            updateBuilder
          );
        })(this.model.root);

        // update plugins
        if (this.settings.plugins) {
          if (this.settings.plugins['unique-filter']) {
            this.updateDisabledFilters();
          }
          if (this.settings.plugins['bt-selectpicker']) {
            this.$el
              .find(QueryBuilder.selectors.rule_filter)
              .selectpicker('render');
          }
					// select2 if 구문 추가
          if (this.settings.plugins['select2']) {
            this.$el
              .find(QueryBuilder.selectors.rule_filter)
              .trigger('change.select2');
          }
        }

        // reset the default_filter if does not exist anymore
        if (this.settings.default_filter) {
          try {
            this.getFilterById(this.settings.default_filter);
          } catch (e) {
            this.settings.default_filter = null;
          }
        }

        /**
         * After {@link module:plugins.ChangeFilters.setFilters} method
         * @event afterSetFilters
         * @memberof module:plugins.ChangeFilters
         * @param {QueryBuilder.Filter[]} filters
         */
        this.trigger('afterSetFilters', filters);
      },

      /**
       * Adds a new filter to the builder
       * @param {QueryBuilder.Filter|Filter[]} newFilters
       * @param {int|string} [position=#end] - index or '#start' or '#end'
       * @fires module:plugins.ChangeFilters.changer:setFilters
       * @fires module:plugins.ChangeFilters.afterSetFilters
       * @throws ChangeFilterError
       */
      addFilter: function (newFilters, position) {
        if (position === undefined || position == '#end') {
          position = this.filters.length;
        } else if (position == '#start') {
          position = 0;
        }

        if (!$.isArray(newFilters)) {
          newFilters = [newFilters];
        }

        var filters = $.extend(true, [], this.filters);

        // numeric position
        if (parseInt(position) == position) {
          Array.prototype.splice.apply(
            filters,
            [position, 0].concat(newFilters)
          );
        } else {
          // after filter by its id
          if (
            this.filters.some(function (filter, index) {
              if (filter.id == position) {
                position = index + 1;
                return true;
              }
            })
          ) {
            Array.prototype.splice.apply(
              filters,
              [position, 0].concat(newFilters)
            );
          }
          // defaults to end of list
          else {
            Array.prototype.push.apply(filters, newFilters);
          }
        }

        this.setFilters(filters);
      },

      /**
       * Removes a filter from the builder
       * @param {string|string[]} filterIds
       * @param {boolean} [deleteOrphans=false] delete rules using old filters
       * @fires module:plugins.ChangeFilters.changer:setFilters
       * @fires module:plugins.ChangeFilters.afterSetFilters
       * @throws ChangeFilterError
       */
      removeFilter: function (filterIds, deleteOrphans) {
        var filters = $.extend(true, [], this.filters);
        if (typeof filterIds === 'string') {
          filterIds = [filterIds];
        }

        filters = filters.filter(function (filter) {
          return filterIds.indexOf(filter.id) === -1;
        });

        this.setFilters(deleteOrphans, filters);
      },
    }
  );

  /**
	 * select2 플러그인 선언
   * @class Select2
   * @memberof module:plugins
   * @descriptioon Applies Select2 on filters and operators combo-boxes.
   * @param {object} [options]
   * @throws MissingLibraryError
   */

  QueryBuilder.define(
    'select2',
    function (options) {
      if (!$.fn.select2) {
        Utils.error(
          'MissingLibrary',
          'Select2 is required to use "select2" plugin.'
        );
      }
			
			if (this.settings.plugins['select2']) {
      Utils.error(
        'Conflict',
        'select2 is already selected as the dropdown plugin. Please remove select2 from the plugin list'
      );
    }
			
      var Selectors = QueryBuilder.selectors;

      // init select2
      this.on('afterCreateRuleFilters', function (e, rule) {
        rule.$el
          .find(Selectors.rule_filter)
          .removeClass('form-control')
          .select2(options);
      });

      this.on('afterCreateRuleOperators', function (e, rule) {
        rule.$el
          .find(Selectors.rule_operator)
          .removeClass('form-control')
          .select2(options);
      });

      // update selectpicker on change
      this.on('afterUpdateRuleFilter', function (e, rule) {
        rule.$el.find(Selectors.rule_filter).trigger('change.select2');
      });

      this.on('afterUpdateRuleOperator', function (e, rule) {
        rule.$el.find(Selectors.rule_operator).trigger('change.select2');
      });

      this.on('beforeDeleteRule', function (e, rule) {
        rule.$el.find(Selectors.rule_filter).select2('destroy');
        rule.$el.find(Selectors.rule_operator).select2('destroy');
      });
    }
  );

  /**
   * @class ChosenSelectpicker
   * @memberof module:plugins
   * @descriptioon Applies chosen-js Select on filters and operators combo-boxes.
   * @param {object} [options] Supports all the options for chosen
   * @throws MissingLibraryError
   */
  QueryBuilder.define('chosen-selectpicker', function (options) {
    if (!$.fn.chosen) {
      Utils.error(
        'MissingLibrary',
        'chosen is required to use "chosen-selectpicker" plugin. Get it here: https://github.com/harvesthq/chosen'
      );
    }

    if (this.settings.plugins['bt-selectpicker']) {
      Utils.error(
        'Conflict',
        'bt-selectpicker is already selected as the dropdown plugin. Please remove chosen-selectpicker from the plugin list'
      );
    }

    var Selectors = QueryBuilder.selectors;

    // init selectpicker
    this.on('afterCreateRuleFilters', function (e, rule) {
      rule.$el
        .find(Selectors.rule_filter)
        .removeClass('form-control')
        .chosen(options);
    });

    this.on('afterCreateRuleOperators', function (e, rule) {
      if (e.builder.getOperators(rule.filter).length > 1) {
        rule.$el
          .find(Selectors.rule_operator)
          .removeClass('form-control')
          .chosen(options);
      }
    });

    // update selectpicker on change
    this.on('afterUpdateRuleFilter', function (e, rule) {
      rule.$el.find(Selectors.rule_filter).trigger('chosen:updated');
    });

    this.on('afterUpdateRuleOperator', function (e, rule) {
      rule.$el.find(Selectors.rule_operator).trigger('chosen:updated');
    });

    this.on('beforeDeleteRule', function (e, rule) {
      rule.$el.find(Selectors.rule_filter).chosen('destroy');
      rule.$el.find(Selectors.rule_operator).chosen('destroy');
    });
  });

  /**
   * @class FilterDescription
   * @memberof module:plugins
   * @description Provides three ways to display a description about a filter: inline, Bootsrap Popover or Bootbox.
   * @param {object} [options]
   * @param {string} [options.icon='glyphicon glyphicon-info-sign']
   * @param {string} [options.mode='popover'] - inline, popover or bootbox
   * @throws ConfigError
   */
  QueryBuilder.define(
    'filter-description',
    function (options) {
      // INLINE
      if (options.mode === 'inline') {
        this.on(
          'afterUpdateRuleFilter afterUpdateRuleOperator',
          function (e, rule) {
            var $p = rule.$el.find('p.filter-description');
            var description = e.builder.getFilterDescription(rule.filter, rule);

            if (!description) {
              $p.hide();
            } else {
              if ($p.length === 0) {
                $p = $($.parseHTML('<p class="filter-description"></p>'));
                $p.appendTo(rule.$el);
              } else {
                $p.css('display', '');
              }

              $p.html('<i class="' + options.icon + '"></i> ' + description);
            }
          }
        );
      }
      // POPOVER
      else if (options.mode === 'popover') {
        if (
          !$.fn.popover ||
          !$.fn.popover.Constructor ||
          !$.fn.popover.Constructor.prototype.fixTitle
        ) {
          Utils.error(
            'MissingLibrary',
            'Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com'
          );
        }

        this.on(
          'afterUpdateRuleFilter afterUpdateRuleOperator',
          function (e, rule) {
            var $b = rule.$el.find('button.filter-description');
            var description = e.builder.getFilterDescription(rule.filter, rule);

            if (!description) {
              $b.hide();

              if ($b.data('bs.popover')) {
                $b.popover('hide');
              }
            } else {
              if ($b.length === 0) {
                $b = $(
                  $.parseHTML(
                    '<button type="button" class="btn btn-xs btn-info filter-description" data-toggle="popover"><i class="' +
                      options.icon +
                      '"></i></button>'
                  )
                );
                $b.prependTo(
                  rule.$el.find(QueryBuilder.selectors.rule_actions)
                );

                $b.popover({
                  placement: 'left',
                  container: 'body',
                  html: true,
                });

                $b.on('mouseout', function () {
                  $b.popover('hide');
                });
              } else {
                $b.css('display', '');
              }

              $b.data('bs.popover').options.content = description;

              if ($b.attr('aria-describedby')) {
                $b.popover('show');
              }
            }
          }
        );
      }
      // BOOTBOX
      else if (options.mode === 'bootbox') {
        if (!('bootbox' in window)) {
          Utils.error(
            'MissingLibrary',
            'Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com'
          );
        }

        this.on(
          'afterUpdateRuleFilter afterUpdateRuleOperator',
          function (e, rule) {
            var $b = rule.$el.find('button.filter-description');
            var description = e.builder.getFilterDescription(rule.filter, rule);

            if (!description) {
              $b.hide();
            } else {
              if ($b.length === 0) {
                $b = $(
                  $.parseHTML(
                    '<button type="button" class="btn btn-xs btn-info filter-description" data-toggle="bootbox"><i class="' +
                      options.icon +
                      '"></i></button>'
                  )
                );
                $b.prependTo(
                  rule.$el.find(QueryBuilder.selectors.rule_actions)
                );

                $b.on('click', function () {
                  bootbox.alert($b.data('description'));
                });
              } else {
                $b.css('display', '');
              }

              $b.data('description', description);
            }
          }
        );
      }
    },
    {
      icon: 'glyphicon glyphicon-info-sign',
      mode: 'popover',
    }
  );

  QueryBuilder.extend(
    /** @lends module:plugins.FilterDescription.prototype */ {
      /**
       * Returns the description of a filter for a particular rule (if present)
       * @param {object} filter
       * @param {Rule} [rule]
       * @returns {string}
       * @private
       */
      getFilterDescription: function (filter, rule) {
        if (!filter) {
          return undefined;
        } else if (typeof filter.description == 'function') {
          return filter.description.call(this, rule);
        } else {
          return filter.description;
        }
      },
    }
  );

  /**
   * @class Invert
   * @memberof module:plugins
   * @description Allows to invert a rule operator, a group condition or the entire builder.
   * @param {object} [options]
   * @param {string} [options.icon='glyphicon glyphicon-random']
   * @param {boolean} [options.recursive=true]
   * @param {boolean} [options.invert_rules=true]
   * @param {boolean} [options.display_rules_button=false]
   * @param {boolean} [options.silent_fail=false]
   */
  QueryBuilder.define(
    'invert',
    function (options) {
      var self = this;
      var Selectors = QueryBuilder.selectors;

      // Bind events
      this.on('afterInit', function () {
        self.$el.on('click.queryBuilder', '[data-invert=group]', function () {
          var $group = $(this).closest(Selectors.group_container);
          self.invert(self.getModel($group), options);
        });

        if (options.display_rules_button && options.invert_rules) {
          self.$el.on('click.queryBuilder', '[data-invert=rule]', function () {
            var $rule = $(this).closest(Selectors.rule_container);
            self.invert(self.getModel($rule), options);
          });
        }
      });

      // Modify templates
      if (!options.disable_template) {
        this.on('getGroupTemplate.filter', function (h) {
          var $h = $($.parseHTML(h.value));
          $h.find(Selectors.condition_container).after(
            '<button type="button" class="btn btn-xs btn-default" data-invert="group">' +
              '<i class="' +
              options.icon +
              '"></i> ' +
              self.translate('invert') +
              '</button>'
          );
          h.value = $h.prop('outerHTML');
        });

        if (options.display_rules_button && options.invert_rules) {
          this.on('getRuleTemplate.filter', function (h) {
            var $h = $($.parseHTML(h.value));
            $h.find(Selectors.rule_actions).prepend(
              '<button type="button" class="btn btn-xs btn-default" data-invert="rule">' +
                '<i class="' +
                options.icon +
                '"></i> ' +
                self.translate('invert') +
                '</button>'
            );
            h.value = $h.prop('outerHTML');
          });
        }
      }
    },
    {
      icon: 'glyphicon glyphicon-random',
      recursive: true,
      invert_rules: true,
      display_rules_button: false,
      silent_fail: false,
      disable_template: false,
    }
  );

  QueryBuilder.defaults({
    operatorOpposites: {
      equal: 'not_equal',
      not_equal: 'equal',
      in: 'not_in',
      not_in: 'in',
      less: 'greater_or_equal',
      less_or_equal: 'greater',
      greater: 'less_or_equal',
      greater_or_equal: 'less',
      between: 'not_between',
      not_between: 'between',
      begins_with: 'not_begins_with',
      not_begins_with: 'begins_with',
      contains: 'not_contains',
      not_contains: 'contains',
      ends_with: 'not_ends_with',
      not_ends_with: 'ends_with',
      is_empty: 'is_not_empty',
      is_not_empty: 'is_empty',
      is_null: 'is_not_null',
      is_not_null: 'is_null',
    },

    conditionOpposites: {
      AND: 'OR',
      OR: 'AND',
    },
  });

  QueryBuilder.extend(
    /** @lends module:plugins.Invert.prototype */ {
      /**
       * Invert a Group, a Rule or the whole builder
       * @param {Node} [node]
       * @param {object} [options] {@link module:plugins.Invert}
       * @fires module:plugins.Invert.afterInvert
       * @throws InvertConditionError, InvertOperatorError
       */
      invert: function (node, options) {
        if (!(node instanceof Node)) {
          if (!this.model.root) return;
          options = node;
          node = this.model.root;
        }

        if (typeof options != 'object') options = {};
        if (options.recursive === undefined) options.recursive = true;
        if (options.invert_rules === undefined) options.invert_rules = true;
        if (options.silent_fail === undefined) options.silent_fail = false;
        if (options.trigger === undefined) options.trigger = true;

        if (node instanceof Group) {
          // invert group condition
          if (this.settings.conditionOpposites[node.condition]) {
            node.condition = this.settings.conditionOpposites[node.condition];
          } else if (!options.silent_fail) {
            Utils.error(
              'InvertCondition',
              'Unknown inverse of condition "{0}"',
              node.condition
            );
          }

          // recursive call
          if (options.recursive) {
            var tempOpts = $.extend({}, options, { trigger: false });
            node.each(
              function (rule) {
                if (options.invert_rules) {
                  this.invert(rule, tempOpts);
                }
              },
              function (group) {
                this.invert(group, tempOpts);
              },
              this
            );
          }
        } else if (node instanceof Rule) {
          if (node.operator && !node.filter.no_invert) {
            // invert rule operator
            if (this.settings.operatorOpposites[node.operator.type]) {
              var invert = this.settings.operatorOpposites[node.operator.type];
              // check if the invert is "authorized"
              if (
                !node.filter.operators ||
                node.filter.operators.indexOf(invert) != -1
              ) {
                node.operator = this.getOperatorByType(invert);
              }
            } else if (!options.silent_fail) {
              Utils.error(
                'InvertOperator',
                'Unknown inverse of operator "{0}"',
                node.operator.type
              );
            }
          }
        }

        if (options.trigger) {
          /**
           * After {@link module:plugins.Invert.invert} method
           * @event afterInvert
           * @memberof module:plugins.Invert
           * @param {Node} node - the main group or rule that has been modified
           * @param {object} options
           */
          this.trigger('afterInvert', node, options);

          this.trigger('rulesChanged');
        }
      },
    }
  );

  /**
   * @class MongoDbSupport
   * @memberof module:plugins
   * @description Allows to export rules as a MongoDB find object as well as populating the builder from a MongoDB object.
   */

  QueryBuilder.defaults({
    mongoOperators: {
      // @formatter:off
      equal: function (v) {
        return v[0];
      },
      not_equal: function (v) {
        return { $ne: v[0] };
      },
      in: function (v) {
        return { $in: v };
      },
      not_in: function (v) {
        return { $nin: v };
      },
      less: function (v) {
        return { $lt: v[0] };
      },
      less_or_equal: function (v) {
        return { $lte: v[0] };
      },
      greater: function (v) {
        return { $gt: v[0] };
      },
      greater_or_equal: function (v) {
        return { $gte: v[0] };
      },
      between: function (v) {
        return { $gte: v[0], $lte: v[1] };
      },
      not_between: function (v) {
        return { $lt: v[0], $gt: v[1] };
      },
      begins_with: function (v) {
        return { $regex: '^' + Utils.escapeRegExp(v[0]) };
      },
      not_begins_with: function (v) {
        return { $regex: '^(?!' + Utils.escapeRegExp(v[0]) + ')' };
      },
      contains: function (v) {
        return { $regex: Utils.escapeRegExp(v[0]) };
      },
      not_contains: function (v) {
        return {
          $regex: '^((?!' + Utils.escapeRegExp(v[0]) + ').)*$',
          $options: 's',
        };
      },
      ends_with: function (v) {
        return { $regex: Utils.escapeRegExp(v[0]) + '$' };
      },
      not_ends_with: function (v) {
        return { $regex: '(?<!' + Utils.escapeRegExp(v[0]) + ')$' };
      },
      is_empty: function (v) {
        return '';
      },
      is_not_empty: function (v) {
        return { $ne: '' };
      },
      is_null: function (v) {
        return null;
      },
      is_not_null: function (v) {
        return { $ne: null };
      },
      // @formatter:on
    },

    mongoRuleOperators: {
      $eq: function (v) {
        return {
          val: v,
          op: v === null ? 'is_null' : v === '' ? 'is_empty' : 'equal',
        };
      },
      $ne: function (v) {
        v = v.$ne;
        return {
          val: v,
          op:
            v === null
              ? 'is_not_null'
              : v === ''
              ? 'is_not_empty'
              : 'not_equal',
        };
      },
      $regex: function (v) {
        v = v.$regex;
        if (v.slice(0, 4) == '^(?!' && v.slice(-1) == ')') {
          return { val: v.slice(4, -1), op: 'not_begins_with' };
        } else if (v.slice(0, 5) == '^((?!' && v.slice(-5) == ').)*$') {
          return { val: v.slice(5, -5), op: 'not_contains' };
        } else if (v.slice(0, 4) == '(?<!' && v.slice(-2) == ')$') {
          return { val: v.slice(4, -2), op: 'not_ends_with' };
        } else if (v.slice(-1) == '$') {
          return { val: v.slice(0, -1), op: 'ends_with' };
        } else if (v.slice(0, 1) == '^') {
          return { val: v.slice(1), op: 'begins_with' };
        } else {
          return { val: v, op: 'contains' };
        }
      },
      between: function (v) {
        return { val: [v.$gte, v.$lte], op: 'between' };
      },
      not_between: function (v) {
        return { val: [v.$lt, v.$gt], op: 'not_between' };
      },
      $in: function (v) {
        return { val: v.$in, op: 'in' };
      },
      $nin: function (v) {
        return { val: v.$nin, op: 'not_in' };
      },
      $lt: function (v) {
        return { val: v.$lt, op: 'less' };
      },
      $lte: function (v) {
        return { val: v.$lte, op: 'less_or_equal' };
      },
      $gt: function (v) {
        return { val: v.$gt, op: 'greater' };
      },
      $gte: function (v) {
        return { val: v.$gte, op: 'greater_or_equal' };
      },
    },
  });

  QueryBuilder.extend(
    /** @lends module:plugins.MongoDbSupport.prototype */ {
      /**
       * Returns rules as a MongoDB query
       * @param {object} [data] - current rules by default
       * @returns {object}
       * @fires module:plugins.MongoDbSupport.changer:getMongoDBField
       * @fires module:plugins.MongoDbSupport.changer:ruleToMongo
       * @fires module:plugins.MongoDbSupport.changer:groupToMongo
       * @throws UndefinedMongoConditionError, UndefinedMongoOperatorError
       */
      getMongo: function (data) {
        data = data === undefined ? this.getRules() : data;

        if (!data) {
          return null;
        }

        var self = this;

        return (function parse(group) {
          if (!group.condition) {
            group.condition = self.settings.default_condition;
          }
          if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) {
            Utils.error(
              'UndefinedMongoCondition',
              'Unable to build MongoDB query with condition "{0}"',
              group.condition
            );
          }

          if (!group.rules) {
            return {};
          }

          var parts = [];

          group.rules.forEach(function (rule) {
            if (rule.rules && rule.rules.length > 0) {
              parts.push(parse(rule));
            } else {
              var mdb = self.settings.mongoOperators[rule.operator];
              var ope = self.getOperatorByType(rule.operator);

              if (mdb === undefined) {
                Utils.error(
                  'UndefinedMongoOperator',
                  'Unknown MongoDB operation for operator "{0}"',
                  rule.operator
                );
              }

              if (ope.nb_inputs !== 0) {
                if (!(rule.value instanceof Array)) {
                  rule.value = [rule.value];
                }
              }

              /**
               * Modifies the MongoDB field used by a rule
               * @event changer:getMongoDBField
               * @memberof module:plugins.MongoDbSupport
               * @param {string} field
               * @param {Rule} rule
               * @returns {string}
               */
              var field = self.change('getMongoDBField', rule.field, rule);

              var ruleExpression = {};
              ruleExpression[field] = mdb.call(self, rule.value);

              /**
               * Modifies the MongoDB expression generated for a rul
               * @event changer:ruleToMongo
               * @memberof module:plugins.MongoDbSupport
               * @param {object} expression
               * @param {Rule} rule
               * @param {*} value
               * @param {function} valueWrapper - function that takes the value and adds the operator
               * @returns {object}
               */
              parts.push(
                self.change(
                  'ruleToMongo',
                  ruleExpression,
                  rule,
                  rule.value,
                  mdb
                )
              );
            }
          });

          var groupExpression = {};
          groupExpression['$' + group.condition.toLowerCase()] = parts;

          /**
           * Modifies the MongoDB expression generated for a group
           * @event changer:groupToMongo
           * @memberof module:plugins.MongoDbSupport
           * @param {object} expression
           * @param {Group} group
           * @returns {object}
           */
          return self.change('groupToMongo', groupExpression, group);
        })(data);
      },

      /**
       * Converts a MongoDB query to rules
       * @param {object} query
       * @returns {object}
       * @fires module:plugins.MongoDbSupport.changer:parseMongoNode
       * @fires module:plugins.MongoDbSupport.changer:getMongoDBFieldID
       * @fires module:plugins.MongoDbSupport.changer:mongoToRule
       * @fires module:plugins.MongoDbSupport.changer:mongoToGroup
       * @throws MongoParseError, UndefinedMongoConditionError, UndefinedMongoOperatorError
       */
      getRulesFromMongo: function (query) {
        if (query === undefined || query === null) {
          return null;
        }

        var self = this;

        /**
         * Custom parsing of a MongoDB expression, you can return a sub-part of the expression, or a well formed group or rule JSON
         * @event changer:parseMongoNode
         * @memberof module:plugins.MongoDbSupport
         * @param {object} expression
         * @returns {object} expression, rule or group
         */
        query = self.change('parseMongoNode', query);

        // a plugin returned a group
        if ('rules' in query && 'condition' in query) {
          return query;
        }

        // a plugin returned a rule
        if ('id' in query && 'operator' in query && 'value' in query) {
          return {
            condition: this.settings.default_condition,
            rules: [query],
          };
        }

        var key = self.getMongoCondition(query);
        if (!key) {
          Utils.error('MongoParse', 'Invalid MongoDB query format');
        }

        return (function parse(data, topKey) {
          var rules = data[topKey];
          var parts = [];

          rules.forEach(function (data) {
            // allow plugins to manually parse or handle special cases
            data = self.change('parseMongoNode', data);

            // a plugin returned a group
            if ('rules' in data && 'condition' in data) {
              parts.push(data);
              return;
            }

            // a plugin returned a rule
            if ('id' in data && 'operator' in data && 'value' in data) {
              parts.push(data);
              return;
            }

            var key = self.getMongoCondition(data);
            if (key) {
              parts.push(parse(data, key));
            } else {
              var field = Object.keys(data)[0];
              var value = data[field];

              var operator = self.getMongoOperator(value);
              if (operator === undefined) {
                Utils.error('MongoParse', 'Invalid MongoDB query format');
              }

              var mdbrl = self.settings.mongoRuleOperators[operator];
              if (mdbrl === undefined) {
                Utils.error(
                  'UndefinedMongoOperator',
                  'JSON Rule operation unknown for operator "{0}"',
                  operator
                );
              }

              var opVal = mdbrl.call(self, value);

              var id = self.getMongoDBFieldID(field, value);

              /**
               * Modifies the rule generated from the MongoDB expression
               * @event changer:mongoToRule
               * @memberof module:plugins.MongoDbSupport
               * @param {object} rule
               * @param {object} expression
               * @returns {object}
               */
              var rule = self.change(
                'mongoToRule',
                {
                  id: id,
                  field: field,
                  operator: opVal.op,
                  value: opVal.val,
                },
                data
              );

              parts.push(rule);
            }
          });

          /**
           * Modifies the group generated from the MongoDB expression
           * @event changer:mongoToGroup
           * @memberof module:plugins.MongoDbSupport
           * @param {object} group
           * @param {object} expression
           * @returns {object}
           */
          return self.change(
            'mongoToGroup',
            {
              condition: topKey.replace('$', '').toUpperCase(),
              rules: parts,
            },
            data
          );
        })(query, key);
      },

      /**
       * Sets rules a from MongoDB query
       * @see module:plugins.MongoDbSupport.getRulesFromMongo
       */
      setRulesFromMongo: function (query) {
        this.setRules(this.getRulesFromMongo(query));
      },

      /**
       * Returns a filter identifier from the MongoDB field.
       * Automatically use the only one filter with a matching field, fires a changer otherwise.
       * @param {string} field
       * @param {*} value
       * @fires module:plugins.MongoDbSupport:changer:getMongoDBFieldID
       * @returns {string}
       * @private
       */
      getMongoDBFieldID: function (field, value) {
        var matchingFilters = this.filters.filter(function (filter) {
          return filter.field === field;
        });

        var id;
        if (matchingFilters.length === 1) {
          id = matchingFilters[0].id;
        } else {
          /**
           * Returns a filter identifier from the MongoDB field
           * @event changer:getMongoDBFieldID
           * @memberof module:plugins.MongoDbSupport
           * @param {string} field
           * @param {*} value
           * @returns {string}
           */
          id = this.change('getMongoDBFieldID', field, value);
        }

        return id;
      },

      /**
       * Finds which operator is used in a MongoDB sub-object
       * @param {*} data
       * @returns {string|undefined}
       * @private
       */
      getMongoOperator: function (data) {
        if (data !== null && typeof data === 'object') {
          if (data.$gte !== undefined && data.$lte !== undefined) {
            return 'between';
          }
          if (data.$lt !== undefined && data.$gt !== undefined) {
            return 'not_between';
          }

          var knownKeys = Object.keys(data).filter(
            function (key) {
              return !!this.settings.mongoRuleOperators[key];
            }.bind(this)
          );

          if (knownKeys.length === 1) {
            return knownKeys[0];
          }
        } else {
          return '$eq';
        }
      },

      /**
       * Returns the key corresponding to "$or" or "$and"
       * @param {object} data
       * @returns {string|undefined}
       * @private
       */
      getMongoCondition: function (data) {
        var keys = Object.keys(data);

        for (var i = 0, l = keys.length; i < l; i++) {
          if (
            keys[i].toLowerCase() === '$or' ||
            keys[i].toLowerCase() === '$and'
          ) {
            return keys[i];
          }
        }
      },
    }
  );

  /**
   * @class NotGroup
   * @memberof module:plugins
   * @description Adds a "Not" checkbox in front of group conditions.
   * @param {object} [options]
   * @param {string} [options.icon_checked='glyphicon glyphicon-checked']
   * @param {string} [options.icon_unchecked='glyphicon glyphicon-unchecked']
   */
  QueryBuilder.define(
    'not-group',
    function (options) {
      var self = this;

      // Bind events
      this.on('afterInit', function () {
        self.$el.on('click.queryBuilder', '[data-not=group]', function () {
          var $group = $(this).closest(QueryBuilder.selectors.group_container);
          var group = self.getModel($group);
          group.not = !group.not;
        });

        self.model.on('update', function (e, node, field) {
          if (node instanceof Group && field === 'not') {
            self.updateGroupNot(node);
          }
        });
      });

      // Init "not" property
      this.on('afterAddGroup', function (e, group) {
        group.__.not = false;
      });

      // Modify templates
      if (!options.disable_template) {
        this.on('getGroupTemplate.filter', function (h) {
          var $h = $($.parseHTML(h.value));
          $h.find(QueryBuilder.selectors.condition_container).prepend(
            '<button type="button" class="btn btn-xs btn-default" data-not="group">' +
              '<i class="' +
              options.icon_unchecked +
              '"></i> ' +
              self.translate('NOT') +
              '</button>'
          );
          h.value = $h.prop('outerHTML');
        });
      }

      // Export "not" to JSON
      this.on('groupToJson.filter', function (e, group) {
        e.value.not = group.not;
      });

      // Read "not" from JSON
      this.on('jsonToGroup.filter', function (e, json) {
        e.value.not = !!json.not;
      });

      // Export "not" to SQL
      this.on('groupToSQL.filter', function (e, group) {
        if (group.not) {
          e.value = 'NOT ( ' + e.value + ' )';
        }
      });

      // Parse "NOT" function from sqlparser
      this.on('parseSQLNode.filter', function (e) {
        if (e.value.name && e.value.name.toUpperCase() == 'NOT') {
          e.value = e.value.arguments.value[0];

          // if the there is no sub-group, create one
          if (['AND', 'OR'].indexOf(e.value.operation.toUpperCase()) === -1) {
            e.value = new SQLParser.nodes.Op(
              self.settings.default_condition,
              e.value,
              null
            );
          }

          e.value.not = true;
        }
      });

      // Request to create sub-group if the "not" flag is set
      this.on('sqlGroupsDistinct.filter', function (e, group, data, i) {
        if (data.not && i > 0) {
          e.value = true;
        }
      });

      // Read "not" from parsed SQL
      this.on('sqlToGroup.filter', function (e, data) {
        e.value.not = !!data.not;
      });

      // Export "not" to Mongo
      this.on('groupToMongo.filter', function (e, group) {
        var key = '$' + group.condition.toLowerCase();
        if (group.not && e.value[key]) {
          e.value = { $nor: [e.value] };
        }
      });

      // Parse "$nor" operator from Mongo
      this.on('parseMongoNode.filter', function (e) {
        var keys = Object.keys(e.value);

        if (keys[0] == '$nor') {
          e.value = e.value[keys[0]][0];
          e.value.not = true;
        }
      });

      // Read "not" from parsed Mongo
      this.on('mongoToGroup.filter', function (e, data) {
        e.value.not = !!data.not;
      });
    },
    {
      icon_unchecked: 'glyphicon glyphicon-unchecked',
      icon_checked: 'glyphicon glyphicon-check',
      disable_template: false,
    }
  );

  /**
   * From {@link module:plugins.NotGroup}
   * @name not
   * @member {boolean}
   * @memberof Group
   * @instance
   */
  Utils.defineModelProperties(Group, ['not']);

  QueryBuilder.selectors.group_not =
    QueryBuilder.selectors.group_header + ' [data-not=group]';

  QueryBuilder.extend(
    /** @lends module:plugins.NotGroup.prototype */ {
      /**
       * Performs actions when a group's not changes
       * @param {Group} group
       * @fires module:plugins.NotGroup.afterUpdateGroupNot
       * @private
       */
      updateGroupNot: function (group) {
        var options = this.plugins['not-group'];
        group.$el
          .find('>' + QueryBuilder.selectors.group_not)
          .toggleClass('active', group.not)
          .find('i')
          .attr(
            'class',
            group.not ? options.icon_checked : options.icon_unchecked
          );

        /**
         * After the group's not flag has been modified
         * @event afterUpdateGroupNot
         * @memberof module:plugins.NotGroup
         * @param {Group} group
         */
        this.trigger('afterUpdateGroupNot', group);

        this.trigger('rulesChanged');
      },
    }
  );

  /**
   * @class Sortable
   * @memberof module:plugins
   * @description Enables drag & drop sort of rules.
   * @param {object} [options]
   * @param {boolean} [options.inherit_no_drop=true]
   * @param {boolean} [options.inherit_no_sortable=true]
   * @param {string} [options.icon='glyphicon glyphicon-sort']
   * @throws MissingLibraryError, ConfigError
   */
  QueryBuilder.define(
    'sortable',
    function (options) {
      if (!('interact' in window)) {
        Utils.error(
          'MissingLibrary',
          'interact.js is required to use "sortable" plugin. Get it here: http://interactjs.io'
        );
      }

      if (options.default_no_sortable !== undefined) {
        Utils.error(
          false,
          'Config',
          'Sortable plugin : "default_no_sortable" options is deprecated, use standard "default_rule_flags" and "default_group_flags" instead'
        );
        this.settings.default_rule_flags.no_sortable =
          this.settings.default_group_flags.no_sortable =
            options.default_no_sortable;
      }

      // recompute drop-zones during drag (when a rule is hidden)
      interact.dynamicDrop(true);

      // set move threshold to 10px
      interact.pointerMoveTolerance(10);

      var placeholder;
      var ghost;
      var src;
      var moved;

      // Init drag and drop
      this.on('afterAddRule afterAddGroup', function (e, node) {
        if (node == placeholder) {
          return;
        }

        var self = e.builder;

        // Inherit flags
        if (
          options.inherit_no_sortable &&
          node.parent &&
          node.parent.flags.no_sortable
        ) {
          node.flags.no_sortable = true;
        }
        if (
          options.inherit_no_drop &&
          node.parent &&
          node.parent.flags.no_drop
        ) {
          node.flags.no_drop = true;
        }

        // Configure drag
        if (!node.flags.no_sortable) {
          interact(node.$el[0]).draggable({
            allowFrom: QueryBuilder.selectors.drag_handle,
            onstart: function (event) {
              moved = false;

              // get model of dragged element
              src = self.getModel(event.target);

              // create ghost
              ghost = src.$el
                .clone()
                .appendTo(src.$el.parent())
                .width(src.$el.outerWidth())
                .addClass('dragging');

              // create drop placeholder
              var ph = $(
                $.parseHTML('<div class="rule-placeholder">&nbsp;</div>')
              ).height(src.$el.outerHeight());

              placeholder = src.parent.addRule(ph, src.getPos());

              // hide dragged element
              src.$el.hide();
            },
            onmove: function (event) {
              // make the ghost follow the cursor
              ghost[0].style.top = event.clientY - 15 + 'px';
              ghost[0].style.left = event.clientX - 15 + 'px';
            },
            onend: function (event) {
              // starting from Interact 1.3.3, onend is called before ondrop
              if (event.dropzone) {
                moveSortableToTarget(src, $(event.relatedTarget), self);
                moved = true;
              }

              // remove ghost
              ghost.remove();
              ghost = undefined;

              // remove placeholder
              placeholder.drop();
              placeholder = undefined;

              // show element
              src.$el.css('display', '');

              /**
               * After a node has been moved with {@link module:plugins.Sortable}
               * @event afterMove
               * @memberof module:plugins.Sortable
               * @param {Node} node
               */
              self.trigger('afterMove', src);

              self.trigger('rulesChanged');
            },
          });
        }

        if (!node.flags.no_drop) {
          //  Configure drop on groups and rules
          interact(node.$el[0]).dropzone({
            accept: QueryBuilder.selectors.rule_and_group_containers,
            ondragenter: function (event) {
              moveSortableToTarget(placeholder, $(event.target), self);
            },
            ondrop: function (event) {
              if (!moved) {
                moveSortableToTarget(src, $(event.target), self);
              }
            },
          });

          // Configure drop on group headers
          if (node instanceof Group) {
            interact(
              node.$el.find(QueryBuilder.selectors.group_header)[0]
            ).dropzone({
              accept: QueryBuilder.selectors.rule_and_group_containers,
              ondragenter: function (event) {
                moveSortableToTarget(placeholder, $(event.target), self);
              },
              ondrop: function (event) {
                if (!moved) {
                  moveSortableToTarget(src, $(event.target), self);
                }
              },
            });
          }
        }
      });

      // Detach interactables
      this.on('beforeDeleteRule beforeDeleteGroup', function (e, node) {
        if (!e.isDefaultPrevented()) {
          interact(node.$el[0]).unset();

          if (node instanceof Group) {
            interact(
              node.$el.find(QueryBuilder.selectors.group_header)[0]
            ).unset();
          }
        }
      });

      // Remove drag handle from non-sortable items
      this.on('afterApplyRuleFlags afterApplyGroupFlags', function (e, node) {
        if (node.flags.no_sortable) {
          node.$el.find('.drag-handle').remove();
        }
      });

      // Modify templates
      if (!options.disable_template) {
        this.on('getGroupTemplate.filter', function (h, level) {
          if (level > 1) {
            var $h = $($.parseHTML(h.value));
            $h.find(QueryBuilder.selectors.condition_container).after(
              '<div class="drag-handle"><i class="' +
                options.icon +
                '"></i></div>'
            );
            h.value = $h.prop('outerHTML');
          }
        });

        this.on('getRuleTemplate.filter', function (h) {
          var $h = $($.parseHTML(h.value));
          $h.find(QueryBuilder.selectors.rule_header).after(
            '<div class="drag-handle"><i class="' +
              options.icon +
              '"></i></div>'
          );
          h.value = $h.prop('outerHTML');
        });
      }
    },
    {
      inherit_no_sortable: true,
      inherit_no_drop: true,
      icon: 'glyphicon glyphicon-sort',
      disable_template: false,
    }
  );

  QueryBuilder.selectors.rule_and_group_containers =
    QueryBuilder.selectors.rule_container +
    ', ' +
    QueryBuilder.selectors.group_container;
  QueryBuilder.selectors.drag_handle = '.drag-handle';

  QueryBuilder.defaults({
    default_rule_flags: {
      no_sortable: false,
      no_drop: false,
    },
    default_group_flags: {
      no_sortable: false,
      no_drop: false,
    },
  });

  /**
   * Moves an element (placeholder or actual object) depending on active target
   * @memberof module:plugins.Sortable
   * @param {Node} node
   * @param {jQuery} target
   * @param {QueryBuilder} [builder]
   * @private
   */
  function moveSortableToTarget(node, target, builder) {
    var parent, method;
    var Selectors = QueryBuilder.selectors;

    // on rule
    parent = target.closest(Selectors.rule_container);
    if (parent.length) {
      method = 'moveAfter';
    }

    // on group header
    if (!method) {
      parent = target.closest(Selectors.group_header);
      if (parent.length) {
        parent = target.closest(Selectors.group_container);
        method = 'moveAtBegin';
      }
    }

    // on group
    if (!method) {
      parent = target.closest(Selectors.group_container);
      if (parent.length) {
        method = 'moveAtEnd';
      }
    }

    if (method) {
      node[method](builder.getModel(parent));

      // refresh radio value
      if (builder && node instanceof Rule) {
        builder.setRuleInputValue(node, node.value);
      }
    }
  }

  /**
   * @class SqlSupport
   * @memberof module:plugins
   * @description Allows to export rules as a SQL WHERE statement as well as populating the builder from an SQL query.
   * @param {object} [options]
   * @param {boolean} [options.boolean_as_integer=true] - `true` to convert boolean values to integer in the SQL output
   */
  QueryBuilder.define('sql-support', function (options) {}, {
    boolean_as_integer: true,
  });

  QueryBuilder.defaults({
    // operators for internal -> SQL conversion
    sqlOperators: {
      equal: { op: '= ?' },
      not_equal: { op: '!= ?' },
      in: { op: 'IN(?)', sep: ', ' },
      not_in: { op: 'NOT IN(?)', sep: ', ' },
      less: { op: '< ?' },
      less_or_equal: { op: '<= ?' },
      greater: { op: '> ?' },
      greater_or_equal: { op: '>= ?' },
      between: { op: 'BETWEEN ?', sep: ' AND ' },
      not_between: { op: 'NOT BETWEEN ?', sep: ' AND ' },
      begins_with: { op: 'LIKE ?', mod: '{0}%', escape: '%_' },
      not_begins_with: { op: 'NOT LIKE ?', mod: '{0}%', escape: '%_' },
      contains: { op: 'LIKE ?', mod: '%{0}%', escape: '%_' },
      not_contains: { op: 'NOT LIKE ?', mod: '%{0}%', escape: '%_' },
      ends_with: { op: 'LIKE ?', mod: '%{0}', escape: '%_' },
      not_ends_with: { op: 'NOT LIKE ?', mod: '%{0}', escape: '%_' },
      is_empty: { op: "= ''" },
      is_not_empty: { op: "!= ''" },
      is_null: { op: 'IS NULL' },
      is_not_null: { op: 'IS NOT NULL' },
    },

    // operators for SQL -> internal conversion
    sqlRuleOperator: {
      '=': function (v) {
        return {
          val: v,
          op: v === '' ? 'is_empty' : 'equal',
        };
      },
      '!=': function (v) {
        return {
          val: v,
          op: v === '' ? 'is_not_empty' : 'not_equal',
        };
      },
      LIKE: function (v) {
        if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
          return {
            val: v.slice(1, -1),
            op: 'contains',
          };
        } else if (v.slice(0, 1) == '%') {
          return {
            val: v.slice(1),
            op: 'ends_with',
          };
        } else if (v.slice(-1) == '%') {
          return {
            val: v.slice(0, -1),
            op: 'begins_with',
          };
        } else {
          Utils.error('SQLParse', 'Invalid value for LIKE operator "{0}"', v);
        }
      },
      'NOT LIKE': function (v) {
        if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
          return {
            val: v.slice(1, -1),
            op: 'not_contains',
          };
        } else if (v.slice(0, 1) == '%') {
          return {
            val: v.slice(1),
            op: 'not_ends_with',
          };
        } else if (v.slice(-1) == '%') {
          return {
            val: v.slice(0, -1),
            op: 'not_begins_with',
          };
        } else {
          Utils.error(
            'SQLParse',
            'Invalid value for NOT LIKE operator "{0}"',
            v
          );
        }
      },
      IN: function (v) {
        return { val: v, op: 'in' };
      },
      'NOT IN': function (v) {
        return { val: v, op: 'not_in' };
      },
      '<': function (v) {
        return { val: v, op: 'less' };
      },
      '<=': function (v) {
        return { val: v, op: 'less_or_equal' };
      },
      '>': function (v) {
        return { val: v, op: 'greater' };
      },
      '>=': function (v) {
        return { val: v, op: 'greater_or_equal' };
      },
      BETWEEN: function (v) {
        return { val: v, op: 'between' };
      },
      'NOT BETWEEN': function (v) {
        return { val: v, op: 'not_between' };
      },
      IS: function (v) {
        if (v !== null) {
          Utils.error('SQLParse', 'Invalid value for IS operator');
        }
        return { val: null, op: 'is_null' };
      },
      'IS NOT': function (v) {
        if (v !== null) {
          Utils.error('SQLParse', 'Invalid value for IS operator');
        }
        return { val: null, op: 'is_not_null' };
      },
    },

    // statements for internal -> SQL conversion
    sqlStatements: {
      question_mark: function () {
        var params = [];
        return {
          add: function (rule, value) {
            params.push(value);
            return '?';
          },
          run: function () {
            return params;
          },
        };
      },

      numbered: function (char) {
        if (!char || char.length > 1) char = '$';
        var index = 0;
        var params = [];
        return {
          add: function (rule, value) {
            params.push(value);
            index++;
            return char + index;
          },
          run: function () {
            return params;
          },
        };
      },

      named: function (char) {
        if (!char || char.length > 1) char = ':';
        var indexes = {};
        var params = {};
        return {
          add: function (rule, value) {
            if (!indexes[rule.field]) indexes[rule.field] = 1;
            var key = rule.field + '_' + indexes[rule.field]++;
            params[key] = value;
            return char + key;
          },
          run: function () {
            return params;
          },
        };
      },
    },

    // statements for SQL -> internal conversion
    sqlRuleStatement: {
      question_mark: function (values) {
        var index = 0;
        return {
          parse: function (v) {
            return v == '?' ? values[index++] : v;
          },
          esc: function (sql) {
            return sql.replace(/\?/g, "'?'");
          },
        };
      },

      numbered: function (values, char) {
        if (!char || char.length > 1) char = '$';
        var regex1 = new RegExp('^\\' + char + '[0-9]+$');
        var regex2 = new RegExp('\\' + char + '([0-9]+)', 'g');
        return {
          parse: function (v) {
            return regex1.test(v) ? values[v.slice(1) - 1] : v;
          },
          esc: function (sql) {
            return sql.replace(
              regex2,
              "'" + (char == '$' ? '$$' : char) + "$1'"
            );
          },
        };
      },

      named: function (values, char) {
        if (!char || char.length > 1) char = ':';
        var regex1 = new RegExp('^\\' + char);
        var regex2 = new RegExp(
          '\\' + char + '(' + Object.keys(values).join('|') + ')\\b',
          'g'
        );
        return {
          parse: function (v) {
            return regex1.test(v) ? values[v.slice(1)] : v;
          },
          esc: function (sql) {
            return sql.replace(
              regex2,
              "'" + (char == '$' ? '$$' : char) + "$1'"
            );
          },
        };
      },
    },
  });

  /**
   * @typedef {object} SqlQuery
   * @memberof module:plugins.SqlSupport
   * @property {string} sql
   * @property {object} params
   */

  QueryBuilder.extend(
    /** @lends module:plugins.SqlSupport.prototype */ {
      /**
       * Returns rules as a SQL query
       * @param {boolean|string} [stmt] - use prepared statements: false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)'
       * @param {boolean} [nl=false] output with new lines
       * @param {object} [data] - current rules by default
       * @returns {module:plugins.SqlSupport.SqlQuery}
       * @fires module:plugins.SqlSupport.changer:getSQLField
       * @fires module:plugins.SqlSupport.changer:ruleToSQL
       * @fires module:plugins.SqlSupport.changer:groupToSQL
       * @throws UndefinedSQLConditionError, UndefinedSQLOperatorError
       */
      getSQL: function (stmt, nl, data) {
        data = data === undefined ? this.getRules() : data;

        if (!data) {
          return null;
        }

        nl = !!nl ? '\n' : ' ';
        var boolean_as_integer = this.getPluginOptions(
          'sql-support',
          'boolean_as_integer'
        );

        if (stmt === true) {
          stmt = 'question_mark';
        }
        if (typeof stmt == 'string') {
          var config = getStmtConfig(stmt);
          stmt = this.settings.sqlStatements[config[1]](config[2]);
        }

        var self = this;

        var sql = (function parse(group) {
          if (!group.condition) {
            group.condition = self.settings.default_condition;
          }
          if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) {
            Utils.error(
              'UndefinedSQLCondition',
              'Unable to build SQL query with condition "{0}"',
              group.condition
            );
          }

          if (!group.rules) {
            return '';
          }

          var parts = [];

          group.rules.forEach(function (rule) {
            if (rule.rules && rule.rules.length > 0) {
              parts.push('(' + nl + parse(rule) + nl + ')' + nl);
            } else {
              var sql = self.settings.sqlOperators[rule.operator];
              var ope = self.getOperatorByType(rule.operator);
              var value = '';

              if (sql === undefined) {
                Utils.error(
                  'UndefinedSQLOperator',
                  'Unknown SQL operation for operator "{0}"',
                  rule.operator
                );
              }

              if (ope.nb_inputs !== 0) {
                if (!(rule.value instanceof Array)) {
                  rule.value = [rule.value];
                }

                rule.value.forEach(function (v, i) {
                  if (i > 0) {
                    value += sql.sep;
                  }

                  if (rule.type == 'boolean' && boolean_as_integer) {
                    v = v ? 1 : 0;
                  } else if (
                    !stmt &&
                    rule.type !== 'integer' &&
                    rule.type !== 'double' &&
                    rule.type !== 'boolean'
                  ) {
                    v = Utils.escapeString(v, sql.escape);
                  }

                  if (sql.mod) {
                    v = Utils.fmt(sql.mod, v);
                  }

                  if (stmt) {
                    value += stmt.add(rule, v);
                  } else {
                    if (typeof v == 'string') {
                      v = "'" + v + "'";
                    }

                    value += v;
                  }
                });
              }

              var sqlFn = function (v) {
                return sql.op.replace('?', function () {
                  return v;
                });
              };

              /**
               * Modifies the SQL field used by a rule
               * @event changer:getSQLField
               * @memberof module:plugins.SqlSupport
               * @param {string} field
               * @param {Rule} rule
               * @returns {string}
               */
              var field = self.change('getSQLField', rule.field, rule);

              var ruleExpression = field + ' ' + sqlFn(value);

              /**
               * Modifies the SQL generated for a rule
               * @event changer:ruleToSQL
               * @memberof module:plugins.SqlSupport
               * @param {string} expression
               * @param {Rule} rule
               * @param {*} value
               * @param {function} valueWrapper - function that takes the value and adds the operator
               * @returns {string}
               */
              parts.push(
                self.change('ruleToSQL', ruleExpression, rule, value, sqlFn)
              );
            }
          });

          var groupExpression = parts.join(' ' + group.condition + nl);

          /**
           * Modifies the SQL generated for a group
           * @event changer:groupToSQL
           * @memberof module:plugins.SqlSupport
           * @param {string} expression
           * @param {Group} group
           * @returns {string}
           */
          return self.change('groupToSQL', groupExpression, group);
        })(data);

        if (stmt) {
          return {
            sql: sql,
            params: stmt.run(),
          };
        } else {
          return {
            sql: sql,
          };
        }
      },

      /**
       * Convert a SQL query to rules
       * @param {string|module:plugins.SqlSupport.SqlQuery} query
       * @param {boolean|string} stmt
       * @returns {object}
       * @fires module:plugins.SqlSupport.changer:parseSQLNode
       * @fires module:plugins.SqlSupport.changer:getSQLFieldID
       * @fires module:plugins.SqlSupport.changer:sqlToRule
       * @fires module:plugins.SqlSupport.changer:sqlToGroup
       * @throws MissingLibraryError, SQLParseError, UndefinedSQLOperatorError
       */
      getRulesFromSQL: function (query, stmt) {
        if (!('SQLParser' in window)) {
          Utils.error(
            'MissingLibrary',
            'SQLParser is required to parse SQL queries. Get it here https://github.com/mistic100/sql-parser'
          );
        }

        var self = this;

        if (typeof query == 'string') {
          query = { sql: query };
        }

        if (stmt === true) stmt = 'question_mark';
        if (typeof stmt == 'string') {
          var config = getStmtConfig(stmt);
          stmt = this.settings.sqlRuleStatement[config[1]](
            query.params,
            config[2]
          );
        }

        if (stmt) {
          query.sql = stmt.esc(query.sql);
        }

        if (query.sql.toUpperCase().indexOf('SELECT') !== 0) {
          query.sql = 'SELECT * FROM table WHERE ' + query.sql;
        }

        var parsed = SQLParser.parse(query.sql);

        if (!parsed.where) {
          Utils.error('SQLParse', 'No WHERE clause found');
        }

        /**
         * Custom parsing of an AST node generated by SQLParser, you can return a sub-part of the tree, or a well formed group or rule JSON
         * @event changer:parseSQLNode
         * @memberof module:plugins.SqlSupport
         * @param {object} AST node
         * @returns {object} tree, rule or group
         */
        var data = self.change('parseSQLNode', parsed.where.conditions);

        // a plugin returned a group
        if ('rules' in data && 'condition' in data) {
          return data;
        }

        // a plugin returned a rule
        if ('id' in data && 'operator' in data && 'value' in data) {
          return {
            condition: this.settings.default_condition,
            rules: [data],
          };
        }

        // create root group
        var out = self.change(
          'sqlToGroup',
          {
            condition: this.settings.default_condition,
            rules: [],
          },
          data
        );

        // keep track of current group
        var curr = out;

        (function flatten(data, i) {
          if (data === null) {
            return;
          }

          // allow plugins to manually parse or handle special cases
          data = self.change('parseSQLNode', data);

          // a plugin returned a group
          if ('rules' in data && 'condition' in data) {
            curr.rules.push(data);
            return;
          }

          // a plugin returned a rule
          if ('id' in data && 'operator' in data && 'value' in data) {
            curr.rules.push(data);
            return;
          }

          // data must be a SQL parser node
          if (
            !('left' in data) ||
            !('right' in data) ||
            !('operation' in data)
          ) {
            Utils.error('SQLParse', 'Unable to parse WHERE clause');
          }

          // it's a node
          if (['AND', 'OR'].indexOf(data.operation.toUpperCase()) !== -1) {
            // create a sub-group if the condition is not the same and it's not the first level

            /**
             * Given an existing group and an AST node, determines if a sub-group must be created
             * @event changer:sqlGroupsDistinct
             * @memberof module:plugins.SqlSupport
             * @param {boolean} create - true by default if the group condition is different
             * @param {object} group
             * @param {object} AST
             * @param {int} current group level
             * @returns {boolean}
             */
            var createGroup = self.change(
              'sqlGroupsDistinct',
              i > 0 && curr.condition != data.operation.toUpperCase(),
              curr,
              data,
              i
            );

            if (createGroup) {
              /**
               * Modifies the group generated from the SQL expression (this is called before the group is filled with rules)
               * @event changer:sqlToGroup
               * @memberof module:plugins.SqlSupport
               * @param {object} group
               * @param {object} AST
               * @returns {object}
               */
              var group = self.change(
                'sqlToGroup',
                {
                  condition: self.settings.default_condition,
                  rules: [],
                },
                data
              );

              curr.rules.push(group);
              curr = group;
            }

            curr.condition = data.operation.toUpperCase();
            i++;

            // some magic !
            var next = curr;
            flatten(data.left, i);

            curr = next;
            flatten(data.right, i);
          }
          // it's a leaf
          else {
            if ($.isPlainObject(data.right.value)) {
              Utils.error(
                'SQLParse',
                'Value format not supported for {0}.',
                data.left.value
              );
            }

            // convert array
            var value;
            if ($.isArray(data.right.value)) {
              value = data.right.value.map(function (v) {
                return v.value;
              });
            } else {
              value = data.right.value;
            }

            // get actual values
            if (stmt) {
              if ($.isArray(value)) {
                value = value.map(stmt.parse);
              } else {
                value = stmt.parse(value);
              }
            }

            // convert operator
            var operator = data.operation.toUpperCase();
            if (operator == '<>') {
              operator = '!=';
            }

            var sqlrl = self.settings.sqlRuleOperator[operator];
            if (sqlrl === undefined) {
              Utils.error(
                'UndefinedSQLOperator',
                'Invalid SQL operation "{0}".',
                data.operation
              );
            }

            var opVal = sqlrl.call(this, value, data.operation);

            // find field name
            var field;
            if ('values' in data.left) {
              field = data.left.values.join('.');
            } else if ('value' in data.left) {
              field = data.left.value;
            } else {
              Utils.error(
                'SQLParse',
                'Cannot find field name in {0}',
                JSON.stringify(data.left)
              );
            }

            // unescape chars declared by the operator
            var finalValue = opVal.val;
            var sql = self.settings.sqlOperators[opVal.op];
            if (!stmt && sql && sql.escape) {
              var searchChars = sql.escape
                .split('')
                .map(function (c) {
                  return '\\\\' + c;
                })
                .join('|');
              finalValue = finalValue.replace(
                new RegExp('(' + searchChars + ')', 'g'),
                function (s) {
                  return s[1];
                }
              );
            }

            var id = self.getSQLFieldID(field, value);

            /**
             * Modifies the rule generated from the SQL expression
             * @event changer:sqlToRule
             * @memberof module:plugins.SqlSupport
             * @param {object} rule
             * @param {object} AST
             * @returns {object}
             */
            var rule = self.change(
              'sqlToRule',
              {
                id: id,
                field: field,
                operator: opVal.op,
                value: finalValue,
              },
              data
            );

            curr.rules.push(rule);
          }
        })(data, 0);

        return out;
      },

      /**
       * Sets the builder's rules from a SQL query
       * @see module:plugins.SqlSupport.getRulesFromSQL
       */
      setRulesFromSQL: function (query, stmt) {
        this.setRules(this.getRulesFromSQL(query, stmt));
      },

      /**
       * Returns a filter identifier from the SQL field.
       * Automatically use the only one filter with a matching field, fires a changer otherwise.
       * @param {string} field
       * @param {*} value
       * @fires module:plugins.SqlSupport:changer:getSQLFieldID
       * @returns {string}
       * @private
       */
      getSQLFieldID: function (field, value) {
        var matchingFilters = this.filters.filter(function (filter) {
          return filter.field.toLowerCase() === field.toLowerCase();
        });

        var id;
        if (matchingFilters.length === 1) {
          id = matchingFilters[0].id;
        } else {
          /**
           * Returns a filter identifier from the SQL field
           * @event changer:getSQLFieldID
           * @memberof module:plugins.SqlSupport
           * @param {string} field
           * @param {*} value
           * @returns {string}
           */
          id = this.change('getSQLFieldID', field, value);
        }

        return id;
      },
    }
  );

  /**
   * Parses the statement configuration
   * @memberof module:plugins.SqlSupport
   * @param {string} stmt
   * @returns {Array} null, mode, option
   * @private
   */
  function getStmtConfig(stmt) {
    var config = stmt.match(/(question_mark|numbered|named)(?:\((.)\))?/);
    if (!config) config = [null, 'question_mark', undefined];
    return config;
  }

  /**
   * @class UniqueFilter
   * @memberof module:plugins
   * @description Allows to define some filters as "unique": ie which can be used for only one rule, globally or in the same group.
   */
  QueryBuilder.define('unique-filter', function () {
    this.status.used_filters = {};

    this.on('afterUpdateRuleFilter', this.updateDisabledFilters);
    this.on('afterDeleteRule', this.updateDisabledFilters);
    this.on('afterCreateRuleFilters', this.applyDisabledFilters);
    this.on('afterReset', this.clearDisabledFilters);
    this.on('afterClear', this.clearDisabledFilters);

    // Ensure that the default filter is not already used if unique
    this.on('getDefaultFilter.filter', function (e, model) {
      var self = e.builder;

      self.updateDisabledFilters();

      if (e.value.id in self.status.used_filters) {
        var found = self.filters.some(function (filter) {
          if (
            !(filter.id in self.status.used_filters) ||
            (self.status.used_filters[filter.id].length > 0 &&
              self.status.used_filters[filter.id].indexOf(model.parent) === -1)
          ) {
            e.value = filter;
            return true;
          }
        });

        if (!found) {
          Utils.error(
            false,
            'UniqueFilter',
            'No more non-unique filters available'
          );
          e.value = undefined;
        }
      }
    });
  });

  QueryBuilder.extend(
    /** @lends module:plugins.UniqueFilter.prototype */ {
      /**
       * Updates the list of used filters
       * @param {$.Event} [e]
       * @private
       */
      updateDisabledFilters: function (e) {
        var self = e ? e.builder : this;

        self.status.used_filters = {};

        if (!self.model) {
          return;
        }

        // get used filters
        (function walk(group) {
          group.each(
            function (rule) {
              if (rule.filter && rule.filter.unique) {
                if (!self.status.used_filters[rule.filter.id]) {
                  self.status.used_filters[rule.filter.id] = [];
                }
                if (rule.filter.unique == 'group') {
                  self.status.used_filters[rule.filter.id].push(rule.parent);
                }
              }
            },
            function (group) {
              walk(group);
            }
          );
        })(self.model.root);

        self.applyDisabledFilters(e);
      },

      /**
       * Clear the list of used filters
       * @param {$.Event} [e]
       * @private
       */
      clearDisabledFilters: function (e) {
        var self = e ? e.builder : this;

        self.status.used_filters = {};

        self.applyDisabledFilters(e);
      },

      /**
       * Disabled filters depending on the list of used ones
       * @param {$.Event} [e]
       * @private
       */
      applyDisabledFilters: function (e) {
        var self = e ? e.builder : this;

        // re-enable everything
        self.$el
          .find(QueryBuilder.selectors.filter_container + ' option')
          .prop('disabled', false);

        // disable some
        $.each(self.status.used_filters, function (filterId, groups) {
          if (groups.length === 0) {
            self.$el
              .find(
                QueryBuilder.selectors.filter_container +
                  ' option[value="' +
                  filterId +
                  '"]:not(:selected)'
              )
              .prop('disabled', true);
          } else {
            groups.forEach(function (group) {
              group.each(function (rule) {
                rule.$el
                  .find(
                    QueryBuilder.selectors.filter_container +
                      ' option[value="' +
                      filterId +
                      '"]:not(:selected)'
                  )
                  .prop('disabled', true);
              });
            });
          }
        });

        // update Plugins
        if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) {
          self.$el
            .find(QueryBuilder.selectors.rule_filter)
            .selectpicker('render');
        }
				
				// select2 구문 추가
        if (self.settings.plugins && self.settings.plugins['select2']) {
          self.$el
            .find(QueryBuilder.selectors.rule_filter)
            .trigger('change.select2');
        }
      },
    }
  );

  /*!
   * jQuery QueryBuilder 2.6.2
   * Locale: English (en)
   * Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr
   * Licensed under MIT (https://opensource.org/licenses/MIT)
   */

  QueryBuilder.regional['en'] = {
    __locale: 'English (en)',
    __author: 'Damien "Mistic" Sorel, http://www.strangeplanet.fr',
    add_rule: '하위필터',
    add_group: '조건그룹',
    delete_rule: 'Delete',
    delete_group: 'Delete',
    conditions: {
      AND: 'AND',
      OR: 'OR',
      NOT: 'NOT',
    },
    operators: {
      equal: 'equal',
      not_equal: 'not equal',
      in: 'in',
      not_in: 'not in',
      less: 'less',
      less_or_equal: 'less or equal',
      greater: 'greater',
      greater_or_equal: 'greater or equal',
      between: 'between',
      not_between: 'not between',
      begins_with: 'begins with',
      not_begins_with: "doesn't begin with",
      contains: '포함', // text display 설정
      not_contains: '미포함', // text display 설정
      ends_with: 'ends with',
      not_ends_with: "doesn't end with",
      is_empty: 'is empty',
      is_not_empty: 'is not empty',
      is_null: 'is null',
      is_not_null: 'is not null',
    },
    errors: {
      no_filter: 'No filter selected',
      empty_group: 'The group is empty',
      radio_empty: 'No value selected',
      checkbox_empty: 'No value selected',
      select_empty: 'No value selected',
      string_empty: 'Empty value',
      string_exceed_min_length: 'Must contain at least {0} characters',
      string_exceed_max_length: 'Must not contain more than {0} characters',
      string_invalid_format: 'Invalid format ({0})',
      number_nan: 'Not a number',
      number_not_integer: 'Not an integer',
      number_not_double: 'Not a real number',
      number_exceed_min: 'Must be greater than {0}',
      number_exceed_max: 'Must be lower than {0}',
      number_wrong_step: 'Must be a multiple of {0}',
      number_between_invalid: 'Invalid values, {0} is greater than {1}',
      datetime_empty: 'Empty value',
      datetime_invalid: 'Invalid date format ({0})',
      datetime_exceed_min: 'Must be after {0}',
      datetime_exceed_max: 'Must be before {0}',
      datetime_between_invalid: 'Invalid values, {0} is greater than {1}',
      boolean_not_valid: 'Not a boolean',
      operator_not_multiple: 'Operator "{1}" cannot accept multiple values',
    },
    invert: 'Invert',
    NOT: 'NOT',
  };

  QueryBuilder.defaults({ lang_code: 'en' });
  return QueryBuilder;
});


var rules_basic = {
  condition: 'AND',
  rules: [{
    id: 'category',
    operator: 'equal',
    value: ""
  }, {
    condition: 'OR',
    rules: [{
      id: 'name',
      operator: 'equal',
      value: 2
    }, {
      id: 'category',
      operator: 'equal',
      value: 1
    }]
  }]
};

$('#builder').queryBuilder({
	plugins: ['select2'],// 추가한 plugin 설정
  filters: [{
    id: 'name',
    label: 'Name',
    type: 'string',
		operators: ['contains', 'not_contains']
  }, {
    id: 'category',
    label: 'Category',
    type: 'string',
		input: 'select',
		plugin: 'select2', // select2 플러그인 추가
    values: {
      1: 'Books',
      2: 'Movies',
      3: 'Music',
      4: 'Tools',
      5: 'Goodies',
      6: 'Clothes'
    },
    operators: ['equal']
  }],

  rules: rules_basic
});

External CSS

  1. https://cdn.jsdelivr.net/npm/jQuery-QueryBuilder/dist/css/query-builder.default.min.css
  2. https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css
  3. https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.0.2/css/bootstrap.min.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js
  2. https://cdn.jsdelivr.net/jquery.query-builder/2.4.1/js/query-builder.standalone.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js