From 1e5d348cfd83d9565dd1930597ffa388ef09d319 Mon Sep 17 00:00:00 2001
From: LochNess
Date: Fri, 26 Apr 2019 11:27:34 +0300
Subject: [PATCH 1/8] Standalone versions
---
src/query-builder.default.css | 176 +
src/query-builder.standalone.js | 6478 +++++++++++++++++++++++++++++++
2 files changed, 6654 insertions(+)
create mode 100644 src/query-builder.default.css
create mode 100644 src/query-builder.standalone.js
diff --git a/src/query-builder.default.css b/src/query-builder.default.css
new file mode 100644
index 00000000..cb6c2130
--- /dev/null
+++ b/src/query-builder.default.css
@@ -0,0 +1,176 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Copyright 2014-2018 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+.query-builder .rules-group-container, .query-builder .rule-container, .query-builder .rule-placeholder {
+ position: relative;
+ margin: 4px 0;
+ border-radius: 5px;
+ padding: 5px;
+ border: 1px solid #EEE;
+ background: rgba(255, 255, 255, 0.9);
+}
+
+.query-builder .rule-container .rule-filter-container,
+.query-builder .rule-container .rule-operator-container,
+.query-builder .rule-container .rule-visibility-container,
+.query-builder .rule-container .rule-value-container, .query-builder .error-container, .query-builder .drag-handle {
+ display: inline-block;
+ margin: 0 5px 0 0;
+ vertical-align: middle;
+}
+
+.query-builder .rules-group-container {
+ padding: 10px;
+ padding-bottom: 6px;
+ border: 1px solid #DCC896;
+ background: rgba(250, 240, 210, 0.5);
+}
+
+.query-builder .rules-group-header {
+ margin-bottom: 10px;
+}
+
+.query-builder .rules-group-header .group-conditions .btn.readonly:not(.active),
+.query-builder .rules-group-header .group-conditions input[name$='_cond'] {
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+ white-space: nowrap;
+}
+
+.query-builder .rules-group-header .group-conditions .btn.readonly {
+ border-radius: 3px;
+}
+
+.query-builder .rules-list {
+ list-style: none;
+ padding: 0 0 0 15px;
+ margin: 0;
+}
+
+.query-builder .rule-value-container {
+ border-left: 1px solid #DDD;
+ padding-left: 5px;
+ width: 50px;
+}
+
+.query-builder .rule-value-container label {
+ margin-bottom: 0;
+ font-weight: normal;
+}
+
+.query-builder .rule-value-container label.block {
+ display: block;
+}
+
+.query-builder .rule-value-container select,
+.query-builder .rule-value-container input[type='text'],
+.query-builder .rule-value-container input[type='number'] {
+ padding: 1px;
+ width: inherit;
+}
+
+.query-builder .error-container {
+ display: none;
+ cursor: help;
+ color: #F00;
+}
+
+.query-builder .has-error {
+ background-color: #FDD;
+ border-color: #F99;
+}
+
+.query-builder .has-error .error-container {
+ display: inline-block !important;
+}
+
+.query-builder .rules-list > *::before, .query-builder .rules-list > *::after {
+ content: '';
+ position: absolute;
+ left: -10px;
+ width: 10px;
+ height: calc(50% + 4px);
+ border-color: #CCC;
+ border-style: solid;
+}
+
+.query-builder .rules-list > *::before {
+ top: -4px;
+ border-width: 0 0 2px 2px;
+}
+
+.query-builder .rules-list > *::after {
+ top: 50%;
+ border-width: 0 0 0 2px;
+}
+
+.query-builder .rules-list > *:first-child::before {
+ top: -12px;
+ height: calc(50% + 14px);
+}
+
+.query-builder .rules-list > *:last-child::before {
+ border-radius: 0 0 0 4px;
+}
+
+.query-builder .rules-list > *:last-child::after {
+ display: none;
+}
+
+.query-builder.bt-checkbox-glyphicons .checkbox input[type='checkbox']:checked + label::after {
+ font-family: 'Glyphicons Halflings';
+ content: '\e013';
+}
+
+.query-builder.bt-checkbox-glyphicons .checkbox label::after {
+ padding-left: 4px;
+ padding-top: 2px;
+ font-size: 9px;
+}
+
+.query-builder .error-container + .tooltip .tooltip-inner {
+ color: #F99 !important;
+}
+
+.query-builder p.filter-description {
+ margin: 5px 0 0 0;
+ background: #D9EDF7;
+ border: 1px solid #BCE8F1;
+ color: #31708F;
+ border-radius: 5px;
+ padding: 2.5px 5px;
+ font-size: .8em;
+}
+
+.query-builder .rules-group-header [data-invert] {
+ margin-left: 5px;
+}
+
+.query-builder .drag-handle {
+ cursor: move;
+ vertical-align: middle;
+ margin-left: 5px;
+}
+
+.query-builder .dragging {
+ position: fixed;
+ opacity: .5;
+ z-index: 100;
+}
+
+.query-builder .dragging::before, .query-builder .dragging::after {
+ display: none;
+}
+
+.query-builder .rule-placeholder {
+ border: 1px dashed #BBB;
+ opacity: .7;
+}
diff --git a/src/query-builder.standalone.js b/src/query-builder.standalone.js
new file mode 100644
index 00000000..ff41e667
--- /dev/null
+++ b/src/query-builder.standalone.js
@@ -0,0 +1,6478 @@
+/*!
+ * jQuery.extendext 0.1.2
+ *
+ * Copyright 2014-2016 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) {
+ src = target[name];
+ copy = options[name];
+
+ // Prevent never-ending loop
+ if (target === copy) {
+ continue;
+ }
+
+ // Recurse if we're merging plain objects or arrays
+ if (deep && copy && ( $.isPlainObject(copy) ||
+ (copyIsArray = $.isArray(copy)) )) {
+
+ if (copyIsArray) {
+ copyIsArray = false;
+ clone = src && $.isArray(src) ? src : [];
+
+ } else {
+ clone = src && $.isPlainObject(src) ? src : {};
+ }
+
+ // 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 = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "/": "/" },
+ 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+"}
+ * @readonly
+ */
+ this.icons = this.settings.icons;
+
+ /**
+ * List of operators
+ * @member {QueryBuilder.Operator[]}
+ * @readonly
+ */
+ this.operators = this.settings.operators;
+
+ /**
+ * List of templates
+ * @member {object.}
+ * @readonly
+ */
+ this.templates = this.settings.templates;
+
+ /**
+ * Plugins configuration
+ * @member {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');
+
+ 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.}
+ * @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.}
+ * @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.}
+ * @readonly
+ */
+QueryBuilder.templates = {};
+
+/**
+ * Localized strings (see i18n/)
+ * @type {object.}
+ * @readonly
+ */
+QueryBuilder.regional = {};
+
+/**
+ * Default operators
+ * @type {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: true,
+ 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.}
+ */
+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.} 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 = $(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 = $(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 = $(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 = $(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 = $(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.
+ * 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.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
+ * @private
+ */
+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
+ * @private
+ */
+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
+ * @private
+ */
+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 - not 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 - not 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 = '\
+';
+
+QueryBuilder.templates.rule = '\
+ \
+ \
+ {{? it.settings.display_errors }} \
+
\
+ {{?}} \
+
\
+
\
+
\
+
\
+
';
+
+QueryBuilder.templates.filterSelect = '\
+{{ var optgroup = null; }} \
+';
+
+QueryBuilder.templates.operatorSelect = '\
+{{? it.operators.length === 1 }} \
+ \
+{{= it.translate("operators", it.operators[0].type) }} \
+ \
+{{?}} \
+{{ var optgroup = null; }} \
+';
+
+QueryBuilder.templates.ruleValueSelect = '\
+{{ var optgroup = null; }} \
+';
+
+/**
+ * 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 = '';
+
+ 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 += ' ';
+ });
+ break;
+
+ case 'select':
+ h = this.getRuleValueSelect(name, rule);
+ break;
+
+ case 'textarea':
+ h += '" ; + break + + case number : + h +='" ; + break + + default + 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 + * @returns {string + * +Utils.escapeString="function(value)" { + if (typeof value !="string" ) { + return value + } + + return 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 ; + default return \ + s + @formatter:off + } + } + uglify compliant + .replace(/\t/g \\t ) + .replace(/\x1a/g \\Z ) + + + + * 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 />
+ * Update events are emitted in the setter through root Model (if any).
+ * 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+= '\
+ \
+ \
+ \
+
';
+ });
+ }
+ });
+}, {
+ 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 = $(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');
+ }
+ }
+
+ // 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);
+ }
+});
+
+
+/**
+ * @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) {
+ 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 = $('');
+ $p.appendTo(rule.$el);
+ }
+ else {
+ $p.css('display', '');
+ }
+
+ $p.html(' ' + 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 = $('');
+ $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 = $('');
+ $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 = $(h.value);
+ $h.find(Selectors.condition_container).after(
+ ''
+ );
+ h.value = $h.prop('outerHTML');
+ });
+
+ if (options.display_rules_button && options.invert_rules) {
+ this.on('getRuleTemplate.filter', function(h) {
+ var $h = $(h.value);
+ $h.find(Selectors.rule_actions).prepend(
+ ''
+ );
+ 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': '(? 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 = $(h.value);
+ $h.find(QueryBuilder.selectors.condition_container).prepend(
+ ''
+ );
+ 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 = $('
')
+ .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 = $(h.value);
+ $h.find(QueryBuilder.selectors.condition_container).after('
');
+ h.value = $h.prop('outerHTML');
+ }
+ });
+
+ this.on('getRuleTemplate.filter', function(h) {
+ var $h = $(h.value);
+ $h.find(QueryBuilder.selectors.rule_header).after('
');
+ 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}%' },
+ not_begins_with: { op: 'NOT LIKE(?)', mod: '{0}%' },
+ contains: { op: 'LIKE(?)', mod: '%{0}%' },
+ not_contains: { op: 'NOT LIKE(?)', mod: '%{0}%' },
+ ends_with: { op: 'LIKE(?)', mod: '%{0}' },
+ not_ends_with: { op: 'NOT LIKE(?)', mod: '%{0}' },
+ 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('|') + ')', '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);
+ }
+
+ 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));
+ }
+
+ 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: opVal.val
+ }, 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 Selectpicker
+ if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) {
+ self.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render');
+ }
+ }
+});
+
+
+/*!
+ * jQuery QueryBuilder 2.5.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 rule",
+ "add_group": "Add group",
+ "delete_rule": "Delete",
+ "delete_group": "Delete",
+ "conditions": {
+ "AND": "AND",
+ "OR": "OR"
+ },
+ "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": "contains",
+ "not_contains": "doesn't contain",
+ "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": "Не"
+};
+
+QueryBuilder.defaults({ lang_code: 'en' });
+return QueryBuilder;
+
+}));
From 9719738ec011de324abe3e4535fad7a61344b62d Mon Sep 17 00:00:00 2001
From: LochNess
Date: Fri, 26 Apr 2019 11:54:06 +0300
Subject: [PATCH 2/8] dist
---
.gitignore | 1 -
src/dist/css/query-builder.dark.css | 173 +
src/dist/css/query-builder.dark.min.css | 6 +
src/dist/css/query-builder.default.css | 173 +
src/dist/css/query-builder.default.min.css | 6 +
src/dist/i18n/query-builder.ar.js | 80 +
src/dist/i18n/query-builder.az.js | 79 +
src/dist/i18n/query-builder.bg.js | 79 +
src/dist/i18n/query-builder.cs.js | 79 +
src/dist/i18n/query-builder.da.js | 56 +
src/dist/i18n/query-builder.de.js | 76 +
src/dist/i18n/query-builder.el.js | 80 +
src/dist/i18n/query-builder.en.js | 83 +
src/dist/i18n/query-builder.es.js | 81 +
src/dist/i18n/query-builder.fa-IR.js | 79 +
src/dist/i18n/query-builder.fr.js | 83 +
src/dist/i18n/query-builder.he.js | 81 +
src/dist/i18n/query-builder.it.js | 79 +
src/dist/i18n/query-builder.nl.js | 76 +
src/dist/i18n/query-builder.no.js | 54 +
src/dist/i18n/query-builder.pl.js | 80 +
src/dist/i18n/query-builder.pt-BR.js | 80 +
src/dist/i18n/query-builder.pt-PT.js | 75 +
src/dist/i18n/query-builder.ro.js | 54 +
src/dist/i18n/query-builder.ru.js | 77 +
src/dist/i18n/query-builder.sq.js | 78 +
src/dist/i18n/query-builder.tr.js | 82 +
src/dist/i18n/query-builder.ua.js | 79 +
src/dist/i18n/query-builder.zh-CN.js | 80 +
src/dist/js/query-builder.js | 6200 ++++++++++++++++
src/dist/js/query-builder.min.js | 7 +
src/dist/js/query-builder.standalone.js | 6477 +++++++++++++++++
src/dist/js/query-builder.standalone.min.js | 7 +
src/dist/scss/dark.scss | 19 +
src/dist/scss/default.scss | 186 +
src/dist/scss/plugins/_bt-checkbox.scss | 12 +
src/dist/scss/plugins/_bt-tooltip-errors.scss | 9 +
.../scss/plugins/_filter-description.scss | 21 +
src/dist/scss/plugins/_invert.scss | 5 +
src/dist/scss/plugins/_sortable.scss | 28 +
src/dist/scss/plugins/bt-tooltip-errors.scss | 9 +
src/dist/scss/plugins/filter-description.scss | 21 +
src/dist/scss/plugins/invert.scss | 5 +
src/dist/scss/plugins/sortable.scss | 27 +
44 files changed, 15221 insertions(+), 1 deletion(-)
create mode 100644 src/dist/css/query-builder.dark.css
create mode 100644 src/dist/css/query-builder.dark.min.css
create mode 100644 src/dist/css/query-builder.default.css
create mode 100644 src/dist/css/query-builder.default.min.css
create mode 100644 src/dist/i18n/query-builder.ar.js
create mode 100644 src/dist/i18n/query-builder.az.js
create mode 100644 src/dist/i18n/query-builder.bg.js
create mode 100644 src/dist/i18n/query-builder.cs.js
create mode 100644 src/dist/i18n/query-builder.da.js
create mode 100644 src/dist/i18n/query-builder.de.js
create mode 100644 src/dist/i18n/query-builder.el.js
create mode 100644 src/dist/i18n/query-builder.en.js
create mode 100644 src/dist/i18n/query-builder.es.js
create mode 100644 src/dist/i18n/query-builder.fa-IR.js
create mode 100644 src/dist/i18n/query-builder.fr.js
create mode 100644 src/dist/i18n/query-builder.he.js
create mode 100644 src/dist/i18n/query-builder.it.js
create mode 100644 src/dist/i18n/query-builder.nl.js
create mode 100644 src/dist/i18n/query-builder.no.js
create mode 100644 src/dist/i18n/query-builder.pl.js
create mode 100644 src/dist/i18n/query-builder.pt-BR.js
create mode 100644 src/dist/i18n/query-builder.pt-PT.js
create mode 100644 src/dist/i18n/query-builder.ro.js
create mode 100644 src/dist/i18n/query-builder.ru.js
create mode 100644 src/dist/i18n/query-builder.sq.js
create mode 100644 src/dist/i18n/query-builder.tr.js
create mode 100644 src/dist/i18n/query-builder.ua.js
create mode 100644 src/dist/i18n/query-builder.zh-CN.js
create mode 100644 src/dist/js/query-builder.js
create mode 100644 src/dist/js/query-builder.min.js
create mode 100644 src/dist/js/query-builder.standalone.js
create mode 100644 src/dist/js/query-builder.standalone.min.js
create mode 100644 src/dist/scss/dark.scss
create mode 100644 src/dist/scss/default.scss
create mode 100644 src/dist/scss/plugins/_bt-checkbox.scss
create mode 100644 src/dist/scss/plugins/_bt-tooltip-errors.scss
create mode 100644 src/dist/scss/plugins/_filter-description.scss
create mode 100644 src/dist/scss/plugins/_invert.scss
create mode 100644 src/dist/scss/plugins/_sortable.scss
create mode 100644 src/dist/scss/plugins/bt-tooltip-errors.scss
create mode 100644 src/dist/scss/plugins/filter-description.scss
create mode 100644 src/dist/scss/plugins/invert.scss
create mode 100644 src/dist/scss/plugins/sortable.scss
diff --git a/.gitignore b/.gitignore
index 2b7141a4..81b7cea3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,5 @@
bower_components
node_modules
-dist
doc
.sass-cache
.coverage-results
diff --git a/src/dist/css/query-builder.dark.css b/src/dist/css/query-builder.dark.css
new file mode 100644
index 00000000..0bad0ecb
--- /dev/null
+++ b/src/dist/css/query-builder.dark.css
@@ -0,0 +1,173 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Copyright 2014-2018 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+.query-builder .rules-group-container, .query-builder .rule-container, .query-builder .rule-placeholder {
+ position: relative;
+ margin: 4px 0;
+ border-radius: 5px;
+ padding: 5px;
+ border: 1px solid #111;
+ background: rgba(40, 40, 40, 0.9);
+}
+
+.query-builder .rule-container .rule-filter-container,
+.query-builder .rule-container .rule-operator-container,
+.query-builder .rule-container .rule-value-container, .query-builder .error-container, .query-builder .drag-handle {
+ display: inline-block;
+ margin: 0 5px 0 0;
+ vertical-align: middle;
+}
+
+.query-builder .rules-group-container {
+ padding: 10px;
+ padding-bottom: 6px;
+ border: 1px solid #00164A;
+ background: rgba(50, 70, 80, 0.5);
+}
+
+.query-builder .rules-group-header {
+ margin-bottom: 10px;
+}
+
+.query-builder .rules-group-header .group-conditions .btn.readonly:not(.active),
+.query-builder .rules-group-header .group-conditions input[name$='_cond'] {
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+ white-space: nowrap;
+}
+
+.query-builder .rules-group-header .group-conditions .btn.readonly {
+ border-radius: 3px;
+}
+
+.query-builder .rules-list {
+ list-style: none;
+ padding: 0 0 0 15px;
+ margin: 0;
+}
+
+.query-builder .rule-value-container {
+ border-left: 1px solid #DDD;
+ padding-left: 5px;
+}
+
+.query-builder .rule-value-container label {
+ margin-bottom: 0;
+ font-weight: normal;
+}
+
+.query-builder .rule-value-container label.block {
+ display: block;
+}
+
+.query-builder .rule-value-container select,
+.query-builder .rule-value-container input[type='text'],
+.query-builder .rule-value-container input[type='number'] {
+ padding: 1px;
+}
+
+.query-builder .error-container {
+ display: none;
+ cursor: help;
+ color: #F00;
+}
+
+.query-builder .has-error {
+ background-color: #322;
+ border-color: #800;
+}
+
+.query-builder .has-error .error-container {
+ display: inline-block !important;
+}
+
+.query-builder .rules-list > *::before, .query-builder .rules-list > *::after {
+ content: '';
+ position: absolute;
+ left: -10px;
+ width: 10px;
+ height: calc(50% + 4px);
+ border-color: #222;
+ border-style: solid;
+}
+
+.query-builder .rules-list > *::before {
+ top: -4px;
+ border-width: 0 0 2px 2px;
+}
+
+.query-builder .rules-list > *::after {
+ top: 50%;
+ border-width: 0 0 0 2px;
+}
+
+.query-builder .rules-list > *:first-child::before {
+ top: -12px;
+ height: calc(50% + 14px);
+}
+
+.query-builder .rules-list > *:last-child::before {
+ border-radius: 0 0 0 4px;
+}
+
+.query-builder .rules-list > *:last-child::after {
+ display: none;
+}
+
+.query-builder.bt-checkbox-glyphicons .checkbox input[type='checkbox']:checked + label::after {
+ font-family: 'Glyphicons Halflings';
+ content: '\e013';
+}
+
+.query-builder.bt-checkbox-glyphicons .checkbox label::after {
+ padding-left: 4px;
+ padding-top: 2px;
+ font-size: 9px;
+}
+
+.query-builder .error-container + .tooltip .tooltip-inner {
+ color: #F22 !important;
+}
+
+.query-builder p.filter-description {
+ margin: 5px 0 0 0;
+ background: rgba(0, 170, 255, 0.2);
+ border: 1px solid #346F7B;
+ color: #AAD1E4;
+ border-radius: 5px;
+ padding: 2.5px 5px;
+ font-size: .8em;
+}
+
+.query-builder .rules-group-header [data-invert] {
+ margin-left: 5px;
+}
+
+.query-builder .drag-handle {
+ cursor: move;
+ vertical-align: middle;
+ margin-left: 5px;
+}
+
+.query-builder .dragging {
+ position: fixed;
+ opacity: .5;
+ z-index: 100;
+}
+
+.query-builder .dragging::before, .query-builder .dragging::after {
+ display: none;
+}
+
+.query-builder .rule-placeholder {
+ border: 1px dashed #BBB;
+ opacity: .7;
+}
diff --git a/src/dist/css/query-builder.dark.min.css b/src/dist/css/query-builder.dark.min.css
new file mode 100644
index 00000000..b7d94c05
--- /dev/null
+++ b/src/dist/css/query-builder.dark.min.css
@@ -0,0 +1,6 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Copyright 2014-2018 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+.query-builder .rule-container,.query-builder .rule-placeholder,.query-builder .rules-group-container{position:relative;margin:4px 0;border-radius:5px;padding:5px;border:1px solid #111;background:rgba(40,40,40,.9)}.query-builder .drag-handle,.query-builder .error-container,.query-builder .rule-container .rule-filter-container,.query-builder .rule-container .rule-operator-container,.query-builder .rule-container .rule-value-container{display:inline-block;margin:0 5px 0 0;vertical-align:middle}.query-builder .rules-group-container{padding:10px;padding-bottom:6px;border:1px solid #00164a;background:rgba(50,70,80,.5)}.query-builder .rules-group-header{margin-bottom:10px}.query-builder .rules-group-header .group-conditions .btn.readonly:not(.active),.query-builder .rules-group-header .group-conditions input[name$='_cond']{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;white-space:nowrap}.query-builder .rules-group-header .group-conditions .btn.readonly{border-radius:3px}.query-builder .rules-list{list-style:none;padding:0 0 0 15px;margin:0}.query-builder .rule-value-container{border-left:1px solid #ddd;padding-left:5px}.query-builder .rule-value-container label{margin-bottom:0;font-weight:400}.query-builder .rule-value-container label.block{display:block}.query-builder .rule-value-container input[type=number],.query-builder .rule-value-container input[type=text],.query-builder .rule-value-container select{padding:1px}.query-builder .error-container{display:none;cursor:help;color:red}.query-builder .has-error{background-color:#322;border-color:#800}.query-builder .has-error .error-container{display:inline-block!important}.query-builder .rules-list>::after,.query-builder .rules-list>::before{content:'';position:absolute;left:-10px;width:10px;height:calc(50% + 4px);border-color:#222;border-style:solid}.query-builder .rules-list>::before{top:-4px;border-width:0 0 2px 2px}.query-builder .rules-list>::after{top:50%;border-width:0 0 0 2px}.query-builder .rules-list>:first-child::before{top:-12px;height:calc(50% + 14px)}.query-builder .rules-list>:last-child::before{border-radius:0 0 0 4px}.query-builder .rules-list>:last-child::after{display:none}.query-builder.bt-checkbox-glyphicons .checkbox input[type=checkbox]:checked+label::after{font-family:'Glyphicons Halflings';content:'\e013'}.query-builder.bt-checkbox-glyphicons .checkbox label::after{padding-left:4px;padding-top:2px;font-size:9px}.query-builder .error-container+.tooltip .tooltip-inner{color:#f22!important}.query-builder p.filter-description{margin:5px 0 0 0;background:rgba(0,170,255,.2);border:1px solid #346f7b;color:#aad1e4;border-radius:5px;padding:2.5px 5px;font-size:.8em}.query-builder .rules-group-header [data-invert]{margin-left:5px}.query-builder .drag-handle{cursor:move;vertical-align:middle;margin-left:5px}.query-builder .dragging{position:fixed;opacity:.5;z-index:100}.query-builder .dragging::after,.query-builder .dragging::before{display:none}.query-builder .rule-placeholder{border:1px dashed #bbb;opacity:.7}
\ No newline at end of file
diff --git a/src/dist/css/query-builder.default.css b/src/dist/css/query-builder.default.css
new file mode 100644
index 00000000..465fe2d6
--- /dev/null
+++ b/src/dist/css/query-builder.default.css
@@ -0,0 +1,173 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Copyright 2014-2018 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+.query-builder .rules-group-container, .query-builder .rule-container, .query-builder .rule-placeholder {
+ position: relative;
+ margin: 4px 0;
+ border-radius: 5px;
+ padding: 5px;
+ border: 1px solid #EEE;
+ background: rgba(255, 255, 255, 0.9);
+}
+
+.query-builder .rule-container .rule-filter-container,
+.query-builder .rule-container .rule-operator-container,
+.query-builder .rule-container .rule-value-container, .query-builder .error-container, .query-builder .drag-handle {
+ display: inline-block;
+ margin: 0 5px 0 0;
+ vertical-align: middle;
+}
+
+.query-builder .rules-group-container {
+ padding: 10px;
+ padding-bottom: 6px;
+ border: 1px solid #DCC896;
+ background: rgba(250, 240, 210, 0.5);
+}
+
+.query-builder .rules-group-header {
+ margin-bottom: 10px;
+}
+
+.query-builder .rules-group-header .group-conditions .btn.readonly:not(.active),
+.query-builder .rules-group-header .group-conditions input[name$='_cond'] {
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+ white-space: nowrap;
+}
+
+.query-builder .rules-group-header .group-conditions .btn.readonly {
+ border-radius: 3px;
+}
+
+.query-builder .rules-list {
+ list-style: none;
+ padding: 0 0 0 15px;
+ margin: 0;
+}
+
+.query-builder .rule-value-container {
+ border-left: 1px solid #DDD;
+ padding-left: 5px;
+}
+
+.query-builder .rule-value-container label {
+ margin-bottom: 0;
+ font-weight: normal;
+}
+
+.query-builder .rule-value-container label.block {
+ display: block;
+}
+
+.query-builder .rule-value-container select,
+.query-builder .rule-value-container input[type='text'],
+.query-builder .rule-value-container input[type='number'] {
+ padding: 1px;
+}
+
+.query-builder .error-container {
+ display: none;
+ cursor: help;
+ color: #F00;
+}
+
+.query-builder .has-error {
+ background-color: #FDD;
+ border-color: #F99;
+}
+
+.query-builder .has-error .error-container {
+ display: inline-block !important;
+}
+
+.query-builder .rules-list > *::before, .query-builder .rules-list > *::after {
+ content: '';
+ position: absolute;
+ left: -10px;
+ width: 10px;
+ height: calc(50% + 4px);
+ border-color: #CCC;
+ border-style: solid;
+}
+
+.query-builder .rules-list > *::before {
+ top: -4px;
+ border-width: 0 0 2px 2px;
+}
+
+.query-builder .rules-list > *::after {
+ top: 50%;
+ border-width: 0 0 0 2px;
+}
+
+.query-builder .rules-list > *:first-child::before {
+ top: -12px;
+ height: calc(50% + 14px);
+}
+
+.query-builder .rules-list > *:last-child::before {
+ border-radius: 0 0 0 4px;
+}
+
+.query-builder .rules-list > *:last-child::after {
+ display: none;
+}
+
+.query-builder.bt-checkbox-glyphicons .checkbox input[type='checkbox']:checked + label::after {
+ font-family: 'Glyphicons Halflings';
+ content: '\e013';
+}
+
+.query-builder.bt-checkbox-glyphicons .checkbox label::after {
+ padding-left: 4px;
+ padding-top: 2px;
+ font-size: 9px;
+}
+
+.query-builder .error-container + .tooltip .tooltip-inner {
+ color: #F99 !important;
+}
+
+.query-builder p.filter-description {
+ margin: 5px 0 0 0;
+ background: #D9EDF7;
+ border: 1px solid #BCE8F1;
+ color: #31708F;
+ border-radius: 5px;
+ padding: 2.5px 5px;
+ font-size: .8em;
+}
+
+.query-builder .rules-group-header [data-invert] {
+ margin-left: 5px;
+}
+
+.query-builder .drag-handle {
+ cursor: move;
+ vertical-align: middle;
+ margin-left: 5px;
+}
+
+.query-builder .dragging {
+ position: fixed;
+ opacity: .5;
+ z-index: 100;
+}
+
+.query-builder .dragging::before, .query-builder .dragging::after {
+ display: none;
+}
+
+.query-builder .rule-placeholder {
+ border: 1px dashed #BBB;
+ opacity: .7;
+}
diff --git a/src/dist/css/query-builder.default.min.css b/src/dist/css/query-builder.default.min.css
new file mode 100644
index 00000000..19d64ae9
--- /dev/null
+++ b/src/dist/css/query-builder.default.min.css
@@ -0,0 +1,6 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Copyright 2014-2018 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+.query-builder .rule-container,.query-builder .rule-placeholder,.query-builder .rules-group-container{position:relative;margin:4px 0;border-radius:5px;padding:5px;border:1px solid #eee;background:rgba(255,255,255,.9)}.query-builder .drag-handle,.query-builder .error-container,.query-builder .rule-container .rule-filter-container,.query-builder .rule-container .rule-operator-container,.query-builder .rule-container .rule-value-container{display:inline-block;margin:0 5px 0 0;vertical-align:middle}.query-builder .rules-group-container{padding:10px;padding-bottom:6px;border:1px solid #dcc896;background:rgba(250,240,210,.5)}.query-builder .rules-group-header{margin-bottom:10px}.query-builder .rules-group-header .group-conditions .btn.readonly:not(.active),.query-builder .rules-group-header .group-conditions input[name$='_cond']{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;white-space:nowrap}.query-builder .rules-group-header .group-conditions .btn.readonly{border-radius:3px}.query-builder .rules-list{list-style:none;padding:0 0 0 15px;margin:0}.query-builder .rule-value-container{border-left:1px solid #ddd;padding-left:5px}.query-builder .rule-value-container label{margin-bottom:0;font-weight:400}.query-builder .rule-value-container label.block{display:block}.query-builder .rule-value-container input[type=number],.query-builder .rule-value-container input[type=text],.query-builder .rule-value-container select{padding:1px}.query-builder .error-container{display:none;cursor:help;color:red}.query-builder .has-error{background-color:#fdd;border-color:#f99}.query-builder .has-error .error-container{display:inline-block!important}.query-builder .rules-list>::after,.query-builder .rules-list>::before{content:'';position:absolute;left:-10px;width:10px;height:calc(50% + 4px);border-color:#ccc;border-style:solid}.query-builder .rules-list>::before{top:-4px;border-width:0 0 2px 2px}.query-builder .rules-list>::after{top:50%;border-width:0 0 0 2px}.query-builder .rules-list>:first-child::before{top:-12px;height:calc(50% + 14px)}.query-builder .rules-list>:last-child::before{border-radius:0 0 0 4px}.query-builder .rules-list>:last-child::after{display:none}.query-builder.bt-checkbox-glyphicons .checkbox input[type=checkbox]:checked+label::after{font-family:'Glyphicons Halflings';content:'\e013'}.query-builder.bt-checkbox-glyphicons .checkbox label::after{padding-left:4px;padding-top:2px;font-size:9px}.query-builder .error-container+.tooltip .tooltip-inner{color:#f99!important}.query-builder p.filter-description{margin:5px 0 0 0;background:#d9edf7;border:1px solid #bce8f1;color:#31708f;border-radius:5px;padding:2.5px 5px;font-size:.8em}.query-builder .rules-group-header [data-invert]{margin-left:5px}.query-builder .drag-handle{cursor:move;vertical-align:middle;margin-left:5px}.query-builder .dragging{position:fixed;opacity:.5;z-index:100}.query-builder .dragging::after,.query-builder .dragging::before{display:none}.query-builder .rule-placeholder{border:1px dashed #bbb;opacity:.7}
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.ar.js b/src/dist/i18n/query-builder.ar.js
new file mode 100644
index 00000000..00783c48
--- /dev/null
+++ b/src/dist/i18n/query-builder.ar.js
@@ -0,0 +1,80 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Arabic (ar)
+ * Author: Mohamed YOUNES, https://github.com/MedYOUNES
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['ar'] = {
+ "__locale": "Arabic (ar)",
+ "__author": "Mohamed YOUNES, https://github.com/MedYOUNES",
+ "add_rule": "إضافة حُكم",
+ "add_group": "إضافة زُمْرَة",
+ "delete_rule": "حذف",
+ "delete_group": "حذف",
+ "conditions": {
+ "AND": "و",
+ "OR": "أو"
+ },
+ "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": "ليس صفرا"
+ },
+ "errors": {
+ "no_filter": "لم تحدد أي مرشح",
+ "empty_group": "الزمرة فارغة",
+ "radio_empty": "لم تحدد أي قيمة",
+ "checkbox_empty": "لم تحدد أي قيمة",
+ "select_empty": "لم تحدد أي قيمة",
+ "string_empty": "النص فارغ",
+ "string_exceed_min_length": "النص دون الأدنى المسموح به",
+ "string_exceed_max_length": "النص فوق الأقصى المسموح به",
+ "string_invalid_format": "تركيبة غير صحيحة",
+ "number_nan": "ليس عددا",
+ "number_not_integer": "ليس عددا صحيحا",
+ "number_not_double": "ليس عددا كسريا",
+ "number_exceed_min": "العدد أصغر من الأدنى المسموح به",
+ "number_exceed_max": "العدد أكبر من الأقصى المسموح به",
+ "number_wrong_step": "أخطأت في حساب مضاعفات العدد",
+ "datetime_empty": "لم تحدد التاريخ",
+ "datetime_invalid": "صيغة التاريخ غير صحيحة",
+ "datetime_exceed_min": "التاريخ دون الأدنى المسموح به",
+ "datetime_exceed_max": "التاريخ أكبر من الأقصى المسموح به",
+ "boolean_not_valid": "ليست قيمة منطقية ثنائية",
+ "operator_not_multiple": "العامل ليس متعدد القيَم"
+ },
+ "invert": "قَلْبُ"
+};
+
+QueryBuilder.defaults({ lang_code: 'ar' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.az.js b/src/dist/i18n/query-builder.az.js
new file mode 100644
index 00000000..c1174bc9
--- /dev/null
+++ b/src/dist/i18n/query-builder.az.js
@@ -0,0 +1,79 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Azerbaijan (az)
+ * Author: Megaplan, mborisv
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['az'] = {
+ "__locale": "Azerbaijan (az)",
+ "__author": "Megaplan, mborisv ",
+ "add_rule": "Əlavə etmək",
+ "add_group": "Qrup əlavə etmək",
+ "delete_rule": "Silmək",
+ "delete_group": "Silmək",
+ "conditions": {
+ "AND": "VƏ",
+ "OR": "VƏ YA"
+ },
+ "operators": {
+ "equal": "bərabərdir",
+ "not_equal": "bərabər deyil",
+ "in": "qeyd edilmişlərdən",
+ "not_in": "qeyd olunmamışlardan",
+ "less": "daha az",
+ "less_or_equal": "daha az və ya bərabər",
+ "greater": "daha çox",
+ "greater_or_equal": "daha çox və ya bərabər",
+ "between": "arasında",
+ "begins_with": "başlayır",
+ "not_begins_with": "başlamır",
+ "contains": "ibarətdir",
+ "not_contains": "yoxdur",
+ "ends_with": "başa çatır",
+ "not_ends_with": "başa çatmır",
+ "is_empty": "boş sətir",
+ "is_not_empty": "boş olmayan sətir",
+ "is_null": "boşdur",
+ "is_not_null": "boş deyil"
+ },
+ "errors": {
+ "no_filter": "Filterlər seçilməyib",
+ "empty_group": "Qrup boşdur",
+ "radio_empty": "Məna seçilməyib",
+ "checkbox_empty": "Məna seçilməyib",
+ "select_empty": "Məna seçilməyib",
+ "string_empty": "Doldurulmayıb",
+ "string_exceed_min_length": "{0} daha çox simvol olmalıdır",
+ "string_exceed_max_length": "{0} daha az simvol olmalıdır",
+ "string_invalid_format": "Yanlış format ({0})",
+ "number_nan": "Rəqəm deyil",
+ "number_not_integer": "Rəqəm deyil",
+ "number_not_double": "Rəqəm deyil",
+ "number_exceed_min": "{0} daha çox olmalıdır",
+ "number_exceed_max": "{0} daha az olmalıdır",
+ "number_wrong_step": "{0} bölünən olmalıdır",
+ "datetime_empty": "Doldurulmayıb",
+ "datetime_invalid": "Yanlış tarix formatı ({0})",
+ "datetime_exceed_min": "{0} sonra olmalıdır",
+ "datetime_exceed_max": "{0} əvvəl olmalıdır",
+ "boolean_not_valid": "Loqik olmayan",
+ "operator_not_multiple": "\"{1}\" operatoru çoxlu məna daşımır"
+ },
+ "invert": "invert"
+};
+
+QueryBuilder.defaults({ lang_code: 'az' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.bg.js b/src/dist/i18n/query-builder.bg.js
new file mode 100644
index 00000000..abde20a5
--- /dev/null
+++ b/src/dist/i18n/query-builder.bg.js
@@ -0,0 +1,79 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Bulgarian (bg)
+ * Author: Valentin Hristov
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['bg'] = {
+ "__locale": "Bulgarian (bg)",
+ "__author": "Valentin Hristov",
+ "add_rule": "Добави правило",
+ "add_group": "Добави група",
+ "delete_rule": "Изтрий",
+ "delete_group": "Изтрий",
+ "conditions": {
+ "AND": "И",
+ "OR": "ИЛИ"
+ },
+ "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": "различно от нищо"
+ },
+ "errors": {
+ "no_filter": "Не е избран филтър",
+ "empty_group": "Групата е празна",
+ "radio_empty": "Не е селектирана стойност",
+ "checkbox_empty": "Не е селектирана стойност",
+ "select_empty": "Не е селектирана стойност",
+ "string_empty": "Празна стойност",
+ "string_exceed_min_length": "Необходимо е да съдържа поне {0} символа",
+ "string_exceed_max_length": "Необходимо е да съдържа повече от {0} символа",
+ "string_invalid_format": "Невалиден формат ({0})",
+ "number_nan": "Не е число",
+ "number_not_integer": "Не е цяло число",
+ "number_not_double": "Не е реално число",
+ "number_exceed_min": "Трябва да е по-голямо от {0}",
+ "number_exceed_max": "Трябва да е по-малко от {0}",
+ "number_wrong_step": "Трябва да е кратно на {0}",
+ "datetime_empty": "Празна стойност",
+ "datetime_invalid": "Невалиден формат на дата ({0})",
+ "datetime_exceed_min": "Трябва да е след {0}",
+ "datetime_exceed_max": "Трябва да е преди {0}",
+ "boolean_not_valid": "Не е булева",
+ "operator_not_multiple": "Оператора \"{1}\" не може да приеме множество стойности"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'bg' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.cs.js b/src/dist/i18n/query-builder.cs.js
new file mode 100644
index 00000000..111316e9
--- /dev/null
+++ b/src/dist/i18n/query-builder.cs.js
@@ -0,0 +1,79 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Čeština (cs)
+ * Author: Megaplan, mborisv
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['cs'] = {
+ "__locale": "Čeština (cs)",
+ "__author": "Megaplan, mborisv ",
+ "add_rule": "Přidat",
+ "add_group": "Přidat skupinu",
+ "delete_rule": "Odstranit",
+ "delete_group": "Odstranit skupinu",
+ "conditions": {
+ "AND": "I",
+ "OR": "NEBO"
+ },
+ "operators": {
+ "equal": "stejně",
+ "not_equal": "liší se",
+ "in": "z uvedených",
+ "not_in": "ne z uvedených",
+ "less": "méně",
+ "less_or_equal": "méně nebo stejně",
+ "greater": "více",
+ "greater_or_equal": "více nebo stejně",
+ "between": "mezi",
+ "begins_with": "začíná z",
+ "not_begins_with": "nezačíná z",
+ "contains": "obsahuje",
+ "not_contains": "neobsahuje",
+ "ends_with": "končí na",
+ "not_ends_with": "nekončí na",
+ "is_empty": "prázdný řádek",
+ "is_not_empty": "neprázdný řádek",
+ "is_null": "prázdno",
+ "is_not_null": "plno"
+ },
+ "errors": {
+ "no_filter": "není vybraný filtr",
+ "empty_group": "prázdná skupina",
+ "radio_empty": "Není udaná hodnota",
+ "checkbox_empty": "Není udaná hodnota",
+ "select_empty": "Není udaná hodnota",
+ "string_empty": "Nevyplněno",
+ "string_exceed_min_length": "Musí obsahovat více {0} symbolů",
+ "string_exceed_max_length": "Musí obsahovat méně {0} symbolů",
+ "string_invalid_format": "Nesprávný formát ({0})",
+ "number_nan": "Žádné číslo",
+ "number_not_integer": "Žádné číslo",
+ "number_not_double": "Žádné číslo",
+ "number_exceed_min": "Musí být více {0}",
+ "number_exceed_max": "Musí být méně {0}",
+ "number_wrong_step": "Musí být násobkem {0}",
+ "datetime_empty": "Nevyplněno",
+ "datetime_invalid": "Nesprávný formát datumu ({0})",
+ "datetime_exceed_min": "Musí být po {0}",
+ "datetime_exceed_max": "Musí být do {0}",
+ "boolean_not_valid": "Nelogické",
+ "operator_not_multiple": "Operátor \"{1}\" nepodporuje mnoho hodnot"
+ },
+ "invert": "invertní"
+};
+
+QueryBuilder.defaults({ lang_code: 'cs' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.da.js b/src/dist/i18n/query-builder.da.js
new file mode 100644
index 00000000..242440dd
--- /dev/null
+++ b/src/dist/i18n/query-builder.da.js
@@ -0,0 +1,56 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Danish (da)
+ * Author: Jna Borup Coyle, github@coyle.dk
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['da'] = {
+ "__locale": "Danish (da)",
+ "__author": "Jna Borup Coyle, github@coyle.dk",
+ "add_rule": "Tilføj regel",
+ "add_group": "Tilføj gruppe",
+ "delete_rule": "Slet regel",
+ "delete_group": "Slet gruppe",
+ "conditions": {
+ "AND": "OG",
+ "OR": "ELLER"
+ },
+ "condition_and": "OG",
+ "condition_or": "ELLER",
+ "operators": {
+ "equal": "lig med",
+ "not_equal": "ikke lige med",
+ "in": "i",
+ "not_in": "ikke i",
+ "less": "mindre",
+ "less_or_equal": "mindre eller lig med",
+ "greater": "større",
+ "greater_or_equal": "større eller lig med",
+ "begins_with": "begynder med",
+ "not_begins_with": "begynder ikke med",
+ "contains": "indeholder",
+ "not_contains": "indeholder ikke",
+ "ends_with": "slutter med",
+ "not_ends_with": "slutter ikke med",
+ "is_empty": "er tom",
+ "is_not_empty": "er ikke tom",
+ "is_null": "er null",
+ "is_not_null": "er ikke null"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'da' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.de.js b/src/dist/i18n/query-builder.de.js
new file mode 100644
index 00000000..1145df0a
--- /dev/null
+++ b/src/dist/i18n/query-builder.de.js
@@ -0,0 +1,76 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: German (de)
+ * Author: "raimu"
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['de'] = {
+ "__locale": "German (de)",
+ "__author": "\"raimu\"",
+ "add_rule": "neue Regel",
+ "add_group": "neue Gruppe",
+ "delete_rule": "löschen",
+ "delete_group": "löschen",
+ "conditions": {
+ "AND": "UND",
+ "OR": "ODER"
+ },
+ "operators": {
+ "equal": "gleich",
+ "not_equal": "ungleich",
+ "in": "in",
+ "not_in": "nicht in",
+ "less": "kleiner",
+ "less_or_equal": "kleiner gleich",
+ "greater": "größer",
+ "greater_or_equal": "größer gleich",
+ "between": "zwischen",
+ "not_between": "nicht zwischen",
+ "begins_with": "beginnt mit",
+ "not_begins_with": "beginnt nicht mit",
+ "contains": "enthält",
+ "not_contains": "enthält nicht",
+ "ends_with": "endet mit",
+ "not_ends_with": "endet nicht mit",
+ "is_empty": "ist leer",
+ "is_not_empty": "ist nicht leer",
+ "is_null": "ist null",
+ "is_not_null": "ist nicht null"
+ },
+ "errors": {
+ "no_filter": "Kein Filter ausgewählt",
+ "empty_group": "Die Gruppe ist leer",
+ "radio_empty": "Kein Wert ausgewählt",
+ "checkbox_empty": "Kein Wert ausgewählt",
+ "select_empty": "Kein Wert ausgewählt",
+ "string_empty": "Leerer Wert",
+ "string_exceed_min_length": "Muss mindestens {0} Zeichen enthalten",
+ "string_exceed_max_length": "Darf nicht mehr als {0} Zeichen enthalten",
+ "string_invalid_format": "Ungültiges Format ({0})",
+ "number_nan": "Keine Zahl",
+ "number_not_integer": "Keine Ganzzahl",
+ "number_not_double": "Keine Dezimalzahl",
+ "number_exceed_min": "Muss größer als {0} sein",
+ "number_exceed_max": "Muss kleiner als {0} sein",
+ "number_wrong_step": "Muss ein Vielfaches von {0} sein",
+ "datetime_invalid": "Ungültiges Datumsformat ({0})",
+ "datetime_exceed_min": "Muss nach dem {0} sein",
+ "datetime_exceed_max": "Muss vor dem {0} sein"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'de' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.el.js b/src/dist/i18n/query-builder.el.js
new file mode 100644
index 00000000..7f11b0d5
--- /dev/null
+++ b/src/dist/i18n/query-builder.el.js
@@ -0,0 +1,80 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Greek (el)
+ * Author: Stelios Patsatzis, https://www.linkedin.com/in/stelios-patsatzis-89841561
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['el'] = {
+ "__locale": "Greek (el)",
+ "__author": "Stelios Patsatzis, https://www.linkedin.com/in/stelios-patsatzis-89841561",
+ "add_rule": "Προσθήκη Συνθήκης",
+ "add_group": "Προσθήκη Ομάδας",
+ "delete_rule": "Διαγραφή",
+ "delete_group": "Διαγραφή",
+ "conditions": {
+ "AND": "Λογικό ΚΑΙ",
+ "OR": "Λογικό Η"
+ },
+ "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": "Είναι NULL",
+ "is_not_null": "Δεν είναι NULL"
+ },
+ "errors": {
+ "no_filter": "Χωρίς φίλτρα",
+ "empty_group": "Άδεια ομάδα",
+ "radio_empty": "Χωρίς τιμή",
+ "checkbox_empty": "Χωρίς τιμή",
+ "select_empty": "Χωρίς τιμή",
+ "string_empty": "Χωρίς τιμή",
+ "string_exceed_min_length": "Ελάχιστο όριο {0} χαρακτήρων",
+ "string_exceed_max_length": "Μέγιστο όριο {0} χαρακτήρων",
+ "string_invalid_format": "Λανθασμένη μορφή ({0})",
+ "number_nan": "Δεν είναι αριθμός",
+ "number_not_integer": "Δεν είναι ακέραιος αριθμός",
+ "number_not_double": "Δεν είναι πραγματικός αριθμός",
+ "number_exceed_min": "Πρέπει να είναι μεγαλύτερο απο {0}",
+ "number_exceed_max": "Πρέπει να είναι μικρότερο απο {0}",
+ "number_wrong_step": "Πρέπει να είναι πολλαπλάσιο του {0}",
+ "datetime_empty": "Χωρίς τιμή",
+ "datetime_invalid": "Λανθασμένη μορφή ημερομηνίας ({0})",
+ "datetime_exceed_min": "Νεότερο από {0}",
+ "datetime_exceed_max": "Παλαιότερο από {0}",
+ "boolean_not_valid": "Δεν είναι BOOLEAN",
+ "operator_not_multiple": "Η συνθήκη \"{1}\" δεν μπορεί να δεχθεί πολλαπλές τιμές"
+ },
+ "invert": "Εναλλαγή"
+};
+
+QueryBuilder.defaults({ lang_code: 'el' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.en.js b/src/dist/i18n/query-builder.en.js
new file mode 100644
index 00000000..e142d110
--- /dev/null
+++ b/src/dist/i18n/query-builder.en.js
@@ -0,0 +1,83 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: English (en)
+ * Author: 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(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['en'] = {
+ "__locale": "English (en)",
+ "__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr",
+ "add_rule": "Add rule",
+ "add_group": "Add group",
+ "delete_rule": "Delete",
+ "delete_group": "Delete",
+ "conditions": {
+ "AND": "AND",
+ "OR": "OR"
+ },
+ "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": "contains",
+ "not_contains": "doesn't contain",
+ "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' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.es.js b/src/dist/i18n/query-builder.es.js
new file mode 100644
index 00000000..af736958
--- /dev/null
+++ b/src/dist/i18n/query-builder.es.js
@@ -0,0 +1,81 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Spanish (es)
+ * Author: "pyarza", "kddlb"
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['es'] = {
+ "__locale": "Spanish (es)",
+ "__author": "\"pyarza\", \"kddlb\"",
+ "add_rule": "Añadir regla",
+ "add_group": "Añadir grupo",
+ "delete_rule": "Borrar",
+ "delete_group": "Borrar",
+ "conditions": {
+ "AND": "Y",
+ "OR": "O"
+ },
+ "operators": {
+ "equal": "igual",
+ "not_equal": "distinto",
+ "in": "en",
+ "not_in": "no en",
+ "less": "menor",
+ "less_or_equal": "menor o igual",
+ "greater": "mayor",
+ "greater_or_equal": "mayor o igual",
+ "between": "entre",
+ "not_between": "no está entre",
+ "begins_with": "empieza por",
+ "not_begins_with": "no empieza por",
+ "contains": "contiene",
+ "not_contains": "no contiene",
+ "ends_with": "acaba con",
+ "not_ends_with": "no acaba con",
+ "is_empty": "está vacío",
+ "is_not_empty": "no está vacío",
+ "is_null": "es nulo",
+ "is_not_null": "no es nulo"
+ },
+ "errors": {
+ "no_filter": "No se ha seleccionado ningún filtro",
+ "empty_group": "El grupo está vacío",
+ "radio_empty": "Ningún valor seleccionado",
+ "checkbox_empty": "Ningún valor seleccionado",
+ "select_empty": "Ningún valor seleccionado",
+ "string_empty": "Cadena vacía",
+ "string_exceed_min_length": "Debe contener al menos {0} caracteres",
+ "string_exceed_max_length": "No debe contener más de {0} caracteres",
+ "string_invalid_format": "Formato inválido ({0})",
+ "number_nan": "No es un número",
+ "number_not_integer": "No es un número entero",
+ "number_not_double": "No es un número real",
+ "number_exceed_min": "Debe ser mayor que {0}",
+ "number_exceed_max": "Debe ser menor que {0}",
+ "number_wrong_step": "Debe ser múltiplo de {0}",
+ "datetime_invalid": "Formato de fecha inválido ({0})",
+ "datetime_exceed_min": "Debe ser posterior a {0}",
+ "datetime_exceed_max": "Debe ser anterior a {0}",
+ "number_between_invalid": "Valores Inválidos, {0} es mayor que {1}",
+ "datetime_empty": "Campo vacio",
+ "datetime_between_invalid": "Valores Inválidos, {0} es mayor que {1}",
+ "boolean_not_valid": "No es booleano",
+ "operator_not_multiple": "El operador \"{1}\" no puede aceptar valores multiples"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'es' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.fa-IR.js b/src/dist/i18n/query-builder.fa-IR.js
new file mode 100644
index 00000000..ef27b355
--- /dev/null
+++ b/src/dist/i18n/query-builder.fa-IR.js
@@ -0,0 +1,79 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Farsi (fa-ir)
+ * Author: Behzad Sedighzade, behzad.sedighzade@gmail.com
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['fa-IR'] = {
+ "__locale": "Farsi (fa-ir)",
+ "__author": "Behzad Sedighzade, behzad.sedighzade@gmail.com",
+ "add_rule": "افزودن قاعده",
+ "add_group": "افزودن گروه",
+ "delete_rule": "حذف قاعده",
+ "delete_group": "حذف گروه",
+ "conditions": {
+ "AND": "و",
+ "OR": "یا"
+ },
+ "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": "باشد ( null ) پوچ",
+ "is_not_null": "نباشد( null ) پوچ "
+ },
+ "errors": {
+ "no_filter": "هیچ قاعده ای انتخاب نشده است",
+ "empty_group": "گروه خالی است",
+ "radio_empty": "مقداری انتخاب نشده است",
+ "checkbox_empty": "مقداری انتخاب نشده است",
+ "select_empty": "مقداری انتخاب نشده است",
+ "string_empty": "مقدار متنی خالی است",
+ "string_exceed_min_length": "رشته حداقل باید {0} عدد حرف داشته باشد",
+ "string_exceed_max_length": "رشته حداکثر {0} عدد حرف می تواند قبول کند",
+ "string_invalid_format": "قالب رشته {0} نامعتبر ست",
+ "number_nan": "عدد وارد کنید",
+ "number_not_integer": "مقدار صحیح وارد کنید",
+ "number_not_double": "مقدار اعشاری وارد کنید",
+ "number_exceed_min": "باید از {0} بزرگتر باشد",
+ "number_exceed_max": "باید از {0} کمتر باشد",
+ "number_wrong_step": "باید مضربی از {0} باشد",
+ "datetime_empty": "مقدار تاریخ خالی وارد شده!",
+ "datetime_invalid": "قالب تاریخ ( {0} ) اشتباه است",
+ "datetime_exceed_min": "باید بعد از {0} باشد",
+ "datetime_exceed_max": "باید قبل از {0} باشد",
+ "boolean_not_valid": "مقدار دودویی وارد کنید",
+ "operator_not_multiple": "اپراتور \"{1}\" نمی تواند چند مقدار قبول کند"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'fa-IR' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.fr.js b/src/dist/i18n/query-builder.fr.js
new file mode 100644
index 00000000..d0e601a8
--- /dev/null
+++ b/src/dist/i18n/query-builder.fr.js
@@ -0,0 +1,83 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: French (fr)
+ * Author: 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(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['fr'] = {
+ "__locale": "French (fr)",
+ "__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr",
+ "add_rule": "Ajouter une règle",
+ "add_group": "Ajouter un groupe",
+ "delete_rule": "Supprimer",
+ "delete_group": "Supprimer",
+ "conditions": {
+ "AND": "ET",
+ "OR": "OU"
+ },
+ "operators": {
+ "equal": "est égal à",
+ "not_equal": "n'est pas égal à",
+ "in": "est compris dans",
+ "not_in": "n'est pas compris dans",
+ "less": "est inférieur à",
+ "less_or_equal": "est inférieur ou égal à",
+ "greater": "est supérieur à",
+ "greater_or_equal": "est supérieur ou égal à",
+ "between": "est entre",
+ "not_between": "n'est pas entre",
+ "begins_with": "commence par",
+ "not_begins_with": "ne commence pas par",
+ "contains": "contient",
+ "not_contains": "ne contient pas",
+ "ends_with": "finit par",
+ "not_ends_with": "ne finit pas par",
+ "is_empty": "est vide",
+ "is_not_empty": "n'est pas vide",
+ "is_null": "est nul",
+ "is_not_null": "n'est pas nul"
+ },
+ "errors": {
+ "no_filter": "Aucun filtre sélectionné",
+ "empty_group": "Le groupe est vide",
+ "radio_empty": "Pas de valeur selectionnée",
+ "checkbox_empty": "Pas de valeur selectionnée",
+ "select_empty": "Pas de valeur selectionnée",
+ "string_empty": "Valeur vide",
+ "string_exceed_min_length": "Doit contenir au moins {0} caractères",
+ "string_exceed_max_length": "Ne doit pas contenir plus de {0} caractères",
+ "string_invalid_format": "Format invalide ({0})",
+ "number_nan": "N'est pas un nombre",
+ "number_not_integer": "N'est pas un entier",
+ "number_not_double": "N'est pas un nombre réel",
+ "number_exceed_min": "Doit être plus grand que {0}",
+ "number_exceed_max": "Doit être plus petit que {0}",
+ "number_wrong_step": "Doit être un multiple de {0}",
+ "number_between_invalid": "Valeurs invalides, {0} est plus grand que {1}",
+ "datetime_empty": "Valeur vide",
+ "datetime_invalid": "Fomat de date invalide ({0})",
+ "datetime_exceed_min": "Doit être après {0}",
+ "datetime_exceed_max": "Doit être avant {0}",
+ "datetime_between_invalid": "Valeurs invalides, {0} est plus grand que {1}",
+ "boolean_not_valid": "N'est pas un booléen",
+ "operator_not_multiple": "L'opérateur \"{1}\" ne peut utiliser plusieurs valeurs"
+ },
+ "invert": "Inverser",
+ "NOT": "NON"
+};
+
+QueryBuilder.defaults({ lang_code: 'fr' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.he.js b/src/dist/i18n/query-builder.he.js
new file mode 100644
index 00000000..68d2063d
--- /dev/null
+++ b/src/dist/i18n/query-builder.he.js
@@ -0,0 +1,81 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Hebrew (he)
+ * Author: Kfir Stri https://github.com/kfirstri
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['he'] = {
+ "__locale": "Hebrew (he)",
+ "__author": "Kfir Stri https://github.com/kfirstri",
+ "add_rule": "הוסף כלל",
+ "add_group": "הוסף קבוצה",
+ "delete_rule": "מחק",
+ "delete_group": "מחק",
+ "conditions": {
+ "AND": "וגם",
+ "OR": "או"
+ },
+ "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": "לא חסר ערך"
+ },
+ "errors": {
+ "no_filter": "לא נבחרו מסננים",
+ "empty_group": "הקבוצה רירקה",
+ "radio_empty": "לא נבחר אף ערך",
+ "checkbox_empty": "לא נבחר אף ערך",
+ "select_empty": "לא נבחר אף ערך",
+ "string_empty": "חסר ערך",
+ "string_exceed_min_length": "המחרוזת חייבת להכיל לפחות {0} תווים",
+ "string_exceed_max_length": "המחרוזת לא יכולה להכיל יותר מ{0} תווים",
+ "string_invalid_format": "המחרוזת בפורמט שגוי ({0})",
+ "number_nan": "זהו לא מספר",
+ "number_not_integer": "המספר אינו מספר שלם",
+ "number_not_double": "המספר אינו מספר עשרוני",
+ "number_exceed_min": "המספר צריך להיות גדול מ {0}",
+ "number_exceed_max": "המספר צריך להיות קטן מ{0}",
+ "number_wrong_step": "המספר צריך להיות כפולה של {0}",
+ "datetime_empty": "תאריך ריק",
+ "datetime_invalid": "פורמט תאריך שגוי ({0})",
+ "datetime_exceed_min": "התאריך חייב להיות אחרי {0}",
+ "datetime_exceed_max": "התאריך חייב להיות לפני {0}",
+ "boolean_not_valid": "זהו לא בוליאני",
+ "operator_not_multiple": "האופרטור \"{1}\" לא יכול לקבל ערכים מרובים"
+ },
+ "invert": "הפוך שאילתא",
+ "NOT": "לא"
+};
+
+QueryBuilder.defaults({ lang_code: 'he' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.it.js b/src/dist/i18n/query-builder.it.js
new file mode 100644
index 00000000..e0ca54f2
--- /dev/null
+++ b/src/dist/i18n/query-builder.it.js
@@ -0,0 +1,79 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Italian (it)
+ * Author: davegraziosi
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['it'] = {
+ "__locale": "Italian (it)",
+ "__author": "davegraziosi",
+ "add_rule": "Aggiungi regola",
+ "add_group": "Aggiungi gruppo",
+ "delete_rule": "Elimina",
+ "delete_group": "Elimina",
+ "conditions": {
+ "AND": "E",
+ "OR": "O"
+ },
+ "operators": {
+ "equal": "uguale",
+ "not_equal": "non uguale",
+ "in": "in",
+ "not_in": "non in",
+ "less": "minore",
+ "less_or_equal": "minore o uguale",
+ "greater": "maggiore",
+ "greater_or_equal": "maggiore o uguale",
+ "between": "compreso tra",
+ "not_between": "non compreso tra",
+ "begins_with": "inizia con",
+ "not_begins_with": "non inizia con",
+ "contains": "contiene",
+ "not_contains": "non contiene",
+ "ends_with": "finisce con",
+ "not_ends_with": "non finisce con",
+ "is_empty": "è vuoto",
+ "is_not_empty": "non è vuoto",
+ "is_null": "è nullo",
+ "is_not_null": "non è nullo"
+ },
+ "errors": {
+ "no_filter": "Nessun filtro selezionato",
+ "empty_group": "Il gruppo è vuoto",
+ "radio_empty": "No value selected",
+ "checkbox_empty": "Nessun valore selezionato",
+ "select_empty": "Nessun valore selezionato",
+ "string_empty": "Valore vuoto",
+ "string_exceed_min_length": "Deve contenere almeno {0} caratteri",
+ "string_exceed_max_length": "Non deve contenere più di {0} caratteri",
+ "string_invalid_format": "Formato non valido ({0})",
+ "number_nan": "Non è un numero",
+ "number_not_integer": "Non è un intero",
+ "number_not_double": "Non è un numero con la virgola",
+ "number_exceed_min": "Deve essere maggiore di {0}",
+ "number_exceed_max": "Deve essere minore di {0}",
+ "number_wrong_step": "Deve essere multiplo di {0}",
+ "datetime_empty": "Valore vuoto",
+ "datetime_invalid": "Formato data non valido ({0})",
+ "datetime_exceed_min": "Deve essere successivo a {0}",
+ "datetime_exceed_max": "Deve essere precedente a {0}",
+ "boolean_not_valid": "Non è un booleano",
+ "operator_not_multiple": "L'Operatore {0} non può accettare valori multipli"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'it' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.nl.js b/src/dist/i18n/query-builder.nl.js
new file mode 100644
index 00000000..1cb59338
--- /dev/null
+++ b/src/dist/i18n/query-builder.nl.js
@@ -0,0 +1,76 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Dutch (nl)
+ * Author: "Roywcm"
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['nl'] = {
+ "__locale": "Dutch (nl)",
+ "__author": "\"Roywcm\"",
+ "add_rule": "Nieuwe regel",
+ "add_group": "Nieuwe groep",
+ "delete_rule": "Verwijder",
+ "delete_group": "Verwijder",
+ "conditions": {
+ "AND": "EN",
+ "OR": "OF"
+ },
+ "operators": {
+ "equal": "gelijk",
+ "not_equal": "niet gelijk",
+ "in": "in",
+ "not_in": "niet in",
+ "less": "minder",
+ "less_or_equal": "minder of gelijk",
+ "greater": "groter",
+ "greater_or_equal": "groter of gelijk",
+ "between": "tussen",
+ "not_between": "niet tussen",
+ "begins_with": "begint met",
+ "not_begins_with": "begint niet met",
+ "contains": "bevat",
+ "not_contains": "bevat niet",
+ "ends_with": "eindigt met",
+ "not_ends_with": "eindigt niet met",
+ "is_empty": "is leeg",
+ "is_not_empty": "is niet leeg",
+ "is_null": "is null",
+ "is_not_null": "is niet null"
+ },
+ "errors": {
+ "no_filter": "Geen filter geselecteerd",
+ "empty_group": "De groep is leeg",
+ "radio_empty": "Geen waarde geselecteerd",
+ "checkbox_empty": "Geen waarde geselecteerd",
+ "select_empty": "Geen waarde geselecteerd",
+ "string_empty": "Lege waarde",
+ "string_exceed_min_length": "Dient minstens {0} karakters te bevatten",
+ "string_exceed_max_length": "Dient niet meer dan {0} karakters te bevatten",
+ "string_invalid_format": "Ongeldig format ({0})",
+ "number_nan": "Niet een nummer",
+ "number_not_integer": "Geen geheel getal",
+ "number_not_double": "Geen echt nummer",
+ "number_exceed_min": "Dient groter te zijn dan {0}",
+ "number_exceed_max": "Dient lager te zijn dan {0}",
+ "number_wrong_step": "Dient een veelvoud te zijn van {0}",
+ "datetime_invalid": "Ongeldige datumformat ({0})",
+ "datetime_exceed_min": "Dient na {0}",
+ "datetime_exceed_max": "Dient voor {0}"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'nl' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.no.js b/src/dist/i18n/query-builder.no.js
new file mode 100644
index 00000000..6387aef7
--- /dev/null
+++ b/src/dist/i18n/query-builder.no.js
@@ -0,0 +1,54 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Norwegian (no)
+ * Author: Jna Borup Coyle, github@coyle.dk
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['no'] = {
+ "__locale": "Norwegian (no)",
+ "__author": "Jna Borup Coyle, github@coyle.dk",
+ "add_rule": "Legg til regel",
+ "add_group": "Legg til gruppe",
+ "delete_rule": "Slett regel",
+ "delete_group": "Slett gruppe",
+ "conditions": {
+ "AND": "OG",
+ "OR": "ELLER"
+ },
+ "operators": {
+ "equal": "er lik",
+ "not_equal": "er ikke lik",
+ "in": "finnes i",
+ "not_in": "finnes ikke i",
+ "less": "er mindre enn",
+ "less_or_equal": "er mindre eller lik",
+ "greater": "er større enn",
+ "greater_or_equal": "er større eller lik",
+ "begins_with": "begynner med",
+ "not_begins_with": "begynner ikke med",
+ "contains": "inneholder",
+ "not_contains": "inneholder ikke",
+ "ends_with": "slutter med",
+ "not_ends_with": "slutter ikke med",
+ "is_empty": "er tom",
+ "is_not_empty": "er ikke tom",
+ "is_null": "er null",
+ "is_not_null": "er ikke null"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'no' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.pl.js b/src/dist/i18n/query-builder.pl.js
new file mode 100644
index 00000000..b766fbf1
--- /dev/null
+++ b/src/dist/i18n/query-builder.pl.js
@@ -0,0 +1,80 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Polish (pl)
+ * Author: Artur Smolarek
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['pl'] = {
+ "__locale": "Polish (pl)",
+ "__author": "Artur Smolarek",
+ "add_rule": "Dodaj regułę",
+ "add_group": "Dodaj grupę",
+ "delete_rule": "Usuń",
+ "delete_group": "Usuń",
+ "conditions": {
+ "AND": "ORAZ",
+ "OR": "LUB"
+ },
+ "operators": {
+ "equal": "równa się",
+ "not_equal": "jest różne od",
+ "in": "zawiera",
+ "not_in": "nie zawiera",
+ "less": "mniejsze",
+ "less_or_equal": "mniejsze lub równe",
+ "greater": "większe",
+ "greater_or_equal": "większe lub równe",
+ "between": "pomiędzy",
+ "not_between": "nie jest pomiędzy",
+ "begins_with": "rozpoczyna się od",
+ "not_begins_with": "nie rozpoczyna się od",
+ "contains": "zawiera",
+ "not_contains": "nie zawiera",
+ "ends_with": "kończy się na",
+ "not_ends_with": "nie kończy się na",
+ "is_empty": "jest puste",
+ "is_not_empty": "nie jest puste",
+ "is_null": "jest niezdefiniowane",
+ "is_not_null": "nie jest niezdefiniowane"
+ },
+ "errors": {
+ "no_filter": "Nie wybrano żadnego filtra",
+ "empty_group": "Grupa jest pusta",
+ "radio_empty": "Nie wybrano wartości",
+ "checkbox_empty": "Nie wybrano wartości",
+ "select_empty": "Nie wybrano wartości",
+ "string_empty": "Nie wpisano wartości",
+ "string_exceed_min_length": "Minimalna długość to {0} znaków",
+ "string_exceed_max_length": "Maksymalna długość to {0} znaków",
+ "string_invalid_format": "Nieprawidłowy format ({0})",
+ "number_nan": "To nie jest liczba",
+ "number_not_integer": "To nie jest liczba całkowita",
+ "number_not_double": "To nie jest liczba rzeczywista",
+ "number_exceed_min": "Musi być większe niż {0}",
+ "number_exceed_max": "Musi być mniejsze niż {0}",
+ "number_wrong_step": "Musi być wielokrotnością {0}",
+ "datetime_empty": "Nie wybrano wartości",
+ "datetime_invalid": "Nieprawidłowy format daty ({0})",
+ "datetime_exceed_min": "Musi być po {0}",
+ "datetime_exceed_max": "Musi być przed {0}",
+ "boolean_not_valid": "Niepoprawna wartość logiczna",
+ "operator_not_multiple": "Operator \"{1}\" nie przyjmuje wielu wartości"
+ },
+ "invert": "Odwróć"
+};
+
+QueryBuilder.defaults({ lang_code: 'pl' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.pt-BR.js b/src/dist/i18n/query-builder.pt-BR.js
new file mode 100644
index 00000000..c0b5bd98
--- /dev/null
+++ b/src/dist/i18n/query-builder.pt-BR.js
@@ -0,0 +1,80 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Brazilian Portuguese (pr-BR)
+ * Author: Leandro Gehlen, leandrogehlen@gmail.com; Marcos Ferretti, marcosvferretti@gmail.com
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['pt-BR'] = {
+ "__locale": "Brazilian Portuguese (pr-BR)",
+ "__author": "Leandro Gehlen, leandrogehlen@gmail.com; Marcos Ferretti, marcosvferretti@gmail.com",
+ "add_rule": "Nova Regra",
+ "add_group": "Novo Grupo",
+ "delete_rule": "Excluir",
+ "delete_group": "Excluir",
+ "conditions": {
+ "AND": "E",
+ "OR": "OU"
+ },
+ "operators": {
+ "equal": "Igual",
+ "not_equal": "Diferente",
+ "in": "Contido",
+ "not_in": "Não contido",
+ "less": "Menor",
+ "less_or_equal": "Menor ou igual",
+ "greater": "Maior",
+ "greater_or_equal": "Maior ou igual",
+ "between": "Entre",
+ "not_between": "Não entre",
+ "begins_with": "Iniciando com",
+ "not_begins_with": "Não iniciando com",
+ "contains": "Contém",
+ "not_contains": "Não contém",
+ "ends_with": "Terminando com",
+ "not_ends_with": "Terminando sem",
+ "is_empty": "É vazio",
+ "is_not_empty": "Não é vazio",
+ "is_null": "É nulo",
+ "is_not_null": "Não é nulo"
+ },
+ "errors": {
+ "no_filter": "Nenhum filtro selecionado",
+ "empty_group": "O grupo está vazio",
+ "radio_empty": "Nenhum valor selecionado",
+ "checkbox_empty": "Nenhum valor selecionado",
+ "select_empty": "Nenhum valor selecionado",
+ "string_empty": "Valor vazio",
+ "string_exceed_min_length": "É necessário conter pelo menos {0} caracteres",
+ "string_exceed_max_length": "É necessário conter mais de {0} caracteres",
+ "string_invalid_format": "Formato inválido ({0})",
+ "number_nan": "Não é um número",
+ "number_not_integer": "Não é um número inteiro",
+ "number_not_double": "Não é um número real",
+ "number_exceed_min": "É necessário ser maior que {0}",
+ "number_exceed_max": "É necessário ser menor que {0}",
+ "number_wrong_step": "É necessário ser múltiplo de {0}",
+ "datetime_invalid": "Formato de data inválido ({0})",
+ "datetime_exceed_min": "É necessário ser superior a {0}",
+ "datetime_exceed_max": "É necessário ser inferior a {0}",
+ "datetime_empty": "Nenhuma data selecionada",
+ "boolean_not_valid": "Não é um valor booleano",
+ "operator_not_multiple": "O operador \"{1}\" não aceita valores múltiplos"
+ },
+ "invert": "Inverter"
+};
+
+QueryBuilder.defaults({ lang_code: 'pt-BR' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.pt-PT.js b/src/dist/i18n/query-builder.pt-PT.js
new file mode 100644
index 00000000..d88cfc01
--- /dev/null
+++ b/src/dist/i18n/query-builder.pt-PT.js
@@ -0,0 +1,75 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Portuguese (pt-PT)
+ * Author: Miguel Guerreiro, migas.csi@gmail.com
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['pt-PT'] = {
+ "__locale": "Portuguese (pt-PT)",
+ "__author": "Miguel Guerreiro, migas.csi@gmail.com",
+ "add_rule": "Nova Regra",
+ "add_group": "Novo Grupo",
+ "delete_rule": "Excluir",
+ "delete_group": "Excluir",
+ "conditions": {
+ "AND": "E",
+ "OR": "OU"
+ },
+ "operators": {
+ "equal": "Igual a",
+ "not_equal": "Diferente de",
+ "in": "Contido",
+ "not_in": "Não contido",
+ "less": "Menor que",
+ "less_or_equal": "Menor ou igual a",
+ "greater": "Maior que",
+ "greater_or_equal": "Maior ou igual que",
+ "between": "Entre",
+ "begins_with": "Começar por",
+ "not_begins_with": "Não a começar por",
+ "contains": "Contém",
+ "not_contains": "Não contém",
+ "ends_with": "Terminando com",
+ "not_ends_with": "Terminando sem",
+ "is_empty": "É vazio",
+ "is_not_empty": "Não é vazio",
+ "is_null": "É nulo",
+ "is_not_null": "Não é nulo"
+ },
+ "errors": {
+ "no_filter": "Nenhum filtro selecionado",
+ "empty_group": "O grupo está vazio",
+ "radio_empty": "Nenhum valor selecionado",
+ "checkbox_empty": "Nenhum valor selecionado",
+ "select_empty": "Nenhum valor selecionado",
+ "string_empty": "Valor vazio",
+ "string_exceed_min_length": "É necessário conter pelo menos {0} caracteres",
+ "string_exceed_max_length": "É necessário conter mais de {0} caracteres",
+ "string_invalid_format": "Formato inválido ({0})",
+ "number_nan": "Não é um número",
+ "number_not_integer": "Não é um número inteiro",
+ "number_not_double": "Não é um número real",
+ "number_exceed_min": "É necessário ser maior que {0}",
+ "number_exceed_max": "É necessário ser menor que {0}",
+ "number_wrong_step": "É necessário ser múltiplo de {0}",
+ "datetime_invalid": "Formato de data inválido ({0})",
+ "datetime_exceed_min": "É necessário ser superior a {0}",
+ "datetime_exceed_max": "É necessário ser inferior a {0}"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'pt-PT' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.ro.js b/src/dist/i18n/query-builder.ro.js
new file mode 100644
index 00000000..c629b4bd
--- /dev/null
+++ b/src/dist/i18n/query-builder.ro.js
@@ -0,0 +1,54 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Romanian (ro)
+ * Author: ArianServ
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['ro'] = {
+ "__locale": "Romanian (ro)",
+ "__author": "ArianServ",
+ "add_rule": "Adaugă regulă",
+ "add_group": "Adaugă grup",
+ "delete_rule": "Şterge",
+ "delete_group": "Şterge",
+ "conditions": {
+ "AND": "ŞI",
+ "OR": "SAU"
+ },
+ "operators": {
+ "equal": "egal",
+ "not_equal": "diferit",
+ "in": "în",
+ "not_in": "nu în",
+ "less": "mai puţin",
+ "less_or_equal": "mai puţin sau egal",
+ "greater": "mai mare",
+ "greater_or_equal": "mai mare sau egal",
+ "begins_with": "începe cu",
+ "not_begins_with": "nu începe cu",
+ "contains": "conţine",
+ "not_contains": "nu conţine",
+ "ends_with": "se termină cu",
+ "not_ends_with": "nu se termină cu",
+ "is_empty": "este gol",
+ "is_not_empty": "nu este gol",
+ "is_null": "e nul",
+ "is_not_null": "nu e nul"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'ro' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.ru.js b/src/dist/i18n/query-builder.ru.js
new file mode 100644
index 00000000..e09665cd
--- /dev/null
+++ b/src/dist/i18n/query-builder.ru.js
@@ -0,0 +1,77 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Russian (ru)
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['ru'] = {
+ "__locale": "Russian (ru)",
+ "add_rule": "Добавить",
+ "add_group": "Добавить группу",
+ "delete_rule": "Удалить",
+ "delete_group": "Удалить",
+ "conditions": {
+ "AND": "И",
+ "OR": "ИЛИ"
+ },
+ "operators": {
+ "equal": "равно",
+ "not_equal": "не равно",
+ "in": "из указанных",
+ "not_in": "не из указанных",
+ "less": "меньше",
+ "less_or_equal": "меньше или равно",
+ "greater": "больше",
+ "greater_or_equal": "больше или равно",
+ "between": "между",
+ "begins_with": "начинается с",
+ "not_begins_with": "не начинается с",
+ "contains": "содержит",
+ "not_contains": "не содержит",
+ "ends_with": "оканчивается на",
+ "not_ends_with": "не оканчивается на",
+ "is_empty": "пустая строка",
+ "is_not_empty": "не пустая строка",
+ "is_null": "пусто",
+ "is_not_null": "не пусто"
+ },
+ "errors": {
+ "no_filter": "Фильтр не выбран",
+ "empty_group": "Группа пуста",
+ "radio_empty": "Не выбранно значение",
+ "checkbox_empty": "Не выбранно значение",
+ "select_empty": "Не выбранно значение",
+ "string_empty": "Не заполненно",
+ "string_exceed_min_length": "Должен содержать больше {0} символов",
+ "string_exceed_max_length": "Должен содержать меньше {0} символов",
+ "string_invalid_format": "Неверный формат ({0})",
+ "number_nan": "Не число",
+ "number_not_integer": "Не число",
+ "number_not_double": "Не число",
+ "number_exceed_min": "Должно быть больше {0}",
+ "number_exceed_max": "Должно быть меньше, чем {0}",
+ "number_wrong_step": "Должно быть кратно {0}",
+ "datetime_empty": "Не заполненно",
+ "datetime_invalid": "Неверный формат даты ({0})",
+ "datetime_exceed_min": "Должно быть, после {0}",
+ "datetime_exceed_max": "Должно быть, до {0}",
+ "boolean_not_valid": "Не логическое",
+ "operator_not_multiple": "Оператор \"{1}\" не поддерживает много значений"
+ },
+ "invert": "Инвертировать"
+};
+
+QueryBuilder.defaults({ lang_code: 'ru' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.sq.js b/src/dist/i18n/query-builder.sq.js
new file mode 100644
index 00000000..ef2663e8
--- /dev/null
+++ b/src/dist/i18n/query-builder.sq.js
@@ -0,0 +1,78 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Albanian (sq)
+ * Author: Tomor Pupovci
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['sq'] = {
+ "__locale": "Albanian (sq)",
+ "__author": "Tomor Pupovci",
+ "add_rule": "Shto rregull",
+ "add_group": "Shto grup",
+ "delete_rule": "Fshij",
+ "delete_group": "Fshij",
+ "conditions": {
+ "AND": "DHE",
+ "OR": "OSE"
+ },
+ "operators": {
+ "equal": "barabartë",
+ "not_equal": "e ndryshme prej",
+ "in": "në",
+ "not_in": "jo në",
+ "less": "më e vogël",
+ "less_or_equal": "më e vogël ose e barabartë me",
+ "greater": "më e madhe",
+ "greater_or_equal": "më e madhe ose e barabartë",
+ "between": "në mes",
+ "begins_with": "fillon me",
+ "not_begins_with": "nuk fillon me",
+ "contains": "përmban",
+ "not_contains": "nuk përmban",
+ "ends_with": "mbaron me",
+ "not_ends_with": "nuk mbaron me",
+ "is_empty": "është e zbrazët",
+ "is_not_empty": "nuk është e zbrazët",
+ "is_null": "është null",
+ "is_not_null": "nuk është null"
+ },
+ "errors": {
+ "no_filter": "Nuk ka filter të zgjedhur",
+ "empty_group": "Grupi është i zbrazët",
+ "radio_empty": "Nuk ka vlerë të zgjedhur",
+ "checkbox_empty": "Nuk ka vlerë të zgjedhur",
+ "select_empty": "Nuk ka vlerë të zgjedhur",
+ "string_empty": "Vlerë e zbrazët",
+ "string_exceed_min_length": "Duhet të përmbajë së paku {0} karaktere",
+ "string_exceed_max_length": "Nuk duhet të përmbajë më shumë se {0} karaktere",
+ "string_invalid_format": "Format i pasaktë ({0})",
+ "number_nan": "Nuk është numër",
+ "number_not_integer": "Nuk është numër i plotë",
+ "number_not_double": "Nuk është numër me presje",
+ "number_exceed_min": "Duhet të jetë më i madh se {0}",
+ "number_exceed_max": "Duhet të jetë më i vogël se {0}",
+ "number_wrong_step": "Duhet të jetë shumëfish i {0}",
+ "datetime_empty": "Vlerë e zbrazët",
+ "datetime_invalid": "Format i pasaktë i datës ({0})",
+ "datetime_exceed_min": "Duhet të jetë pas {0}",
+ "datetime_exceed_max": "Duhet të jetë para {0}",
+ "boolean_not_valid": "Nuk është boolean",
+ "operator_not_multiple": "Operatori \"{1}\" nuk mund të pranojë vlera të shumëfishta"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'sq' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.tr.js b/src/dist/i18n/query-builder.tr.js
new file mode 100644
index 00000000..c299bc2d
--- /dev/null
+++ b/src/dist/i18n/query-builder.tr.js
@@ -0,0 +1,82 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Turkish (tr)
+ * Author: Aykut Alpgiray Ateş
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['tr'] = {
+ "__locale": "Turkish (tr)",
+ "__author": "Aykut Alpgiray Ateş",
+ "add_rule": "Kural Ekle",
+ "add_group": "Grup Ekle",
+ "delete_rule": "Sil",
+ "delete_group": "Sil",
+ "conditions": {
+ "AND": "Ve",
+ "OR": "Veya"
+ },
+ "operators": {
+ "equal": "eşit",
+ "not_equal": "eşit değil",
+ "in": "içinde",
+ "not_in": "içinde değil",
+ "less": "küçük",
+ "less_or_equal": "küçük veya eşit",
+ "greater": "büyük",
+ "greater_or_equal": "büyük veya eşit",
+ "between": "arasında",
+ "not_between": "arasında değil",
+ "begins_with": "ile başlayan",
+ "not_begins_with": "ile başlamayan",
+ "contains": "içeren",
+ "not_contains": "içermeyen",
+ "ends_with": "ile biten",
+ "not_ends_with": "ile bitmeyen",
+ "is_empty": "boş ise",
+ "is_not_empty": "boş değil ise",
+ "is_null": "var ise",
+ "is_not_null": "yok ise"
+ },
+ "errors": {
+ "no_filter": "Bir filtre seçili değil",
+ "empty_group": "Grup bir eleman içermiyor",
+ "radio_empty": "Seçim yapılmalı",
+ "checkbox_empty": "Seçim yapılmalı",
+ "select_empty": "Seçim yapılmalı",
+ "string_empty": "Bir metin girilmeli",
+ "string_exceed_min_length": "En az {0} karakter girilmeli",
+ "string_exceed_max_length": "En fazla {0} karakter girilebilir",
+ "string_invalid_format": "Uyumsuz format ({0})",
+ "number_nan": "Sayı değil",
+ "number_not_integer": "Tam sayı değil",
+ "number_not_double": "Ondalıklı sayı değil",
+ "number_exceed_min": "Sayı {0}'den/dan daha büyük olmalı",
+ "number_exceed_max": "Sayı {0}'den/dan daha küçük olmalı",
+ "number_wrong_step": "{0} veya katı olmalı",
+ "number_between_invalid": "Geçersiz değerler, {0} değeri {1} değerinden büyük",
+ "datetime_empty": "Tarih Seçilmemiş",
+ "datetime_invalid": "Uygun olmayan tarih formatı ({0})",
+ "datetime_exceed_min": "{0} Tarihinden daha sonrası olmalı.",
+ "datetime_exceed_max": "{0} Tarihinden daha öncesi olmalı.",
+ "datetime_between_invalid": "Geçersiz değerler, {0} değeri {1} değerinden büyük",
+ "boolean_not_valid": "Değer Doğru/Yanlış(bool) olmalı",
+ "operator_not_multiple": "Operatör \"{1}\" birden fazla değer kabul etmiyor"
+ },
+ "invert": "Ters Çevir"
+};
+
+QueryBuilder.defaults({ lang_code: 'tr' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.ua.js b/src/dist/i18n/query-builder.ua.js
new file mode 100644
index 00000000..feec8afe
--- /dev/null
+++ b/src/dist/i18n/query-builder.ua.js
@@ -0,0 +1,79 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Ukrainian (ua)
+ * Author: Megaplan, mborisv
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['ua'] = {
+ "__locale": "Ukrainian (ua)",
+ "__author": "Megaplan, mborisv ",
+ "add_rule": "Додати",
+ "add_group": "Додати групу",
+ "delete_rule": "Видалити",
+ "delete_group": "Видалити",
+ "conditions": {
+ "AND": "І",
+ "OR": "АБО"
+ },
+ "operators": {
+ "equal": "дорівнює",
+ "not_equal": "не дорівнює",
+ "in": "з вказаних",
+ "not_in": "не з вказаних",
+ "less": "менше",
+ "less_or_equal": "менше або дорівнюж",
+ "greater": "більше",
+ "greater_or_equal": "більше або дорівнює",
+ "between": "між",
+ "begins_with": "починається з",
+ "not_begins_with": "не починається з",
+ "contains": "містить",
+ "not_contains": "не містить",
+ "ends_with": "закінчується на",
+ "not_ends_with": "не не закінчується на",
+ "is_empty": "порожній рядок",
+ "is_not_empty": "не порожній рядок",
+ "is_null": "порожньо",
+ "is_not_null": "не порожньо"
+ },
+ "errors": {
+ "no_filter": "Фільтр не вибраний",
+ "empty_group": "Група порожня",
+ "radio_empty": "Значення не вибрано",
+ "checkbox_empty": "Значення не вибрано",
+ "select_empty": "Значення не вибрано",
+ "string_empty": "Не заповнено",
+ "string_exceed_min_length": "Повинен містити більше {0} символів",
+ "string_exceed_max_length": "Повинен містити менше {0} символів",
+ "string_invalid_format": "Невірний формат ({0})",
+ "number_nan": "Не число",
+ "number_not_integer": "Не число",
+ "number_not_double": "Не число",
+ "number_exceed_min": "Повинне бути більше {0}",
+ "number_exceed_max": "Повинне бути менше, ніж {0}",
+ "number_wrong_step": "Повинне бути кратне {0}",
+ "datetime_empty": "Не заповнено",
+ "datetime_invalid": "Невірний формат дати ({0})",
+ "datetime_exceed_min": "Повинне бути, після {0}",
+ "datetime_exceed_max": "Повинне бути, до {0}",
+ "boolean_not_valid": "Не логічне",
+ "operator_not_multiple": "Оператор \"{1}\" не підтримує багато значень"
+ },
+ "invert": "інвертувати"
+};
+
+QueryBuilder.defaults({ lang_code: 'ua' });
+}));
\ No newline at end of file
diff --git a/src/dist/i18n/query-builder.zh-CN.js b/src/dist/i18n/query-builder.zh-CN.js
new file mode 100644
index 00000000..a7615251
--- /dev/null
+++ b/src/dist/i18n/query-builder.zh-CN.js
@@ -0,0 +1,80 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: Simplified Chinese (zh_CN)
+ * Author: shadowwind, shatteredwindgo@gmail.com
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['zh-CN'] = {
+ "__locale": "Simplified Chinese (zh_CN)",
+ "__author": "shadowwind, shatteredwindgo@gmail.com",
+ "add_rule": "添加规则",
+ "add_group": "添加组",
+ "delete_rule": "删除",
+ "delete_group": "删除组",
+ "conditions": {
+ "AND": "和",
+ "OR": "或"
+ },
+ "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": "为 null",
+ "is_not_null": "不为 null"
+ },
+ "errors": {
+ "no_filter": "没有选择过滤器",
+ "empty_group": "该组为空",
+ "radio_empty": "没有选中项",
+ "checkbox_empty": "没有选中项",
+ "select_empty": "没有选中项",
+ "string_empty": "没有输入值",
+ "string_exceed_min_length": "必须至少包含{0}个字符",
+ "string_exceed_max_length": "必须不超过{0}个字符",
+ "string_invalid_format": "无效格式({0})",
+ "number_nan": "值不是数字",
+ "number_not_integer": "不是整数",
+ "number_not_double": "不是浮点数",
+ "number_exceed_min": "必须大于{0}",
+ "number_exceed_max": "必须小于{0}",
+ "number_wrong_step": "必须是{0}的倍数",
+ "datetime_empty": "值为空",
+ "datetime_invalid": "不是有效日期({0})",
+ "datetime_exceed_min": "必须在{0}之后",
+ "datetime_exceed_max": "必须在{0}之前",
+ "boolean_not_valid": "不是布尔值",
+ "operator_not_multiple": "选项\"{1}\"无法接受多个值"
+ },
+ "invert": "倒置"
+};
+
+QueryBuilder.defaults({ lang_code: 'zh-CN' });
+}));
\ No newline at end of file
diff --git a/src/dist/js/query-builder.js b/src/dist/js/query-builder.js
new file mode 100644
index 00000000..7c1253e0
--- /dev/null
+++ b/src/dist/js/query-builder.js
@@ -0,0 +1,6200 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Copyright 2014-2018 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(['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.}
+ * @readonly
+ */
+ this.icons = this.settings.icons;
+
+ /**
+ * List of operators
+ * @member {QueryBuilder.Operator[]}
+ * @readonly
+ */
+ this.operators = this.settings.operators;
+
+ /**
+ * List of templates
+ * @member {object.}
+ * @readonly
+ */
+ this.templates = this.settings.templates;
+
+ /**
+ * Plugins configuration
+ * @member {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.}
+ * @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.}
+ * @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.}
+ * @readonly
+ */
+QueryBuilder.templates = {};
+
+/**
+ * Localized strings (see i18n/)
+ * @type {object.}
+ * @readonly
+ */
+QueryBuilder.regional = {};
+
+/**
+ * Default operators
+ * @type {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: true,
+ 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.}
+ */
+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.} 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 = $(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 = $(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 = $(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 = $(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 = $(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.
+ * 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.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
+ * @private
+ */
+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
+ * @private
+ */
+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
+ * @private
+ */
+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 - not 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 - not 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 = '\
+';
+
+QueryBuilder.templates.rule = '\
+ \
+ \
+ {{? it.settings.display_errors }} \
+
\
+ {{?}} \
+
\
+
\
+
\
+
';
+
+QueryBuilder.templates.filterSelect = '\
+{{ var optgroup = null; }} \
+';
+
+QueryBuilder.templates.operatorSelect = '\
+{{? it.operators.length === 1 }} \
+ \
+{{= it.translate("operators", it.operators[0].type) }} \
+ \
+{{?}} \
+{{ var optgroup = null; }} \
+';
+
+QueryBuilder.templates.ruleValueSelect = '\
+{{ var optgroup = null; }} \
+';
+
+/**
+ * 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 = '';
+
+ 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 += ' ';
+ });
+ break;
+
+ case 'select':
+ h = this.getRuleValueSelect(name, rule);
+ break;
+
+ case 'textarea':
+ h += '';
+ break;
+
+ case 'number':
+ h += '" ; + break + + default + 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 + * @returns {string + * +Utils.escapeString="function(value)" { + if (typeof value !="string" ) { + return value + } + + return 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 ; + default return \ + s + @formatter:off + } + } + uglify compliant + .replace(/\t/g \\t ) + .replace(/\x1a/g \\Z ) + + + + * 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 />
+ * Update events are emitted in the setter through root Model (if any).
+ * 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+= '\
+ \
+ \
+ \
+
';
+ });
+ }
+ });
+}, {
+ 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 = $(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');
+ }
+ }
+
+ // 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);
+ }
+});
+
+
+/**
+ * @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) {
+ 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 = $('');
+ $p.appendTo(rule.$el);
+ }
+ else {
+ $p.css('display', '');
+ }
+
+ $p.html(' ' + 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 = $('');
+ $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 = $('');
+ $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 = $(h.value);
+ $h.find(Selectors.condition_container).after(
+ ''
+ );
+ h.value = $h.prop('outerHTML');
+ });
+
+ if (options.display_rules_button && options.invert_rules) {
+ this.on('getRuleTemplate.filter', function(h) {
+ var $h = $(h.value);
+ $h.find(Selectors.rule_actions).prepend(
+ ''
+ );
+ 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': '(? 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 = $(h.value);
+ $h.find(QueryBuilder.selectors.condition_container).prepend(
+ ''
+ );
+ 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 = $('
')
+ .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 = $(h.value);
+ $h.find(QueryBuilder.selectors.condition_container).after('
');
+ h.value = $h.prop('outerHTML');
+ }
+ });
+
+ this.on('getRuleTemplate.filter', function(h) {
+ var $h = $(h.value);
+ $h.find(QueryBuilder.selectors.rule_header).after('
');
+ 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}%' },
+ not_begins_with: { op: 'NOT LIKE(?)', mod: '{0}%' },
+ contains: { op: 'LIKE(?)', mod: '%{0}%' },
+ not_contains: { op: 'NOT LIKE(?)', mod: '%{0}%' },
+ ends_with: { op: 'LIKE(?)', mod: '%{0}' },
+ not_ends_with: { op: 'NOT LIKE(?)', mod: '%{0}' },
+ 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('|') + ')', '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);
+ }
+
+ 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));
+ }
+
+ 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: opVal.val
+ }, 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 Selectpicker
+ if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) {
+ self.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render');
+ }
+ }
+});
+
+
+/*!
+ * jQuery QueryBuilder 2.5.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 rule",
+ "add_group": "Add group",
+ "delete_rule": "Delete",
+ "delete_group": "Delete",
+ "conditions": {
+ "AND": "AND",
+ "OR": "OR"
+ },
+ "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": "contains",
+ "not_contains": "doesn't contain",
+ "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;
+
+}));
diff --git a/src/dist/js/query-builder.min.js b/src/dist/js/query-builder.min.js
new file mode 100644
index 00000000..4bee8793
--- /dev/null
+++ b/src/dist/js/query-builder.min.js
@@ -0,0 +1,7 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Copyright 2014-2018 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+!function(e,t){"function"==typeof define&&define.amd?define(["jquery","dot/doT","jquery-extendext"],t):"object"==typeof module&&module.exports?module.exports=t(require("jquery"),require("dot/doT"),require("jquery-extendext")):t(e.jQuery,e.doT)}(this,function($,r){"use strict";var c=function(e,t){(e[0].queryBuilder=this).$el=e,this.settings=$.extendext(!0,"replace",{},c.DEFAULTS,t),this.model=new i,this.status={id:null,generated_id:!1,group_id:0,rule_id:0,has_optgroup:!1,has_operator_optgroup:!1},this.filters=this.settings.filters,this.icons=this.settings.icons,this.operators=this.settings.operators,this.templates=this.settings.templates,this.plugins=this.settings.plugins,this.lang=null,void 0===c.regional.en&&h.error("Config",'"i18n/en.js" not loaded.'),this.lang=$.extendext(!0,"replace",{},c.regional.en,c.regional[this.settings.lang_code],this.settings.lang),!1===this.settings.allow_groups?this.settings.allow_groups=0:!0===this.settings.allow_groups&&(this.settings.allow_groups=-1),Object.keys(this.templates).forEach(function(e){this.templates[e]||(this.templates[e]=c.templates[e]),"string"==typeof this.templates[e]&&(this.templates[e]=r.template(this.templates[e]))},this),this.$el.attr("id")||(this.$el.attr("id","qb_"+Math.floor(99999*Math.random())),this.status.generated_id=!0),this.status.id=this.$el.attr("id"),this.$el.addClass("query-builder form-inline"),this.filters=this.checkFilters(this.filters),this.operators=this.checkOperators(this.operators),this.bindEvents(),this.initPlugins()};$.extend(c.prototype,{trigger:function(e){var t=new $.Event(this._tojQueryEvent(e),{builder:this});return this.$el.triggerHandler(t,Array.prototype.slice.call(arguments,1)),t},change:function(e,t){var r=new $.Event(this._tojQueryEvent(e,!0),{builder:this,value:t});return this.$el.triggerHandler(r,Array.prototype.slice.call(arguments,2)),r.value},on:function(e,t){return this.$el.on(this._tojQueryEvent(e),t),this},off:function(e,t){return this.$el.off(this._tojQueryEvent(e),t),this},once:function(e,t){return this.$el.one(this._tojQueryEvent(e),t),this},_tojQueryEvent:function(e,t){return e.split(" ").map(function(e){return e+".queryBuilder"+(t?".filter":"")}).join(" ")}}),c.types={string:"string",integer:"number",double:"number",date:"datetime",time:"datetime",datetime:"datetime",boolean:"boolean"},c.inputs=["text","number","textarea","radio","checkbox","select"],c.modifiable_options=["display_errors","allow_groups","allow_empty","default_condition","default_filter"],c.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]"},c.templates={},c.regional={},c.OPERATORS={equal:{type:"equal",nb_inputs:1,multiple:!1,apply_to:["string","number","datetime","boolean"]},not_equal:{type:"not_equal",nb_inputs:1,multiple:!1,apply_to:["string","number","datetime","boolean"]},in:{type:"in",nb_inputs:1,multiple:!0,apply_to:["string","number","datetime"]},not_in:{type:"not_in",nb_inputs:1,multiple:!0,apply_to:["string","number","datetime"]},less:{type:"less",nb_inputs:1,multiple:!1,apply_to:["number","datetime"]},less_or_equal:{type:"less_or_equal",nb_inputs:1,multiple:!1,apply_to:["number","datetime"]},greater:{type:"greater",nb_inputs:1,multiple:!1,apply_to:["number","datetime"]},greater_or_equal:{type:"greater_or_equal",nb_inputs:1,multiple:!1,apply_to:["number","datetime"]},between:{type:"between",nb_inputs:2,multiple:!1,apply_to:["number","datetime"]},not_between:{type:"not_between",nb_inputs:2,multiple:!1,apply_to:["number","datetime"]},begins_with:{type:"begins_with",nb_inputs:1,multiple:!1,apply_to:["string"]},not_begins_with:{type:"not_begins_with",nb_inputs:1,multiple:!1,apply_to:["string"]},contains:{type:"contains",nb_inputs:1,multiple:!1,apply_to:["string"]},not_contains:{type:"not_contains",nb_inputs:1,multiple:!1,apply_to:["string"]},ends_with:{type:"ends_with",nb_inputs:1,multiple:!1,apply_to:["string"]},not_ends_with:{type:"not_ends_with",nb_inputs:1,multiple:!1,apply_to:["string"]},is_empty:{type:"is_empty",nb_inputs:0,multiple:!1,apply_to:["string"]},is_not_empty:{type:"is_not_empty",nb_inputs:0,multiple:!1,apply_to:["string"]},is_null:{type:"is_null",nb_inputs:0,multiple:!1,apply_to:["string","number","datetime","boolean"]},is_not_null:{type:"is_not_null",nb_inputs:0,multiple:!1,apply_to:["string","number","datetime","boolean"]}},c.DEFAULTS={filters:[],plugins:[],sort_filters:!1,display_errors:!0,allow_groups:-1,allow_empty:!1,conditions:["AND","OR"],default_condition:"AND",inputs_separator:" , ",select_placeholder:"------",display_empty_filter:!0,default_filter:null,optgroups:{},default_rule_flags:{filter_readonly:!1,operator_readonly:!1,value_readonly:!1,no_delete:!1},default_group_flags:{condition_readonly:!1,no_add_rule:!1,no_add_group:!1,no_delete:!1},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"}},c.plugins={},c.defaults=function(e){if("object"!=typeof e)return"string"==typeof e?"object"==typeof c.DEFAULTS[e]?$.extend(!0,{},c.DEFAULTS[e]):c.DEFAULTS[e]:$.extend(!0,{},c.DEFAULTS);$.extendext(!0,"replace",c.DEFAULTS,e)},c.define=function(e,t,r){c.plugins[e]={fct:t,def:r||{}}},c.extend=function(e){$.extend(c.prototype,e)},c.prototype.initPlugins=function(){if(this.plugins){if($.isArray(this.plugins)){var t={};this.plugins.forEach(function(e){t[e]=null}),this.plugins=t}Object.keys(this.plugins).forEach(function(e){e in c.plugins?(this.plugins[e]=$.extend(!0,{},c.plugins[e].def,this.plugins[e]||{}),c.plugins[e].fct.call(this,this.plugins[e])):h.error("Config",'Unable to find plugin "{0}"',e)},this)}},c.prototype.getPluginOptions=function(e,t){var r;if(this.plugins&&this.plugins[e]?r=this.plugins[e]:c.plugins[e]&&(r=c.plugins[e].def),r)return t?r[t]:r;h.error("Config",'Unable to find plugin "{0}"',e)},c.prototype.init=function(e){this.trigger("afterInit"),e?(this.setRules(e),delete this.settings.rules):this.setRoot(!0)},c.prototype.checkFilters=function(e){var t=[];if(e&&0!==e.length||h.error("Config","Missing filters list"),e.forEach(function(i,e){switch(i.id||h.error("Config","Missing filter {0} id",e),-1!=t.indexOf(i.id)&&h.error("Config",'Filter "{0}" already defined',i.id),t.push(i.id),i.type?c.types[i.type]||h.error("Config",'Invalid type "{0}"',i.type):i.type="string",i.input?"function"!=typeof i.input&&-1==c.inputs.indexOf(i.input)&&h.error("Config",'Invalid input "{0}"',i.input):i.input="number"===c.types[i.type]?"number":"text",i.operators&&i.operators.forEach(function(e){"string"!=typeof e&&h.error("Config","Filter operators must be global operators types (string)")}),i.field||(i.field=i.id),i.label||(i.label=i.field),i.optgroup?(this.status.has_optgroup=!0,this.settings.optgroups[i.optgroup]||(this.settings.optgroups[i.optgroup]=i.optgroup)):i.optgroup=null,i.input){case"radio":case"checkbox":(!i.values||i.values.length<1)&&h.error("Config",'Missing filter "{0}" values',i.id);break;case"select":var o=[];i.has_optgroup=!1,h.iterateOptions(i.values,function(e,t,r){o.push({value:e,label:t,optgroup:r||null}),r&&(i.has_optgroup=!0,this.settings.optgroups[r]||(this.settings.optgroups[r]=r))}.bind(this)),i.has_optgroup?i.values=h.groupSort(o,"optgroup"):i.values=o,i.placeholder&&(void 0===i.placeholder_value&&(i.placeholder_value=-1),i.values.forEach(function(e){e.value==i.placeholder_value&&h.error("Config",'Placeholder of filter "{0}" overlaps with one of its values',i.id)}))}},this),this.settings.sort_filters)if("function"==typeof this.settings.sort_filters)e.sort(this.settings.sort_filters);else{var r=this;e.sort(function(e,t){return r.translate(e.label).localeCompare(r.translate(t.label))})}return this.status.has_optgroup&&(e=h.groupSort(e,"optgroup")),e},c.prototype.checkOperators=function(r){var i=[];return r.forEach(function(e,t){"string"==typeof e?(c.OPERATORS[e]||h.error("Config",'Unknown operator "{0}"',e),r[t]=e=$.extendext(!0,"replace",{},c.OPERATORS[e])):(e.type||h.error("Config",'Missing "type" for operator {0}',t),c.OPERATORS[e.type]&&(r[t]=e=$.extendext(!0,"replace",{},c.OPERATORS[e.type],e)),void 0!==e.nb_inputs&&void 0!==e.apply_to||h.error("Config",'Missing "nb_inputs" and/or "apply_to" for operator "{0}"',e.type)),-1!=i.indexOf(e.type)&&h.error("Config",'Operator "{0}" already defined',e.type),i.push(e.type),e.optgroup?(this.status.has_operator_optgroup=!0,this.settings.optgroups[e.optgroup]||(this.settings.optgroups[e.optgroup]=e.optgroup)):e.optgroup=null},this),this.status.has_operator_optgroup&&(r=h.groupSort(r,"optgroup")),r},c.prototype.bindEvents=function(){var n=this,t=c.selectors;this.$el.on("change.queryBuilder",t.group_condition,function(){if($(this).is(":checked")){var e=$(this).closest(t.group_container);n.getModel(e).condition=$(this).val()}}),this.$el.on("change.queryBuilder",t.rule_filter,function(){var e=$(this).closest(t.rule_container);n.getModel(e).filter=n.getFilterById($(this).val())}),this.$el.on("change.queryBuilder",t.rule_operator,function(){var e=$(this).closest(t.rule_container);n.getModel(e).operator=n.getOperatorByType($(this).val())}),this.$el.on("click.queryBuilder",t.add_rule,function(){var e=$(this).closest(t.group_container);n.addRule(n.getModel(e))}),this.$el.on("click.queryBuilder",t.delete_rule,function(){var e=$(this).closest(t.rule_container);n.deleteRule(n.getModel(e))}),0!==this.settings.allow_groups&&(this.$el.on("click.queryBuilder",t.add_group,function(){var e=$(this).closest(t.group_container);n.addGroup(n.getModel(e))}),this.$el.on("click.queryBuilder",t.delete_group,function(){var e=$(this).closest(t.group_container);n.deleteGroup(n.getModel(e))})),this.model.on({drop:function(e,t){t.$el.remove(),n.refreshGroupsConditions()},add:function(e,t,r,i){0===i?r.$el.prependTo(t.$el.find(">"+c.selectors.rules_list)):r.$el.insertAfter(t.rules[i-1].$el),n.refreshGroupsConditions()},move:function(e,t,r,i){t.$el.detach(),0===i?t.$el.prependTo(r.$el.find(">"+c.selectors.rules_list)):t.$el.insertAfter(r.rules[i-1].$el),n.refreshGroupsConditions()},update:function(e,t,r,i,o){if(t instanceof l)switch(r){case"error":n.updateError(t);break;case"flags":n.applyRuleFlags(t);break;case"filter":n.updateRuleFilter(t,o);break;case"operator":n.updateRuleOperator(t,o);break;case"value":n.updateRuleValue(t,o)}else switch(r){case"error":n.updateError(t);break;case"flags":n.applyGroupFlags(t);break;case"condition":n.updateGroupCondition(t,o)}}})},c.prototype.setRoot=function(e,t,r){e=void 0===e||!0===e;var i=this.nextGroupId(),o=$(this.getGroupTemplate(i,1));return this.$el.append(o),this.model.root=new a(null,o),this.model.root.model=this.model,this.model.root.data=t,this.model.root.flags=$.extend({},this.settings.default_group_flags,r),this.model.root.condition=this.settings.default_condition,this.trigger("afterAddGroup",this.model.root),e&&this.addRule(this.model.root),this.model.root},c.prototype.addGroup=function(e,t,r,i){t=void 0===t||!0===t;var o=e.level+1;if(this.trigger("beforeAddGroup",e,t,o).isDefaultPrevented())return null;var n=this.nextGroupId(),l=$(this.getGroupTemplate(n,o)),s=e.addGroup(l);return s.data=r,s.flags=$.extend({},this.settings.default_group_flags,i),s.condition=this.settings.default_condition,this.trigger("afterAddGroup",s),this.trigger("rulesChanged"),t&&this.addRule(s),s},c.prototype.deleteGroup=function(e){if(e.isRoot())return!1;if(this.trigger("beforeDeleteGroup",e).isDefaultPrevented())return!1;var t=!0;return e.each("reverse",function(e){t&=this.deleteRule(e)},function(e){t&=this.deleteGroup(e)},this),t&&(e.drop(),this.trigger("afterDeleteGroup"),this.trigger("rulesChanged")),t},c.prototype.updateGroupCondition=function(t,e){t.$el.find(">"+c.selectors.group_condition).each(function(){var e=$(this);e.prop("checked",e.val()===t.condition),e.parent().toggleClass("active",e.val()===t.condition)}),this.trigger("afterUpdateGroupCondition",t,e),this.trigger("rulesChanged")},c.prototype.refreshGroupsConditions=function(){!function t(e){(!e.flags||e.flags&&!e.flags.condition_readonly)&&e.$el.find(">"+c.selectors.group_condition).prop("disabled",e.rules.length<=1).parent().toggleClass("disabled",e.rules.length<=1),e.each(null,function(e){t(e)},this)}(this.model.root)},c.prototype.addRule=function(e,t,r){if(this.trigger("beforeAddRule",e).isDefaultPrevented())return null;var i=this.nextRuleId(),o=$(this.getRuleTemplate(i)),n=e.addRule(o);return n.data=t,n.flags=$.extend({},this.settings.default_rule_flags,r),this.trigger("afterAddRule",n),this.trigger("rulesChanged"),this.createRuleFilters(n),!this.settings.default_filter&&this.settings.display_empty_filter||(n.filter=this.change("getDefaultFilter",this.getFilterById(this.settings.default_filter||this.filters[0].id),n)),n},c.prototype.deleteRule=function(e){return!e.flags.no_delete&&(!this.trigger("beforeDeleteRule",e).isDefaultPrevented()&&(e.drop(),this.trigger("afterDeleteRule"),this.trigger("rulesChanged"),!0))},c.prototype.createRuleFilters=function(e){var t=this.change("getRuleFilters",this.filters,e),r=$(this.getRuleFilterSelect(e,t));e.$el.find(c.selectors.filter_container).html(r),this.trigger("afterCreateRuleFilters",e),this.applyRuleFlags(e)},c.prototype.createRuleOperators=function(e){var t=e.$el.find(c.selectors.operator_container).empty();if(e.filter){var r=this.getOperators(e.filter),i=$(this.getRuleOperatorSelect(e,r));t.html(i),e.filter.default_operator?e.__.operator=this.getOperatorByType(e.filter.default_operator):e.__.operator=r[0],e.$el.find(c.selectors.rule_operator).val(e.operator.type),this.trigger("afterCreateRuleOperators",e,r),this.applyRuleFlags(e)}},c.prototype.createRuleInput=function(e){var t=e.$el.find(c.selectors.value_container).empty();if(e.__.value=void 0,e.filter&&e.operator&&0!==e.operator.nb_inputs){for(var r=this,i=$(),o=e.filter,n=0;n"+r.group_condition).prop("disabled",t.condition_readonly).parent().toggleClass("readonly",t.condition_readonly),t.no_add_rule&&e.$el.find(r.add_rule).remove(),t.no_add_group&&e.$el.find(r.add_group).remove(),t.no_delete&&e.$el.find(r.delete_group).remove(),this.trigger("afterApplyGroupFlags",e)},c.prototype.clearErrors=function(e){(e=e||this.model.root)&&(e.error=null,e instanceof a&&e.each(function(e){e.error=null},function(e){this.clearErrors(e)},this))},c.prototype.updateError=function(e){if(this.settings.display_errors)if(null===e.error)e.$el.removeClass("has-error");else{var t=this.translate("errors",e.error[0]);t=h.fmt(t,e.error.slice(1)),t=this.change("displayError",t,e.error,e),e.$el.addClass("has-error").find(c.selectors.error_container).eq(0).attr("title",t)}},c.prototype.triggerValidationError=function(e,t,r){$.isArray(t)||(t=[t]),this.trigger("validationError",e,t,r).isDefaultPrevented()||(e.error=t)},c.prototype.destroy=function(){this.trigger("beforeDestroy"),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},c.prototype.reset=function(){this.trigger("beforeReset").isDefaultPrevented()||(this.status.group_id=1,this.status.rule_id=0,this.model.root.empty(),this.model.root.data=void 0,this.model.root.flags=$.extend({},this.settings.default_group_flags),this.model.root.condition=this.settings.default_condition,this.addRule(this.model.root),this.trigger("afterReset"),this.trigger("rulesChanged"))},c.prototype.clear=function(){this.trigger("beforeClear").isDefaultPrevented()||(this.status.group_id=0,this.status.rule_id=0,this.model.root&&(this.model.root.drop(),this.model.root=null),this.trigger("afterClear"),this.trigger("rulesChanged"))},c.prototype.setOptions=function(e){$.each(e,function(e,t){-1!==c.modifiable_options.indexOf(e)&&(this.settings[e]=t)}.bind(this))},c.prototype.getModel=function(e){return e?e instanceof o?e:$(e).data("queryBuilderModel"):this.model.root},c.prototype.validate=function(n){n=$.extend({skip_empty:!1},n),this.clearErrors();var l=this,e=function r(e){var i=0,o=0;return e.each(function(e){if(e.filter||!n.skip_empty){if(!e.filter)return l.triggerValidationError(e,"no_filter",null),void o++;if(!e.operator)return l.triggerValidationError(e,"no_operator",null),void o++;if(0!==e.operator.nb_inputs){var t=l.validateValue(e,e.value);if(!0!==t)return l.triggerValidationError(e,t,e.value),void o++}i++}},function(e){var t=r(e);!0===t?i++:!1===t&&o++}),!(0parseInt(l.max)){s=[this.getValidationMessage(l,"max","string_exceed_max_length"),l.max];break}if(l.format&&("string"==typeof l.format&&(l.format=new RegExp(l.format)),!l.format.test(i[u]))){s=[this.getValidationMessage(l,"format","string_invalid_format"),l.format];break}break;case"number":if(void 0===i[u]||0===i[u].length){l.allow_empty_value||(s=["number_nan"]);break}if(isNaN(i[u])){s=["number_nan"];break}if("integer"==o.type){if(parseInt(i[u])!=i[u]){s=["number_not_integer"];break}}else if(parseFloat(i[u])!=i[u]){s=["number_not_double"];break}if(void 0!==l.min&&i[u]parseFloat(l.max)){s=[this.getValidationMessage(l,"max","number_exceed_max"),l.max];break}if(void 0!==l.step&&"any"!==l.step){var p=(i[u]/l.step).toPrecision(14);if(parseInt(p)!=p){s=[this.getValidationMessage(l,"step","number_wrong_step"),l.step];break}}break;case"datetime":if(void 0===i[u]||0===i[u].length){l.allow_empty_value||(s=["datetime_empty"]);break}if(l.format){"moment"in window||h.error("MissingLibrary","MomentJS is required for Date/Time validation. Get it here http://momentjs.com");var d=moment(i[u],l.format);if(!d.isValid()){s=[this.getValidationMessage(l,"format","datetime_invalid"),l.format];break}if(l.min&&dmoment(l.max,l.format)){s=[this.getValidationMessage(l,"max","datetime_exceed_max"),l.max];break}}break;case"boolean":if(void 0===i[u]||0===i[u].length){l.allow_empty_value||(s=["boolean_not_valid"]);break}if("true"!==(r=(""+i[u]).trim().toLowerCase())&&"false"!==r&&"1"!==r&&"0"!==r&&1!==i[u]&&0!==i[u]){s=["boolean_not_valid"];break}}if(!0!==s)break}}if(!0!==s)break}if(("between"===e.operator.type||"not_between"===e.operator.type)&&2===t.length)switch(c.types[o.type]){case"number":t[0]>t[1]&&(s=["number_between_invalid",t[0],t[1]]);break;case"datetime":l.format&&("moment"in window||h.error("MissingLibrary","MomentJS is required for Date/Time validation. Get it here http://momentjs.com"),moment(t[0],l.format).isAfter(moment(t[1],l.format))&&(s=["datetime_between_invalid",t[0],t[1]]))}return s},c.prototype.nextGroupId=function(){return this.status.id+"_group_"+this.status.group_id++},c.prototype.nextRuleId=function(){return this.status.id+"_rule_"+this.status.rule_id++},c.prototype.getOperators=function(r){"string"==typeof r&&(r=this.getFilterById(r));for(var e=[],t=0,i=this.operators.length;t ' ,c.templates.rule=' {{? it.settings.display_errors }}
{{?}}
' ,c.templates.filterSelect='{{ var optgroup = null; }} ' ,c.templates.operatorSelect='{{? it.operators.length === 1 }} {{= it.translate("operators", it.operators[0].type) }} {{?}} {{ var optgroup = null; }} ' ,c.templates.ruleValueSelect='{{ var optgroup = null; }} ' ,c.prototype.getGroupTemplate="function(e,t){var" r="this.templates.group({builder:this,group_id:e,level:t,conditions:this.settings.conditions,icons:this.icons,settings:this.settings,translate:this.translate.bind(this)});return" this.change getGroupTemplate ,r,t)},c.prototype.getRuleTemplate="function(e){var" t="this.templates.rule({builder:this,rule_id:e,icons:this.icons,settings:this.settings,translate:this.translate.bind(this)});return" this.change getRuleTemplate ,t)},c.prototype.getRuleFilterSelect="function(e,t){var" r="this.templates.filterSelect({builder:this,rule:e,filters:t,icons:this.icons,settings:this.settings,translate:this.translate.bind(this)});return" this.change getRuleFilterSelect ,r,e,t)},c.prototype.getRuleOperatorSelect="function(e,t){var" r="this.templates.operatorSelect({builder:this,rule:e,operators:t,icons:this.icons,settings:this.settings,translate:this.translate.bind(this)});return" this.change getRuleOperatorSelect ,r,e,t)},c.prototype.getRuleValueSelect="function(e,t){var" r="this.templates.ruleValueSelect({builder:this,name:e,rule:t,icons:this.icons,settings:this.settings,translate:this.translate.bind(this)});return" this.change getRuleValueSelect ,r,e,t)},c.prototype.getRuleInput="function(e,t){var" r='e.filter,i=e.filter.validation||{},o=e.id+"_value_"+t,n=r.vertical?"' class='block":"",l="";if("function"==typeof' r.input)l="r.input.call(this,e,o);else" switch(r.input){case radio :case checkbox :h.iterateOptions(r.values,function(e,t){l=" "});break;case"select":l=this.getRuleValueSelect(o,e);break;case"textarea":l+='" ;break;case number :l='" ;break;default:l='" }return this.change getRuleInput ,l,e,o)};var h="{};function" i(){this.root='null,this.$=$(this)}(c.utils=h).iterateOptions=function(e,r){e&&($.isArray(e)?e.forEach(function(e){$.isPlainObject(e)?"value"in' e?r(e.value,e.label||e.value,e.optgroup):$.each(e,function(e,t){return r(e,t),!1}):r(e,e)}):$.each(e,function(e,t){r(e,t)}))},h.fmt="function(e,r){return" Array.isArray(r)||(r="Array.prototype.slice.call(arguments,1)),e.replace(/{([0-9]+)}/g,function(e,t){return" r[parseInt(t)]})},h.error="function(){var" e='0,t="boolean"!=typeof' arguments[e]||arguments[e++],r="arguments[e++],i=arguments[e++],o=Array.isArray(arguments[e])?arguments[e]:Array.prototype.slice.call(arguments,e);if(t){var" n="new" Error(h.fmt(i,o));throw n.name='r+"Error",n.args=o,n}console.error(r+"Error:' +h.fmt(i,o))},h.changeType='function(e,t){if(""!==e&&void' 0='=e)switch(t){case"integer":return"string"!=typeof' e||/^-?\d+$/.test(e)?parseInt(e):e;case double :return string !="typeof" e||/^-?\d+\.?\d*$/.test(e)?parseFloat(e):e;case boolean :return string !="typeof" e||/^(0|1|true|false){1}$/i.test(e)?!0='==e||1===e||"true"===e.toLowerCase()||"1"===e:e;default:return' e}},h.escapeString='function(e){return"string"!=typeof' e?e:e.replace(/[\0\n\r\b \ ]/g,function(e){switch(e){case \0 :return \\0 ;case \n :return \\n ;case \r :return \\r ;case \b :return \\b ;default:return \ +e}}).replace(/\t/g \\t ).replace(/\x1a/g \\Z )},h.escapeRegExp="function(e){return" e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g \ )},h.escapeElementId="function(e){return" e?e.replace(/(\\)?([:.\[\],])/g,function(e,t,r){return t?e \ +r}):e},h.groupSort="function(e,r){var" i="[],o=[];return" e.forEach(function(e){var t;e[r]?-1="=(t=i.lastIndexOf(e[r]))?t=i.length:t++:t=i.length,i.splice(t,0,e[r]),o.splice(t,0,e)}),o},h.defineModelProperties=function(e,t){t.forEach(function(r){Object.defineProperty(e.prototype,r,{enumerable:!0,get:function(){return" this.__[r]},set:function(e){var t='null!==this.__[r]&&"object"==typeof' this.__[r]?$.extend({},this.__[r]):this.__[r];this.__[r='e,null!==this.model&&this.model.trigger("update",this,r,e,t)}})})},$.extend(i.prototype,{trigger:function(e){var' t="new" $.Event(e);return this.$.triggerHandler(t,Array.prototype.slice.call(arguments,1)),t},on:function(){return this.$.on.apply(this.$,Array.prototype.slice.call(arguments)),this},off:function(){return this.$.off.apply(this.$,Array.prototype.slice.call(arguments)),this},once:function(){return this.$.one.apply(this.$,Array.prototype.slice.call(arguments)),this}});var o="function(e,t){if(!(this" instanceof o))return new o(e,t);Object.defineProperty(this _ ,{value:{}}),t.data queryBuilderModel ,this),this.__.level="1,this.__.error=null,this.__.flags={},this.__.data=void" 0,this.$el='t,this.id=t[0].id,this.model=null,this.parent=e};h.defineModelProperties(o,["level","error","data","flags"]),Object.defineProperty(o.prototype,"parent",{enumerable:!0,get:function(){return' this.__.parent},set:function(e){this.__.parent="e,this.level=null===e?1:e.level+1,this.model=null===e?null:e.model}}),o.prototype.isRoot=function(){return" 1="==this.level},o.prototype.getPos=function(){return" this.isRoot()?-1:this.parent.getNodePos(this)},o.prototype.drop="function(){var" e='this.model;this.parent&&this.parent.removeNode(this),this.$el.removeData("queryBuilderModel"),null!==e&&e.trigger("drop",this)},o.prototype.moveAfter=function(e){this.isRoot()||this.move(e.parent,e.getPos()+1)},o.prototype.moveAtBegin=function(e){this.isRoot()||(void' 0="==e&&(e=this.parent),this.move(e,0))},o.prototype.moveAtEnd=function(e){this.isRoot()||(void" 0='==e&&(e=this.parent),this.move(e,0===e.length()?0:e.length()-1))},o.prototype.move=function(e,t){this.isRoot()||("number"==typeof' e&&(t='e,e=this.parent),this.parent.removeNode(this),e.insertNode(this,t,!1),null!==this.model&&this.model.trigger("move",this,e,t))};var' a="function(e,t){if(!(this" instanceof a))return new a(e,t);o.call(this,e,t),this.rules='[],this.__.condition=null};a.prototype=Object.create(o.prototype),a.prototype.constructor=a,h.defineModelProperties(a,["condition"]),a.prototype.empty=function(){this.each("reverse",function(e){e.drop()},function(e){e.drop()})},a.prototype.drop=function(){this.empty(),o.prototype.drop.call(this)},a.prototype.length=function(){return' this.rules.length},a.prototype.insertNode="function(e,t,r){return" void 0='==t&&(t=this.length()),this.rules.splice(t,0,e),e.parent=this,r&&null!==this.model&&this.model.trigger("add",this,e,t),e},a.prototype.addGroup=function(e,t){return' this.insertNode(new a(this,e),t,!0)},a.prototype.addRule="function(e,t){return" this.insertNode(new l(this,e),t,!0)},a.prototype.removeNode="function(e){var" t="this.getNodePos(e);-1!==t&&(e.parent=null,this.rules.splice(t,1))},a.prototype.getNodePos=function(e){return" this.rules.indexOf(e)},a.prototype.each='function(e,t,r,i){"boolean"!=typeof' e string !="typeof" e&&(i="r,r=t,t=e,e=!1),i=void" 0="==i?null:i;for(var" o="e?this.rules.length-1:0,n=e?0:this.rules.length-1,l=e?-1:1,s=!1;(e?n<=o:o<=n)&&(this.rules[o]instanceof" a?r&&(s="!1===r.call(i,this.rules[o])):t&&(s=!1===t.call(i,this.rules[o])),!s);o+=l);return!s},a.prototype.contains=function(t,e){return-1!==this.getNodePos(t)||!!e&&!this.each(function(){return!0},function(e){return!e.contains(t,!0)})};var" l="function(e,t){if(!(this" instanceof l))return new l(e,t);o.call(this,e,t),this._updating_value="!1,this._updating_input=!1,this.__.filter=null,this.__.operator=null,this.__.value=void" 0};function u(e,t,r){var i,o,n='c.selectors;(i=t.closest(n.rule_container)).length&&(o="moveAfter"),o||(i=t.closest(n.group_header)).length&&(i=t.closest(n.group_container),o="moveAtBegin"),o||(i=t.closest(n.group_container)).length&&(o="moveAtEnd"),o&&(e[o](r.getModel(i)),r&&e' instanceof l&&r.setRuleInputValue(e,e.value))}function n(e){var t="e.match(/(question_mark|numbered|named)(?:\((.)\))?/);return" t||(t='[null,"question_mark",void' 0]),t}return l.prototype='Object.create(o.prototype),l.prototype.constructor=l,h.defineModelProperties(l,["filter","operator","value"]),l.prototype.isRoot=function(){return!1},c.Group=a,c.Rule=l,$.fn.queryBuilder=function(e){0===this.length&&h.error("Config","No' target defined ),1> "})}})},{font:"glyphicons",color:"default"}),c.define("bt-selectpicker",function(r){$.fn.selectpicker&&$.fn.selectpicker.Constructor||h.error("MissingLibrary",'Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select');var i=c.selectors;this.on("afterCreateRuleFilters",function(e,t){t.$el.find(i.rule_filter).removeClass("form-control").selectpicker(r)}),this.on("afterCreateRuleOperators",function(e,t){t.$el.find(i.rule_operator).removeClass("form-control").selectpicker(r)}),this.on("afterUpdateRuleFilter",function(e,t){t.$el.find(i.rule_filter).selectpicker("render")}),this.on("afterUpdateRuleOperator",function(e,t){t.$el.find(i.rule_operator).selectpicker("render")}),this.on("beforeDeleteRule",function(e,t){t.$el.find(i.rule_filter).selectpicker("destroy"),t.$el.find(i.rule_operator).selectpicker("destroy")})},{container:"body",style:"btn-inverse btn-xs",width:"auto",showIcon:!1}),c.define("bt-tooltip-errors",function(i){$.fn.tooltip&&$.fn.tooltip.Constructor&&$.fn.tooltip.Constructor.prototype.fixTitle||h.error("MissingLibrary",'Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com');var o=this;this.on("getRuleTemplate.filter getGroupTemplate.filter",function(e){var t=$(e.value);t.find(c.selectors.error_container).attr("data-toggle","tooltip"),e.value=t.prop("outerHTML")}),this.model.on("update",function(e,t,r){"error"==r&&o.settings.display_errors&&t.$el.find(c.selectors.error_container).eq(0).tooltip(i).tooltip("hide").tooltip("fixTitle")})},{placement:"right"}),c.extend({setFilters:function(e,t){var r=this;void 0===t&&(t=e,e=!1),t=this.checkFilters(t);var i=(t=this.change("setFilters",t)).map(function(e){return e.id});if(e||function e(t){t.each(function(e){e.filter&&-1===i.indexOf(e.filter.id)&&h.error("ChangeFilter",'A rule is using filter "{0}"',e.filter.id)},e)}(this.model.root),this.filters=t,function e(t){t.each(!0,function(e){e.filter&&-1===i.indexOf(e.filter.id)?(e.drop(),r.trigger("rulesChanged")):(r.createRuleFilters(e),e.$el.find(c.selectors.rule_filter).val(e.filter?e.filter.id:"-1"),r.trigger("afterUpdateRuleFilter",e))},e)}(this.model.root),this.settings.plugins&&(this.settings.plugins["unique-filter"]&&this.updateDisabledFilters(),this.settings.plugins["bt-selectpicker"]&&this.$el.find(c.selectors.rule_filter).selectpicker("render")),this.settings.default_filter)try{this.getFilterById(this.settings.default_filter)}catch(e){this.settings.default_filter=null}this.trigger("afterSetFilters",t)},addFilter:function(e,r){void 0===r||"#end"==r?r=this.filters.length:"#start"==r&&(r=0),$.isArray(e)||(e=[e]);var t=$.extend(!0,[],this.filters);parseInt(r)==r?Array.prototype.splice.apply(t,[r,0].concat(e)):this.filters.some(function(e,t){if(e.id==r)return r=t+1,!0})?Array.prototype.splice.apply(t,[r,0].concat(e)):Array.prototype.push.apply(t,e),this.setFilters(t)},removeFilter:function(t,e){var r=$.extend(!0,[],this.filters);"string"==typeof t&&(t=[t]),r=r.filter(function(e){return-1===t.indexOf(e.id)}),this.setFilters(e,r)}}),c.define("chosen-selectpicker",function(r){$.fn.chosen||h.error("MissingLibrary",'chosen is required to use "chosen-selectpicker" plugin. Get it here: https://github.com/harvesthq/chosen'),this.settings.plugins["bt-selectpicker"]&&h.error("Conflict","bt-selectpicker is already selected as the dropdown plugin. Please remove chosen-selectpicker from the plugin list");var i=c.selectors;this.on("afterCreateRuleFilters",function(e,t){t.$el.find(i.rule_filter).removeClass("form-control").chosen(r)}),this.on("afterCreateRuleOperators",function(e,t){t.$el.find(i.rule_operator).removeClass("form-control").chosen(r)}),this.on("afterUpdateRuleFilter",function(e,t){t.$el.find(i.rule_filter).trigger("chosen:updated")}),this.on("afterUpdateRuleOperator",function(e,t){t.$el.find(i.rule_operator).trigger("chosen:updated")}),this.on("beforeDeleteRule",function(e,t){t.$el.find(i.rule_filter).chosen("destroy"),t.$el.find(i.rule_operator).chosen("destroy")})}),c.define("filter-description",function(o){"inline"===o.mode?this.on("afterUpdateRuleFilter afterUpdateRuleOperator",function(e,t){var r=t.$el.find("p.filter-description"),i=e.builder.getFilterDescription(t.filter,t);i?(0===r.length?(r=$('')).appendTo(t.$el):r.css("display",""),r.html(' '+i)):r.hide()}):"popover"===o.mode?($.fn.popover&&$.fn.popover.Constructor&&$.fn.popover.Constructor.prototype.fixTitle||h.error("MissingLibrary",'Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com'),this.on("afterUpdateRuleFilter afterUpdateRuleOperator",function(e,t){var r=t.$el.find("button.filter-description"),i=e.builder.getFilterDescription(t.filter,t);i?(0===r.length?((r=$('')).prependTo(t.$el.find(c.selectors.rule_actions)),r.popover({placement:"left",container:"body",html:!0}),r.on("mouseout",function(){r.popover("hide")})):r.css("display",""),r.data("bs.popover").options.content=i,r.attr("aria-describedby")&&r.popover("show")):(r.hide(),r.data("bs.popover")&&r.popover("hide"))})):"bootbox"===o.mode&&("bootbox"in window||h.error("MissingLibrary",'Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com'),this.on("afterUpdateRuleFilter afterUpdateRuleOperator",function(e,t){var r=t.$el.find("button.filter-description"),i=e.builder.getFilterDescription(t.filter,t);i?(0===r.length?((r=$('')).prependTo(t.$el.find(c.selectors.rule_actions)),r.on("click",function(){bootbox.alert(r.data("description"))})):r.css("display",""),r.data("description",i)):r.hide()}))},{icon:"glyphicon glyphicon-info-sign",mode:"popover"}),c.extend({getFilterDescription:function(e,t){return e?"function"==typeof e.description?e.description.call(this,t):e.description:void 0}}),c.define("invert",function(r){var i=this,o=c.selectors;this.on("afterInit",function(){i.$el.on("click.queryBuilder","[data-invert=group]",function(){var e=$(this).closest(o.group_container);i.invert(i.getModel(e),r)}),r.display_rules_button&&r.invert_rules&&i.$el.on("click.queryBuilder","[data-invert=rule]",function(){var e=$(this).closest(o.rule_container);i.invert(i.getModel(e),r)})}),r.disable_template||(this.on("getGroupTemplate.filter",function(e){var t=$(e.value);t.find(o.condition_container).after('"),e.value=t.prop("outerHTML")}),r.display_rules_button&&r.invert_rules&&this.on("getRuleTemplate.filter",function(e){var t=$(e.value);t.find(o.rule_actions).prepend('"),e.value=t.prop("outerHTML")}))},{icon:"glyphicon glyphicon-random",recursive:!0,invert_rules:!0,display_rules_button:!1,silent_fail:!1,disable_template:!1}),c.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"}}),c.extend({invert:function(e,t){if(!(e instanceof o)){if(!this.model.root)return;t=e,e=this.model.root}if("object"!=typeof t&&(t={}),void 0===t.recursive&&(t.recursive=!0),void 0===t.invert_rules&&(t.invert_rules=!0),void 0===t.silent_fail&&(t.silent_fail=!1),void 0===t.trigger&&(t.trigger=!0),e instanceof a){if(this.settings.conditionOpposites[e.condition]?e.condition=this.settings.conditionOpposites[e.condition]:t.silent_fail||h.error("InvertCondition",'Unknown inverse of condition "{0}"',e.condition),t.recursive){var r=$.extend({},t,{trigger:!1});e.each(function(e){t.invert_rules&&this.invert(e,r)},function(e){this.invert(e,r)},this)}}else if(e instanceof l&&e.operator&&!e.filter.no_invert)if(this.settings.operatorOpposites[e.operator.type]){var i=this.settings.operatorOpposites[e.operator.type];e.filter.operators&&-1==e.filter.operators.indexOf(i)||(e.operator=this.getOperatorByType(i))}else t.silent_fail||h.error("InvertOperator",'Unknown inverse of operator "{0}"',e.operator.type);t.trigger&&(this.trigger("afterInvert",e,t),this.trigger("rulesChanged"))}}),c.defaults({mongoOperators:{equal:function(e){return e[0]},not_equal:function(e){return{$ne:e[0]}},in:function(e){return{$in:e}},not_in:function(e){return{$nin:e}},less:function(e){return{$lt:e[0]}},less_or_equal:function(e){return{$lte:e[0]}},greater:function(e){return{$gt:e[0]}},greater_or_equal:function(e){return{$gte:e[0]}},between:function(e){return{$gte:e[0],$lte:e[1]}},not_between:function(e){return{$lt:e[0],$gt:e[1]}},begins_with:function(e){return{$regex:"^"+h.escapeRegExp(e[0])}},not_begins_with:function(e){return{$regex:"^(?!"+h.escapeRegExp(e[0])+")"}},contains:function(e){return{$regex:h.escapeRegExp(e[0])}},not_contains:function(e){return{$regex:"^((?!"+h.escapeRegExp(e[0])+").)*$",$options:"s"}},ends_with:function(e){return{$regex:h.escapeRegExp(e[0])+"$"}},not_ends_with:function(e){return{$regex:"(? '+i.translate("NOT")+""),e.value=t.prop("outerHTML")}),this.on("groupToJson.filter",function(e,t){e.value.not=t.not}),this.on("jsonToGroup.filter",function(e,t){e.value.not=!!t.not}),this.on("groupToSQL.filter",function(e,t){t.not&&(e.value="NOT ( "+e.value+" )")}),this.on("parseSQLNode.filter",function(e){e.value.name&&"NOT"==e.value.name.toUpperCase()&&(e.value=e.value.arguments.value[0],-1===["AND","OR"].indexOf(e.value.operation.toUpperCase())&&(e.value=new SQLParser.nodes.Op(i.settings.default_condition,e.value,null)),e.value.not=!0)}),this.on("sqlGroupsDistinct.filter",function(e,t,r,i){r.not&&0"+c.selectors.group_not).toggleClass("active",e.not).find("i").attr("class",e.not?t.icon_checked:t.icon_unchecked),this.trigger("afterUpdateGroupNot",e),this.trigger("rulesChanged")}}),c.define("sortable",function(i){var o,n,l,s;"interact"in window||h.error("MissingLibrary",'interact.js is required to use "sortable" plugin. Get it here: http://interactjs.io'),void 0!==i.default_no_sortable&&(h.error(!1,"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=i.default_no_sortable),interact.dynamicDrop(!0),interact.pointerMoveTolerance(10),this.on("afterAddRule afterAddGroup",function(e,t){if(t!=o){var r=e.builder;i.inherit_no_sortable&&t.parent&&t.parent.flags.no_sortable&&(t.flags.no_sortable=!0),i.inherit_no_drop&&t.parent&&t.parent.flags.no_drop&&(t.flags.no_drop=!0),t.flags.no_sortable||interact(t.$el[0]).draggable({allowFrom:c.selectors.drag_handle,onstart:function(e){s=!1,l=r.getModel(e.target),n=l.$el.clone().appendTo(l.$el.parent()).width(l.$el.outerWidth()).addClass("dragging");var t=$('
').height(l.$el.outerHeight());o=l.parent.addRule(t,l.getPos()),l.$el.hide()},onmove:function(e){n[0].style.top=e.clientY-15+"px",n[0].style.left=e.clientX-15+"px"},onend:function(e){e.dropzone&&(u(l,$(e.relatedTarget),r),s=!0),n.remove(),n=void 0,o.drop(),o=void 0,l.$el.css("display",""),r.trigger("afterMove",l),r.trigger("rulesChanged")}}),t.flags.no_drop||(interact(t.$el[0]).dropzone({accept:c.selectors.rule_and_group_containers,ondragenter:function(e){u(o,$(e.target),r)},ondrop:function(e){s||u(l,$(e.target),r)}}),t instanceof a&&interact(t.$el.find(c.selectors.group_header)[0]).dropzone({accept:c.selectors.rule_and_group_containers,ondragenter:function(e){u(o,$(e.target),r)},ondrop:function(e){s||u(l,$(e.target),r)}}))}}),this.on("beforeDeleteRule beforeDeleteGroup",function(e,t){e.isDefaultPrevented()||(interact(t.$el[0]).unset(),t instanceof a&&interact(t.$el.find(c.selectors.group_header)[0]).unset())}),this.on("afterApplyRuleFlags afterApplyGroupFlags",function(e,t){t.flags.no_sortable&&t.$el.find(".drag-handle").remove()}),i.disable_template||(this.on("getGroupTemplate.filter",function(e,t){if(1'),e.value=r.prop("outerHTML")}}),this.on("getRuleTemplate.filter",function(e){var t=$(e.value);t.find(c.selectors.rule_header).after('
'),e.value=t.prop("outerHTML")}))},{inherit_no_sortable:!0,inherit_no_drop:!0,icon:"glyphicon glyphicon-sort",disable_template:!1}),c.selectors.rule_and_group_containers=c.selectors.rule_container+", "+c.selectors.group_container,c.selectors.drag_handle=".drag-handle",c.defaults({default_rule_flags:{no_sortable:!1,no_drop:!1},default_group_flags:{no_sortable:!1,no_drop:!1}}),c.define("sql-support",function(e){},{boolean_as_integer:!0}),c.defaults({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}%"},not_begins_with:{op:"NOT LIKE(?)",mod:"{0}%"},contains:{op:"LIKE(?)",mod:"%{0}%"},not_contains:{op:"NOT LIKE(?)",mod:"%{0}%"},ends_with:{op:"LIKE(?)",mod:"%{0}"},not_ends_with:{op:"NOT LIKE(?)",mod:"%{0}"},is_empty:{op:"= ''"},is_not_empty:{op:"!= ''"},is_null:{op:"IS NULL"},is_not_null:{op:"IS NOT NULL"}},sqlRuleOperator:{"=":function(e){return{val:e,op:""===e?"is_empty":"equal"}},"!=":function(e){return{val:e,op:""===e?"is_not_empty":"not_equal"}},LIKE:function(e){return"%"==e.slice(0,1)&&"%"==e.slice(-1)?{val:e.slice(1,-1),op:"contains"}:"%"==e.slice(0,1)?{val:e.slice(1),op:"ends_with"}:"%"==e.slice(-1)?{val:e.slice(0,-1),op:"begins_with"}:void h.error("SQLParse",'Invalid value for LIKE operator "{0}"',e)},"NOT LIKE":function(e){return"%"==e.slice(0,1)&&"%"==e.slice(-1)?{val:e.slice(1,-1),op:"not_contains"}:"%"==e.slice(0,1)?{val:e.slice(1),op:"not_ends_with"}:"%"==e.slice(-1)?{val:e.slice(0,-1),op:"not_begins_with"}:void h.error("SQLParse",'Invalid value for NOT LIKE operator "{0}"',e)},IN:function(e){return{val:e,op:"in"}},"NOT IN":function(e){return{val:e,op:"not_in"}},"<":function(e){return{val:e,op:"less"}},"<=":function(e){return{val:e,op:"less_or_equal"}},">":function(e){return{val:e,op:"greater"}},">=":function(e){return{val:e,op:"greater_or_equal"}},BETWEEN:function(e){return{val:e,op:"between"}},"NOT BETWEEN":function(e){return{val:e,op:"not_between"}},IS:function(e){return null!==e&&h.error("SQLParse","Invalid value for IS operator"),{val:null,op:"is_null"}},"IS NOT":function(e){return null!==e&&h.error("SQLParse","Invalid value for IS operator"),{val:null,op:"is_not_null"}}},sqlStatements:{question_mark:function(){var r=[];return{add:function(e,t){return r.push(t),"?"},run:function(){return r}}},numbered:function(r){(!r||1"==l&&(l="!=");var s=f.settings.sqlRuleOperator[l];void 0===s&&h.error("UndefinedSQLOperator",'Invalid SQL operation "{0}".',t.operation);var a,u=s.call(this,n,t.operation);"values"in t.left?a=t.left.values.join("."):"value"in t.left?a=t.left.value:h.error("SQLParse","Cannot find field name in {0}",JSON.stringify(t.left));var p=f.getSQLFieldID(a,n),d=f.change("sqlToRule",{id:p,field:a,operator:u.op,value:u.val},t);g.rules.push(d)}}(i,0),o},setRulesFromSQL:function(e,t){this.setRules(this.getRulesFromSQL(e,t))},getSQLFieldID:function(t,e){var r=this.filters.filter(function(e){return e.field.toLowerCase()===t.toLowerCase()});return 1===r.length?r[0].id:this.change("getSQLFieldID",t,e)}}),c.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),this.on("getDefaultFilter.filter",function(t,r){var i=t.builder;(i.updateDisabledFilters(),t.value.id in i.status.used_filters)&&(i.filters.some(function(e){if(!(e.id in i.status.used_filters)||0": ">", '"': """, "'": "'", "/": "/" },
+ 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+"}
+ * @readonly
+ */
+ this.icons = this.settings.icons;
+
+ /**
+ * List of operators
+ * @member {QueryBuilder.Operator[]}
+ * @readonly
+ */
+ this.operators = this.settings.operators;
+
+ /**
+ * List of templates
+ * @member {object.}
+ * @readonly
+ */
+ this.templates = this.settings.templates;
+
+ /**
+ * Plugins configuration
+ * @member {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.}
+ * @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.}
+ * @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.}
+ * @readonly
+ */
+QueryBuilder.templates = {};
+
+/**
+ * Localized strings (see i18n/)
+ * @type {object.}
+ * @readonly
+ */
+QueryBuilder.regional = {};
+
+/**
+ * Default operators
+ * @type {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: true,
+ 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.}
+ */
+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.} 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 = $(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 = $(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 = $(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 = $(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 = $(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.
+ * 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.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
+ * @private
+ */
+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
+ * @private
+ */
+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
+ * @private
+ */
+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 - not 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 - not 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 = '\
+';
+
+QueryBuilder.templates.rule = '\
+ \
+ \
+ {{? it.settings.display_errors }} \
+
\
+ {{?}} \
+
\
+
\
+
\
+
';
+
+QueryBuilder.templates.filterSelect = '\
+{{ var optgroup = null; }} \
+';
+
+QueryBuilder.templates.operatorSelect = '\
+{{? it.operators.length === 1 }} \
+ \
+{{= it.translate("operators", it.operators[0].type) }} \
+ \
+{{?}} \
+{{ var optgroup = null; }} \
+';
+
+QueryBuilder.templates.ruleValueSelect = '\
+{{ var optgroup = null; }} \
+';
+
+/**
+ * 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 = '';
+
+ 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 += ' ';
+ });
+ break;
+
+ case 'select':
+ h = this.getRuleValueSelect(name, rule);
+ break;
+
+ case 'textarea':
+ h += '';
+ break;
+
+ case 'number':
+ h += '" ; + break + + default + 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 + * @returns {string + * +Utils.escapeString="function(value)" { + if (typeof value !="string" ) { + return value + } + + return 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 ; + default return \ + s + @formatter:off + } + } + uglify compliant + .replace(/\t/g \\t ) + .replace(/\x1a/g \\Z ) + + + + * 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 />
+ * Update events are emitted in the setter through root Model (if any).
+ * 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+= '\
+ \
+ \
+ \
+
';
+ });
+ }
+ });
+}, {
+ 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 = $(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');
+ }
+ }
+
+ // 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);
+ }
+});
+
+
+/**
+ * @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) {
+ 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 = $('');
+ $p.appendTo(rule.$el);
+ }
+ else {
+ $p.css('display', '');
+ }
+
+ $p.html(' ' + 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 = $('');
+ $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 = $('');
+ $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 = $(h.value);
+ $h.find(Selectors.condition_container).after(
+ ''
+ );
+ h.value = $h.prop('outerHTML');
+ });
+
+ if (options.display_rules_button && options.invert_rules) {
+ this.on('getRuleTemplate.filter', function(h) {
+ var $h = $(h.value);
+ $h.find(Selectors.rule_actions).prepend(
+ ''
+ );
+ 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': '(? 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 = $(h.value);
+ $h.find(QueryBuilder.selectors.condition_container).prepend(
+ ''
+ );
+ 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 = $('
')
+ .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 = $(h.value);
+ $h.find(QueryBuilder.selectors.condition_container).after('
');
+ h.value = $h.prop('outerHTML');
+ }
+ });
+
+ this.on('getRuleTemplate.filter', function(h) {
+ var $h = $(h.value);
+ $h.find(QueryBuilder.selectors.rule_header).after('
');
+ 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}%' },
+ not_begins_with: { op: 'NOT LIKE(?)', mod: '{0}%' },
+ contains: { op: 'LIKE(?)', mod: '%{0}%' },
+ not_contains: { op: 'NOT LIKE(?)', mod: '%{0}%' },
+ ends_with: { op: 'LIKE(?)', mod: '%{0}' },
+ not_ends_with: { op: 'NOT LIKE(?)', mod: '%{0}' },
+ 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('|') + ')', '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);
+ }
+
+ 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));
+ }
+
+ 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: opVal.val
+ }, 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 Selectpicker
+ if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) {
+ self.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render');
+ }
+ }
+});
+
+
+/*!
+ * jQuery QueryBuilder 2.5.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 rule",
+ "add_group": "Add group",
+ "delete_rule": "Delete",
+ "delete_group": "Delete",
+ "conditions": {
+ "AND": "AND",
+ "OR": "OR"
+ },
+ "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": "contains",
+ "not_contains": "doesn't contain",
+ "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;
+
+}));
diff --git a/src/dist/js/query-builder.standalone.min.js b/src/dist/js/query-builder.standalone.min.js
new file mode 100644
index 00000000..2fab5e13
--- /dev/null
+++ b/src/dist/js/query-builder.standalone.min.js
@@ -0,0 +1,7 @@
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Copyright 2014-2018 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+!function(e,t){"function"==typeof define&&define.amd?define("jQuery.extendext",["jquery"],t):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function($){"use strict";$.extendext=function(){var e,t,r,n,i,o,l=arguments[0]||{},s=1,a=arguments.length,u=!1,p="default";for("boolean"==typeof l&&(u=l,l=arguments[s++]||{}),"string"==typeof l&&("concat"!==(p=l.toLowerCase())&&"replace"!==p&&"extend"!==p&&(p="default"),l=arguments[s++]||{}),"object"==typeof l||$.isFunction(l)||(l={}),s===a&&(l=this,s--);s":">",'"':""","'":"'","/":"/"},r=e?/[&<>"'\/]/g:/&(?!#?\w+;)|<|>|"|'|\//g;return function(e){return e?e.toString().replace(r,function(e){return t[e]||e}):""}},a=function(){return this||(0,eval)("this")}(),"undefined"!=typeof module&&module.exports?module.exports=u:"function"==typeof define&&define.amd?define("doT",function(){return u}):a.doT=u;var p={append:{start:"'+(",end:")+'",startencode:"'+encodeHTML("},split:{start:"';out+=(",end:");out+='",startencode:"';out+=encodeHTML("}},d=/$^/;function c(e){return e.replace(/\\('|\\)/g,"$1").replace(/[\r\t\n]/g," ")}u.template=function(e,t,r){var n,i,o=(t=t||u.templateSettings).append?p.append:p.split,l=0,s=t.use||t.define?function n(i,e,o){return("string"==typeof e?e:e.toString()).replace(i.define||d,function(e,n,t,r){return 0===n.indexOf("def.")&&(n=n.substring(4)),n in o||(":"===t?(i.defineParams&&r.replace(i.defineParams,function(e,t,r){o[n]={arg:t,text:r}}),n in o||(o[n]=r)):new Function("def","def['"+n+"']="+r)(o)),""}).replace(i.use||d,function(e,t){i.useParams&&(t=t.replace(i.useParams,function(e,t,r,n){if(o[r]&&o[r].arg&&n){var i=(r+":"+n).replace(/'|\\/g,"_");return o.__exp=o.__exp||{},o.__exp[i]=o[r].text.replace(new RegExp("(^|[^\\w$])"+o[r].arg+"([^\\w$])","g"),"$1"+n+"$2"),t+"def.__exp['"+i+"']"}}));var r=new Function("def","return "+t)(o);return r?n(i,r,o):r})}(t,e,r||{}):e;s=("var out='"+(t.strip?s.replace(/(^|\r|\n)\t* +| +\t*(\r|\n|$)/g," ").replace(/\r|\n|\t|\/\*[\s\S]*?\*\//g,""):s).replace(/'|\\/g,"\\$&").replace(t.interpolate||d,function(e,t){return o.start+c(t)+o.end}).replace(t.encode||d,function(e,t){return n=!0,o.startencode+c(t)+o.end}).replace(t.conditional||d,function(e,t,r){return t?r?"';}else if("+c(r)+"){out+='":"';}else{out+='":r?"';if("+c(r)+"){out+='":"';}out+='"}).replace(t.iterate||d,function(e,t,r,n){return t?(l+=1,i=n||"i"+l,t=c(t),"';var arr"+l+"="+t+";if(arr"+l+"){var "+r+","+i+"=-1,l"+l+"=arr"+l+".length-1;while("+i+".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]"},c.templates={},c.regional={},c.OPERATORS={equal:{type:"equal",nb_inputs:1,multiple:!1,apply_to:["string","number","datetime","boolean"]},not_equal:{type:"not_equal",nb_inputs:1,multiple:!1,apply_to:["string","number","datetime","boolean"]},in:{type:"in",nb_inputs:1,multiple:!0,apply_to:["string","number","datetime"]},not_in:{type:"not_in",nb_inputs:1,multiple:!0,apply_to:["string","number","datetime"]},less:{type:"less",nb_inputs:1,multiple:!1,apply_to:["number","datetime"]},less_or_equal:{type:"less_or_equal",nb_inputs:1,multiple:!1,apply_to:["number","datetime"]},greater:{type:"greater",nb_inputs:1,multiple:!1,apply_to:["number","datetime"]},greater_or_equal:{type:"greater_or_equal",nb_inputs:1,multiple:!1,apply_to:["number","datetime"]},between:{type:"between",nb_inputs:2,multiple:!1,apply_to:["number","datetime"]},not_between:{type:"not_between",nb_inputs:2,multiple:!1,apply_to:["number","datetime"]},begins_with:{type:"begins_with",nb_inputs:1,multiple:!1,apply_to:["string"]},not_begins_with:{type:"not_begins_with",nb_inputs:1,multiple:!1,apply_to:["string"]},contains:{type:"contains",nb_inputs:1,multiple:!1,apply_to:["string"]},not_contains:{type:"not_contains",nb_inputs:1,multiple:!1,apply_to:["string"]},ends_with:{type:"ends_with",nb_inputs:1,multiple:!1,apply_to:["string"]},not_ends_with:{type:"not_ends_with",nb_inputs:1,multiple:!1,apply_to:["string"]},is_empty:{type:"is_empty",nb_inputs:0,multiple:!1,apply_to:["string"]},is_not_empty:{type:"is_not_empty",nb_inputs:0,multiple:!1,apply_to:["string"]},is_null:{type:"is_null",nb_inputs:0,multiple:!1,apply_to:["string","number","datetime","boolean"]},is_not_null:{type:"is_not_null",nb_inputs:0,multiple:!1,apply_to:["string","number","datetime","boolean"]}},c.DEFAULTS={filters:[],plugins:[],sort_filters:!1,display_errors:!0,allow_groups:-1,allow_empty:!1,conditions:["AND","OR"],default_condition:"AND",inputs_separator:" , ",select_placeholder:"------",display_empty_filter:!0,default_filter:null,optgroups:{},default_rule_flags:{filter_readonly:!1,operator_readonly:!1,value_readonly:!1,no_delete:!1},default_group_flags:{condition_readonly:!1,no_add_rule:!1,no_add_group:!1,no_delete:!1},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"}},c.plugins={},c.defaults=function(e){if("object"!=typeof e)return"string"==typeof e?"object"==typeof c.DEFAULTS[e]?$.extend(!0,{},c.DEFAULTS[e]):c.DEFAULTS[e]:$.extend(!0,{},c.DEFAULTS);$.extendext(!0,"replace",c.DEFAULTS,e)},c.define=function(e,t,r){c.plugins[e]={fct:t,def:r||{}}},c.extend=function(e){$.extend(c.prototype,e)},c.prototype.initPlugins=function(){if(this.plugins){if($.isArray(this.plugins)){var t={};this.plugins.forEach(function(e){t[e]=null}),this.plugins=t}Object.keys(this.plugins).forEach(function(e){e in c.plugins?(this.plugins[e]=$.extend(!0,{},c.plugins[e].def,this.plugins[e]||{}),c.plugins[e].fct.call(this,this.plugins[e])):h.error("Config",'Unable to find plugin "{0}"',e)},this)}},c.prototype.getPluginOptions=function(e,t){var r;if(this.plugins&&this.plugins[e]?r=this.plugins[e]:c.plugins[e]&&(r=c.plugins[e].def),r)return t?r[t]:r;h.error("Config",'Unable to find plugin "{0}"',e)},c.prototype.init=function(e){this.trigger("afterInit"),e?(this.setRules(e),delete this.settings.rules):this.setRoot(!0)},c.prototype.checkFilters=function(e){var t=[];if(e&&0!==e.length||h.error("Config","Missing filters list"),e.forEach(function(n,e){switch(n.id||h.error("Config","Missing filter {0} id",e),-1!=t.indexOf(n.id)&&h.error("Config",'Filter "{0}" already defined',n.id),t.push(n.id),n.type?c.types[n.type]||h.error("Config",'Invalid type "{0}"',n.type):n.type="string",n.input?"function"!=typeof n.input&&-1==c.inputs.indexOf(n.input)&&h.error("Config",'Invalid input "{0}"',n.input):n.input="number"===c.types[n.type]?"number":"text",n.operators&&n.operators.forEach(function(e){"string"!=typeof e&&h.error("Config","Filter operators must be global operators types (string)")}),n.field||(n.field=n.id),n.label||(n.label=n.field),n.optgroup?(this.status.has_optgroup=!0,this.settings.optgroups[n.optgroup]||(this.settings.optgroups[n.optgroup]=n.optgroup)):n.optgroup=null,n.input){case"radio":case"checkbox":(!n.values||n.values.length<1)&&h.error("Config",'Missing filter "{0}" values',n.id);break;case"select":var i=[];n.has_optgroup=!1,h.iterateOptions(n.values,function(e,t,r){i.push({value:e,label:t,optgroup:r||null}),r&&(n.has_optgroup=!0,this.settings.optgroups[r]||(this.settings.optgroups[r]=r))}.bind(this)),n.has_optgroup?n.values=h.groupSort(i,"optgroup"):n.values=i,n.placeholder&&(void 0===n.placeholder_value&&(n.placeholder_value=-1),n.values.forEach(function(e){e.value==n.placeholder_value&&h.error("Config",'Placeholder of filter "{0}" overlaps with one of its values',n.id)}))}},this),this.settings.sort_filters)if("function"==typeof this.settings.sort_filters)e.sort(this.settings.sort_filters);else{var r=this;e.sort(function(e,t){return r.translate(e.label).localeCompare(r.translate(t.label))})}return this.status.has_optgroup&&(e=h.groupSort(e,"optgroup")),e},c.prototype.checkOperators=function(r){var n=[];return r.forEach(function(e,t){"string"==typeof e?(c.OPERATORS[e]||h.error("Config",'Unknown operator "{0}"',e),r[t]=e=$.extendext(!0,"replace",{},c.OPERATORS[e])):(e.type||h.error("Config",'Missing "type" for operator {0}',t),c.OPERATORS[e.type]&&(r[t]=e=$.extendext(!0,"replace",{},c.OPERATORS[e.type],e)),void 0!==e.nb_inputs&&void 0!==e.apply_to||h.error("Config",'Missing "nb_inputs" and/or "apply_to" for operator "{0}"',e.type)),-1!=n.indexOf(e.type)&&h.error("Config",'Operator "{0}" already defined',e.type),n.push(e.type),e.optgroup?(this.status.has_operator_optgroup=!0,this.settings.optgroups[e.optgroup]||(this.settings.optgroups[e.optgroup]=e.optgroup)):e.optgroup=null},this),this.status.has_operator_optgroup&&(r=h.groupSort(r,"optgroup")),r},c.prototype.bindEvents=function(){var o=this,t=c.selectors;this.$el.on("change.queryBuilder",t.group_condition,function(){if($(this).is(":checked")){var e=$(this).closest(t.group_container);o.getModel(e).condition=$(this).val()}}),this.$el.on("change.queryBuilder",t.rule_filter,function(){var e=$(this).closest(t.rule_container);o.getModel(e).filter=o.getFilterById($(this).val())}),this.$el.on("change.queryBuilder",t.rule_operator,function(){var e=$(this).closest(t.rule_container);o.getModel(e).operator=o.getOperatorByType($(this).val())}),this.$el.on("click.queryBuilder",t.add_rule,function(){var e=$(this).closest(t.group_container);o.addRule(o.getModel(e))}),this.$el.on("click.queryBuilder",t.delete_rule,function(){var e=$(this).closest(t.rule_container);o.deleteRule(o.getModel(e))}),0!==this.settings.allow_groups&&(this.$el.on("click.queryBuilder",t.add_group,function(){var e=$(this).closest(t.group_container);o.addGroup(o.getModel(e))}),this.$el.on("click.queryBuilder",t.delete_group,function(){var e=$(this).closest(t.group_container);o.deleteGroup(o.getModel(e))})),this.model.on({drop:function(e,t){t.$el.remove(),o.refreshGroupsConditions()},add:function(e,t,r,n){0===n?r.$el.prependTo(t.$el.find(">"+c.selectors.rules_list)):r.$el.insertAfter(t.rules[n-1].$el),o.refreshGroupsConditions()},move:function(e,t,r,n){t.$el.detach(),0===n?t.$el.prependTo(r.$el.find(">"+c.selectors.rules_list)):t.$el.insertAfter(r.rules[n-1].$el),o.refreshGroupsConditions()},update:function(e,t,r,n,i){if(t instanceof l)switch(r){case"error":o.updateError(t);break;case"flags":o.applyRuleFlags(t);break;case"filter":o.updateRuleFilter(t,i);break;case"operator":o.updateRuleOperator(t,i);break;case"value":o.updateRuleValue(t,i)}else switch(r){case"error":o.updateError(t);break;case"flags":o.applyGroupFlags(t);break;case"condition":o.updateGroupCondition(t,i)}}})},c.prototype.setRoot=function(e,t,r){e=void 0===e||!0===e;var n=this.nextGroupId(),i=$(this.getGroupTemplate(n,1));return this.$el.append(i),this.model.root=new a(null,i),this.model.root.model=this.model,this.model.root.data=t,this.model.root.flags=$.extend({},this.settings.default_group_flags,r),this.model.root.condition=this.settings.default_condition,this.trigger("afterAddGroup",this.model.root),e&&this.addRule(this.model.root),this.model.root},c.prototype.addGroup=function(e,t,r,n){t=void 0===t||!0===t;var i=e.level+1;if(this.trigger("beforeAddGroup",e,t,i).isDefaultPrevented())return null;var o=this.nextGroupId(),l=$(this.getGroupTemplate(o,i)),s=e.addGroup(l);return s.data=r,s.flags=$.extend({},this.settings.default_group_flags,n),s.condition=this.settings.default_condition,this.trigger("afterAddGroup",s),this.trigger("rulesChanged"),t&&this.addRule(s),s},c.prototype.deleteGroup=function(e){if(e.isRoot())return!1;if(this.trigger("beforeDeleteGroup",e).isDefaultPrevented())return!1;var t=!0;return e.each("reverse",function(e){t&=this.deleteRule(e)},function(e){t&=this.deleteGroup(e)},this),t&&(e.drop(),this.trigger("afterDeleteGroup"),this.trigger("rulesChanged")),t},c.prototype.updateGroupCondition=function(t,e){t.$el.find(">"+c.selectors.group_condition).each(function(){var e=$(this);e.prop("checked",e.val()===t.condition),e.parent().toggleClass("active",e.val()===t.condition)}),this.trigger("afterUpdateGroupCondition",t,e),this.trigger("rulesChanged")},c.prototype.refreshGroupsConditions=function(){!function t(e){(!e.flags||e.flags&&!e.flags.condition_readonly)&&e.$el.find(">"+c.selectors.group_condition).prop("disabled",e.rules.length<=1).parent().toggleClass("disabled",e.rules.length<=1),e.each(null,function(e){t(e)},this)}(this.model.root)},c.prototype.addRule=function(e,t,r){if(this.trigger("beforeAddRule",e).isDefaultPrevented())return null;var n=this.nextRuleId(),i=$(this.getRuleTemplate(n)),o=e.addRule(i);return o.data=t,o.flags=$.extend({},this.settings.default_rule_flags,r),this.trigger("afterAddRule",o),this.trigger("rulesChanged"),this.createRuleFilters(o),!this.settings.default_filter&&this.settings.display_empty_filter||(o.filter=this.change("getDefaultFilter",this.getFilterById(this.settings.default_filter||this.filters[0].id),o)),o},c.prototype.deleteRule=function(e){return!e.flags.no_delete&&(!this.trigger("beforeDeleteRule",e).isDefaultPrevented()&&(e.drop(),this.trigger("afterDeleteRule"),this.trigger("rulesChanged"),!0))},c.prototype.createRuleFilters=function(e){var t=this.change("getRuleFilters",this.filters,e),r=$(this.getRuleFilterSelect(e,t));e.$el.find(c.selectors.filter_container).html(r),this.trigger("afterCreateRuleFilters",e),this.applyRuleFlags(e)},c.prototype.createRuleOperators=function(e){var t=e.$el.find(c.selectors.operator_container).empty();if(e.filter){var r=this.getOperators(e.filter),n=$(this.getRuleOperatorSelect(e,r));t.html(n),e.filter.default_operator?e.__.operator=this.getOperatorByType(e.filter.default_operator):e.__.operator=r[0],e.$el.find(c.selectors.rule_operator).val(e.operator.type),this.trigger("afterCreateRuleOperators",e,r),this.applyRuleFlags(e)}},c.prototype.createRuleInput=function(e){var t=e.$el.find(c.selectors.value_container).empty();if(e.__.value=void 0,e.filter&&e.operator&&0!==e.operator.nb_inputs){for(var r=this,n=$(),i=e.filter,o=0;o"+r.group_condition).prop("disabled",t.condition_readonly).parent().toggleClass("readonly",t.condition_readonly),t.no_add_rule&&e.$el.find(r.add_rule).remove(),t.no_add_group&&e.$el.find(r.add_group).remove(),t.no_delete&&e.$el.find(r.delete_group).remove(),this.trigger("afterApplyGroupFlags",e)},c.prototype.clearErrors=function(e){(e=e||this.model.root)&&(e.error=null,e instanceof a&&e.each(function(e){e.error=null},function(e){this.clearErrors(e)},this))},c.prototype.updateError=function(e){if(this.settings.display_errors)if(null===e.error)e.$el.removeClass("has-error");else{var t=this.translate("errors",e.error[0]);t=h.fmt(t,e.error.slice(1)),t=this.change("displayError",t,e.error,e),e.$el.addClass("has-error").find(c.selectors.error_container).eq(0).attr("title",t)}},c.prototype.triggerValidationError=function(e,t,r){$.isArray(t)||(t=[t]),this.trigger("validationError",e,t,r).isDefaultPrevented()||(e.error=t)},c.prototype.destroy=function(){this.trigger("beforeDestroy"),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},c.prototype.reset=function(){this.trigger("beforeReset").isDefaultPrevented()||(this.status.group_id=1,this.status.rule_id=0,this.model.root.empty(),this.model.root.data=void 0,this.model.root.flags=$.extend({},this.settings.default_group_flags),this.model.root.condition=this.settings.default_condition,this.addRule(this.model.root),this.trigger("afterReset"),this.trigger("rulesChanged"))},c.prototype.clear=function(){this.trigger("beforeClear").isDefaultPrevented()||(this.status.group_id=0,this.status.rule_id=0,this.model.root&&(this.model.root.drop(),this.model.root=null),this.trigger("afterClear"),this.trigger("rulesChanged"))},c.prototype.setOptions=function(e){$.each(e,function(e,t){-1!==c.modifiable_options.indexOf(e)&&(this.settings[e]=t)}.bind(this))},c.prototype.getModel=function(e){return e?e instanceof i?e:$(e).data("queryBuilderModel"):this.model.root},c.prototype.validate=function(o){o=$.extend({skip_empty:!1},o),this.clearErrors();var l=this,e=function r(e){var n=0,i=0;return e.each(function(e){if(e.filter||!o.skip_empty){if(!e.filter)return l.triggerValidationError(e,"no_filter",null),void i++;if(!e.operator)return l.triggerValidationError(e,"no_operator",null),void i++;if(0!==e.operator.nb_inputs){var t=l.validateValue(e,e.value);if(!0!==t)return l.triggerValidationError(e,t,e.value),void i++}n++}},function(e){var t=r(e);!0===t?n++:!1===t&&i++}),!(0parseInt(l.max)){s=[this.getValidationMessage(l,"max","string_exceed_max_length"),l.max];break}if(l.format&&("string"==typeof l.format&&(l.format=new RegExp(l.format)),!l.format.test(n[u]))){s=[this.getValidationMessage(l,"format","string_invalid_format"),l.format];break}break;case"number":if(void 0===n[u]||0===n[u].length){l.allow_empty_value||(s=["number_nan"]);break}if(isNaN(n[u])){s=["number_nan"];break}if("integer"==i.type){if(parseInt(n[u])!=n[u]){s=["number_not_integer"];break}}else if(parseFloat(n[u])!=n[u]){s=["number_not_double"];break}if(void 0!==l.min&&n[u]parseFloat(l.max)){s=[this.getValidationMessage(l,"max","number_exceed_max"),l.max];break}if(void 0!==l.step&&"any"!==l.step){var p=(n[u]/l.step).toPrecision(14);if(parseInt(p)!=p){s=[this.getValidationMessage(l,"step","number_wrong_step"),l.step];break}}break;case"datetime":if(void 0===n[u]||0===n[u].length){l.allow_empty_value||(s=["datetime_empty"]);break}if(l.format){"moment"in window||h.error("MissingLibrary","MomentJS is required for Date/Time validation. Get it here http://momentjs.com");var d=moment(n[u],l.format);if(!d.isValid()){s=[this.getValidationMessage(l,"format","datetime_invalid"),l.format];break}if(l.min&&dmoment(l.max,l.format)){s=[this.getValidationMessage(l,"max","datetime_exceed_max"),l.max];break}}break;case"boolean":if(void 0===n[u]||0===n[u].length){l.allow_empty_value||(s=["boolean_not_valid"]);break}if("true"!==(r=(""+n[u]).trim().toLowerCase())&&"false"!==r&&"1"!==r&&"0"!==r&&1!==n[u]&&0!==n[u]){s=["boolean_not_valid"];break}}if(!0!==s)break}}if(!0!==s)break}if(("between"===e.operator.type||"not_between"===e.operator.type)&&2===t.length)switch(c.types[i.type]){case"number":t[0]>t[1]&&(s=["number_between_invalid",t[0],t[1]]);break;case"datetime":l.format&&("moment"in window||h.error("MissingLibrary","MomentJS is required for Date/Time validation. Get it here http://momentjs.com"),moment(t[0],l.format).isAfter(moment(t[1],l.format))&&(s=["datetime_between_invalid",t[0],t[1]]))}return s},c.prototype.nextGroupId=function(){return this.status.id+"_group_"+this.status.group_id++},c.prototype.nextRuleId=function(){return this.status.id+"_rule_"+this.status.rule_id++},c.prototype.getOperators=function(r){"string"==typeof r&&(r=this.getFilterById(r));for(var e=[],t=0,n=this.operators.length;t ' ,c.templates.rule=' {{? it.settings.display_errors }}
{{?}}
' ,c.templates.filterSelect='{{ var optgroup = null; }} ' ,c.templates.operatorSelect='{{? it.operators.length === 1 }} {{= it.translate("operators", it.operators[0].type) }} {{?}} {{ var optgroup = null; }} ' ,c.templates.ruleValueSelect='{{ var optgroup = null; }} ' ,c.prototype.getGroupTemplate="function(e,t){var" r="this.templates.group({builder:this,group_id:e,level:t,conditions:this.settings.conditions,icons:this.icons,settings:this.settings,translate:this.translate.bind(this)});return" this.change getGroupTemplate ,r,t)},c.prototype.getRuleTemplate="function(e){var" t="this.templates.rule({builder:this,rule_id:e,icons:this.icons,settings:this.settings,translate:this.translate.bind(this)});return" this.change getRuleTemplate ,t)},c.prototype.getRuleFilterSelect="function(e,t){var" r="this.templates.filterSelect({builder:this,rule:e,filters:t,icons:this.icons,settings:this.settings,translate:this.translate.bind(this)});return" this.change getRuleFilterSelect ,r,e,t)},c.prototype.getRuleOperatorSelect="function(e,t){var" r="this.templates.operatorSelect({builder:this,rule:e,operators:t,icons:this.icons,settings:this.settings,translate:this.translate.bind(this)});return" this.change getRuleOperatorSelect ,r,e,t)},c.prototype.getRuleValueSelect="function(e,t){var" r="this.templates.ruleValueSelect({builder:this,name:e,rule:t,icons:this.icons,settings:this.settings,translate:this.translate.bind(this)});return" this.change getRuleValueSelect ,r,e,t)},c.prototype.getRuleInput="function(e,t){var" r='e.filter,n=e.filter.validation||{},i=e.id+"_value_"+t,o=r.vertical?"' class='block":"",l="";if("function"==typeof' r.input)l="r.input.call(this,e,i);else" switch(r.input){case radio :case checkbox :h.iterateOptions(r.values,function(e,t){l=" "});break;case"select":l=this.getRuleValueSelect(i,e);break;case"textarea":l+='" ;break;case number :l='" ;break;default:l='" }return this.change getRuleInput ,l,e,i)};var h="{};function" n(){this.root='null,this.$=$(this)}(c.utils=h).iterateOptions=function(e,r){e&&($.isArray(e)?e.forEach(function(e){$.isPlainObject(e)?"value"in' e?r(e.value,e.label||e.value,e.optgroup):$.each(e,function(e,t){return r(e,t),!1}):r(e,e)}):$.each(e,function(e,t){r(e,t)}))},h.fmt="function(e,r){return" Array.isArray(r)||(r="Array.prototype.slice.call(arguments,1)),e.replace(/{([0-9]+)}/g,function(e,t){return" r[parseInt(t)]})},h.error="function(){var" e='0,t="boolean"!=typeof' arguments[e]||arguments[e++],r="arguments[e++],n=arguments[e++],i=Array.isArray(arguments[e])?arguments[e]:Array.prototype.slice.call(arguments,e);if(t){var" o="new" Error(h.fmt(n,i));throw o.name='r+"Error",o.args=i,o}console.error(r+"Error:' +h.fmt(n,i))},h.changeType='function(e,t){if(""!==e&&void' 0='=e)switch(t){case"integer":return"string"!=typeof' e||/^-?\d+$/.test(e)?parseInt(e):e;case double :return string !="typeof" e||/^-?\d+\.?\d*$/.test(e)?parseFloat(e):e;case boolean :return string !="typeof" e||/^(0|1|true|false){1}$/i.test(e)?!0='==e||1===e||"true"===e.toLowerCase()||"1"===e:e;default:return' e}},h.escapeString='function(e){return"string"!=typeof' e?e:e.replace(/[\0\n\r\b \ ]/g,function(e){switch(e){case \0 :return \\0 ;case \n :return \\n ;case \r :return \\r ;case \b :return \\b ;default:return \ +e}}).replace(/\t/g \\t ).replace(/\x1a/g \\Z )},h.escapeRegExp="function(e){return" e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g \ )},h.escapeElementId="function(e){return" e?e.replace(/(\\)?([:.\[\],])/g,function(e,t,r){return t?e \ +r}):e},h.groupSort="function(e,r){var" n="[],i=[];return" e.forEach(function(e){var t;e[r]?-1="=(t=n.lastIndexOf(e[r]))?t=n.length:t++:t=n.length,n.splice(t,0,e[r]),i.splice(t,0,e)}),i},h.defineModelProperties=function(e,t){t.forEach(function(r){Object.defineProperty(e.prototype,r,{enumerable:!0,get:function(){return" this.__[r]},set:function(e){var t='null!==this.__[r]&&"object"==typeof' this.__[r]?$.extend({},this.__[r]):this.__[r];this.__[r='e,null!==this.model&&this.model.trigger("update",this,r,e,t)}})})},$.extend(n.prototype,{trigger:function(e){var' t="new" $.Event(e);return this.$.triggerHandler(t,Array.prototype.slice.call(arguments,1)),t},on:function(){return this.$.on.apply(this.$,Array.prototype.slice.call(arguments)),this},off:function(){return this.$.off.apply(this.$,Array.prototype.slice.call(arguments)),this},once:function(){return this.$.one.apply(this.$,Array.prototype.slice.call(arguments)),this}});var i="function(e,t){if(!(this" instanceof i))return new i(e,t);Object.defineProperty(this _ ,{value:{}}),t.data queryBuilderModel ,this),this.__.level="1,this.__.error=null,this.__.flags={},this.__.data=void" 0,this.$el='t,this.id=t[0].id,this.model=null,this.parent=e};h.defineModelProperties(i,["level","error","data","flags"]),Object.defineProperty(i.prototype,"parent",{enumerable:!0,get:function(){return' this.__.parent},set:function(e){this.__.parent="e,this.level=null===e?1:e.level+1,this.model=null===e?null:e.model}}),i.prototype.isRoot=function(){return" 1="==this.level},i.prototype.getPos=function(){return" this.isRoot()?-1:this.parent.getNodePos(this)},i.prototype.drop="function(){var" e='this.model;this.parent&&this.parent.removeNode(this),this.$el.removeData("queryBuilderModel"),null!==e&&e.trigger("drop",this)},i.prototype.moveAfter=function(e){this.isRoot()||this.move(e.parent,e.getPos()+1)},i.prototype.moveAtBegin=function(e){this.isRoot()||(void' 0="==e&&(e=this.parent),this.move(e,0))},i.prototype.moveAtEnd=function(e){this.isRoot()||(void" 0='==e&&(e=this.parent),this.move(e,0===e.length()?0:e.length()-1))},i.prototype.move=function(e,t){this.isRoot()||("number"==typeof' e&&(t='e,e=this.parent),this.parent.removeNode(this),e.insertNode(this,t,!1),null!==this.model&&this.model.trigger("move",this,e,t))};var' a="function(e,t){if(!(this" instanceof a))return new a(e,t);i.call(this,e,t),this.rules='[],this.__.condition=null};a.prototype=Object.create(i.prototype),a.prototype.constructor=a,h.defineModelProperties(a,["condition"]),a.prototype.empty=function(){this.each("reverse",function(e){e.drop()},function(e){e.drop()})},a.prototype.drop=function(){this.empty(),i.prototype.drop.call(this)},a.prototype.length=function(){return' this.rules.length},a.prototype.insertNode="function(e,t,r){return" void 0='==t&&(t=this.length()),this.rules.splice(t,0,e),e.parent=this,r&&null!==this.model&&this.model.trigger("add",this,e,t),e},a.prototype.addGroup=function(e,t){return' this.insertNode(new a(this,e),t,!0)},a.prototype.addRule="function(e,t){return" this.insertNode(new l(this,e),t,!0)},a.prototype.removeNode="function(e){var" t="this.getNodePos(e);-1!==t&&(e.parent=null,this.rules.splice(t,1))},a.prototype.getNodePos=function(e){return" this.rules.indexOf(e)},a.prototype.each='function(e,t,r,n){"boolean"!=typeof' e string !="typeof" e&&(n="r,r=t,t=e,e=!1),n=void" 0="==n?null:n;for(var" i="e?this.rules.length-1:0,o=e?0:this.rules.length-1,l=e?-1:1,s=!1;(e?o<=i:i<=o)&&(this.rules[i]instanceof" a?r&&(s="!1===r.call(n,this.rules[i])):t&&(s=!1===t.call(n,this.rules[i])),!s);i+=l);return!s},a.prototype.contains=function(t,e){return-1!==this.getNodePos(t)||!!e&&!this.each(function(){return!0},function(e){return!e.contains(t,!0)})};var" l="function(e,t){if(!(this" instanceof l))return new l(e,t);i.call(this,e,t),this._updating_value="!1,this._updating_input=!1,this.__.filter=null,this.__.operator=null,this.__.value=void" 0};function u(e,t,r){var n,i,o='c.selectors;(n=t.closest(o.rule_container)).length&&(i="moveAfter"),i||(n=t.closest(o.group_header)).length&&(n=t.closest(o.group_container),i="moveAtBegin"),i||(n=t.closest(o.group_container)).length&&(i="moveAtEnd"),i&&(e[i](r.getModel(n)),r&&e' instanceof l&&r.setRuleInputValue(e,e.value))}function o(e){var t="e.match(/(question_mark|numbered|named)(?:\((.)\))?/);return" t||(t='[null,"question_mark",void' 0]),t}return l.prototype='Object.create(i.prototype),l.prototype.constructor=l,h.defineModelProperties(l,["filter","operator","value"]),l.prototype.isRoot=function(){return!1},c.Group=a,c.Rule=l,$.fn.queryBuilder=function(e){0===this.length&&h.error("Config","No' target defined ),1> "})}})},{font:"glyphicons",color:"default"}),c.define("bt-selectpicker",function(r){$.fn.selectpicker&&$.fn.selectpicker.Constructor||h.error("MissingLibrary",'Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select');var n=c.selectors;this.on("afterCreateRuleFilters",function(e,t){t.$el.find(n.rule_filter).removeClass("form-control").selectpicker(r)}),this.on("afterCreateRuleOperators",function(e,t){t.$el.find(n.rule_operator).removeClass("form-control").selectpicker(r)}),this.on("afterUpdateRuleFilter",function(e,t){t.$el.find(n.rule_filter).selectpicker("render")}),this.on("afterUpdateRuleOperator",function(e,t){t.$el.find(n.rule_operator).selectpicker("render")}),this.on("beforeDeleteRule",function(e,t){t.$el.find(n.rule_filter).selectpicker("destroy"),t.$el.find(n.rule_operator).selectpicker("destroy")})},{container:"body",style:"btn-inverse btn-xs",width:"auto",showIcon:!1}),c.define("bt-tooltip-errors",function(n){$.fn.tooltip&&$.fn.tooltip.Constructor&&$.fn.tooltip.Constructor.prototype.fixTitle||h.error("MissingLibrary",'Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com');var i=this;this.on("getRuleTemplate.filter getGroupTemplate.filter",function(e){var t=$(e.value);t.find(c.selectors.error_container).attr("data-toggle","tooltip"),e.value=t.prop("outerHTML")}),this.model.on("update",function(e,t,r){"error"==r&&i.settings.display_errors&&t.$el.find(c.selectors.error_container).eq(0).tooltip(n).tooltip("hide").tooltip("fixTitle")})},{placement:"right"}),c.extend({setFilters:function(e,t){var r=this;void 0===t&&(t=e,e=!1),t=this.checkFilters(t);var n=(t=this.change("setFilters",t)).map(function(e){return e.id});if(e||function e(t){t.each(function(e){e.filter&&-1===n.indexOf(e.filter.id)&&h.error("ChangeFilter",'A rule is using filter "{0}"',e.filter.id)},e)}(this.model.root),this.filters=t,function e(t){t.each(!0,function(e){e.filter&&-1===n.indexOf(e.filter.id)?(e.drop(),r.trigger("rulesChanged")):(r.createRuleFilters(e),e.$el.find(c.selectors.rule_filter).val(e.filter?e.filter.id:"-1"),r.trigger("afterUpdateRuleFilter",e))},e)}(this.model.root),this.settings.plugins&&(this.settings.plugins["unique-filter"]&&this.updateDisabledFilters(),this.settings.plugins["bt-selectpicker"]&&this.$el.find(c.selectors.rule_filter).selectpicker("render")),this.settings.default_filter)try{this.getFilterById(this.settings.default_filter)}catch(e){this.settings.default_filter=null}this.trigger("afterSetFilters",t)},addFilter:function(e,r){void 0===r||"#end"==r?r=this.filters.length:"#start"==r&&(r=0),$.isArray(e)||(e=[e]);var t=$.extend(!0,[],this.filters);parseInt(r)==r?Array.prototype.splice.apply(t,[r,0].concat(e)):this.filters.some(function(e,t){if(e.id==r)return r=t+1,!0})?Array.prototype.splice.apply(t,[r,0].concat(e)):Array.prototype.push.apply(t,e),this.setFilters(t)},removeFilter:function(t,e){var r=$.extend(!0,[],this.filters);"string"==typeof t&&(t=[t]),r=r.filter(function(e){return-1===t.indexOf(e.id)}),this.setFilters(e,r)}}),c.define("chosen-selectpicker",function(r){$.fn.chosen||h.error("MissingLibrary",'chosen is required to use "chosen-selectpicker" plugin. Get it here: https://github.com/harvesthq/chosen'),this.settings.plugins["bt-selectpicker"]&&h.error("Conflict","bt-selectpicker is already selected as the dropdown plugin. Please remove chosen-selectpicker from the plugin list");var n=c.selectors;this.on("afterCreateRuleFilters",function(e,t){t.$el.find(n.rule_filter).removeClass("form-control").chosen(r)}),this.on("afterCreateRuleOperators",function(e,t){t.$el.find(n.rule_operator).removeClass("form-control").chosen(r)}),this.on("afterUpdateRuleFilter",function(e,t){t.$el.find(n.rule_filter).trigger("chosen:updated")}),this.on("afterUpdateRuleOperator",function(e,t){t.$el.find(n.rule_operator).trigger("chosen:updated")}),this.on("beforeDeleteRule",function(e,t){t.$el.find(n.rule_filter).chosen("destroy"),t.$el.find(n.rule_operator).chosen("destroy")})}),c.define("filter-description",function(i){"inline"===i.mode?this.on("afterUpdateRuleFilter afterUpdateRuleOperator",function(e,t){var r=t.$el.find("p.filter-description"),n=e.builder.getFilterDescription(t.filter,t);n?(0===r.length?(r=$('')).appendTo(t.$el):r.css("display",""),r.html(' '+n)):r.hide()}):"popover"===i.mode?($.fn.popover&&$.fn.popover.Constructor&&$.fn.popover.Constructor.prototype.fixTitle||h.error("MissingLibrary",'Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com'),this.on("afterUpdateRuleFilter afterUpdateRuleOperator",function(e,t){var r=t.$el.find("button.filter-description"),n=e.builder.getFilterDescription(t.filter,t);n?(0===r.length?((r=$('')).prependTo(t.$el.find(c.selectors.rule_actions)),r.popover({placement:"left",container:"body",html:!0}),r.on("mouseout",function(){r.popover("hide")})):r.css("display",""),r.data("bs.popover").options.content=n,r.attr("aria-describedby")&&r.popover("show")):(r.hide(),r.data("bs.popover")&&r.popover("hide"))})):"bootbox"===i.mode&&("bootbox"in window||h.error("MissingLibrary",'Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com'),this.on("afterUpdateRuleFilter afterUpdateRuleOperator",function(e,t){var r=t.$el.find("button.filter-description"),n=e.builder.getFilterDescription(t.filter,t);n?(0===r.length?((r=$('')).prependTo(t.$el.find(c.selectors.rule_actions)),r.on("click",function(){bootbox.alert(r.data("description"))})):r.css("display",""),r.data("description",n)):r.hide()}))},{icon:"glyphicon glyphicon-info-sign",mode:"popover"}),c.extend({getFilterDescription:function(e,t){return e?"function"==typeof e.description?e.description.call(this,t):e.description:void 0}}),c.define("invert",function(r){var n=this,i=c.selectors;this.on("afterInit",function(){n.$el.on("click.queryBuilder","[data-invert=group]",function(){var e=$(this).closest(i.group_container);n.invert(n.getModel(e),r)}),r.display_rules_button&&r.invert_rules&&n.$el.on("click.queryBuilder","[data-invert=rule]",function(){var e=$(this).closest(i.rule_container);n.invert(n.getModel(e),r)})}),r.disable_template||(this.on("getGroupTemplate.filter",function(e){var t=$(e.value);t.find(i.condition_container).after('"),e.value=t.prop("outerHTML")}),r.display_rules_button&&r.invert_rules&&this.on("getRuleTemplate.filter",function(e){var t=$(e.value);t.find(i.rule_actions).prepend('"),e.value=t.prop("outerHTML")}))},{icon:"glyphicon glyphicon-random",recursive:!0,invert_rules:!0,display_rules_button:!1,silent_fail:!1,disable_template:!1}),c.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"}}),c.extend({invert:function(e,t){if(!(e instanceof i)){if(!this.model.root)return;t=e,e=this.model.root}if("object"!=typeof t&&(t={}),void 0===t.recursive&&(t.recursive=!0),void 0===t.invert_rules&&(t.invert_rules=!0),void 0===t.silent_fail&&(t.silent_fail=!1),void 0===t.trigger&&(t.trigger=!0),e instanceof a){if(this.settings.conditionOpposites[e.condition]?e.condition=this.settings.conditionOpposites[e.condition]:t.silent_fail||h.error("InvertCondition",'Unknown inverse of condition "{0}"',e.condition),t.recursive){var r=$.extend({},t,{trigger:!1});e.each(function(e){t.invert_rules&&this.invert(e,r)},function(e){this.invert(e,r)},this)}}else if(e instanceof l&&e.operator&&!e.filter.no_invert)if(this.settings.operatorOpposites[e.operator.type]){var n=this.settings.operatorOpposites[e.operator.type];e.filter.operators&&-1==e.filter.operators.indexOf(n)||(e.operator=this.getOperatorByType(n))}else t.silent_fail||h.error("InvertOperator",'Unknown inverse of operator "{0}"',e.operator.type);t.trigger&&(this.trigger("afterInvert",e,t),this.trigger("rulesChanged"))}}),c.defaults({mongoOperators:{equal:function(e){return e[0]},not_equal:function(e){return{$ne:e[0]}},in:function(e){return{$in:e}},not_in:function(e){return{$nin:e}},less:function(e){return{$lt:e[0]}},less_or_equal:function(e){return{$lte:e[0]}},greater:function(e){return{$gt:e[0]}},greater_or_equal:function(e){return{$gte:e[0]}},between:function(e){return{$gte:e[0],$lte:e[1]}},not_between:function(e){return{$lt:e[0],$gt:e[1]}},begins_with:function(e){return{$regex:"^"+h.escapeRegExp(e[0])}},not_begins_with:function(e){return{$regex:"^(?!"+h.escapeRegExp(e[0])+")"}},contains:function(e){return{$regex:h.escapeRegExp(e[0])}},not_contains:function(e){return{$regex:"^((?!"+h.escapeRegExp(e[0])+").)*$",$options:"s"}},ends_with:function(e){return{$regex:h.escapeRegExp(e[0])+"$"}},not_ends_with:function(e){return{$regex:"(? '+n.translate("NOT")+""),e.value=t.prop("outerHTML")}),this.on("groupToJson.filter",function(e,t){e.value.not=t.not}),this.on("jsonToGroup.filter",function(e,t){e.value.not=!!t.not}),this.on("groupToSQL.filter",function(e,t){t.not&&(e.value="NOT ( "+e.value+" )")}),this.on("parseSQLNode.filter",function(e){e.value.name&&"NOT"==e.value.name.toUpperCase()&&(e.value=e.value.arguments.value[0],-1===["AND","OR"].indexOf(e.value.operation.toUpperCase())&&(e.value=new SQLParser.nodes.Op(n.settings.default_condition,e.value,null)),e.value.not=!0)}),this.on("sqlGroupsDistinct.filter",function(e,t,r,n){r.not&&0"+c.selectors.group_not).toggleClass("active",e.not).find("i").attr("class",e.not?t.icon_checked:t.icon_unchecked),this.trigger("afterUpdateGroupNot",e),this.trigger("rulesChanged")}}),c.define("sortable",function(n){var i,o,l,s;"interact"in window||h.error("MissingLibrary",'interact.js is required to use "sortable" plugin. Get it here: http://interactjs.io'),void 0!==n.default_no_sortable&&(h.error(!1,"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=n.default_no_sortable),interact.dynamicDrop(!0),interact.pointerMoveTolerance(10),this.on("afterAddRule afterAddGroup",function(e,t){if(t!=i){var r=e.builder;n.inherit_no_sortable&&t.parent&&t.parent.flags.no_sortable&&(t.flags.no_sortable=!0),n.inherit_no_drop&&t.parent&&t.parent.flags.no_drop&&(t.flags.no_drop=!0),t.flags.no_sortable||interact(t.$el[0]).draggable({allowFrom:c.selectors.drag_handle,onstart:function(e){s=!1,l=r.getModel(e.target),o=l.$el.clone().appendTo(l.$el.parent()).width(l.$el.outerWidth()).addClass("dragging");var t=$('
').height(l.$el.outerHeight());i=l.parent.addRule(t,l.getPos()),l.$el.hide()},onmove:function(e){o[0].style.top=e.clientY-15+"px",o[0].style.left=e.clientX-15+"px"},onend:function(e){e.dropzone&&(u(l,$(e.relatedTarget),r),s=!0),o.remove(),o=void 0,i.drop(),i=void 0,l.$el.css("display",""),r.trigger("afterMove",l),r.trigger("rulesChanged")}}),t.flags.no_drop||(interact(t.$el[0]).dropzone({accept:c.selectors.rule_and_group_containers,ondragenter:function(e){u(i,$(e.target),r)},ondrop:function(e){s||u(l,$(e.target),r)}}),t instanceof a&&interact(t.$el.find(c.selectors.group_header)[0]).dropzone({accept:c.selectors.rule_and_group_containers,ondragenter:function(e){u(i,$(e.target),r)},ondrop:function(e){s||u(l,$(e.target),r)}}))}}),this.on("beforeDeleteRule beforeDeleteGroup",function(e,t){e.isDefaultPrevented()||(interact(t.$el[0]).unset(),t instanceof a&&interact(t.$el.find(c.selectors.group_header)[0]).unset())}),this.on("afterApplyRuleFlags afterApplyGroupFlags",function(e,t){t.flags.no_sortable&&t.$el.find(".drag-handle").remove()}),n.disable_template||(this.on("getGroupTemplate.filter",function(e,t){if(1'),e.value=r.prop("outerHTML")}}),this.on("getRuleTemplate.filter",function(e){var t=$(e.value);t.find(c.selectors.rule_header).after('
'),e.value=t.prop("outerHTML")}))},{inherit_no_sortable:!0,inherit_no_drop:!0,icon:"glyphicon glyphicon-sort",disable_template:!1}),c.selectors.rule_and_group_containers=c.selectors.rule_container+", "+c.selectors.group_container,c.selectors.drag_handle=".drag-handle",c.defaults({default_rule_flags:{no_sortable:!1,no_drop:!1},default_group_flags:{no_sortable:!1,no_drop:!1}}),c.define("sql-support",function(e){},{boolean_as_integer:!0}),c.defaults({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}%"},not_begins_with:{op:"NOT LIKE(?)",mod:"{0}%"},contains:{op:"LIKE(?)",mod:"%{0}%"},not_contains:{op:"NOT LIKE(?)",mod:"%{0}%"},ends_with:{op:"LIKE(?)",mod:"%{0}"},not_ends_with:{op:"NOT LIKE(?)",mod:"%{0}"},is_empty:{op:"= ''"},is_not_empty:{op:"!= ''"},is_null:{op:"IS NULL"},is_not_null:{op:"IS NOT NULL"}},sqlRuleOperator:{"=":function(e){return{val:e,op:""===e?"is_empty":"equal"}},"!=":function(e){return{val:e,op:""===e?"is_not_empty":"not_equal"}},LIKE:function(e){return"%"==e.slice(0,1)&&"%"==e.slice(-1)?{val:e.slice(1,-1),op:"contains"}:"%"==e.slice(0,1)?{val:e.slice(1),op:"ends_with"}:"%"==e.slice(-1)?{val:e.slice(0,-1),op:"begins_with"}:void h.error("SQLParse",'Invalid value for LIKE operator "{0}"',e)},"NOT LIKE":function(e){return"%"==e.slice(0,1)&&"%"==e.slice(-1)?{val:e.slice(1,-1),op:"not_contains"}:"%"==e.slice(0,1)?{val:e.slice(1),op:"not_ends_with"}:"%"==e.slice(-1)?{val:e.slice(0,-1),op:"not_begins_with"}:void h.error("SQLParse",'Invalid value for NOT LIKE operator "{0}"',e)},IN:function(e){return{val:e,op:"in"}},"NOT IN":function(e){return{val:e,op:"not_in"}},"<":function(e){return{val:e,op:"less"}},"<=":function(e){return{val:e,op:"less_or_equal"}},">":function(e){return{val:e,op:"greater"}},">=":function(e){return{val:e,op:"greater_or_equal"}},BETWEEN:function(e){return{val:e,op:"between"}},"NOT BETWEEN":function(e){return{val:e,op:"not_between"}},IS:function(e){return null!==e&&h.error("SQLParse","Invalid value for IS operator"),{val:null,op:"is_null"}},"IS NOT":function(e){return null!==e&&h.error("SQLParse","Invalid value for IS operator"),{val:null,op:"is_not_null"}}},sqlStatements:{question_mark:function(){var r=[];return{add:function(e,t){return r.push(t),"?"},run:function(){return r}}},numbered:function(r){(!r||1"==l&&(l="!=");var s=f.settings.sqlRuleOperator[l];void 0===s&&h.error("UndefinedSQLOperator",'Invalid SQL operation "{0}".',t.operation);var a,u=s.call(this,o,t.operation);"values"in t.left?a=t.left.values.join("."):"value"in t.left?a=t.left.value:h.error("SQLParse","Cannot find field name in {0}",JSON.stringify(t.left));var p=f.getSQLFieldID(a,o),d=f.change("sqlToRule",{id:p,field:a,operator:u.op,value:u.val},t);g.rules.push(d)}}(n,0),i},setRulesFromSQL:function(e,t){this.setRules(this.getRulesFromSQL(e,t))},getSQLFieldID:function(t,e){var r=this.filters.filter(function(e){return e.field.toLowerCase()===t.toLowerCase()});return 1===r.length?r[0].id:this.change("getSQLFieldID",t,e)}}),c.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),this.on("getDefaultFilter.filter",function(t,r){var n=t.builder;(n.updateDisabledFilters(),t.value.id in n.status.used_filters)&&(n.filters.some(function(e){if(!(e.id in n.status.used_filters)||0* {
+ &::before,
+ &::after {
+ content: '';
+ position: absolute;
+ left: #{-1 * nth($ticks-position, 2)};
+ width: nth($ticks-position, 2);
+ height: calc(50% + #{$item-vertical-spacing});
+ border-color: $ticks-color;
+ border-style: solid;
+ }
+
+ &::before {
+ top: #{-2 * $ticks-width};
+ border-width: 0 0 $ticks-width $ticks-width;
+ }
+
+ &::after {
+ top: 50%;
+ border-width: 0 0 0 $ticks-width;
+ }
+
+ &:first-child::before {
+ top: #{-$group-padding - $ticks-width};
+ height: calc(50% + #{$group-padding + $item-vertical-spacing});
+ }
+
+ &:last-child::before {
+ border-radius: 0 0 0 #{2 * $ticks-width};
+ }
+
+ &:last-child::after {
+ display: none;
+ }
+ }
+}
+
+// import
+@import "plugins/_bt-checkbox";
+@import "plugins/_bt-tooltip-errors";
+@import "plugins/_filter-description";
+@import "plugins/_invert";
+@import "plugins/_sortable";
+// endimport
diff --git a/src/dist/scss/plugins/_bt-checkbox.scss b/src/dist/scss/plugins/_bt-checkbox.scss
new file mode 100644
index 00000000..324f610f
--- /dev/null
+++ b/src/dist/scss/plugins/_bt-checkbox.scss
@@ -0,0 +1,12 @@
+.query-builder.bt-checkbox-glyphicons {
+ .checkbox input[type='checkbox']:checked + label::after {
+ font-family: 'Glyphicons Halflings';
+ content: '\e013';
+ }
+
+ .checkbox label::after {
+ padding-left: 4px;
+ padding-top: 2px;
+ font-size: 9px;
+ }
+}
diff --git a/src/dist/scss/plugins/_bt-tooltip-errors.scss b/src/dist/scss/plugins/_bt-tooltip-errors.scss
new file mode 100644
index 00000000..21323e5f
--- /dev/null
+++ b/src/dist/scss/plugins/_bt-tooltip-errors.scss
@@ -0,0 +1,9 @@
+$error-tooltip-color: #F99;
+
+@if $theme-name == 'dark' {
+ $error-tooltip-color: #F22;
+}
+
+.query-builder .error-container + .tooltip .tooltip-inner {
+ color: $error-tooltip-color !important;
+}
diff --git a/src/dist/scss/plugins/_filter-description.scss b/src/dist/scss/plugins/_filter-description.scss
new file mode 100644
index 00000000..80a2af71
--- /dev/null
+++ b/src/dist/scss/plugins/_filter-description.scss
@@ -0,0 +1,21 @@
+$description-background-color: #D9EDF7;
+$description-border-color: #BCE8F1;
+$description-text-color: #31708F;
+
+@if $theme-name == 'dark' {
+ $description-background-color: rgba(0, 170, 255, .2);
+ $description-text-color: #AAD1E4;
+ $description-border-color: #346F7B;
+}
+
+$description-border: 1px solid $description-border-color;
+
+.query-builder p.filter-description {
+ margin: $rule-padding 0 0 0;
+ background: $description-background-color;
+ border: $description-border;
+ color: $description-text-color;
+ border-radius: $item-border-radius;
+ padding: #{$rule-padding / 2} $rule-padding;
+ font-size: .8em;
+}
diff --git a/src/dist/scss/plugins/_invert.scss b/src/dist/scss/plugins/_invert.scss
new file mode 100644
index 00000000..5eb0458b
--- /dev/null
+++ b/src/dist/scss/plugins/_invert.scss
@@ -0,0 +1,5 @@
+.query-builder {
+ .rules-group-header [data-invert] {
+ margin-left: 5px;
+ }
+}
diff --git a/src/dist/scss/plugins/_sortable.scss b/src/dist/scss/plugins/_sortable.scss
new file mode 100644
index 00000000..ac902fe1
--- /dev/null
+++ b/src/dist/scss/plugins/_sortable.scss
@@ -0,0 +1,28 @@
+$placeholder-border-color: #BBB;
+$placeholder-border: 1px dashed $placeholder-border-color;
+
+.query-builder {
+ .drag-handle {
+ @extend %rule-component;
+ cursor: move;
+ vertical-align: middle;
+ margin-left: 5px;
+ }
+
+ .dragging {
+ position: fixed;
+ opacity: .5;
+ z-index: 100;
+
+ &::before,
+ &::after {
+ display: none;
+ }
+ }
+
+ .rule-placeholder {
+ @extend %base-container;
+ border: $placeholder-border;
+ opacity: .7;
+ }
+}
diff --git a/src/dist/scss/plugins/bt-tooltip-errors.scss b/src/dist/scss/plugins/bt-tooltip-errors.scss
new file mode 100644
index 00000000..21323e5f
--- /dev/null
+++ b/src/dist/scss/plugins/bt-tooltip-errors.scss
@@ -0,0 +1,9 @@
+$error-tooltip-color: #F99;
+
+@if $theme-name == 'dark' {
+ $error-tooltip-color: #F22;
+}
+
+.query-builder .error-container + .tooltip .tooltip-inner {
+ color: $error-tooltip-color !important;
+}
diff --git a/src/dist/scss/plugins/filter-description.scss b/src/dist/scss/plugins/filter-description.scss
new file mode 100644
index 00000000..80a2af71
--- /dev/null
+++ b/src/dist/scss/plugins/filter-description.scss
@@ -0,0 +1,21 @@
+$description-background-color: #D9EDF7;
+$description-border-color: #BCE8F1;
+$description-text-color: #31708F;
+
+@if $theme-name == 'dark' {
+ $description-background-color: rgba(0, 170, 255, .2);
+ $description-text-color: #AAD1E4;
+ $description-border-color: #346F7B;
+}
+
+$description-border: 1px solid $description-border-color;
+
+.query-builder p.filter-description {
+ margin: $rule-padding 0 0 0;
+ background: $description-background-color;
+ border: $description-border;
+ color: $description-text-color;
+ border-radius: $item-border-radius;
+ padding: #{$rule-padding / 2} $rule-padding;
+ font-size: .8em;
+}
diff --git a/src/dist/scss/plugins/invert.scss b/src/dist/scss/plugins/invert.scss
new file mode 100644
index 00000000..5eb0458b
--- /dev/null
+++ b/src/dist/scss/plugins/invert.scss
@@ -0,0 +1,5 @@
+.query-builder {
+ .rules-group-header [data-invert] {
+ margin-left: 5px;
+ }
+}
diff --git a/src/dist/scss/plugins/sortable.scss b/src/dist/scss/plugins/sortable.scss
new file mode 100644
index 00000000..cf57c795
--- /dev/null
+++ b/src/dist/scss/plugins/sortable.scss
@@ -0,0 +1,27 @@
+$placeholder-border-color: #BBB;
+$placeholder-border: 1px dashed $placeholder-border-color;
+
+.query-builder {
+ .drag-handle {
+ @extend %rule-component;
+ cursor: move;
+ vertical-align: middle;
+ margin-left: 5px;
+ }
+
+ .dragging {
+ position: fixed;
+ opacity: .5;
+ z-index: 100;
+
+ &::before, &::after {
+ display: none;
+ }
+ }
+
+ .rule-placeholder {
+ @extend %base-container;
+ border: $placeholder-border;
+ opacity: .7;
+ }
+}
From f94ed889d2a9cb7a7ceb7da1e1b744f0162b7918 Mon Sep 17 00:00:00 2001
From: LochNess
Date: Fri, 26 Apr 2019 19:15:52 +0300
Subject: [PATCH 3/8] added active state checkbox
---
src/dist/css/query-builder.default.css | 158 +-
src/dist/i18n/query-builder.ru.js | 120 +-
src/dist/js/query-builder.standalone.js | 10364 +++++++++++-----------
src/query-builder.default.css | 176 -
src/query-builder.standalone.js | 6478 --------------
5 files changed, 5342 insertions(+), 11954 deletions(-)
delete mode 100644 src/query-builder.default.css
delete mode 100644 src/query-builder.standalone.js
diff --git a/src/dist/css/query-builder.default.css b/src/dist/css/query-builder.default.css
index 465fe2d6..9a3f9cab 100644
--- a/src/dist/css/query-builder.default.css
+++ b/src/dist/css/query-builder.default.css
@@ -4,170 +4,174 @@
* Licensed under MIT (https://opensource.org/licenses/MIT)
*/
.query-builder .rules-group-container, .query-builder .rule-container, .query-builder .rule-placeholder {
- position: relative;
- margin: 4px 0;
- border-radius: 5px;
- padding: 5px;
- border: 1px solid #EEE;
- background: rgba(255, 255, 255, 0.9);
+ position: relative;
+ margin: 4px 0;
+ border-radius: 5px;
+ padding: 5px;
+ border: 1px solid #EEE;
+ background: rgba(255, 255, 255, 0.9);
}
.query-builder .rule-container .rule-filter-container,
.query-builder .rule-container .rule-operator-container,
.query-builder .rule-container .rule-value-container, .query-builder .error-container, .query-builder .drag-handle {
- display: inline-block;
- margin: 0 5px 0 0;
- vertical-align: middle;
+ display: inline-block;
+ margin: 0 5px 0 0;
+ vertical-align: middle;
}
.query-builder .rules-group-container {
- padding: 10px;
- padding-bottom: 6px;
- border: 1px solid #DCC896;
- background: rgba(250, 240, 210, 0.5);
+ padding: 10px;
+ padding-bottom: 6px;
+ border: 1px solid #DCC896;
+ background: rgba(250, 240, 210, 0.5);
+}
+
+.query-builder .rules-group-container .rules-group-header .group-actions button{
+ margin-right: 5px;
}
.query-builder .rules-group-header {
- margin-bottom: 10px;
+ margin-bottom: 10px;
}
.query-builder .rules-group-header .group-conditions .btn.readonly:not(.active),
.query-builder .rules-group-header .group-conditions input[name$='_cond'] {
- border: 0;
- clip: rect(0 0 0 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- width: 1px;
- white-space: nowrap;
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+ white-space: nowrap;
}
.query-builder .rules-group-header .group-conditions .btn.readonly {
- border-radius: 3px;
+ border-radius: 3px;
}
.query-builder .rules-list {
- list-style: none;
- padding: 0 0 0 15px;
- margin: 0;
+ list-style: none;
+ padding: 0 0 0 15px;
+ margin: 0;
}
.query-builder .rule-value-container {
- border-left: 1px solid #DDD;
- padding-left: 5px;
+ border-left: 1px solid #DDD;
+ padding-left: 5px;
}
.query-builder .rule-value-container label {
- margin-bottom: 0;
- font-weight: normal;
+ margin-bottom: 0;
+ font-weight: normal;
}
.query-builder .rule-value-container label.block {
- display: block;
+ display: block;
}
.query-builder .rule-value-container select,
.query-builder .rule-value-container input[type='text'],
.query-builder .rule-value-container input[type='number'] {
- padding: 1px;
+ padding: 1px;
}
.query-builder .error-container {
- display: none;
- cursor: help;
- color: #F00;
+ display: none;
+ cursor: help;
+ color: #F00;
}
.query-builder .has-error {
- background-color: #FDD;
- border-color: #F99;
+ background-color: #FDD;
+ border-color: #F99;
}
.query-builder .has-error .error-container {
- display: inline-block !important;
+ display: inline-block !important;
}
.query-builder .rules-list > *::before, .query-builder .rules-list > *::after {
- content: '';
- position: absolute;
- left: -10px;
- width: 10px;
- height: calc(50% + 4px);
- border-color: #CCC;
- border-style: solid;
+ content: '';
+ position: absolute;
+ left: -10px;
+ width: 10px;
+ height: calc(50% + 4px);
+ border-color: #CCC;
+ border-style: solid;
}
.query-builder .rules-list > *::before {
- top: -4px;
- border-width: 0 0 2px 2px;
+ top: -4px;
+ border-width: 0 0 2px 2px;
}
.query-builder .rules-list > *::after {
- top: 50%;
- border-width: 0 0 0 2px;
+ top: 50%;
+ border-width: 0 0 0 2px;
}
.query-builder .rules-list > *:first-child::before {
- top: -12px;
- height: calc(50% + 14px);
+ top: -12px;
+ height: calc(50% + 14px);
}
.query-builder .rules-list > *:last-child::before {
- border-radius: 0 0 0 4px;
+ border-radius: 0 0 0 4px;
}
.query-builder .rules-list > *:last-child::after {
- display: none;
+ display: none;
}
.query-builder.bt-checkbox-glyphicons .checkbox input[type='checkbox']:checked + label::after {
- font-family: 'Glyphicons Halflings';
- content: '\e013';
+ font-family: 'Glyphicons Halflings';
+ content: '\e013';
}
.query-builder.bt-checkbox-glyphicons .checkbox label::after {
- padding-left: 4px;
- padding-top: 2px;
- font-size: 9px;
+ padding-left: 4px;
+ padding-top: 2px;
+ font-size: 9px;
}
.query-builder .error-container + .tooltip .tooltip-inner {
- color: #F99 !important;
+ color: #F99 !important;
}
.query-builder p.filter-description {
- margin: 5px 0 0 0;
- background: #D9EDF7;
- border: 1px solid #BCE8F1;
- color: #31708F;
- border-radius: 5px;
- padding: 2.5px 5px;
- font-size: .8em;
+ margin: 5px 0 0 0;
+ background: #D9EDF7;
+ border: 1px solid #BCE8F1;
+ color: #31708F;
+ border-radius: 5px;
+ padding: 2.5px 5px;
+ font-size: .8em;
}
.query-builder .rules-group-header [data-invert] {
- margin-left: 5px;
+ margin-left: 5px;
}
.query-builder .drag-handle {
- cursor: move;
- vertical-align: middle;
- margin-left: 5px;
+ cursor: move;
+ vertical-align: middle;
+ margin-left: 5px;
}
.query-builder .dragging {
- position: fixed;
- opacity: .5;
- z-index: 100;
+ position: fixed;
+ opacity: .5;
+ z-index: 100;
}
.query-builder .dragging::before, .query-builder .dragging::after {
- display: none;
+ display: none;
}
.query-builder .rule-placeholder {
- border: 1px dashed #BBB;
- opacity: .7;
+ border: 1px dashed #BBB;
+ opacity: .7;
}
diff --git a/src/dist/i18n/query-builder.ru.js b/src/dist/i18n/query-builder.ru.js
index e09665cd..b21101e2 100644
--- a/src/dist/i18n/query-builder.ru.js
+++ b/src/dist/i18n/query-builder.ru.js
@@ -12,66 +12,66 @@
factory(root.jQuery);
}
}(this, function($) {
-"use strict";
+ "use strict";
-var QueryBuilder = $.fn.queryBuilder;
+ var QueryBuilder = $.fn.queryBuilder;
-QueryBuilder.regional['ru'] = {
- "__locale": "Russian (ru)",
- "add_rule": "Добавить",
- "add_group": "Добавить группу",
- "delete_rule": "Удалить",
- "delete_group": "Удалить",
- "conditions": {
- "AND": "И",
- "OR": "ИЛИ"
- },
- "operators": {
- "equal": "равно",
- "not_equal": "не равно",
- "in": "из указанных",
- "not_in": "не из указанных",
- "less": "меньше",
- "less_or_equal": "меньше или равно",
- "greater": "больше",
- "greater_or_equal": "больше или равно",
- "between": "между",
- "begins_with": "начинается с",
- "not_begins_with": "не начинается с",
- "contains": "содержит",
- "not_contains": "не содержит",
- "ends_with": "оканчивается на",
- "not_ends_with": "не оканчивается на",
- "is_empty": "пустая строка",
- "is_not_empty": "не пустая строка",
- "is_null": "пусто",
- "is_not_null": "не пусто"
- },
- "errors": {
- "no_filter": "Фильтр не выбран",
- "empty_group": "Группа пуста",
- "radio_empty": "Не выбранно значение",
- "checkbox_empty": "Не выбранно значение",
- "select_empty": "Не выбранно значение",
- "string_empty": "Не заполненно",
- "string_exceed_min_length": "Должен содержать больше {0} символов",
- "string_exceed_max_length": "Должен содержать меньше {0} символов",
- "string_invalid_format": "Неверный формат ({0})",
- "number_nan": "Не число",
- "number_not_integer": "Не число",
- "number_not_double": "Не число",
- "number_exceed_min": "Должно быть больше {0}",
- "number_exceed_max": "Должно быть меньше, чем {0}",
- "number_wrong_step": "Должно быть кратно {0}",
- "datetime_empty": "Не заполненно",
- "datetime_invalid": "Неверный формат даты ({0})",
- "datetime_exceed_min": "Должно быть, после {0}",
- "datetime_exceed_max": "Должно быть, до {0}",
- "boolean_not_valid": "Не логическое",
- "operator_not_multiple": "Оператор \"{1}\" не поддерживает много значений"
- },
- "invert": "Инвертировать"
-};
+ QueryBuilder.regional['ru'] = {
+ "__locale": "Russian (ru)",
+ "add_rule": "Добавить",
+ "add_group": "Добавить группу",
+ "delete_rule": "Удалить",
+ "delete_group": "Удалить",
+ "conditions": {
+ "AND": "И",
+ "OR": "ИЛИ"
+ },
+ "operators": {
+ "equal": "(=) равно",
+ "not_equal": "(!=) не равно",
+ "in": "из указанных",
+ "not_in": "не из указанных",
+ "less": "(<) меньше",
+ "less_or_equal": "(≤) меньше или равно",
+ "greater": "(>) больше",
+ "greater_or_equal": "(≥) больше или равно",
+ "between": "между",
+ "begins_with": "начинается с",
+ "not_begins_with": "не начинается с",
+ "contains": "содержит",
+ "not_contains": "не содержит",
+ "ends_with": "оканчивается на",
+ "not_ends_with": "не оканчивается на",
+ "is_empty": "пустая строка",
+ "is_not_empty": "не пустая строка",
+ "is_null": "пусто",
+ "is_not_null": "не пусто"
+ },
+ "errors": {
+ "no_filter": "Фильтр не выбран",
+ "empty_group": "Группа пуста",
+ "radio_empty": "Не выбранно значение",
+ "checkbox_empty": "Не выбранно значение",
+ "select_empty": "Не выбранно значение",
+ "string_empty": "Не заполненно",
+ "string_exceed_min_length": "Должен содержать больше {0} символов",
+ "string_exceed_max_length": "Должен содержать меньше {0} символов",
+ "string_invalid_format": "Неверный формат ({0})",
+ "number_nan": "Не число",
+ "number_not_integer": "Не число",
+ "number_not_double": "Не число",
+ "number_exceed_min": "Должно быть больше {0}",
+ "number_exceed_max": "Должно быть меньше, чем {0}",
+ "number_wrong_step": "Должно быть кратно {0}",
+ "datetime_empty": "Не заполненно",
+ "datetime_invalid": "Неверный формат даты ({0})",
+ "datetime_exceed_min": "Должно быть, после {0}",
+ "datetime_exceed_max": "Должно быть, до {0}",
+ "boolean_not_valid": "Не логическое",
+ "operator_not_multiple": "Оператор \"{1}\" не поддерживает много значений"
+ },
+ "invert": "Инвертировать"
+ };
-QueryBuilder.defaults({ lang_code: 'ru' });
-}));
\ No newline at end of file
+ QueryBuilder.defaults({ lang_code: 'ru' });
+}));
diff --git a/src/dist/js/query-builder.standalone.js b/src/dist/js/query-builder.standalone.js
index c899f05b..18d4449b 100644
--- a/src/dist/js/query-builder.standalone.js
+++ b/src/dist/js/query-builder.standalone.js
@@ -3,7 +3,7 @@
*
* Copyright 2014-2016 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
*/
@@ -66,27 +66,27 @@
clone = target && $.isArray(target) ? target : [];
switch (arrayMode) {
- case 'concat':
- target = clone.concat($.extend(deep, [], options));
- break;
+ case 'concat':
+ target = clone.concat($.extend(deep, [], options));
+ break;
- case 'replace':
- target = $.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);
+ 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);
- }
- });
+ } else if (clone.indexOf(e) === -1) {
+ clone.push(e);
+ }
+ });
- target = clone;
- break;
+ target = clone;
+ break;
}
} else {
@@ -134,144 +134,144 @@
// 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 = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "/": "/" },
- 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+"": ">", '"': """, "'": "'", "/": "/" },
+ 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+"}
+ * @readonly
+ */
+ this.icons = this.settings.icons;
+
+ /**
+ * List of operators
+ * @member {QueryBuilder.Operator[]}
+ * @readonly
+ */
+ this.operators = this.settings.operators;
+
+ /**
+ * List of templates
+ * @member {object.}
+ * @readonly
+ */
+ this.templates = this.settings.templates;
+
+ /**
+ * Plugins configuration
+ * @member {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(' ');
+ }
+ });
+
/**
- * Internal model
- * @member {Model}
+ * Allowed types and their internal representation
+ * @type {object.}
* @readonly
+ * @private
*/
- this.model = new Model();
+ QueryBuilder.types = {
+ 'string': 'string',
+ 'integer': 'number',
+ 'double': 'number',
+ 'date': 'datetime',
+ 'time': 'datetime',
+ 'datetime': 'datetime',
+ 'boolean': 'boolean'
+ };
/**
- * 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
+ * Allowed inputs
+ * @type {string[]}
* @readonly
* @private
*/
- this.status = {
- id: null,
- generated_id: false,
- group_id: 0,
- rule_id: 0,
- has_optgroup: false,
- has_operator_optgroup: false
- };
+ QueryBuilder.inputs = [
+ 'text',
+ 'number',
+ 'textarea',
+ 'radio',
+ 'checkbox',
+ 'select'
+ ];
/**
- * List of filters
- * @member {QueryBuilder.Filter[]}
+ * Runtime modifiable options with `setOptions` method
+ * @type {string[]}
* @readonly
+ * @private
*/
- this.filters = this.settings.filters;
+ QueryBuilder.modifiable_options = [
+ 'display_errors',
+ 'allow_groups',
+ 'allow_empty',
+ 'default_condition',
+ 'default_filter'
+ ];
/**
- * List of icons
- * @member {object.}
+ * CSS selectors for common components
+ * @type {object.}
* @readonly
*/
- this.icons = this.settings.icons;
+ 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_]',
+ is_active: '.rule-is_active [name*=_is_active_]',
+
+ add_rule: '[data-add=rule]',
+ delete_rule: '[data-delete=rule]',
+ add_group: '[data-add=group]',
+ delete_group: '[data-delete=group]'
+ };
/**
- * List of operators
- * @member {QueryBuilder.Operator[]}
+ * Template strings (see template.js)
+ * @type {object.}
* @readonly
*/
- this.operators = this.settings.operators;
+ QueryBuilder.templates = {};
/**
- * List of templates
- * @member {object.}
+ * Localized strings (see i18n/)
+ * @type {object.}
* @readonly
*/
- this.templates = this.settings.templates;
+ QueryBuilder.regional = {};
/**
- * Plugins configuration
- * @member {object.}
+ * Default operators
+ * @type {object.}
* @readonly
*/
- this.plugins = this.settings.plugins;
+ 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'] }
+ };
/**
- * Translations object
- * @member {object}
+ * Default configuration
+ * @type {object}
* @readonly
*/
- this.lang = null;
+ 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: true,
+ default_filter: null,
+ optgroups: {},
+
+ default_rule_flags: {
+ filter_readonly: false,
+ operator_readonly: false,
+ value_readonly: false,
+ no_delete: false,
+ is_active: true
+ },
- // 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);
+ default_group_flags: {
+ condition_readonly: false,
+ no_add_rule: false,
+ no_add_group: false,
+ no_delete: false
+ },
- // "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;
- }
+ templates: {
+ group: null,
+ rule: null,
+ filterSelect: null,
+ operatorSelect: null,
+ ruleValueSelect: null
+ },
- // init templates
- Object.keys(this.templates).forEach(function(tpl) {
- if (!this.templates[tpl]) {
- this.templates[tpl] = QueryBuilder.templates[tpl];
+ 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'
}
- 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}
+ * @module plugins
*/
- 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 {*}
+ * Definition of available plugins
+ * @type {object.}
*/
- 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;
- },
+ QueryBuilder.plugins = {};
/**
- * Attaches an event listener on the builder container
- * @param {string} type
- * @param {function} cb
- * @returns {QueryBuilder}
+ * Gets or extends the default configuration
+ * @param {object} [options] - new configuration
+ * @returns {undefined|object} nothing or configuration object (copy)
*/
- on: function(type, cb) {
- this.$el.on(this._tojQueryEvent(type), cb);
- return this;
- },
+ 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);
+ }
+ };
/**
- * Removes an event listener from the builder container
- * @param {string} type
- * @param {function} [cb]
- * @returns {QueryBuilder}
+ * Registers a new plugin
+ * @param {string} name
+ * @param {function} fct - init function
+ * @param {object} [def] - default options
*/
- off: function(type, cb) {
- this.$el.off(this._tojQueryEvent(type), cb);
- return this;
- },
+ QueryBuilder.define = function(name, fct, def) {
+ QueryBuilder.plugins[name] = {
+ fct: fct,
+ def: def || {}
+ };
+ };
/**
- * Attaches an event listener called once on the builder container
- * @param {string} type
- * @param {function} cb
- * @returns {QueryBuilder}
+ * Adds new methods to QueryBuilder prototype
+ * @param {object.} methods
*/
- once: function(type, cb) {
- this.$el.one(this._tojQueryEvent(type), cb);
- return this;
- },
+ QueryBuilder.extend = function(methods) {
+ $.extend(QueryBuilder.prototype, methods);
+ };
/**
- * Appends `.queryBuilder` and optionally `.filter` to the events names
- * @param {string} name
- * @param {boolean} [filter=false]
- * @returns {string}
+ * Initializes plugins for an instance
+ * @throws ConfigError
* @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.}
- * @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.}
- * @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.}
- * @readonly
- */
-QueryBuilder.templates = {};
-
-/**
- * Localized strings (see i18n/)
- * @type {object.}
- * @readonly
- */
-QueryBuilder.regional = {};
+ QueryBuilder.prototype.initPlugins = function() {
+ if (!this.plugins) {
+ return;
+ }
-/**
- * Default operators
- * @type {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: true,
- 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'
- }
-};
+ 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] || {}
+ );
-/**
- * @module plugins
- */
+ QueryBuilder.plugins[plugin].fct.call(this, this.plugins[plugin]);
+ }
+ else {
+ Utils.error('Config', 'Unable to find plugin "{0}"', plugin);
+ }
+ }, this);
+ };
-/**
- * Definition of available plugins
- * @type {object.}
- */
-QueryBuilder.plugins = {};
+ /**
+ * 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;
+ }
-/**
- * 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]);
+ if (plugin) {
+ if (property) {
+ return plugin[property];
+ }
+ else {
+ return plugin;
+ }
}
else {
- return QueryBuilder.DEFAULTS[options];
+ Utils.error('Config', 'Unable to find plugin "{0}"', name);
}
- }
- 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.} 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] || {}
- );
+ /**
+ * 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');
- QueryBuilder.plugins[plugin].fct.call(this, this.plugins[plugin]);
+ if (rules) {
+ this.setRules(rules);
+ delete this.settings.rules;
}
else {
- Utils.error('Config', 'Unable to find plugin "{0}"', plugin);
+ this.setRoot(true);
}
- }, 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;
- }
+ /**
+ * Checks the configuration of each filter
+ * @param {QueryBuilder.Filter[]} filters
+ * @returns {QueryBuilder.Filter[]}
+ * @throws ConfigError
+ */
+ QueryBuilder.prototype.checkFilters = function(filters) {
+ var definedFilters = [];
- if (plugin) {
- if (property) {
- return plugin[property];
+ if (!filters || filters.length === 0) {
+ Utils.error('Config', 'Missing filters list');
}
- 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');
+ 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 (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.type) {
+ filter.type = 'string';
+ }
+ else if (!QueryBuilder.types[filter.type]) {
+ Utils.error('Config', 'Invalid type "{0}"', filter.type);
+ }
- 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.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.field) {
- filter.field = filter.id;
- }
- if (!filter.label) {
- filter.label = filter.field;
- }
+ 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.optgroup) {
- filter.optgroup = null;
- }
- else {
- this.status.has_optgroup = true;
+ if (!filter.field) {
+ filter.field = filter.id;
+ }
+ if (!filter.label) {
+ filter.label = filter.field;
+ }
- // register optgroup if needed
- if (!this.settings.optgroups[filter.optgroup]) {
- this.settings.optgroups[filter.optgroup] = filter.optgroup;
+ if (!filter.optgroup) {
+ filter.optgroup = null;
}
- }
+ else {
+ this.status.has_optgroup = true;
- switch (filter.input) {
- case 'radio':
- case 'checkbox':
- if (!filter.values || filter.values.length < 1) {
- Utils.error('Config', 'Missing filter "{0}" values', filter.id);
+ // register optgroup if needed
+ if (!this.settings.optgroups[filter.optgroup]) {
+ this.settings.optgroups[filter.optgroup] = filter.optgroup;
}
- break;
+ }
- case 'select':
- var cleanValues = [];
- filter.has_optgroup = false;
+ switch (filter.input) {
+ case 'radio':
+ case 'checkbox':
+ if (!filter.values || filter.values.length < 1) {
+ Utils.error('Config', 'Missing filter "{0}" values', filter.id);
+ }
+ break;
- Utils.iterateOptions(filter.values, function(value, label, optgroup) {
- cleanValues.push({
- value: value,
- label: label,
- optgroup: optgroup || null
- });
+ 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;
+ if (optgroup) {
+ filter.has_optgroup = true;
- // register optgroup if needed
- if (!this.settings.optgroups[optgroup]) {
- this.settings.optgroups[optgroup] = optgroup;
+ // 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;
}
- }.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;
+ }
- 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);
- 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;
+ 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));
+ });
+ }
}
- }, 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');
}
- }
- if (this.status.has_optgroup) {
- filters = Utils.groupSort(filters, 'optgroup');
- }
+ return filters;
+ };
- return filters;
-};
+ /**
+ * Checks the configuration of each operator
+ * @param {QueryBuilder.Operator[]} operators
+ * @returns {QueryBuilder.Operator[]}
+ * @throws ConfigError
+ */
+ QueryBuilder.prototype.checkOperators = function(operators) {
+ var definedOperators = [];
-/**
- * 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.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);
+ }
- 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 (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 (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 (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;
+ if (!operator.optgroup) {
+ operator.optgroup = null;
}
- }
- }, this);
-
- if (this.status.has_operator_optgroup) {
- operators = Utils.groupSort(operators, 'optgroup');
- }
-
- return operators;
-};
+ else {
+ this.status.has_operator_optgroup = true;
-/**
- * Adds all events listeners to the builder
- * @private
- */
-QueryBuilder.prototype.bindEvents = function() {
- var self = this;
- var Selectors = QueryBuilder.selectors;
+ // register optgroup if needed
+ if (!this.settings.optgroups[operator.optgroup]) {
+ this.settings.optgroups[operator.optgroup] = operator.optgroup;
+ }
+ }
+ }, this);
- // 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();
+ if (this.status.has_operator_optgroup) {
+ operators = Utils.groupSort(operators, 'optgroup');
}
- });
- // 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());
- });
+ return operators;
+ };
- // add rule button
- this.$el.on('click.queryBuilder', Selectors.add_rule, function() {
- var $group = $(this).closest(Selectors.group_container);
- self.addRule(self.getModel($group));
- });
+ /**
+ * Adds all events listeners to the builder
+ * @private
+ */
+ QueryBuilder.prototype.bindEvents = function() {
+ var self = this;
+ var Selectors = QueryBuilder.selectors;
- // delete rule button
- this.$el.on('click.queryBuilder', Selectors.delete_rule, function() {
- var $rule = $(this).closest(Selectors.rule_container);
- self.deleteRule(self.getModel($rule));
- });
+ // 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();
+ }
+ });
- 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));
+ // 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());
});
- // delete group button
- this.$el.on('click.queryBuilder', Selectors.delete_group, function() {
- var $group = $(this).closest(Selectors.group_container);
- self.deleteGroup(self.getModel($group));
+ // 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());
});
- }
- // 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();
+ // is_active operator change
+ this.$el.on('click.queryBuilder', '.rule-is_active', function() {
+ var $rule = $(this).closest(Selectors.rule_container);
+ self.getModel($rule).flags.is_active = $(this).prop('checked');
+ });
- 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;
+ // add rule button
+ this.$el.on('click.queryBuilder', Selectors.add_rule, function() {
+ var $group = $(this).closest(Selectors.group_container);
+ self.addRule(self.getModel($group));
+ });
- case 'flags':
- self.applyRuleFlags(node);
- break;
+ // delete rule button
+ this.$el.on('click.queryBuilder', Selectors.delete_rule, function() {
+ var $rule = $(this).closest(Selectors.rule_container);
+ self.deleteRule(self.getModel($rule));
+ });
- case 'filter':
- self.updateRuleFilter(node, oldValue);
- break;
+ 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));
+ });
- case 'operator':
- self.updateRuleOperator(node, oldValue);
- break;
+ // delete group button
+ this.$el.on('click.queryBuilder', Selectors.delete_group, function() {
+ var $group = $(this).closest(Selectors.group_container);
+ self.deleteGroup(self.getModel($group));
+ });
+ }
- case 'value':
- self.updateRuleValue(node, oldValue);
- break;
+ // 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 {
- switch (field) {
- case 'error':
- self.updateError(node);
- break;
-
- case 'flags':
- self.applyGroupFlags(node);
- break;
+ else {
+ node.$el.insertAfter(parent.rules[index - 1].$el);
+ }
+ self.refreshGroupsConditions();
+ },
+ 'move': function(e, node, group, index) {
+ node.$el.detach();
- case 'condition':
- self.updateGroupCondition(node, oldValue);
- break;
+ if (index === 0) {
+ node.$el.prependTo(group.$el.find('>' + QueryBuilder.selectors.rules_list));
}
- }
- }
- });
-};
-
-/**
- * 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);
+ 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;
- var group_id = this.nextGroupId();
- var $group = $(this.getGroupTemplate(group_id, 1));
+ case 'flags':
+ self.applyRuleFlags(node);
+ break;
- this.$el.append($group);
- this.model.root = new Group(null, $group);
- this.model.root.model = this.model;
+ case 'filter':
+ self.updateRuleFilter(node, oldValue);
+ break;
- this.model.root.data = data;
- this.model.root.flags = $.extend({}, this.settings.default_group_flags, flags);
- this.model.root.condition = this.settings.default_condition;
+ case 'operator':
+ self.updateRuleOperator(node, oldValue);
+ break;
- this.trigger('afterAddGroup', this.model.root);
+ case 'value':
+ self.updateRuleValue(node, oldValue);
+ break;
- if (addRule) {
- this.addRule(this.model.root);
- }
+ case 'is_active':
+ self.updateIsActive(node, oldValue);
+ break;
+ }
+ }
+ else {
+ switch (field) {
+ case 'error':
+ self.updateError(node);
+ break;
- 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);
+ case 'flags':
+ self.applyGroupFlags(node);
+ break;
- var level = parent.level + 1;
+ case 'condition':
+ self.updateGroupCondition(node, oldValue);
+ break;
+ }
+ }
+ }
+ });
+ };
/**
- * 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
+ * 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
*/
- 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);
+ QueryBuilder.prototype.setRoot = function(addRule, data, flags) {
+ addRule = (addRule === undefined || addRule === true);
- model.data = data;
- model.flags = $.extend({}, this.settings.default_group_flags, flags);
- model.condition = this.settings.default_condition;
+ var group_id = this.nextGroupId();
+ var $group = $(this.getGroupTemplate(group_id, 1));
- /**
- * Just after adding a group
- * @event afterAddGroup
- * @memberof QueryBuilder
- * @param {Group} group
- */
- this.trigger('afterAddGroup', model);
+ this.$el.append($group);
+ this.model.root = new Group(null, $group);
+ this.model.root.model = this.model;
- /**
- * After any change in the rules
- * @event rulesChanged
- * @memberof QueryBuilder
- */
- this.trigger('rulesChanged');
+ this.model.root.data = data;
+ this.model.root.flags = $.extend({}, this.settings.default_group_flags, flags);
+ this.model.root.condition = this.settings.default_condition;
- if (addRule) {
- this.addRule(model);
- }
+ this.trigger('afterAddGroup', this.model.root);
- return model;
-};
+ if (addRule) {
+ this.addRule(this.model.root);
+ }
-/**
- * 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;
- }
+ return this.model.root;
+ };
/**
- * Just before deleting a group, can be prevented
- * @event beforeDeleteGroup
- * @memberof QueryBuilder
+ * 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
*/
- var e = this.trigger('beforeDeleteGroup', group);
- if (e.isDefaultPrevented()) {
- return false;
- }
+ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) {
+ addRule = (addRule === undefined || addRule === true);
+
+ var level = parent.level + 1;
- var del = true;
+ /**
+ * 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;
+ }
- group.each('reverse', function(rule) {
- del &= this.deleteRule(rule);
- }, function(group) {
- del &= this.deleteGroup(group);
- }, this);
+ var group_id = this.nextGroupId();
+ var $group = $(this.getGroupTemplate(group_id, level));
+ var model = parent.addGroup($group);
- if (del) {
- group.drop();
+ model.data = data;
+ model.flags = $.extend({}, this.settings.default_group_flags, flags);
+ model.condition = this.settings.default_condition;
/**
- * Just after deleting a group
- * @event afterDeleteGroup
+ * Just after adding a group
+ * @event afterAddGroup
* @memberof QueryBuilder
+ * @param {Group} group
*/
- this.trigger('afterDeleteGroup');
+ this.trigger('afterAddGroup', model);
+ /**
+ * After any change in the rules
+ * @event rulesChanged
+ * @memberof QueryBuilder
+ */
this.trigger('rulesChanged');
- }
- return del;
-};
+ if (addRule) {
+ this.addRule(model);
+ }
-/**
- * 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);
- });
+ return model;
+ };
/**
- * After the group condition has been modified
- * @event afterUpdateGroupCondition
- * @memberof QueryBuilder
+ * Tries to delete a group. The group is not deleted if at least one rule is flagged `no_delete`.
* @param {Group} group
- * @param {object} previousCondition
+ * @returns {boolean} if the group has been deleted
+ * @fires QueryBuilder.beforeDeleteGroup
+ * @fires QueryBuilder.afterDeleteGroup
*/
- this.trigger('afterUpdateGroupCondition', group, previousCondition);
-
- this.trigger('rulesChanged');
-};
+ QueryBuilder.prototype.deleteGroup = function(group) {
+ if (group.isRoot()) {
+ return false;
+ }
-/**
- * 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);
+ /**
+ * 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;
}
- group.each(null, function(group) {
- walk(group);
+ var del = true;
+
+ group.each('reverse', function(rule) {
+ del &= this.deleteRule(rule);
+ }, function(group) {
+ del &= this.deleteGroup(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 = $(this.getRuleTemplate(rule_id));
- var model = parent.addRule($rule);
+ if (del) {
+ group.drop();
- model.data = data;
- model.flags = $.extend({}, this.settings.default_rule_flags, flags);
+ /**
+ * Just after deleting a group
+ * @event afterDeleteGroup
+ * @memberof QueryBuilder
+ */
+ this.trigger('afterDeleteGroup');
- /**
- * Just after adding a rule
- * @event afterAddRule
- * @memberof QueryBuilder
- * @param {Rule} rule
- */
- this.trigger('afterAddRule', model);
+ this.trigger('rulesChanged');
+ }
- this.trigger('rulesChanged');
+ return del;
+ };
- this.createRuleFilters(model);
+ /**
+ * 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);
+ });
- if (this.settings.default_filter || !this.settings.display_empty_filter) {
/**
- * Modifies the default filter for a rule
- * @event changer:getDefaultFilter
+ * After the group condition has been modified
+ * @event afterUpdateGroupCondition
* @memberof QueryBuilder
- * @param {QueryBuilder.Filter} filter
- * @param {Rule} rule
- * @returns {QueryBuilder.Filter}
+ * @param {Group} group
+ * @param {object} previousCondition
*/
- model.filter = this.change('getDefaultFilter',
- this.getFilterById(this.settings.default_filter || this.filters[0].id),
- model
- );
- }
+ this.trigger('afterUpdateGroupCondition', group, previousCondition);
- return model;
-};
+ this.trigger('rulesChanged');
+ };
-/**
- * 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;
- }
+ /**
+ * 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));
+ };
/**
- * Just before deleting a rule, can be prevented
- * @event beforeDeleteRule
- * @memberof QueryBuilder
- * @param {Rule} rule
+ * 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
*/
- var e = this.trigger('beforeDeleteRule', rule);
- if (e.isDefaultPrevented()) {
- return false;
- }
+ 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 = $(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
+ );
+ }
- rule.drop();
+ return model;
+ };
/**
- * Just after deleting a rule
- * @event afterDeleteRule
- * @memberof QueryBuilder
+ * Tries to delete a rule
+ * @param {Rule} rule
+ * @returns {boolean} if the rule has been deleted
+ * @fires QueryBuilder.beforeDeleteRule
+ * @fires QueryBuilder.afterDeleteRule
*/
- this.trigger('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;
+ }
- this.trigger('rulesChanged');
+ rule.drop();
- return true;
-};
+ /**
+ * 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
+ * Creates the filters for a rule
* @param {Rule} rule
- * @returns {QueryBuilder.Filter[]}
+ * @fires QueryBuilder.changer:getRuleFilters
+ * @fires QueryBuilder.afterCreateRuleFilters
+ * @private
*/
- var filters = this.change('getRuleFilters', this.filters, rule);
- var $filterSelect = $(this.getRuleFilterSelect(rule, filters));
+ 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 = $(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);
- rule.$el.find(QueryBuilder.selectors.filter_container).html($filterSelect);
+ this.applyRuleFlags(rule);
+ };
/**
- * After creating the dropdown for filters
- * @event afterCreateRuleFilters
- * @memberof QueryBuilder
+ * Creates the operators for a rule and init the rule operator
* @param {Rule} rule
+ * @fires QueryBuilder.afterCreateRuleOperators
+ * @private
*/
- this.trigger('afterCreateRuleFilters', rule);
+ QueryBuilder.prototype.createRuleOperators = function(rule) {
+ var $operatorContainer = rule.$el.find(QueryBuilder.selectors.operator_container).empty();
- this.applyRuleFlags(rule);
-};
+ if (!rule.filter) {
+ return;
+ }
-/**
- * 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();
+ var operators = this.getOperators(rule.filter);
+ var $operatorSelect = $(this.getRuleOperatorSelect(rule, operators));
- if (!rule.filter) {
- return;
- }
+ $operatorContainer.html($operatorSelect);
- var operators = this.getOperators(rule.filter);
- var $operatorSelect = $(this.getRuleOperatorSelect(rule, operators));
+ // 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];
+ }
- $operatorContainer.html($operatorSelect);
+ rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
- // 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];
- }
+ /**
+ * 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);
- rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
+ this.applyRuleFlags(rule);
+ };
/**
- * After creating the dropdown for operators
- * @event afterCreateRuleOperators
- * @memberof QueryBuilder
+ * Creates the main input for a rule
* @param {Rule} rule
- * @param {QueryBuilder.Operator[]} operators - allowed operators for this rule
+ * @fires QueryBuilder.afterCreateRuleInput
+ * @private
*/
- this.trigger('afterCreateRuleOperators', rule, operators);
+ QueryBuilder.prototype.createRuleInput = function(rule) {
+ var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container).empty();
- this.applyRuleFlags(rule);
-};
+ rule.__.value = undefined;
-/**
- * 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();
+ if (!rule.filter || !rule.operator || rule.operator.nb_inputs === 0) {
+ return;
+ }
- rule.__.value = undefined;
+ var self = this;
+ var $inputs = $();
+ var filter = rule.filter;
- if (!rule.filter || !rule.operator || rule.operator.nb_inputs === 0) {
- return;
- }
+ for (var i = 0; i < rule.operator.nb_inputs; i++) {
+ var $ruleInput = $(this.getRuleInput(rule, i));
+ if (i > 0) $valueContainer.append(this.settings.inputs_separator);
+ $valueContainer.append($ruleInput);
+ $inputs = $inputs.add($ruleInput);
+ }
- var self = this;
- var $inputs = $();
- var filter = rule.filter;
+ $valueContainer.css('display', '');
- for (var i = 0; i < rule.operator.nb_inputs; i++) {
- var $ruleInput = $(this.getRuleInput(rule, i));
- if (i > 0) $valueContainer.append(this.settings.inputs_separator);
- $valueContainer.append($ruleInput);
- $inputs = $inputs.add($ruleInput);
- }
+ $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 || {});
+ }
- $valueContainer.css('display', '');
+ /**
+ * After creating the input for a rule and initializing optional plugin
+ * @event afterCreateRuleInput
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ */
+ this.trigger('afterCreateRuleInput', rule);
- $inputs.on('change ' + (filter.input_event || ''), function() {
- if (!rule._updating_input) {
+ if (filter.default_value !== undefined) {
+ rule.value = filter.default_value;
+ }
+ else {
rule._updating_value = true;
rule.value = self.getRuleInputValue(rule);
rule._updating_value = false;
}
- });
- if (filter.plugin) {
- $inputs[filter.plugin](filter.plugin_config || {});
- }
+ this.applyRuleFlags(rule);
+ };
/**
- * After creating the input for a rule and initializing optional plugin
- * @event afterCreateRuleInput
- * @memberof QueryBuilder
+ * Performs action when a rule's filter changes
* @param {Rule} rule
+ * @param {object} previousFilter
+ * @fires QueryBuilder.afterUpdateRuleFilter
+ * @private
*/
- 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;
- }
+ QueryBuilder.prototype.updateRuleFilter = function(rule, previousFilter) {
+ this.createRuleOperators(rule);
+ this.createRuleInput(rule);
- this.applyRuleFlags(rule);
-};
+ rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
-/**
- * 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);
+ // clear rule data if the filter changed
+ if (previousFilter && rule.filter && previousFilter.id !== rule.filter.id) {
+ rule.data = undefined;
+ }
- rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
+ /**
+ * 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);
- // clear rule data if the filter changed
- if (previousFilter && rule.filter && previousFilter.id !== rule.filter.id) {
- rule.data = undefined;
- }
+ this.trigger('rulesChanged');
+ };
/**
- * After the filter has been updated and the operators and input re-created
- * @event afterUpdateRuleFilter
- * @memberof QueryBuilder
+ * Performs actions when a rule's operator changes
* @param {Rule} rule
- * @param {object} previousFilter
+ * @param {object} previousOperator
+ * @fires QueryBuilder.afterUpdateRuleOperator
+ * @private
*/
- this.trigger('afterUpdateRuleFilter', rule, previousFilter);
+ QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) {
+ var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container);
- this.trigger('rulesChanged');
-};
+ if (!rule.operator || rule.operator.nb_inputs === 0) {
+ $valueContainer.hide();
-/**
- * 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);
+ rule.__.value = undefined;
+ }
+ else {
+ $valueContainer.css('display', '');
- if (!rule.operator || rule.operator.nb_inputs === 0) {
- $valueContainer.hide();
+ if ($valueContainer.is(':empty') || !previousOperator ||
+ rule.operator.nb_inputs !== previousOperator.nb_inputs ||
+ rule.operator.optgroup !== previousOperator.optgroup
+ ) {
+ this.createRuleInput(rule);
+ }
+ }
- rule.__.value = undefined;
- }
- else {
- $valueContainer.css('display', '');
+ if (rule.operator) {
+ rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
- if ($valueContainer.is(':empty') || !previousOperator ||
- rule.operator.nb_inputs !== previousOperator.nb_inputs ||
- rule.operator.optgroup !== previousOperator.optgroup
- ) {
- this.createRuleInput(rule);
+ // refresh value if the format changed for this operator
+ rule.__.value = this.getRuleInputValue(rule);
}
- }
- if (rule.operator) {
- rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
+ /**
+ * 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);
- // refresh value if the format changed for this operator
- rule.__.value = this.getRuleInputValue(rule);
- }
+ this.trigger('rulesChanged');
+ };
/**
- * After the operator has been updated and the input optionally re-created
- * @event afterUpdateRuleOperator
- * @memberof QueryBuilder
+ * Performs actions when rule's value changes
* @param {Rule} rule
- * @param {object} previousOperator
+ * @param {object} previousValue
+ * @fires QueryBuilder.afterUpdateRuleValue
+ * @private
*/
- this.trigger('afterUpdateRuleOperator', rule, previousOperator);
-
- this.trigger('rulesChanged');
-};
+ QueryBuilder.prototype.updateRuleValue = function(rule, previousValue) {
+ if (!rule._updating_value) {
+ this.setRuleInputValue(rule, rule.value);
+ }
-/**
- * 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');
+ };
/**
- * After the rule value has been modified
- * @event afterUpdateRuleValue
- * @memberof QueryBuilder
+ * Performs actions when rule's value changes
* @param {Rule} rule
- * @param {*} previousValue
+ * @param {object} previousValue
+ * @fires QueryBuilder.afterUpdateRuleValue
+ * @private
*/
- 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;
+ QueryBuilder.prototype.updateIsActive = function(rule, previousValue) {
+ if (!rule._is_Active) {
+ //this.setRuleInputValue(rule, rule.value);
+ }
- 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);
+ /**
+ * After the rule value has been modified
+ * @event afterUpdateRuleValue
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ * @param {*} previousValue
+ */
+ this.trigger('afterUpdateRuleValue', rule, previousValue);
- if (flags.no_delete) {
- rule.$el.find(Selectors.delete_rule).remove();
- }
+ this.trigger('rulesChanged');
+ };
/**
- * After rule's flags has been applied
- * @event afterApplyRuleFlags
- * @memberof QueryBuilder
+ * Changes a rule's properties depending on its flags
* @param {Rule} rule
+ * @fires QueryBuilder.afterApplyRuleFlags
+ * @private
*/
- this.trigger('afterApplyRuleFlags', rule);
-};
+ QueryBuilder.prototype.applyRuleFlags = function(rule) {
+ var flags = rule.flags;
+ var Selectors = QueryBuilder.selectors;
-/**
- * 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;
+ 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);
+ rule.$el.find(Selectors.is_active).prop('checked', flags.is_active);
- group.$el.find('>' + Selectors.group_condition).prop('disabled', flags.condition_readonly)
- .parent().toggleClass('readonly', flags.condition_readonly);
+ if (flags.no_delete) {
+ rule.$el.find(Selectors.delete_rule).remove();
+ }
- 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 rule's flags has been applied
+ * @event afterApplyRuleFlags
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ */
+ this.trigger('afterApplyRuleFlags', rule);
+ };
/**
- * After group's flags has been applied
- * @event afterApplyGroupFlags
- * @memberof QueryBuilder
+ * Changes group's properties depending on its flags
* @param {Group} group
+ * @fires QueryBuilder.afterApplyGroupFlags
+ * @private
*/
- this.trigger('afterApplyGroupFlags', group);
-};
+ QueryBuilder.prototype.applyGroupFlags = function(group) {
+ var flags = group.flags;
+ var Selectors = QueryBuilder.selectors;
-/**
- * Clears all errors markers
- * @param {Node} [node] default is root Group
- */
-QueryBuilder.prototype.clearErrors = function(node) {
- node = node || this.model.root;
+ group.$el.find('>' + Selectors.group_condition).prop('disabled', flags.condition_readonly)
+ .parent().toggleClass('readonly', flags.condition_readonly);
- if (!node) {
- return;
- }
+ 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();
+ }
- node.error = null;
+ /**
+ * After group's flags has been applied
+ * @event afterApplyGroupFlags
+ * @memberof QueryBuilder
+ * @param {Group} group
+ */
+ this.trigger('afterApplyGroupFlags', group);
+ };
- if (node instanceof Group) {
- node.each(function(rule) {
- rule.error = null;
- }, function(group) {
- this.clearErrors(group);
- }, this);
- }
-};
+ /**
+ * Clears all errors markers
+ * @param {Node} [node] default is root Group
+ */
+ QueryBuilder.prototype.clearErrors = function(node) {
+ node = node || this.model.root;
-/**
- * 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');
+ if (!node) {
+ return;
}
- 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.error = null;
- node.$el.addClass('has-error')
- .find(QueryBuilder.selectors.error_container).eq(0)
- .attr('title', errorMessage);
+ if (node instanceof Group) {
+ node.each(function(rule) {
+ rule.error = null;
+ }, function(group) {
+ this.clearErrors(group);
+ }, this);
}
- }
-};
-
-/**
- * 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
+ * Adds/Removes error on a Rule or Group
* @param {Node} node
- * @param {string} error
- * @param {*} value
+ * @fires QueryBuilder.changer:displayError
+ * @private
*/
- var e = this.trigger('validationError', node, error, value);
- if (!e.isDefaultPrevented()) {
- node.error = error;
- }
-};
+ 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);
+ }
+ }
+ };
-/**
- * Destroys the builder
- * @fires QueryBuilder.beforeDestroy
- */
-QueryBuilder.prototype.destroy = function() {
/**
- * Before the {@link QueryBuilder#destroy} method
- * @event beforeDestroy
- * @memberof QueryBuilder
+ * Triggers a validation error event
+ * @param {Node} node
+ * @param {string|array} error
+ * @param {*} value
+ * @fires QueryBuilder.validationError
+ * @private
*/
- this.trigger('beforeDestroy');
-
- if (this.status.generated_id) {
- this.$el.removeAttr('id');
- }
-
- this.clear();
- this.model = null;
+ QueryBuilder.prototype.triggerValidationError = function(node, error, value) {
+ if (!$.isArray(error)) {
+ error = [error];
+ }
- this.$el
- .off('.queryBuilder')
- .removeClass('query-builder')
- .removeData('queryBuilder');
+ /**
+ * 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;
+ }
+ };
- 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
+ * Destroys the builder
+ * @fires QueryBuilder.beforeDestroy
*/
- var e = this.trigger('beforeReset');
- if (e.isDefaultPrevented()) {
- return;
- }
+ QueryBuilder.prototype.destroy = function() {
+ /**
+ * Before the {@link QueryBuilder#destroy} method
+ * @event beforeDestroy
+ * @memberof QueryBuilder
+ */
+ this.trigger('beforeDestroy');
- this.status.group_id = 1;
- this.status.rule_id = 0;
+ if (this.status.generated_id) {
+ this.$el.removeAttr('id');
+ }
- this.model.root.empty();
+ this.clear();
+ this.model = null;
- this.model.root.data = undefined;
- this.model.root.flags = $.extend({}, this.settings.default_group_flags);
- this.model.root.condition = this.settings.default_condition;
+ this.$el
+ .off('.queryBuilder')
+ .removeClass('query-builder')
+ .removeData('queryBuilder');
- this.addRule(this.model.root);
+ delete this.$el[0].queryBuilder;
+ };
/**
- * After the {@link QueryBuilder#reset} method
- * @event afterReset
- * @memberof QueryBuilder
+ * Clear all rules and resets the root group
+ * @fires QueryBuilder.beforeReset
+ * @fires QueryBuilder.afterReset
*/
- this.trigger('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.trigger('rulesChanged');
-};
+ this.status.group_id = 1;
+ this.status.rule_id = 0;
-/**
- * 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.model.root.empty();
- this.status.group_id = 0;
- this.status.rule_id = 0;
+ this.model.root.data = undefined;
+ this.model.root.flags = $.extend({}, this.settings.default_group_flags);
+ this.model.root.condition = this.settings.default_condition;
- if (this.model.root) {
- this.model.root.drop();
- this.model.root = null;
- }
+ this.addRule(this.model.root);
+
+ /**
+ * After the {@link QueryBuilder#reset} method
+ * @event afterReset
+ * @memberof QueryBuilder
+ */
+ this.trigger('afterReset');
+
+ this.trigger('rulesChanged');
+ };
/**
- * After the {@link QueryBuilder#clear} method
- * @event afterClear
- * @memberof QueryBuilder
+ * Clears all rules and removes the root group
+ * @fires QueryBuilder.beforeClear
+ * @fires QueryBuilder.afterClear
*/
- this.trigger('afterClear');
-
- this.trigger('rulesChanged');
-};
+ 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;
+ }
-/**
- * Modifies the builder configuration.
- * 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.status.group_id = 0;
+ this.status.rule_id = 0;
- this.clearErrors();
+ if (this.model.root) {
+ this.model.root.drop();
+ this.model.root = null;
+ }
- var self = this;
+ /**
+ * After the {@link QueryBuilder#clear} method
+ * @event afterClear
+ * @memberof QueryBuilder
+ */
+ this.trigger('afterClear');
- var valid = (function parse(group) {
- var done = 0;
- var errors = 0;
+ this.trigger('rulesChanged');
+ };
- group.each(function(rule) {
- if (!rule.filter && options.skip_empty) {
- return;
+ /**
+ * Modifies the builder configuration.
+ * 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));
+ };
- if (!rule.filter) {
- self.triggerValidationError(rule, 'no_filter', null);
- errors++;
- return;
- }
+ /**
+ * 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');
+ }
+ };
- if (!rule.operator) {
- self.triggerValidationError(rule, 'no_operator', null);
- errors++;
- return;
- }
+ /**
+ * 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;
- if (rule.operator.nb_inputs !== 0) {
- var valid = self.validateValue(rule, rule.value);
+ var valid = (function parse(group) {
+ var done = 0;
+ var errors = 0;
- if (valid !== true) {
- self.triggerValidationError(rule, valid, rule.value);
+ 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;
}
- }
- done++;
+ if (rule.operator.nb_inputs !== 0) {
+ var valid = self.validateValue(rule, rule.value);
+
+ if (valid !== true) {
+ self.triggerValidationError(rule, valid, rule.value);
+ errors++;
+ return;
+ }
+ }
- }, function(group) {
- var res = parse(group);
- if (res === true) {
done++;
+
+ }, function(group) {
+ var res = parse(group);
+ if (res === true) {
+ done++;
+ }
+ else if (res === false) {
+ errors++;
+ }
+ });
+
+ if (errors > 0) {
+ return false;
}
- else if (res === false) {
- errors++;
+ 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;
}
- });
- 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;
- return true;
+ }(this.model.root));
- }(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);
+ };
/**
- * Modifies the result of the {@link QueryBuilder#validate} method
- * @event changer:validate
- * @memberof QueryBuilder
- * @param {boolean} valid
- * @returns {boolean}
+ * 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
*/
- 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;
- }
+ 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;
}
- 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 self = this;
- 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
+ var out = (function parse(group) {
+ var groupData = {
+ condition: group.condition,
+ rules: []
};
- if (rule.filter && rule.filter.data || rule.data) {
- ruleData.data = $.extendext(true, 'replace', {}, rule.filter.data, rule.data);
+ if (group.data) {
+ groupData.data = $.extendext(true, 'replace', {}, group.data);
}
if (options.get_flags) {
- var flags = self.getRuleFlags(rule.flags, options.get_flags === 'all');
+ var flags = self.getGroupFlags(group.flags, options.get_flags === 'all');
if (!$.isEmptyObject(flags)) {
- ruleData.flags = flags;
+ groupData.flags = flags;
}
}
+ group.each(function(rule) {
+ if ((!rule.filter && options.skip_empty) || !rule.flags.is_active) {
+ 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.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 Rule object
- * @event changer:ruleToJson
+ * Modifies the JSON generated from a Group object
+ * @event changer:groupToJson
* @memberof QueryBuilder
* @param {object} json
- * @param {Rule} rule
+ * @param {Group} group
* @returns {object}
*/
- groupData.rules.push(self.change('ruleToJson', ruleData, rule));
+ return self.change('groupToJson', groupData, group);
- }, function(model) {
- var data = parse(model);
- if (data.rules.length !== 0 || !options.skip_empty) {
- groupData.rules.push(data);
- }
- }, this);
+ }(this.model.root));
+
+ out.valid = valid;
/**
- * Modifies the JSON generated from a Group object
- * @event changer:groupToJson
+ * Modifies the result of the {@link QueryBuilder#getRules} method
+ * @event changer:getRules
* @memberof QueryBuilder
* @param {object} json
- * @param {Group} group
* @returns {object}
*/
- return self.change('groupToJson', groupData, group);
-
- }(this.model.root));
-
- out.valid = valid;
+ return this.change('getRules', out);
+ };
/**
- * Modifies the result of the {@link QueryBuilder#getRules} method
- * @event changer:getRules
- * @memberof QueryBuilder
- * @param {object} json
- * @returns {object}
+ * 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
*/
- 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
- };
- }
+ QueryBuilder.prototype.setRules = function(data, options) {
+ options = $.extend({
+ allow_invalid: false
+ }, options);
- if (!data || !data.rules || (data.rules.length === 0 && !this.settings.allow_empty)) {
- Utils.error('RulesParse', 'Incorrect data object passed');
- }
+ if ($.isArray(data)) {
+ data = {
+ condition: this.settings.default_condition,
+ rules: data
+ };
+ }
- this.clear();
- this.setRoot(false, data.data, this.parseGroupFlags(data));
+ if (!data || !data.rules || (data.rules.length === 0 && !this.settings.allow_empty)) {
+ Utils.error('RulesParse', 'Incorrect data object passed');
+ }
- /**
- * 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);
+ this.clear();
+ this.setRoot(false, data.data, this.parseGroupFlags(data));
- var self = this;
+ /**
+ * 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);
- (function add(data, group) {
- if (group === null) {
- return;
- }
+ var self = this;
- 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;
- }
+ (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;
- group.condition = data.condition;
+ data.rules.forEach(function(item) {
+ var model;
- 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;
+ }
- 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();
+ add(item, model);
+ }
}
else {
- model = self.addGroup(group, false, item.data, self.parseGroupFlags(item));
+ 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;
}
- 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';
+ if (!item.empty) {
+ model.filter = self.getFilterById(item.id, !options.allow_invalid);
}
- }
-
- 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.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 = self.getOperators(model.filter)[0];
+ }
}
- }
- if (model.operator && model.operator.nb_inputs !== 0) {
- if (item.value !== undefined) {
- model.value = item.value;
+ 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;
+ }
}
- 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 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));
/**
- * Modifies the Group object generated from the JSON
- * @event changer:jsonToGroup
+ * After the {@link QueryBuilder#setRules} method
+ * @event afterSetRules
* @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');
- }
+ this.trigger('afterSetRules');
+ };
- }(data, this.model.root));
/**
- * After the {@link QueryBuilder#setRules} method
- * @event afterSetRules
- * @memberof QueryBuilder
+ * Performs value validation
+ * @param {Rule} rule
+ * @param {string|string[]} value
+ * @returns {array|boolean} true or error array
+ * @fires QueryBuilder.changer:validateValue
*/
- this.trigger('afterSetRules');
-};
+ 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);
+ }
-/**
- * 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);
+ };
/**
- * Modifies the result of the rule validation method
- * @event changer:validateValue
- * @memberof QueryBuilder
- * @param {array|boolean} result - true or an error array
- * @param {*} value
+ * Default validation function
* @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;
+ * @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];
}
- switch (filter.input) {
- case 'radio':
- if (value[i] === undefined || value[i].length === 0) {
- if (!validation.allow_empty_value) {
- result = ['radio_empty'];
- }
- break;
- }
+ 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;
+ }
- case 'checkbox':
- if (value[i] === undefined || value[i].length === 0) {
- if (!validation.allow_empty_value) {
- result = ['checkbox_empty'];
+ switch (filter.input) {
+ case 'radio':
+ if (value[i] === undefined || value[i].length === 0) {
+ if (!validation.allow_empty_value) {
+ result = ['radio_empty'];
+ }
+ break;
}
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'];
+ case 'checkbox':
+ if (value[i] === undefined || value[i].length === 0) {
+ if (!validation.allow_empty_value) {
+ result = ['checkbox_empty'];
+ }
+ break;
}
break;
- }
- break;
- default:
- tempValue = $.isArray(value[i]) ? value[i] : [value[i]];
+ 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;
- 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];
+ 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.format) {
- if (typeof validation.format == 'string') {
- validation.format = new RegExp(validation.format);
+ 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.format.test(tempValue[j])) {
- result = [this.getValidationMessage(validation, 'format', 'string_invalid_format'), validation.format];
- break;
+ if (validation.max !== undefined) {
+ if (tempValue[j].length > parseInt(validation.max)) {
+ result = [this.getValidationMessage(validation, 'max', 'string_exceed_max_length'), validation.max];
+ break;
+ }
}
- }
- break;
-
- case 'number':
- if (tempValue[j] === undefined || tempValue[j].length === 0) {
- if (!validation.allow_empty_value) {
- result = ['number_nan'];
+ 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;
- }
- if (isNaN(tempValue[j])) {
- result = ['number_nan'];
- break;
- }
- if (filter.type == 'integer') {
- if (parseInt(tempValue[j]) != tempValue[j]) {
- result = ['number_not_integer'];
+
+ case 'number':
+ if (tempValue[j] === undefined || tempValue[j].length === 0) {
+ if (!validation.allow_empty_value) {
+ result = ['number_nan'];
+ }
break;
}
- }
- else {
- if (parseFloat(tempValue[j]) != tempValue[j]) {
- result = ['number_not_double'];
+ if (isNaN(tempValue[j])) {
+ result = ['number_nan'];
break;
}
- }
- if (validation.min !== undefined) {
- if (tempValue[j] < parseFloat(validation.min)) {
- result = [this.getValidationMessage(validation, 'min', 'number_exceed_min'), validation.min];
- break;
+ if (filter.type == 'integer') {
+ if (parseInt(tempValue[j]) != tempValue[j]) {
+ result = ['number_not_integer'];
+ break;
+ }
}
- }
- if (validation.max !== undefined) {
- if (tempValue[j] > parseFloat(validation.max)) {
- result = [this.getValidationMessage(validation, 'max', 'number_exceed_max'), validation.max];
- break;
+ else {
+ if (parseFloat(tempValue[j]) != tempValue[j]) {
+ result = ['number_not_double'];
+ 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;
+ if (validation.min !== undefined) {
+ if (tempValue[j] < parseFloat(validation.min)) {
+ result = [this.getValidationMessage(validation, 'min', 'number_exceed_min'), validation.min];
+ break;
+ }
}
- }
- break;
-
- case 'datetime':
- if (tempValue[j] === undefined || tempValue[j].length === 0) {
- if (!validation.allow_empty_value) {
- result = ['datetime_empty'];
+ if (validation.max !== undefined) {
+ if (tempValue[j] > parseFloat(validation.max)) {
+ result = [this.getValidationMessage(validation, 'max', 'number_exceed_max'), validation.max];
+ break;
+ }
}
- 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');
+ 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;
- var datetime = moment(tempValue[j], validation.format);
- if (!datetime.isValid()) {
- result = [this.getValidationMessage(validation, 'format', 'datetime_invalid'), validation.format];
+ case 'datetime':
+ if (tempValue[j] === undefined || tempValue[j].length === 0) {
+ if (!validation.allow_empty_value) {
+ result = ['datetime_empty'];
+ }
break;
}
- else {
- if (validation.min) {
- if (datetime < moment(validation.min, validation.format)) {
- result = [this.getValidationMessage(validation, 'min', 'datetime_exceed_min'), validation.min];
- 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;
}
- if (validation.max) {
- if (datetime > moment(validation.max, validation.format)) {
- result = [this.getValidationMessage(validation, 'max', 'datetime_exceed_max'), validation.max];
- 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;
+ break;
- case 'boolean':
- if (tempValue[j] === undefined || tempValue[j].length === 0) {
- if (!validation.allow_empty_value) {
+ 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;
}
- 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 (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]];
- }
+ if (result !== true) {
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 ((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]];
+ if (moment(value[0], validation.format).isAfter(moment(value[1], validation.format))) {
+ result = ['datetime_between_invalid', value[0], value[1]];
+ }
}
- }
- break;
+ break;
+ }
}
- }
- return result;
-};
+ 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
- * @private
- */
-QueryBuilder.prototype.getOperators = function(filter) {
- if (typeof filter == 'string') {
- filter = this.getFilterById(filter);
- }
+ /**
+ * Returns an incremented group ID
+ * @returns {string}
+ * @private
+ */
+ QueryBuilder.prototype.nextGroupId = function() {
+ return this.status.id + '_group_' + (this.status.group_id++);
+ };
- var result = [];
+ /**
+ * Returns an incremented rule ID
+ * @returns {string}
+ * @private
+ */
+ QueryBuilder.prototype.nextRuleId = function() {
+ return this.status.id + '_rule_' + (this.status.rule_id++);
+ };
- 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) {
+ /**
+ * Returns the operators for a filter
+ * @param {string|object} filter - filter id or filter object
+ * @returns {object[]}
+ * @fires QueryBuilder.changer:getOperators
+ * @private
+ */
+ 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;
}
- }
- // type check
- else if (this.operators[i].apply_to.indexOf(QueryBuilder.types[filter.type]) == -1) {
- continue;
+
+ result.push(this.operators[i]);
}
- 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);
+ });
+ }
- // 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);
+ };
/**
- * Modifies the operators available for a filter
- * @event changer:getOperators
- * @memberof QueryBuilder
- * @param {QueryBuilder.Operator[]} operators
- * @param {QueryBuilder.Filter} filter
- * @returns {QueryBuilder.Operator[]}
+ * Returns a particular filter by its id
+ * @param {string} id
+ * @param {boolean} [doThrow=true]
+ * @returns {object|null}
+ * @throws UndefinedFilterError
+ * @private
*/
- 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
- * @private
- */
-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];
+ QueryBuilder.prototype.getFilterById = function(id, doThrow) {
+ if (id == '-1') {
+ return null;
}
- }
- Utils.error(doThrow !== false, 'UndefinedFilter', 'Undefined filter "{0}"', id);
+ for (var i = 0, l = this.filters.length; i < l; i++) {
+ if (this.filters[i].id == id) {
+ return this.filters[i];
+ }
+ }
- return null;
-};
+ Utils.error(doThrow !== false, 'UndefinedFilter', 'Undefined filter "{0}"', id);
-/**
- * Returns a particular operator by its type
- * @param {string} type
- * @param {boolean} [doThrow=true]
- * @returns {object|null}
- * @throws UndefinedOperatorError
- * @private
- */
-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];
+ /**
+ * Returns a particular operator by its type
+ * @param {string} type
+ * @param {boolean} [doThrow=true]
+ * @returns {object|null}
+ * @throws UndefinedOperatorError
+ * @private
+ */
+ QueryBuilder.prototype.getOperatorByType = function(type, doThrow) {
+ if (type == '-1') {
+ return null;
}
- }
- Utils.error(doThrow !== false, 'UndefinedOperator', 'Undefined operator "{0}"', type);
+ for (var i = 0, l = this.operators.length; i < l; i++) {
+ if (this.operators[i].type == type) {
+ return this.operators[i];
+ }
+ }
- return null;
-};
+ Utils.error(doThrow !== false, 'UndefinedOperator', 'Undefined operator "{0}"', type);
-/**
- * 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 = [];
+ return null;
+ };
- if (filter.valueGetter) {
- value = filter.valueGetter.call(this, rule);
- }
- else {
- var $value = rule.$el.find(QueryBuilder.selectors.value_container);
+ /**
+ * 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 = [];
- for (var i = 0; i < operator.nb_inputs; i++) {
- var name = Utils.escapeElementId(rule.id + '_value_' + i);
- var tmp;
+ if (filter.valueGetter) {
+ value = filter.valueGetter.call(this, rule);
+ }
+ else {
+ var $value = rule.$el.find(QueryBuilder.selectors.value_container);
- switch (filter.input) {
- case 'radio':
- value.push($value.find('[name=' + name + ']:checked').val());
- break;
+ for (var i = 0; i < operator.nb_inputs; i++) {
+ var name = Utils.escapeElementId(rule.id + '_value_' + i);
+ var tmp;
- case 'checkbox':
- tmp = [];
- // jshint loopfunc:true
- $value.find('[name=' + name + ']:checked').each(function() {
- tmp.push($(this).val());
- });
- // jshint loopfunc:false
- value.push(tmp);
- break;
+ switch (filter.input) {
+ case 'radio':
+ value.push($value.find('[name=' + name + ']:checked').val());
+ break;
- case 'select':
- if (filter.multiple) {
+ case 'checkbox':
tmp = [];
// jshint loopfunc:true
- $value.find('[name=' + name + '] option:selected').each(function() {
+ $value.find('[name=' + name + ']:checked').each(function() {
tmp.push($(this).val());
});
// jshint loopfunc:false
value.push(tmp);
- }
- else {
- value.push($value.find('[name=' + name + '] option:selected').val());
- }
- break;
+ break;
- default:
- value.push($value.find('[name=' + name + ']').val());
- }
- }
+ 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;
- value = value.map(function(val) {
- if (operator.multiple && filter.value_separator && typeof val == 'string') {
- val = val.split(filter.value_separator);
+ default:
+ value.push($value.find('[name=' + name + ']').val());
+ }
}
- if ($.isArray(val)) {
- return val.map(function(subval) {
- return Utils.changeType(subval, filter.type);
- });
- }
- else {
- return Utils.changeType(val, filter.type);
+ 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];
}
- });
- if (operator.nb_inputs === 1) {
- value = value[0];
+ // @deprecated
+ if (filter.valueParser) {
+ value = filter.valueParser.call(this, rule, value);
+ }
}
- // @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);
+ };
/**
- * Modifies the rule's value grabbed from the DOM
- * @event changer:getRuleValue
- * @memberof QueryBuilder
- * @param {*} value
+ * Sets the value of a rule's input
* @param {Rule} rule
- * @returns {*}
+ * @param {*} value
+ * @private
*/
- 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;
- }
+ QueryBuilder.prototype.setRuleInputValue = function(rule, value) {
+ var filter = rule.filter;
+ var operator = rule.operator;
- rule._updating_input = true;
+ if (!filter || !operator) {
+ return;
+ }
- if (filter.valueSetter) {
- filter.valueSetter.call(this, rule, value);
- }
- else {
- var $value = rule.$el.find(QueryBuilder.selectors.value_container);
+ rule._updating_input = true;
- if (operator.nb_inputs == 1) {
- value = [value];
+ if (filter.valueSetter) {
+ filter.valueSetter.call(this, rule, value);
}
+ 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);
+ if (operator.nb_inputs == 1) {
+ value = [value];
+ }
- switch (filter.input) {
- case 'radio':
- $value.find('[name=' + name + '][value="' + value[i] + '"]').prop('checked', true).trigger('change');
- break;
+ for (var i = 0; i < operator.nb_inputs; i++) {
+ var name = Utils.escapeElementId(rule.id + '_value_' + i);
- 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;
+ switch (filter.input) {
+ case 'radio':
+ $value.find('[name=' + name + '][value="' + value[i] + '"]').prop('checked', true).trigger('change');
+ 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;
- }
+ 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;
-};
+ 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
- });
- }
+ /**
+ * 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,
+ is_active: true
+ });
+ }
- if (rule.flags) {
- $.extend(flags, rule.flags);
- }
+ if (rule.flags) {
+ $.extend(flags, rule.flags);
+ }
+
+ /**
+ * Modifies the consolidated rule's flags
+ * @event changer:parseRuleFlags
+ * @memberof QueryBuilder
+ * @param {object} flags
+ * @param {object} rule - not a Rule object
+ * @returns {object}
+ */
+ return this.change('parseRuleFlags', flags, rule);
+ };
/**
- * Modifies the consolidated rule's flags
- * @event changer:parseRuleFlags
- * @memberof QueryBuilder
+ * Gets a copy of flags of a rule
* @param {object} flags
- * @param {object} rule - not a Rule object
+ * @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
*/
- return this.change('parseRuleFlags', flags, rule);
-};
+ 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
+ });
+ }
-/**
- * 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);
+ }
- if (group.flags) {
- $.extend(flags, group.flags);
- }
+ /**
+ * Modifies the consolidated group's flags
+ * @event changer:parseGroupFlags
+ * @memberof QueryBuilder
+ * @param {object} flags
+ * @param {object} group - not a Group object
+ * @returns {object}
+ */
+ return this.change('parseGroupFlags', flags, group);
+ };
/**
- * Modifies the consolidated group's flags
- * @event changer:parseGroupFlags
- * @memberof QueryBuilder
+ * Gets a copy of flags of a group
* @param {object} flags
- * @param {object} group - not a Group object
+ * @param {boolean} [all=false] - return all flags or only changes from default flags
* @returns {object}
+ * @private
*/
- 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;
- }
+ 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;
+ }
+ };
/**
- * Modifies the translated label
- * @event changer:translate
- * @memberof QueryBuilder
- * @param {string} translation
- * @param {string|object} key
+ * 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
*/
- return this.change('translate', translation, key, category);
-};
+ QueryBuilder.prototype.translate = function(category, key) {
+ if (!key) {
+ key = category;
+ category = undefined;
+ }
-/**
- * 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;
-};
+ 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 = '\
+ QueryBuilder.templates.group = '\
\
';
-QueryBuilder.templates.rule = '\
+ QueryBuilder.templates.rule = '\
\