Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div id="builder"></div>
              
            
!

CSS

              
                .form-control {min-width: 60px}
.select2 {min-width: 60px;}
.rule-header {text-align:right;}
.pull-right {float: right;}
              
            
!

JS

              
                /*!
 * 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
});

              
            
!
999px

Console