jquery_query_builder-rails 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ee785e1ac307e057202ab3528d8c5fc3127cd981
4
+ data.tar.gz: 40c5c765ae70e7219a075ad646ce1a1357999999
5
+ SHA512:
6
+ metadata.gz: 5c4a7726931819dbd397d3a2230b713b1a32966a7bd7f43458f89216ae7dfec471ad7b3d05e5d9e01d5bbd33df32c2c82c54d0e3144fc377ccdfb2cbdee91d5e
7
+ data.tar.gz: ee20cd90062921090e1f6bc6e224df97c0fafcb4fcc55e5934041e5fc5129b4f9a35dc1a44fba0415e704f3e58b4aa5c16dc42e047e9481af4cdedd2c5bd1c85
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at matt@devicemagic.com. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Matthew Hirst
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,56 @@
1
+ # jQuery QueryBuilder - rails
2
+
3
+ jquery_query_builer-rails wraps the [query-builder.js](http://querybuilder.js.org//) library
4
+ and some of its dependencies in a rails engine for simple use with the asset pipeline provided by Rails 3.1 and higher.
5
+
6
+ The gem includes the development (non-minified) source for ease of exploration.
7
+ The asset pipeline will minify in production.
8
+
9
+ jQuery QueryBuilder is a jQuery plugin that provides a UI component to create queries and filters.
10
+
11
+ Please see the [documentation](http://querybuilder.js.org/) for details.
12
+
13
+ The two dependencies that are not included with this gem are:
14
+ - jQuery >= 1.10
15
+ - Bootstrap >= 3.1 (CSS Buttons and Utilities only)
16
+
17
+ You probably already have jQuery included in your Rails project and the bootstrap dependency is optional.
18
+ Please see this [how to](http://querybuilder.js.org/dev/no-bootstrap.html) to go on without boostrap.
19
+
20
+ ## Usage
21
+
22
+ Add the following to your gemfile:
23
+
24
+ gem 'jquery_query_builder-rails'
25
+
26
+ Add the following directive to your Javascript manifest file after jQuery (application.js):
27
+
28
+ //= require jquery.extendext
29
+ //= require doT
30
+ //= require query-builder
31
+
32
+ Add the following directive to your Stylesheet manifest file (application.scss):
33
+
34
+ @import "query-builder.default";
35
+ or
36
+
37
+ @import "query-builder.dark";
38
+
39
+ depending on the theme you want to use.
40
+
41
+ After that you can use the QueryBuilder to any \<div\> you want.
42
+ ```html
43
+ <div id="builder"></div>
44
+
45
+ <script>
46
+ $('#builder').queryBuilder({
47
+ filters: [ ... ]
48
+ });
49
+ </script>
50
+ ```
51
+ Read more here:
52
+ [jQuery QueryBuilder](http://querybuilder.js.org//)
53
+
54
+ Coming Soon:
55
+
56
+ Ruby evaluator for the json output.
@@ -0,0 +1,9 @@
1
+ require "jquery_query_builder/rails/version"
2
+
3
+ module JqueryQueryBuilder
4
+ module Rails
5
+ class Engine < ::Rails::Engine
6
+ # Get rails to add app, lib, vendor to load path
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module JqueryQueryBuilder
2
+ module Rails
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,140 @@
1
+ // doT.js
2
+ // 2011-2014, Laura Doktorova, https://github.com/olado/doT
3
+ // Licensed under the MIT license.
4
+
5
+ (function() {
6
+ "use strict";
7
+
8
+ var doT = {
9
+ version: "1.0.3",
10
+ templateSettings: {
11
+ evaluate: /\{\{([\s\S]+?(\}?)+)\}\}/g,
12
+ interpolate: /\{\{=([\s\S]+?)\}\}/g,
13
+ encode: /\{\{!([\s\S]+?)\}\}/g,
14
+ use: /\{\{#([\s\S]+?)\}\}/g,
15
+ useParams: /(^|[^\w$])def(?:\.|\[[\'\"])([\w$\.]+)(?:[\'\"]\])?\s*\:\s*([\w$\.]+|\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})/g,
16
+ define: /\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g,
17
+ defineParams:/^\s*([\w$]+):([\s\S]+)/,
18
+ conditional: /\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g,
19
+ iterate: /\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/g,
20
+ varname: "it",
21
+ strip: true,
22
+ append: true,
23
+ selfcontained: false,
24
+ doNotSkipEncoded: false
25
+ },
26
+ template: undefined, //fn, compile template
27
+ compile: undefined //fn, for express
28
+ }, _globals;
29
+
30
+ doT.encodeHTMLSource = function(doNotSkipEncoded) {
31
+ var encodeHTMLRules = { "&": "&#38;", "<": "&#60;", ">": "&#62;", '"': "&#34;", "'": "&#39;", "/": "&#47;" },
32
+ matchHTML = doNotSkipEncoded ? /[&<>"'\/]/g : /&(?!#?\w+;)|<|>|"|'|\//g;
33
+ return function(code) {
34
+ return code ? code.toString().replace(matchHTML, function(m) {return encodeHTMLRules[m] || m;}) : "";
35
+ };
36
+ };
37
+
38
+ _globals = (function(){ return this || (0,eval)("this"); }());
39
+
40
+ if (typeof module !== "undefined" && module.exports) {
41
+ module.exports = doT;
42
+ } else if (typeof define === "function" && define.amd) {
43
+ define(function(){return doT;});
44
+ } else {
45
+ _globals.doT = doT;
46
+ }
47
+
48
+ var startend = {
49
+ append: { start: "'+(", end: ")+'", startencode: "'+encodeHTML(" },
50
+ split: { start: "';out+=(", end: ");out+='", startencode: "';out+=encodeHTML(" }
51
+ }, skip = /$^/;
52
+
53
+ function resolveDefs(c, block, def) {
54
+ return ((typeof block === "string") ? block : block.toString())
55
+ .replace(c.define || skip, function(m, code, assign, value) {
56
+ if (code.indexOf("def.") === 0) {
57
+ code = code.substring(4);
58
+ }
59
+ if (!(code in def)) {
60
+ if (assign === ":") {
61
+ if (c.defineParams) value.replace(c.defineParams, function(m, param, v) {
62
+ def[code] = {arg: param, text: v};
63
+ });
64
+ if (!(code in def)) def[code]= value;
65
+ } else {
66
+ new Function("def", "def['"+code+"']=" + value)(def);
67
+ }
68
+ }
69
+ return "";
70
+ })
71
+ .replace(c.use || skip, function(m, code) {
72
+ if (c.useParams) code = code.replace(c.useParams, function(m, s, d, param) {
73
+ if (def[d] && def[d].arg && param) {
74
+ var rw = (d+":"+param).replace(/'|\\/g, "_");
75
+ def.__exp = def.__exp || {};
76
+ def.__exp[rw] = def[d].text.replace(new RegExp("(^|[^\\w$])" + def[d].arg + "([^\\w$])", "g"), "$1" + param + "$2");
77
+ return s + "def.__exp['"+rw+"']";
78
+ }
79
+ });
80
+ var v = new Function("def", "return " + code)(def);
81
+ return v ? resolveDefs(c, v, def) : v;
82
+ });
83
+ }
84
+
85
+ function unescape(code) {
86
+ return code.replace(/\\('|\\)/g, "$1").replace(/[\r\t\n]/g, " ");
87
+ }
88
+
89
+ doT.template = function(tmpl, c, def) {
90
+ c = c || doT.templateSettings;
91
+ var cse = c.append ? startend.append : startend.split, needhtmlencode, sid = 0, indv,
92
+ str = (c.use || c.define) ? resolveDefs(c, tmpl, def || {}) : tmpl;
93
+
94
+ str = ("var out='" + (c.strip ? str.replace(/(^|\r|\n)\t* +| +\t*(\r|\n|$)/g," ")
95
+ .replace(/\r|\n|\t|\/\*[\s\S]*?\*\//g,""): str)
96
+ .replace(/'|\\/g, "\\$&")
97
+ .replace(c.interpolate || skip, function(m, code) {
98
+ return cse.start + unescape(code) + cse.end;
99
+ })
100
+ .replace(c.encode || skip, function(m, code) {
101
+ needhtmlencode = true;
102
+ return cse.startencode + unescape(code) + cse.end;
103
+ })
104
+ .replace(c.conditional || skip, function(m, elsecase, code) {
105
+ return elsecase ?
106
+ (code ? "';}else if(" + unescape(code) + "){out+='" : "';}else{out+='") :
107
+ (code ? "';if(" + unescape(code) + "){out+='" : "';}out+='");
108
+ })
109
+ .replace(c.iterate || skip, function(m, iterate, vname, iname) {
110
+ if (!iterate) return "';} } out+='";
111
+ sid+=1; indv=iname || "i"+sid; iterate=unescape(iterate);
112
+ return "';var arr"+sid+"="+iterate+";if(arr"+sid+"){var "+vname+","+indv+"=-1,l"+sid+"=arr"+sid+".length-1;while("+indv+"<l"+sid+"){"
113
+ +vname+"=arr"+sid+"["+indv+"+=1];out+='";
114
+ })
115
+ .replace(c.evaluate || skip, function(m, code) {
116
+ return "';" + unescape(code) + "out+='";
117
+ })
118
+ + "';return out;")
119
+ .replace(/\n/g, "\\n").replace(/\t/g, '\\t').replace(/\r/g, "\\r")
120
+ .replace(/(\s|;|\}|^|\{)out\+='';/g, '$1').replace(/\+''/g, "");
121
+ //.replace(/(\s|;|\}|^|\{)out\+=''\+/g,'$1out+=');
122
+
123
+ if (needhtmlencode) {
124
+ if (!c.selfcontained && _globals && !_globals._encodeHTML) _globals._encodeHTML = doT.encodeHTMLSource(c.doNotSkipEncoded);
125
+ str = "var encodeHTML = typeof _encodeHTML !== 'undefined' ? _encodeHTML : ("
126
+ + doT.encodeHTMLSource.toString() + "(" + (c.doNotSkipEncoded || '') + "));"
127
+ + str;
128
+ }
129
+ try {
130
+ return new Function(c.varname, str);
131
+ } catch (e) {
132
+ if (typeof console !== "undefined") console.log("Could not create a template function: " + str);
133
+ throw e;
134
+ }
135
+ };
136
+
137
+ doT.compile = function(tmpl, def) {
138
+ return doT.template(tmpl, null, def);
139
+ };
140
+ }());
@@ -0,0 +1,132 @@
1
+ /*!
2
+ * jQuery.extendext 0.1.2
3
+ *
4
+ * Copyright 2014-2016 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
5
+ * Licensed under MIT (http://opensource.org/licenses/MIT)
6
+ *
7
+ * Based on jQuery.extend by jQuery Foundation, Inc. and other contributors
8
+ */
9
+
10
+ /*jshint -W083 */
11
+
12
+ (function (root, factory) {
13
+ if (typeof define === 'function' && define.amd) {
14
+ define(['jquery'], factory);
15
+ }
16
+ else if (typeof module === 'object' && module.exports) {
17
+ module.exports = factory(require('jquery'));
18
+ }
19
+ else {
20
+ factory(root.jQuery);
21
+ }
22
+ }(this, function ($) {
23
+ "use strict";
24
+
25
+ $.extendext = function () {
26
+ var options, name, src, copy, copyIsArray, clone,
27
+ target = arguments[0] || {},
28
+ i = 1,
29
+ length = arguments.length,
30
+ deep = false,
31
+ arrayMode = 'default';
32
+
33
+ // Handle a deep copy situation
34
+ if (typeof target === "boolean") {
35
+ deep = target;
36
+
37
+ // Skip the boolean and the target
38
+ target = arguments[i++] || {};
39
+ }
40
+
41
+ // Handle array mode parameter
42
+ if (typeof target === "string") {
43
+ arrayMode = target.toLowerCase();
44
+ if (arrayMode !== 'concat' && arrayMode !== 'replace' && arrayMode !== 'extend') {
45
+ arrayMode = 'default';
46
+ }
47
+
48
+ // Skip the string param
49
+ target = arguments[i++] || {};
50
+ }
51
+
52
+ // Handle case when target is a string or something (possible in deep copy)
53
+ if (typeof target !== "object" && !$.isFunction(target)) {
54
+ target = {};
55
+ }
56
+
57
+ // Extend jQuery itself if only one argument is passed
58
+ if (i === length) {
59
+ target = this;
60
+ i--;
61
+ }
62
+
63
+ for (; i < length; i++) {
64
+ // Only deal with non-null/undefined values
65
+ if ((options = arguments[i]) !== null) {
66
+ // Special operations for arrays
67
+ if ($.isArray(options) && arrayMode !== 'default') {
68
+ clone = target && $.isArray(target) ? target : [];
69
+
70
+ switch (arrayMode) {
71
+ case 'concat':
72
+ target = clone.concat($.extend(deep, [], options));
73
+ break;
74
+
75
+ case 'replace':
76
+ target = $.extend(deep, [], options);
77
+ break;
78
+
79
+ case 'extend':
80
+ options.forEach(function (e, i) {
81
+ if (typeof e === 'object') {
82
+ var type = $.isArray(e) ? [] : {};
83
+ clone[i] = $.extendext(deep, arrayMode, clone[i] || type, e);
84
+
85
+ } else if (clone.indexOf(e) === -1) {
86
+ clone.push(e);
87
+ }
88
+ });
89
+
90
+ target = clone;
91
+ break;
92
+ }
93
+
94
+ } else {
95
+ // Extend the base object
96
+ for (name in options) {
97
+ src = target[name];
98
+ copy = options[name];
99
+
100
+ // Prevent never-ending loop
101
+ if (target === copy) {
102
+ continue;
103
+ }
104
+
105
+ // Recurse if we're merging plain objects or arrays
106
+ if (deep && copy && ( $.isPlainObject(copy) ||
107
+ (copyIsArray = $.isArray(copy)) )) {
108
+
109
+ if (copyIsArray) {
110
+ copyIsArray = false;
111
+ clone = src && $.isArray(src) ? src : [];
112
+
113
+ } else {
114
+ clone = src && $.isPlainObject(src) ? src : {};
115
+ }
116
+
117
+ // Never move original objects, clone them
118
+ target[name] = $.extendext(deep, arrayMode, clone, copy);
119
+
120
+ // Don't bring in undefined values
121
+ } else if (copy !== undefined) {
122
+ target[name] = copy;
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ // Return the modified object
130
+ return target;
131
+ };
132
+ }));
@@ -0,0 +1,4277 @@
1
+ /*!
2
+ * jQuery QueryBuilder 2.3.3
3
+ * Copyright 2014-2016 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
4
+ * Licensed under MIT (http://opensource.org/licenses/MIT)
5
+ */
6
+
7
+ // Languages: en
8
+ // Plugins: bt-checkbox, bt-selectpicker, bt-tooltip-errors, change-filters, filter-description, invert, mongodb-support, sortable, sql-support, unique-filter
9
+ (function(root, factory) {
10
+ if (typeof define == 'function' && define.amd) {
11
+ define(['jquery', 'doT', 'jQuery.extendext'], factory);
12
+ }
13
+ else {
14
+ factory(root.jQuery, root.doT);
15
+ }
16
+ }(this, function($, doT) {
17
+ "use strict";
18
+
19
+ // CLASS DEFINITION
20
+ // ===============================
21
+ var QueryBuilder = function($el, options) {
22
+ this.init($el, options);
23
+ };
24
+
25
+
26
+ // EVENTS SYSTEM
27
+ // ===============================
28
+ $.extend(QueryBuilder.prototype, {
29
+ change: function(type, value) {
30
+ var event = new $.Event(type + '.queryBuilder.filter', {
31
+ builder: this,
32
+ value: value
33
+ });
34
+
35
+ this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 2));
36
+
37
+ return event.value;
38
+ },
39
+
40
+ trigger: function(type) {
41
+ var event = new $.Event(type + '.queryBuilder', {
42
+ builder: this
43
+ });
44
+
45
+ this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 1));
46
+
47
+ return event;
48
+ },
49
+
50
+ on: function(type, cb) {
51
+ this.$el.on(type + '.queryBuilder', cb);
52
+ return this;
53
+ },
54
+
55
+ off: function(type, cb) {
56
+ this.$el.off(type + '.queryBuilder', cb);
57
+ return this;
58
+ },
59
+
60
+ once: function(type, cb) {
61
+ this.$el.one(type + '.queryBuilder', cb);
62
+ return this;
63
+ }
64
+ });
65
+
66
+
67
+ // PLUGINS SYSTEM
68
+ // ===============================
69
+ QueryBuilder.plugins = {};
70
+
71
+ /**
72
+ * Get or extend the default configuration
73
+ * @param options {object,optional} new configuration, leave undefined to get the default config
74
+ * @return {undefined|object} nothing or configuration object (copy)
75
+ */
76
+ QueryBuilder.defaults = function(options) {
77
+ if (typeof options == 'object') {
78
+ $.extendext(true, 'replace', QueryBuilder.DEFAULTS, options);
79
+ }
80
+ else if (typeof options == 'string') {
81
+ if (typeof QueryBuilder.DEFAULTS[options] == 'object') {
82
+ return $.extend(true, {}, QueryBuilder.DEFAULTS[options]);
83
+ }
84
+ else {
85
+ return QueryBuilder.DEFAULTS[options];
86
+ }
87
+ }
88
+ else {
89
+ return $.extend(true, {}, QueryBuilder.DEFAULTS);
90
+ }
91
+ };
92
+
93
+ /**
94
+ * Define a new plugin
95
+ * @param {string}
96
+ * @param {function}
97
+ * @param {object,optional} default configuration
98
+ */
99
+ QueryBuilder.define = function(name, fct, def) {
100
+ QueryBuilder.plugins[name] = {
101
+ fct: fct,
102
+ def: def || {}
103
+ };
104
+ };
105
+
106
+ /**
107
+ * Add new methods
108
+ * @param {object}
109
+ */
110
+ QueryBuilder.extend = function(methods) {
111
+ $.extend(QueryBuilder.prototype, methods);
112
+ };
113
+
114
+ /**
115
+ * Init plugins for an instance
116
+ * @throws ConfigError
117
+ */
118
+ QueryBuilder.prototype.initPlugins = function() {
119
+ if (!this.plugins) {
120
+ return;
121
+ }
122
+
123
+ if ($.isArray(this.plugins)) {
124
+ var tmp = {};
125
+ this.plugins.forEach(function(plugin) {
126
+ tmp[plugin] = null;
127
+ });
128
+ this.plugins = tmp;
129
+ }
130
+
131
+ Object.keys(this.plugins).forEach(function(plugin) {
132
+ if (plugin in QueryBuilder.plugins) {
133
+ this.plugins[plugin] = $.extend(true, {},
134
+ QueryBuilder.plugins[plugin].def,
135
+ this.plugins[plugin] || {}
136
+ );
137
+
138
+ QueryBuilder.plugins[plugin].fct.call(this, this.plugins[plugin]);
139
+ }
140
+ else {
141
+ Utils.error('Config', 'Unable to find plugin "{0}"', plugin);
142
+ }
143
+ }, this);
144
+ };
145
+
146
+
147
+ /**
148
+ * Allowed types and their internal representation
149
+ */
150
+ QueryBuilder.types = {
151
+ 'string': 'string',
152
+ 'integer': 'number',
153
+ 'double': 'number',
154
+ 'date': 'datetime',
155
+ 'time': 'datetime',
156
+ 'datetime': 'datetime',
157
+ 'boolean': 'boolean'
158
+ };
159
+
160
+ /**
161
+ * Allowed inputs
162
+ */
163
+ QueryBuilder.inputs = [
164
+ 'text',
165
+ 'textarea',
166
+ 'radio',
167
+ 'checkbox',
168
+ 'select'
169
+ ];
170
+
171
+ /**
172
+ * Runtime modifiable options with `setOptions` method
173
+ */
174
+ QueryBuilder.modifiable_options = [
175
+ 'display_errors',
176
+ 'allow_groups',
177
+ 'allow_empty',
178
+ 'default_condition',
179
+ 'default_filter'
180
+ ];
181
+
182
+ /**
183
+ * CSS selectors for common components
184
+ */
185
+ var Selectors = QueryBuilder.selectors = {
186
+ group_container: '.rules-group-container',
187
+ rule_container: '.rule-container',
188
+ filter_container: '.rule-filter-container',
189
+ operator_container: '.rule-operator-container',
190
+ value_container: '.rule-value-container',
191
+ error_container: '.error-container',
192
+ condition_container: '.rules-group-header .group-conditions',
193
+
194
+ rule_header: '.rule-header',
195
+ group_header: '.rules-group-header',
196
+ group_actions: '.group-actions',
197
+ rule_actions: '.rule-actions',
198
+
199
+ rules_list: '.rules-group-body>.rules-list',
200
+
201
+ group_condition: '.rules-group-header [name$=_cond]',
202
+ rule_filter: '.rule-filter-container [name$=_filter]',
203
+ rule_operator: '.rule-operator-container [name$=_operator]',
204
+ rule_value: '.rule-value-container [name*=_value_]',
205
+
206
+ add_rule: '[data-add=rule]',
207
+ delete_rule: '[data-delete=rule]',
208
+ add_group: '[data-add=group]',
209
+ delete_group: '[data-delete=group]'
210
+ };
211
+
212
+ /**
213
+ * Template strings (see `template.js`)
214
+ */
215
+ QueryBuilder.templates = {};
216
+
217
+ /**
218
+ * Localized strings (see `i18n/`)
219
+ */
220
+ QueryBuilder.regional = {};
221
+
222
+ /**
223
+ * Default operators
224
+ */
225
+ QueryBuilder.OPERATORS = {
226
+ equal: { type: 'equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] },
227
+ not_equal: { type: 'not_equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] },
228
+ in: { type: 'in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime'] },
229
+ not_in: { type: 'not_in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime'] },
230
+ less: { type: 'less', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
231
+ less_or_equal: { type: 'less_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
232
+ greater: { type: 'greater', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
233
+ greater_or_equal: { type: 'greater_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
234
+ between: { type: 'between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] },
235
+ not_between: { type: 'not_between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] },
236
+ begins_with: { type: 'begins_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
237
+ not_begins_with: { type: 'not_begins_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
238
+ contains: { type: 'contains', nb_inputs: 1, multiple: false, apply_to: ['string'] },
239
+ not_contains: { type: 'not_contains', nb_inputs: 1, multiple: false, apply_to: ['string'] },
240
+ ends_with: { type: 'ends_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
241
+ not_ends_with: { type: 'not_ends_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
242
+ is_empty: { type: 'is_empty', nb_inputs: 0, multiple: false, apply_to: ['string'] },
243
+ is_not_empty: { type: 'is_not_empty', nb_inputs: 0, multiple: false, apply_to: ['string'] },
244
+ is_null: { type: 'is_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] },
245
+ is_not_null: { type: 'is_not_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] }
246
+ };
247
+
248
+ /**
249
+ * Default configuration
250
+ */
251
+ QueryBuilder.DEFAULTS = {
252
+ filters: [],
253
+ plugins: [],
254
+
255
+ sort_filters: false,
256
+ display_errors: true,
257
+ allow_groups: -1,
258
+ allow_empty: false,
259
+ conditions: ['AND', 'OR'],
260
+ default_condition: 'AND',
261
+ inputs_separator: ' , ',
262
+ select_placeholder: '------',
263
+ display_empty_filter: true,
264
+ default_filter: null,
265
+ optgroups: {},
266
+
267
+ default_rule_flags: {
268
+ filter_readonly: false,
269
+ operator_readonly: false,
270
+ value_readonly: false,
271
+ no_delete: false
272
+ },
273
+
274
+ default_group_flags: {
275
+ condition_readonly: false,
276
+ no_delete: false
277
+ },
278
+
279
+ templates: {
280
+ group: null,
281
+ rule: null,
282
+ filterSelect: null,
283
+ operatorSelect: null
284
+ },
285
+
286
+ lang_code: 'en',
287
+ lang: {},
288
+
289
+ operators: [
290
+ 'equal',
291
+ 'not_equal',
292
+ 'in',
293
+ 'not_in',
294
+ 'less',
295
+ 'less_or_equal',
296
+ 'greater',
297
+ 'greater_or_equal',
298
+ 'between',
299
+ 'not_between',
300
+ 'begins_with',
301
+ 'not_begins_with',
302
+ 'contains',
303
+ 'not_contains',
304
+ 'ends_with',
305
+ 'not_ends_with',
306
+ 'is_empty',
307
+ 'is_not_empty',
308
+ 'is_null',
309
+ 'is_not_null'
310
+ ],
311
+
312
+ icons: {
313
+ add_group: 'glyphicon glyphicon-plus-sign',
314
+ add_rule: 'glyphicon glyphicon-plus',
315
+ remove_group: 'glyphicon glyphicon-remove',
316
+ remove_rule: 'glyphicon glyphicon-remove',
317
+ error: 'glyphicon glyphicon-warning-sign'
318
+ }
319
+ };
320
+
321
+
322
+ /**
323
+ * Init the builder
324
+ */
325
+ QueryBuilder.prototype.init = function($el, options) {
326
+ $el[0].queryBuilder = this;
327
+ this.$el = $el;
328
+
329
+ // PROPERTIES
330
+ this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options);
331
+ this.model = new Model();
332
+ this.status = {
333
+ group_id: 0,
334
+ rule_id: 0,
335
+ generated_id: false,
336
+ has_optgroup: false,
337
+ has_operator_oprgroup: false,
338
+ id: null,
339
+ updating_value: false
340
+ };
341
+
342
+ // "allow_groups" can be boolean or int
343
+ if (this.settings.allow_groups === false) {
344
+ this.settings.allow_groups = 0;
345
+ }
346
+ else if (this.settings.allow_groups === true) {
347
+ this.settings.allow_groups = -1;
348
+ }
349
+
350
+ // SETTINGS SHORTCUTS
351
+ this.filters = this.settings.filters;
352
+ this.icons = this.settings.icons;
353
+ this.operators = this.settings.operators;
354
+ this.templates = this.settings.templates;
355
+ this.plugins = this.settings.plugins;
356
+
357
+ // translations : english << 'lang_code' << custom
358
+ if (QueryBuilder.regional['en'] === undefined) {
359
+ Utils.error('Config', '"i18n/en.js" not loaded.');
360
+ }
361
+ this.lang = $.extendext(true, 'replace', {}, QueryBuilder.regional['en'], QueryBuilder.regional[this.settings.lang_code], this.settings.lang);
362
+
363
+ // init templates
364
+ Object.keys(this.templates).forEach(function(tpl) {
365
+ if (!this.templates[tpl]) {
366
+ this.templates[tpl] = QueryBuilder.templates[tpl];
367
+ }
368
+ if (typeof this.templates[tpl] == 'string') {
369
+ this.templates[tpl] = doT.template(this.templates[tpl]);
370
+ }
371
+ }, this);
372
+
373
+ // ensure we have a container id
374
+ if (!this.$el.attr('id')) {
375
+ this.$el.attr('id', 'qb_' + Math.floor(Math.random() * 99999));
376
+ this.status.generated_id = true;
377
+ }
378
+ this.status.id = this.$el.attr('id');
379
+
380
+ // INIT
381
+ this.$el.addClass('query-builder form-inline');
382
+
383
+ this.filters = this.checkFilters(this.filters);
384
+ this.operators = this.checkOperators(this.operators);
385
+ this.bindEvents();
386
+ this.initPlugins();
387
+
388
+ this.trigger('afterInit');
389
+
390
+ if (options.rules) {
391
+ this.setRules(options.rules);
392
+ delete this.settings.rules;
393
+ }
394
+ else {
395
+ this.setRoot(true);
396
+ }
397
+ };
398
+
399
+ /**
400
+ * Checks the configuration of each filter
401
+ * @throws ConfigError
402
+ */
403
+ QueryBuilder.prototype.checkFilters = function(filters) {
404
+ var definedFilters = [];
405
+
406
+ if (!filters || filters.length === 0) {
407
+ Utils.error('Config', 'Missing filters list');
408
+ }
409
+
410
+ filters.forEach(function(filter, i) {
411
+ if (!filter.id) {
412
+ Utils.error('Config', 'Missing filter {0} id', i);
413
+ }
414
+ if (definedFilters.indexOf(filter.id) != -1) {
415
+ Utils.error('Config', 'Filter "{0}" already defined', filter.id);
416
+ }
417
+ definedFilters.push(filter.id);
418
+
419
+ if (!filter.type) {
420
+ filter.type = 'string';
421
+ }
422
+ else if (!QueryBuilder.types[filter.type]) {
423
+ Utils.error('Config', 'Invalid type "{0}"', filter.type);
424
+ }
425
+
426
+ if (!filter.input) {
427
+ filter.input = 'text';
428
+ }
429
+ else if (typeof filter.input != 'function' && QueryBuilder.inputs.indexOf(filter.input) == -1) {
430
+ Utils.error('Config', 'Invalid input "{0}"', filter.input);
431
+ }
432
+
433
+ if (filter.operators) {
434
+ filter.operators.forEach(function(operator) {
435
+ if (typeof operator != 'string') {
436
+ Utils.error('Config', 'Filter operators must be global operators types (string)');
437
+ }
438
+ });
439
+ }
440
+
441
+ if (!filter.field) {
442
+ filter.field = filter.id;
443
+ }
444
+ if (!filter.label) {
445
+ filter.label = filter.field;
446
+ }
447
+
448
+ if (!filter.optgroup) {
449
+ filter.optgroup = null;
450
+ }
451
+ else {
452
+ this.status.has_optgroup = true;
453
+
454
+ // register optgroup if needed
455
+ if (!this.settings.optgroups[filter.optgroup]) {
456
+ this.settings.optgroups[filter.optgroup] = filter.optgroup;
457
+ }
458
+ }
459
+
460
+ switch (filter.input) {
461
+ case 'radio': case 'checkbox':
462
+ if (!filter.values || filter.values.length < 1) {
463
+ Utils.error('Config', 'Missing filter "{0}" values', filter.id);
464
+ }
465
+ break;
466
+
467
+ case 'select':
468
+ if (filter.placeholder) {
469
+ if (filter.placeholder_value === undefined) {
470
+ filter.placeholder_value = -1;
471
+ }
472
+ Utils.iterateOptions(filter.values, function(key) {
473
+ if (key == filter.placeholder_value) {
474
+ Utils.error('Config', 'Placeholder of filter "{0}" overlaps with one of its values', filter.id);
475
+ }
476
+ });
477
+ }
478
+ break;
479
+ }
480
+ }, this);
481
+
482
+ if (this.settings.sort_filters) {
483
+ if (typeof this.settings.sort_filters == 'function') {
484
+ filters.sort(this.settings.sort_filters);
485
+ }
486
+ else {
487
+ var self = this;
488
+ filters.sort(function(a, b) {
489
+ return self.translateLabel(a.label).localeCompare(self.translateLabel(b.label));
490
+ });
491
+ }
492
+ }
493
+
494
+ if (this.status.has_optgroup) {
495
+ filters = Utils.groupSort(filters, 'optgroup');
496
+ }
497
+
498
+ return filters;
499
+ };
500
+
501
+ /**
502
+ * Checks the configuration of each operator
503
+ * @throws ConfigError
504
+ */
505
+ QueryBuilder.prototype.checkOperators = function(operators) {
506
+ var definedOperators = [];
507
+
508
+ operators.forEach(function(operator, i) {
509
+ if (typeof operator == 'string') {
510
+ if (!QueryBuilder.OPERATORS[operator]) {
511
+ Utils.error('Config', 'Unknown operator "{0}"', operator);
512
+ }
513
+
514
+ operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator]);
515
+ }
516
+ else {
517
+ if (!operator.type) {
518
+ Utils.error('Config', 'Missing "type" for operator {0}', i);
519
+ }
520
+
521
+ if (QueryBuilder.OPERATORS[operator.type]) {
522
+ operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator.type], operator);
523
+ }
524
+
525
+ if (operator.nb_inputs === undefined || operator.apply_to === undefined) {
526
+ Utils.error('Config', 'Missing "nb_inputs" and/or "apply_to" for operator "{0}"', operator.type);
527
+ }
528
+ }
529
+
530
+ if (definedOperators.indexOf(operator.type) != -1) {
531
+ Utils.error('Config', 'Operator "{0}" already defined', operator.type);
532
+ }
533
+ definedOperators.push(operator.type);
534
+
535
+ if (!operator.optgroup) {
536
+ operator.optgroup = null;
537
+ }
538
+ else {
539
+ this.status.has_operator_optgroup = true;
540
+
541
+ // register optgroup if needed
542
+ if (!this.settings.optgroups[operator.optgroup]) {
543
+ this.settings.optgroups[operator.optgroup] = operator.optgroup;
544
+ }
545
+ }
546
+ }, this);
547
+
548
+ if (this.status.has_operator_optgroup) {
549
+ operators = Utils.groupSort(operators, 'optgroup');
550
+ }
551
+
552
+ return operators;
553
+ };
554
+
555
+ /**
556
+ * Add all events listeners
557
+ */
558
+ QueryBuilder.prototype.bindEvents = function() {
559
+ var self = this;
560
+
561
+ // group condition change
562
+ this.$el.on('change.queryBuilder', Selectors.group_condition, function() {
563
+ if ($(this).is(':checked')) {
564
+ var $group = $(this).closest(Selectors.group_container);
565
+ Model($group).condition = $(this).val();
566
+ }
567
+ });
568
+
569
+ // rule filter change
570
+ this.$el.on('change.queryBuilder', Selectors.rule_filter, function() {
571
+ var $rule = $(this).closest(Selectors.rule_container);
572
+ Model($rule).filter = self.getFilterById($(this).val());
573
+ });
574
+
575
+ // rule operator change
576
+ this.$el.on('change.queryBuilder', Selectors.rule_operator, function() {
577
+ var $rule = $(this).closest(Selectors.rule_container);
578
+ Model($rule).operator = self.getOperatorByType($(this).val());
579
+ });
580
+
581
+ // add rule button
582
+ this.$el.on('click.queryBuilder', Selectors.add_rule, function() {
583
+ var $group = $(this).closest(Selectors.group_container);
584
+ self.addRule(Model($group));
585
+ });
586
+
587
+ // delete rule button
588
+ this.$el.on('click.queryBuilder', Selectors.delete_rule, function() {
589
+ var $rule = $(this).closest(Selectors.rule_container);
590
+ self.deleteRule(Model($rule));
591
+ });
592
+
593
+ if (this.settings.allow_groups !== 0) {
594
+ // add group button
595
+ this.$el.on('click.queryBuilder', Selectors.add_group, function() {
596
+ var $group = $(this).closest(Selectors.group_container);
597
+ self.addGroup(Model($group));
598
+ });
599
+
600
+ // delete group button
601
+ this.$el.on('click.queryBuilder', Selectors.delete_group, function() {
602
+ var $group = $(this).closest(Selectors.group_container);
603
+ self.deleteGroup(Model($group));
604
+ });
605
+ }
606
+
607
+ // model events
608
+ this.model.on({
609
+ 'drop': function(e, node) {
610
+ node.$el.remove();
611
+ self.refreshGroupsConditions();
612
+ },
613
+ 'add': function(e, node, index) {
614
+ if (index === 0) {
615
+ node.$el.prependTo(node.parent.$el.find('>' + Selectors.rules_list));
616
+ }
617
+ else {
618
+ node.$el.insertAfter(node.parent.rules[index - 1].$el);
619
+ }
620
+ self.refreshGroupsConditions();
621
+ },
622
+ 'move': function(e, node, group, index) {
623
+ node.$el.detach();
624
+
625
+ if (index === 0) {
626
+ node.$el.prependTo(group.$el.find('>' + Selectors.rules_list));
627
+ }
628
+ else {
629
+ node.$el.insertAfter(group.rules[index - 1].$el);
630
+ }
631
+ self.refreshGroupsConditions();
632
+ },
633
+ 'update': function(e, node, field, value, oldValue) {
634
+ if (node instanceof Rule) {
635
+ switch (field) {
636
+ case 'error':
637
+ self.displayError(node);
638
+ break;
639
+
640
+ case 'flags':
641
+ self.applyRuleFlags(node);
642
+ break;
643
+
644
+ case 'filter':
645
+ self.updateRuleFilter(node);
646
+ break;
647
+
648
+ case 'operator':
649
+ self.updateRuleOperator(node, oldValue);
650
+ break;
651
+
652
+ case 'value':
653
+ self.updateRuleValue(node);
654
+ break;
655
+ }
656
+ }
657
+ else {
658
+ switch (field) {
659
+ case 'error':
660
+ self.displayError(node);
661
+ break;
662
+
663
+ case 'flags':
664
+ self.applyGroupFlags(node);
665
+ break;
666
+
667
+ case 'condition':
668
+ self.updateGroupCondition(node);
669
+ break;
670
+ }
671
+ }
672
+ }
673
+ });
674
+ };
675
+
676
+ /**
677
+ * Create the root group
678
+ * @param addRule {bool,optional} add a default empty rule
679
+ * @param data {mixed,optional} group custom data
680
+ * @param flags {object,optional} flags to apply to the group
681
+ * @return group {Root}
682
+ */
683
+ QueryBuilder.prototype.setRoot = function(addRule, data, flags) {
684
+ addRule = (addRule === undefined || addRule === true);
685
+
686
+ var group_id = this.nextGroupId();
687
+ var $group = $(this.getGroupTemplate(group_id, 1));
688
+
689
+ this.$el.append($group);
690
+ this.model.root = new Group(null, $group);
691
+ this.model.root.model = this.model;
692
+
693
+ this.model.root.data = data;
694
+ this.model.root.flags = $.extend({}, this.settings.default_group_flags, flags);
695
+
696
+ this.trigger('afterAddGroup', this.model.root);
697
+
698
+ this.model.root.condition = this.settings.default_condition;
699
+
700
+ if (addRule) {
701
+ this.addRule(this.model.root);
702
+ }
703
+
704
+ return this.model.root;
705
+ };
706
+
707
+ /**
708
+ * Add a new group
709
+ * @param parent {Group}
710
+ * @param addRule {bool,optional} add a default empty rule
711
+ * @param data {mixed,optional} group custom data
712
+ * @param flags {object,optional} flags to apply to the group
713
+ * @return group {Group}
714
+ */
715
+ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) {
716
+ addRule = (addRule === undefined || addRule === true);
717
+
718
+ var level = parent.level + 1;
719
+
720
+ var e = this.trigger('beforeAddGroup', parent, addRule, level);
721
+ if (e.isDefaultPrevented()) {
722
+ return null;
723
+ }
724
+
725
+ var group_id = this.nextGroupId();
726
+ var $group = $(this.getGroupTemplate(group_id, level));
727
+ var model = parent.addGroup($group);
728
+
729
+ model.data = data;
730
+ model.flags = $.extend({}, this.settings.default_group_flags, flags);
731
+
732
+ this.trigger('afterAddGroup', model);
733
+
734
+ model.condition = this.settings.default_condition;
735
+
736
+ if (addRule) {
737
+ this.addRule(model);
738
+ }
739
+
740
+ return model;
741
+ };
742
+
743
+ /**
744
+ * Tries to delete a group. The group is not deleted if at least one rule is no_delete.
745
+ * @param group {Group}
746
+ * @return {boolean} true if the group has been deleted
747
+ */
748
+ QueryBuilder.prototype.deleteGroup = function(group) {
749
+ if (group.isRoot()) {
750
+ return false;
751
+ }
752
+
753
+ var e = this.trigger('beforeDeleteGroup', group);
754
+ if (e.isDefaultPrevented()) {
755
+ return false;
756
+ }
757
+
758
+ var del = true;
759
+
760
+ group.each('reverse', function(rule) {
761
+ del&= this.deleteRule(rule);
762
+ }, function(group) {
763
+ del&= this.deleteGroup(group);
764
+ }, this);
765
+
766
+ if (del) {
767
+ group.drop();
768
+ this.trigger('afterDeleteGroup');
769
+ }
770
+
771
+ return del;
772
+ };
773
+
774
+ /**
775
+ * Changes the condition of a group
776
+ * @param group {Group}
777
+ */
778
+ QueryBuilder.prototype.updateGroupCondition = function(group) {
779
+ group.$el.find('>' + Selectors.group_condition).each(function() {
780
+ var $this = $(this);
781
+ $this.prop('checked', $this.val() === group.condition);
782
+ $this.parent().toggleClass('active', $this.val() === group.condition);
783
+ });
784
+
785
+ this.trigger('afterUpdateGroupCondition', group);
786
+ };
787
+
788
+ /**
789
+ * Update visibility of conditions based on number of rules inside each group
790
+ */
791
+ QueryBuilder.prototype.refreshGroupsConditions = function() {
792
+ (function walk(group) {
793
+ if (!group.flags || (group.flags && !group.flags.condition_readonly)) {
794
+ group.$el.find('>' + Selectors.group_condition).prop('disabled', group.rules.length <= 1)
795
+ .parent().toggleClass('disabled', group.rules.length <= 1);
796
+ }
797
+
798
+ group.each(function(rule) {}, function(group) {
799
+ walk(group);
800
+ }, this);
801
+ }(this.model.root));
802
+ };
803
+
804
+ /**
805
+ * Add a new rule
806
+ * @param parent {Group}
807
+ * @param data {mixed,optional} rule custom data
808
+ * @param flags {object,optional} flags to apply to the rule
809
+ * @return rule {Rule}
810
+ */
811
+ QueryBuilder.prototype.addRule = function(parent, data, flags) {
812
+ var e = this.trigger('beforeAddRule', parent);
813
+ if (e.isDefaultPrevented()) {
814
+ return null;
815
+ }
816
+
817
+ var rule_id = this.nextRuleId();
818
+ var $rule = $(this.getRuleTemplate(rule_id));
819
+ var model = parent.addRule($rule);
820
+
821
+ if (data !== undefined) {
822
+ model.data = data;
823
+ }
824
+
825
+ model.flags = $.extend({}, this.settings.default_rule_flags, flags);
826
+
827
+ this.trigger('afterAddRule', model);
828
+
829
+ this.createRuleFilters(model);
830
+
831
+ if (this.settings.default_filter || !this.settings.display_empty_filter) {
832
+ model.filter = this.getFilterById(this.settings.default_filter || this.filters[0].id);
833
+ }
834
+
835
+ return model;
836
+ };
837
+
838
+ /**
839
+ * Delete a rule.
840
+ * @param rule {Rule}
841
+ * @return {boolean} true if the rule has been deleted
842
+ */
843
+ QueryBuilder.prototype.deleteRule = function(rule) {
844
+ if (rule.flags.no_delete) {
845
+ return false;
846
+ }
847
+
848
+ var e = this.trigger('beforeDeleteRule', rule);
849
+ if (e.isDefaultPrevented()) {
850
+ return false;
851
+ }
852
+
853
+ rule.drop();
854
+
855
+ this.trigger('afterDeleteRule');
856
+
857
+ return true;
858
+ };
859
+
860
+ /**
861
+ * Create the filters <select> for a rule
862
+ * @param rule {Rule}
863
+ */
864
+ QueryBuilder.prototype.createRuleFilters = function(rule) {
865
+ var filters = this.change('getRuleFilters', this.filters, rule);
866
+ var $filterSelect = $(this.getRuleFilterSelect(rule, filters));
867
+
868
+ rule.$el.find(Selectors.filter_container).html($filterSelect);
869
+
870
+ this.trigger('afterCreateRuleFilters', rule);
871
+ };
872
+
873
+ /**
874
+ * Create the operators <select> for a rule and init the rule operator
875
+ * @param rule {Rule}
876
+ */
877
+ QueryBuilder.prototype.createRuleOperators = function(rule) {
878
+ var $operatorContainer = rule.$el.find(Selectors.operator_container).empty();
879
+
880
+ if (!rule.filter) {
881
+ return;
882
+ }
883
+
884
+ var operators = this.getOperators(rule.filter);
885
+ var $operatorSelect = $(this.getRuleOperatorSelect(rule, operators));
886
+
887
+ $operatorContainer.html($operatorSelect);
888
+
889
+ // set the operator without triggering update event
890
+ rule.__.operator = operators[0];
891
+
892
+ this.trigger('afterCreateRuleOperators', rule, operators);
893
+ };
894
+
895
+ /**
896
+ * Create the main input for a rule
897
+ * @param rule {Rule}
898
+ */
899
+ QueryBuilder.prototype.createRuleInput = function(rule) {
900
+ var $valueContainer = rule.$el.find(Selectors.value_container).empty();
901
+
902
+ rule.__.value = undefined;
903
+
904
+ if (!rule.filter || !rule.operator || rule.operator.nb_inputs === 0) {
905
+ return;
906
+ }
907
+
908
+ var self = this;
909
+ var $inputs = $();
910
+ var filter = rule.filter;
911
+
912
+ for (var i = 0; i < rule.operator.nb_inputs; i++) {
913
+ var $ruleInput = $(this.getRuleInput(rule, i));
914
+ if (i > 0) $valueContainer.append(this.settings.inputs_separator);
915
+ $valueContainer.append($ruleInput);
916
+ $inputs = $inputs.add($ruleInput);
917
+ }
918
+
919
+ $valueContainer.show();
920
+
921
+ $inputs.on('change ' + (filter.input_event || ''), function() {
922
+ self.status.updating_value = true;
923
+ rule.value = self.getRuleValue(rule);
924
+ self.status.updating_value = false;
925
+ });
926
+
927
+ if (filter.plugin) {
928
+ $inputs[filter.plugin](filter.plugin_config || {});
929
+ }
930
+
931
+ this.trigger('afterCreateRuleInput', rule);
932
+
933
+ if (filter.default_value !== undefined) {
934
+ rule.value = filter.default_value;
935
+ }
936
+ else {
937
+ self.status.updating_value = true;
938
+ rule.value = self.getRuleValue(rule);
939
+ self.status.updating_value = false;
940
+ }
941
+ };
942
+
943
+ /**
944
+ * Perform action when rule's filter is changed
945
+ * @param rule {Rule}
946
+ */
947
+ QueryBuilder.prototype.updateRuleFilter = function(rule) {
948
+ this.createRuleOperators(rule);
949
+ this.createRuleInput(rule);
950
+
951
+ rule.$el.find(Selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
952
+
953
+ this.trigger('afterUpdateRuleFilter', rule);
954
+ };
955
+
956
+ /**
957
+ * Update main <input> visibility when rule operator changes
958
+ * @param rule {Rule}
959
+ * @param previousOperator {object}
960
+ */
961
+ QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) {
962
+ var $valueContainer = rule.$el.find(Selectors.value_container);
963
+
964
+ if (!rule.operator || rule.operator.nb_inputs === 0) {
965
+ $valueContainer.hide();
966
+
967
+ rule.__.value = undefined;
968
+ }
969
+ else {
970
+ $valueContainer.show();
971
+
972
+ if ($valueContainer.is(':empty') || rule.operator.nb_inputs !== previousOperator.nb_inputs) {
973
+ this.createRuleInput(rule);
974
+ }
975
+ }
976
+
977
+ if (rule.operator) {
978
+ rule.$el.find(Selectors.rule_operator).val(rule.operator.type);
979
+ }
980
+
981
+ this.trigger('afterUpdateRuleOperator', rule);
982
+ };
983
+
984
+ /**
985
+ * Perform action when rule's value is changed
986
+ * @param rule {Rule}
987
+ */
988
+ QueryBuilder.prototype.updateRuleValue = function(rule) {
989
+ if (!this.status.updating_value) {
990
+ this.setRuleValue(rule, rule.value);
991
+ }
992
+
993
+ this.trigger('afterUpdateRuleValue', rule);
994
+ };
995
+
996
+ /**
997
+ * Change rules properties depending on flags.
998
+ * @param rule {Rule}
999
+ */
1000
+ QueryBuilder.prototype.applyRuleFlags = function(rule) {
1001
+ var flags = rule.flags;
1002
+
1003
+ if (flags.filter_readonly) {
1004
+ rule.$el.find(Selectors.rule_filter).prop('disabled', true);
1005
+ }
1006
+ if (flags.operator_readonly) {
1007
+ rule.$el.find(Selectors.rule_operator).prop('disabled', true);
1008
+ }
1009
+ if (flags.value_readonly) {
1010
+ rule.$el.find(Selectors.rule_value).prop('disabled', true);
1011
+ }
1012
+ if (flags.no_delete) {
1013
+ rule.$el.find(Selectors.delete_rule).remove();
1014
+ }
1015
+
1016
+ this.trigger('afterApplyRuleFlags', rule);
1017
+ };
1018
+
1019
+ /**
1020
+ * Change group properties depending on flags.
1021
+ * @param group {Group}
1022
+ */
1023
+ QueryBuilder.prototype.applyGroupFlags = function(group) {
1024
+ var flags = group.flags;
1025
+
1026
+ if (flags.condition_readonly) {
1027
+ group.$el.find('>' + Selectors.group_condition).prop('disabled', true)
1028
+ .parent().addClass('readonly');
1029
+ }
1030
+ if (flags.no_delete) {
1031
+ group.$el.find(Selectors.delete_group).remove();
1032
+ }
1033
+
1034
+ this.trigger('afterApplyGroupFlags', group);
1035
+ };
1036
+
1037
+ /**
1038
+ * Clear all errors markers
1039
+ * @param node {Node,optional} default is root Group
1040
+ */
1041
+ QueryBuilder.prototype.clearErrors = function(node) {
1042
+ node = node || this.model.root;
1043
+
1044
+ if (!node) {
1045
+ return;
1046
+ }
1047
+
1048
+ node.error = null;
1049
+
1050
+ if (node instanceof Group) {
1051
+ node.each(function(rule) {
1052
+ rule.error = null;
1053
+ }, function(group) {
1054
+ this.clearErrors(group);
1055
+ }, this);
1056
+ }
1057
+ };
1058
+
1059
+ /**
1060
+ * Add/Remove class .has-error and update error title
1061
+ * @param node {Node}
1062
+ */
1063
+ QueryBuilder.prototype.displayError = function(node) {
1064
+ if (this.settings.display_errors) {
1065
+ if (node.error === null) {
1066
+ node.$el.removeClass('has-error');
1067
+ }
1068
+ else {
1069
+ // translate the text without modifying event array
1070
+ var error = $.extend([], node.error, [
1071
+ this.lang.errors[node.error[0]] || node.error[0]
1072
+ ]);
1073
+
1074
+ node.$el.addClass('has-error')
1075
+ .find(Selectors.error_container).eq(0)
1076
+ .attr('title', Utils.fmt.apply(null, error));
1077
+ }
1078
+ }
1079
+ };
1080
+
1081
+ /**
1082
+ * Trigger a validation error event
1083
+ * @param node {Node}
1084
+ * @param error {array}
1085
+ * @param value {mixed}
1086
+ */
1087
+ QueryBuilder.prototype.triggerValidationError = function(node, error, value) {
1088
+ if (!$.isArray(error)) {
1089
+ error = [error];
1090
+ }
1091
+
1092
+ var e = this.trigger('validationError', node, error, value);
1093
+ if (!e.isDefaultPrevented()) {
1094
+ node.error = error;
1095
+ }
1096
+ };
1097
+
1098
+
1099
+ /**
1100
+ * Destroy the plugin
1101
+ */
1102
+ QueryBuilder.prototype.destroy = function() {
1103
+ this.trigger('beforeDestroy');
1104
+
1105
+ if (this.status.generated_id) {
1106
+ this.$el.removeAttr('id');
1107
+ }
1108
+
1109
+ this.clear();
1110
+ this.model = null;
1111
+
1112
+ this.$el
1113
+ .off('.queryBuilder')
1114
+ .removeClass('query-builder')
1115
+ .removeData('queryBuilder');
1116
+
1117
+ delete this.$el[0].queryBuilder;
1118
+ };
1119
+
1120
+ /**
1121
+ * Reset the plugin
1122
+ */
1123
+ QueryBuilder.prototype.reset = function() {
1124
+ this.status.group_id = 1;
1125
+ this.status.rule_id = 0;
1126
+
1127
+ this.model.root.empty();
1128
+
1129
+ this.addRule(this.model.root);
1130
+
1131
+ this.trigger('afterReset');
1132
+ };
1133
+
1134
+ /**
1135
+ * Clear the plugin
1136
+ */
1137
+ QueryBuilder.prototype.clear = function() {
1138
+ this.status.group_id = 0;
1139
+ this.status.rule_id = 0;
1140
+
1141
+ if (this.model.root) {
1142
+ this.model.root.drop();
1143
+ this.model.root = null;
1144
+ }
1145
+
1146
+ this.trigger('afterClear');
1147
+ };
1148
+
1149
+ /**
1150
+ * Modify the builder configuration
1151
+ * Only options defined in QueryBuilder.modifiable_options are modifiable
1152
+ * @param {object}
1153
+ */
1154
+ QueryBuilder.prototype.setOptions = function(options) {
1155
+ // use jQuery utils to filter options keys
1156
+ $.makeArray($(Object.keys(options)).filter(QueryBuilder.modifiable_options))
1157
+ .forEach(function(opt) {
1158
+ this.settings[opt] = options[opt];
1159
+ }, this);
1160
+ };
1161
+
1162
+ /**
1163
+ * Return the model associated to a DOM object, or root model
1164
+ * @param {jQuery,optional}
1165
+ * @return {Node}
1166
+ */
1167
+ QueryBuilder.prototype.getModel = function(target) {
1168
+ return !target ? this.model.root : Model(target);
1169
+ };
1170
+
1171
+ /**
1172
+ * Validate the whole builder
1173
+ * @return {boolean}
1174
+ */
1175
+ QueryBuilder.prototype.validate = function() {
1176
+ this.clearErrors();
1177
+
1178
+ var self = this;
1179
+
1180
+ var valid = (function parse(group) {
1181
+ var done = 0;
1182
+ var errors = 0;
1183
+
1184
+ group.each(function(rule) {
1185
+ if (!rule.filter) {
1186
+ self.triggerValidationError(rule, 'no_filter', null);
1187
+ errors++;
1188
+ return;
1189
+ }
1190
+
1191
+ if (rule.operator.nb_inputs !== 0) {
1192
+ var valid = self.validateValue(rule, rule.value);
1193
+
1194
+ if (valid !== true) {
1195
+ self.triggerValidationError(rule, valid, rule.value);
1196
+ errors++;
1197
+ return;
1198
+ }
1199
+ }
1200
+
1201
+ done++;
1202
+
1203
+ }, function(group) {
1204
+ if (parse(group)) {
1205
+ done++;
1206
+ }
1207
+ else {
1208
+ errors++;
1209
+ }
1210
+ });
1211
+
1212
+ if (errors > 0) {
1213
+ return false;
1214
+ }
1215
+ else if (done === 0 && (!self.settings.allow_empty || !group.isRoot())) {
1216
+ self.triggerValidationError(group, 'empty_group', null);
1217
+ return false;
1218
+ }
1219
+
1220
+ return true;
1221
+
1222
+ }(this.model.root));
1223
+
1224
+ return this.change('validate', valid);
1225
+ };
1226
+
1227
+ /**
1228
+ * Get an object representing current rules
1229
+ * @param {object} options
1230
+ * - get_flags: false[default] | true(only changes from default flags) | 'all'
1231
+ * @return {object}
1232
+ */
1233
+ QueryBuilder.prototype.getRules = function(options) {
1234
+ options = $.extend({
1235
+ get_flags: false
1236
+ }, options);
1237
+
1238
+ if (!this.validate()) {
1239
+ return {};
1240
+ }
1241
+
1242
+ var self = this;
1243
+
1244
+ var out = (function parse(group) {
1245
+ var data = {
1246
+ condition: group.condition,
1247
+ rules: []
1248
+ };
1249
+
1250
+ if (group.data) {
1251
+ data.data = $.extendext(true, 'replace', {}, group.data);
1252
+ }
1253
+
1254
+ if (options.get_flags) {
1255
+ var flags = self.getGroupFlags(group.flags, options.get_flags === 'all');
1256
+ if (!$.isEmptyObject(flags)) {
1257
+ data.flags = flags;
1258
+ }
1259
+ }
1260
+
1261
+ group.each(function(model) {
1262
+ var value = null;
1263
+ if (model.operator.nb_inputs !== 0) {
1264
+ value = model.value;
1265
+ }
1266
+
1267
+ var rule = {
1268
+ id: model.filter.id,
1269
+ field: model.filter.field,
1270
+ type: model.filter.type,
1271
+ input: model.filter.input,
1272
+ operator: model.operator.type,
1273
+ value: value
1274
+ };
1275
+
1276
+ if (model.filter.data || model.data) {
1277
+ rule.data = $.extendext(true, 'replace', {}, model.filter.data, model.data);
1278
+ }
1279
+
1280
+ if (options.get_flags) {
1281
+ var flags = self.getRuleFlags(model.flags, options.get_flags === 'all');
1282
+ if (!$.isEmptyObject(flags)) {
1283
+ rule.flags = flags;
1284
+ }
1285
+ }
1286
+
1287
+ data.rules.push(rule);
1288
+
1289
+ }, function(model) {
1290
+ data.rules.push(parse(model));
1291
+ });
1292
+
1293
+ return data;
1294
+
1295
+ }(this.model.root));
1296
+
1297
+ return this.change('getRules', out);
1298
+ };
1299
+
1300
+ /**
1301
+ * Set rules from object
1302
+ * @throws RulesError, UndefinedConditionError
1303
+ * @param data {object}
1304
+ */
1305
+ QueryBuilder.prototype.setRules = function(data) {
1306
+ if ($.isArray(data)) {
1307
+ data = {
1308
+ condition: this.settings.default_condition,
1309
+ rules: data
1310
+ };
1311
+ }
1312
+
1313
+ if (!data || !data.rules || (data.rules.length === 0 && !this.settings.allow_empty)) {
1314
+ Utils.error('RulesParse', 'Incorrect data object passed');
1315
+ }
1316
+
1317
+ this.clear();
1318
+ this.setRoot(false, data.data, this.parseGroupFlags(data));
1319
+
1320
+ data = this.change('setRules', data);
1321
+
1322
+ var self = this;
1323
+
1324
+ (function add(data, group) {
1325
+ if (group === null) {
1326
+ return;
1327
+ }
1328
+
1329
+ if (data.condition === undefined) {
1330
+ data.condition = self.settings.default_condition;
1331
+ }
1332
+ else if (self.settings.conditions.indexOf(data.condition) == -1) {
1333
+ Utils.error('UndefinedCondition', 'Invalid condition "{0}"', data.condition);
1334
+ }
1335
+
1336
+ group.condition = data.condition;
1337
+
1338
+ data.rules.forEach(function(item) {
1339
+ var model;
1340
+ if (item.rules && item.rules.length > 0) {
1341
+ if (self.settings.allow_groups !== -1 && self.settings.allow_groups < group.level) {
1342
+ self.reset();
1343
+ Utils.error('RulesParse', 'No more than {0} groups are allowed', self.settings.allow_groups);
1344
+ }
1345
+ else {
1346
+ model = self.addGroup(group, false, item.data, self.parseGroupFlags(item));
1347
+ if (model === null) {
1348
+ return;
1349
+ }
1350
+
1351
+ add(item, model);
1352
+ }
1353
+ }
1354
+ else {
1355
+ if (item.id === undefined) {
1356
+ Utils.error('RulesParse', 'Missing rule field id');
1357
+ }
1358
+ if (item.operator === undefined) {
1359
+ item.operator = 'equal';
1360
+ }
1361
+
1362
+ model = self.addRule(group, item.data);
1363
+ if (model === null) {
1364
+ return;
1365
+ }
1366
+
1367
+ model.filter = self.getFilterById(item.id);
1368
+ model.operator = self.getOperatorByType(item.operator);
1369
+ model.flags = self.parseRuleFlags(item);
1370
+
1371
+ if (model.operator.nb_inputs !== 0 && item.value !== undefined) {
1372
+ model.value = item.value;
1373
+ }
1374
+ }
1375
+ });
1376
+
1377
+ }(data, this.model.root));
1378
+ };
1379
+
1380
+
1381
+ /**
1382
+ * Check if a value is correct for a filter
1383
+ * @param rule {Rule}
1384
+ * @param value {string|string[]|undefined}
1385
+ * @return {array|true}
1386
+ */
1387
+ QueryBuilder.prototype.validateValue = function(rule, value) {
1388
+ var validation = rule.filter.validation || {};
1389
+ var result = true;
1390
+
1391
+ if (validation.callback) {
1392
+ result = validation.callback.call(this, value, rule);
1393
+ }
1394
+ else {
1395
+ result = this.validateValueInternal(rule, value);
1396
+ }
1397
+
1398
+ return this.change('validateValue', result, value, rule);
1399
+ };
1400
+
1401
+ /**
1402
+ * Default validation function
1403
+ * @throws ConfigError
1404
+ * @param rule {Rule}
1405
+ * @param value {string|string[]|undefined}
1406
+ * @return {array|true}
1407
+ */
1408
+ QueryBuilder.prototype.validateValueInternal = function(rule, value) {
1409
+ var filter = rule.filter;
1410
+ var operator = rule.operator;
1411
+ var validation = filter.validation || {};
1412
+ var result = true;
1413
+ var tmp;
1414
+
1415
+ if (rule.operator.nb_inputs === 1) {
1416
+ value = [value];
1417
+ }
1418
+ else {
1419
+ value = value;
1420
+ }
1421
+
1422
+ for (var i = 0; i < operator.nb_inputs; i++) {
1423
+ switch (filter.input) {
1424
+ case 'radio':
1425
+ if (value[i] === undefined) {
1426
+ result = ['radio_empty'];
1427
+ break;
1428
+ }
1429
+ break;
1430
+
1431
+ case 'checkbox':
1432
+ if (value[i] === undefined || value[i].length === 0) {
1433
+ result = ['checkbox_empty'];
1434
+ break;
1435
+ }
1436
+ else if (!operator.multiple && value[i].length > 1) {
1437
+ result = ['operator_not_multiple', operator.type];
1438
+ break;
1439
+ }
1440
+ break;
1441
+
1442
+ case 'select':
1443
+ if (filter.multiple) {
1444
+ if (value[i] === undefined || value[i].length === 0 || (filter.placeholder && value[i] == filter.placeholder_value)) {
1445
+ result = ['select_empty'];
1446
+ break;
1447
+ }
1448
+ else if (!operator.multiple && value[i].length > 1) {
1449
+ result = ['operator_not_multiple', operator.type];
1450
+ break;
1451
+ }
1452
+ }
1453
+ else {
1454
+ if (value[i] === undefined || (filter.placeholder && value[i] == filter.placeholder_value)) {
1455
+ result = ['select_empty'];
1456
+ break;
1457
+ }
1458
+ }
1459
+ break;
1460
+
1461
+ default:
1462
+ switch (QueryBuilder.types[filter.type]) {
1463
+ case 'string':
1464
+ if (value[i] === undefined || value[i].length === 0) {
1465
+ result = ['string_empty'];
1466
+ break;
1467
+ }
1468
+ if (validation.min !== undefined) {
1469
+ if (value[i].length < parseInt(validation.min)) {
1470
+ result = ['string_exceed_min_length', validation.min];
1471
+ break;
1472
+ }
1473
+ }
1474
+ if (validation.max !== undefined) {
1475
+ if (value[i].length > parseInt(validation.max)) {
1476
+ result = ['string_exceed_max_length', validation.max];
1477
+ break;
1478
+ }
1479
+ }
1480
+ if (validation.format) {
1481
+ if (typeof validation.format == 'string') {
1482
+ validation.format = new RegExp(validation.format);
1483
+ }
1484
+ if (!validation.format.test(value[i])) {
1485
+ result = ['string_invalid_format', validation.format];
1486
+ break;
1487
+ }
1488
+ }
1489
+ break;
1490
+
1491
+ case 'number':
1492
+ if (value[i] === undefined || isNaN(value[i])) {
1493
+ result = ['number_nan'];
1494
+ break;
1495
+ }
1496
+ if (filter.type == 'integer') {
1497
+ if (parseInt(value[i]) != value[i]) {
1498
+ result = ['number_not_integer'];
1499
+ break;
1500
+ }
1501
+ }
1502
+ else {
1503
+ if (parseFloat(value[i]) != value[i]) {
1504
+ result = ['number_not_double'];
1505
+ break;
1506
+ }
1507
+ }
1508
+ if (validation.min !== undefined) {
1509
+ if (value[i] < parseFloat(validation.min)) {
1510
+ result = ['number_exceed_min', validation.min];
1511
+ break;
1512
+ }
1513
+ }
1514
+ if (validation.max !== undefined) {
1515
+ if (value[i] > parseFloat(validation.max)) {
1516
+ result = ['number_exceed_max', validation.max];
1517
+ break;
1518
+ }
1519
+ }
1520
+ if (validation.step !== undefined && validation.step !== 'any') {
1521
+ var v = (value[i] / validation.step).toPrecision(14);
1522
+ if (parseInt(v) != v) {
1523
+ result = ['number_wrong_step', validation.step];
1524
+ break;
1525
+ }
1526
+ }
1527
+ break;
1528
+
1529
+ case 'datetime':
1530
+ if (value[i] === undefined || value[i].length === 0) {
1531
+ result = ['datetime_empty'];
1532
+ break;
1533
+ }
1534
+
1535
+ // we need MomentJS
1536
+ if (validation.format) {
1537
+ if (!('moment' in window)) {
1538
+ Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
1539
+ }
1540
+
1541
+ var datetime = moment(value[i], validation.format);
1542
+ if (!datetime.isValid()) {
1543
+ result = ['datetime_invalid', validation.format];
1544
+ break;
1545
+ }
1546
+ else {
1547
+ if (validation.min) {
1548
+ if (datetime < moment(validation.min, validation.format)) {
1549
+ result = ['datetime_exceed_min', validation.min];
1550
+ break;
1551
+ }
1552
+ }
1553
+ if (validation.max) {
1554
+ if (datetime > moment(validation.max, validation.format)) {
1555
+ result = ['datetime_exceed_max', validation.max];
1556
+ break;
1557
+ }
1558
+ }
1559
+ }
1560
+ }
1561
+ break;
1562
+
1563
+ case 'boolean':
1564
+ tmp = value[i].trim().toLowerCase();
1565
+ if (tmp !== 'true' && tmp !== 'false' && tmp !== '1' && tmp !== '0' && value[i] !== 1 && value[i] !== 0) {
1566
+ result = ['boolean_not_valid'];
1567
+ break;
1568
+ }
1569
+ }
1570
+ }
1571
+
1572
+ if (result !== true) {
1573
+ break;
1574
+ }
1575
+ }
1576
+
1577
+ return result;
1578
+ };
1579
+
1580
+ /**
1581
+ * Returns an incremented group ID
1582
+ * @return {string}
1583
+ */
1584
+ QueryBuilder.prototype.nextGroupId = function() {
1585
+ return this.status.id + '_group_' + (this.status.group_id++);
1586
+ };
1587
+
1588
+ /**
1589
+ * Returns an incremented rule ID
1590
+ * @return {string}
1591
+ */
1592
+ QueryBuilder.prototype.nextRuleId = function() {
1593
+ return this.status.id + '_rule_' + (this.status.rule_id++);
1594
+ };
1595
+
1596
+ /**
1597
+ * Returns the operators for a filter
1598
+ * @param filter {string|object} (filter id name or filter object)
1599
+ * @return {object[]}
1600
+ */
1601
+ QueryBuilder.prototype.getOperators = function(filter) {
1602
+ if (typeof filter == 'string') {
1603
+ filter = this.getFilterById(filter);
1604
+ }
1605
+
1606
+ var result = [];
1607
+
1608
+ for (var i = 0, l = this.operators.length; i < l; i++) {
1609
+ // filter operators check
1610
+ if (filter.operators) {
1611
+ if (filter.operators.indexOf(this.operators[i].type) == -1) {
1612
+ continue;
1613
+ }
1614
+ }
1615
+ // type check
1616
+ else if (this.operators[i].apply_to.indexOf(QueryBuilder.types[filter.type]) == -1) {
1617
+ continue;
1618
+ }
1619
+
1620
+ result.push(this.operators[i]);
1621
+ }
1622
+
1623
+ // keep sort order defined for the filter
1624
+ if (filter.operators) {
1625
+ result.sort(function(a, b) {
1626
+ return filter.operators.indexOf(a.type) - filter.operators.indexOf(b.type);
1627
+ });
1628
+ }
1629
+
1630
+ return this.change('getOperators', result, filter);
1631
+ };
1632
+
1633
+ /**
1634
+ * Returns a particular filter by its id
1635
+ * @throws UndefinedFilterError
1636
+ * @param filterId {string}
1637
+ * @return {object|null}
1638
+ */
1639
+ QueryBuilder.prototype.getFilterById = function(id) {
1640
+ if (id == '-1') {
1641
+ return null;
1642
+ }
1643
+
1644
+ for (var i = 0, l = this.filters.length; i < l; i++) {
1645
+ if (this.filters[i].id == id) {
1646
+ return this.filters[i];
1647
+ }
1648
+ }
1649
+
1650
+ Utils.error('UndefinedFilter', 'Undefined filter "{0}"', id);
1651
+ };
1652
+
1653
+ /**
1654
+ * Return a particular operator by its type
1655
+ * @throws UndefinedOperatorError
1656
+ * @param type {string}
1657
+ * @return {object|null}
1658
+ */
1659
+ QueryBuilder.prototype.getOperatorByType = function(type) {
1660
+ if (type == '-1') {
1661
+ return null;
1662
+ }
1663
+
1664
+ for (var i = 0, l = this.operators.length; i < l; i++) {
1665
+ if (this.operators[i].type == type) {
1666
+ return this.operators[i];
1667
+ }
1668
+ }
1669
+
1670
+ Utils.error('UndefinedOperator', 'Undefined operator "{0}"', type);
1671
+ };
1672
+
1673
+ /**
1674
+ * Returns rule value
1675
+ * @param rule {Rule}
1676
+ * @return {mixed}
1677
+ */
1678
+ QueryBuilder.prototype.getRuleValue = function(rule) {
1679
+ var filter = rule.filter;
1680
+ var operator = rule.operator;
1681
+ var value = [];
1682
+
1683
+ if (filter.valueGetter) {
1684
+ value = filter.valueGetter.call(this, rule);
1685
+ }
1686
+ else {
1687
+ var $value = rule.$el.find(Selectors.value_container);
1688
+
1689
+ for (var i = 0; i < operator.nb_inputs; i++) {
1690
+ var name = Utils.escapeElementId(rule.id + '_value_' + i);
1691
+ var tmp;
1692
+
1693
+ switch (filter.input) {
1694
+ case 'radio':
1695
+ value.push($value.find('[name=' + name + ']:checked').val());
1696
+ break;
1697
+
1698
+ case 'checkbox':
1699
+ tmp = [];
1700
+ $value.find('[name=' + name + ']:checked').each(function() {
1701
+ tmp.push($(this).val());
1702
+ });
1703
+ value.push(tmp);
1704
+ break;
1705
+
1706
+ case 'select':
1707
+ if (filter.multiple) {
1708
+ tmp = [];
1709
+ $value.find('[name=' + name + '] option:selected').each(function() {
1710
+ tmp.push($(this).val());
1711
+ });
1712
+ value.push(tmp);
1713
+ }
1714
+ else {
1715
+ value.push($value.find('[name=' + name + '] option:selected').val());
1716
+ }
1717
+ break;
1718
+
1719
+ default:
1720
+ value.push($value.find('[name=' + name + ']').val());
1721
+ }
1722
+ }
1723
+
1724
+ if (operator.nb_inputs === 1) {
1725
+ value = value[0];
1726
+ }
1727
+
1728
+ // @deprecated
1729
+ if (filter.valueParser) {
1730
+ value = filter.valueParser.call(this, rule, value);
1731
+ }
1732
+ }
1733
+
1734
+ return this.change('getRuleValue', value, rule);
1735
+ };
1736
+
1737
+ /**
1738
+ * Sets the value of a rule.
1739
+ * @param rule {Rule}
1740
+ * @param value {mixed}
1741
+ */
1742
+ QueryBuilder.prototype.setRuleValue = function(rule, value) {
1743
+ var filter = rule.filter;
1744
+ var operator = rule.operator;
1745
+
1746
+ if (filter.valueSetter) {
1747
+ filter.valueSetter.call(this, rule, value);
1748
+ }
1749
+ else {
1750
+ var $value = rule.$el.find(Selectors.value_container);
1751
+
1752
+ if (operator.nb_inputs == 1) {
1753
+ value = [value];
1754
+ }
1755
+ else {
1756
+ value = value;
1757
+ }
1758
+
1759
+ for (var i = 0; i < operator.nb_inputs; i++) {
1760
+ var name = Utils.escapeElementId(rule.id + '_value_' + i);
1761
+
1762
+ switch (filter.input) {
1763
+ case 'radio':
1764
+ $value.find('[name=' + name + '][value="' + value[i] + '"]').prop('checked', true).trigger('change');
1765
+ break;
1766
+
1767
+ case 'checkbox':
1768
+ if (!$.isArray(value[i])) {
1769
+ value[i] = [value[i]];
1770
+ }
1771
+ value[i].forEach(function(value) {
1772
+ $value.find('[name=' + name + '][value="' + value + '"]').prop('checked', true).trigger('change');
1773
+ });
1774
+ break;
1775
+
1776
+ default:
1777
+ $value.find('[name=' + name + ']').val(value[i]).trigger('change');
1778
+ break;
1779
+ }
1780
+ }
1781
+ }
1782
+ };
1783
+
1784
+ /**
1785
+ * Clean rule flags.
1786
+ * @param rule {object}
1787
+ * @return {object}
1788
+ */
1789
+ QueryBuilder.prototype.parseRuleFlags = function(rule) {
1790
+ var flags = $.extend({}, this.settings.default_rule_flags);
1791
+
1792
+ if (rule.readonly) {
1793
+ $.extend(flags, {
1794
+ filter_readonly: true,
1795
+ operator_readonly: true,
1796
+ value_readonly: true,
1797
+ no_delete: true
1798
+ });
1799
+ }
1800
+
1801
+ if (rule.flags) {
1802
+ $.extend(flags, rule.flags);
1803
+ }
1804
+
1805
+ return this.change('parseRuleFlags', flags, rule);
1806
+ };
1807
+
1808
+ /**
1809
+ * Get a copy of flags of a rule.
1810
+ * @param {object} flags
1811
+ * @param {boolean} all - true to return all flags, false to return only changes from default
1812
+ * @returns {object}
1813
+ */
1814
+ QueryBuilder.prototype.getRuleFlags = function(flags, all) {
1815
+ if (all) {
1816
+ return $.extend({}, flags);
1817
+ }
1818
+ else {
1819
+ var ret = {};
1820
+ $.each(this.settings.default_rule_flags, function(key, value) {
1821
+ if (flags[key] !== value) {
1822
+ ret[key] = flags[key];
1823
+ }
1824
+ });
1825
+ return ret;
1826
+ }
1827
+ };
1828
+
1829
+ /**
1830
+ * Clean group flags.
1831
+ * @param group {object}
1832
+ * @return {object}
1833
+ */
1834
+ QueryBuilder.prototype.parseGroupFlags = function(group) {
1835
+ var flags = $.extend({}, this.settings.default_group_flags);
1836
+
1837
+ if (group.readonly) {
1838
+ $.extend(flags, {
1839
+ condition_readonly: true,
1840
+ no_delete: true
1841
+ });
1842
+ }
1843
+
1844
+ if (group.flags) {
1845
+ $.extend(flags, group.flags);
1846
+ }
1847
+
1848
+ return this.change('parseGroupFlags', flags, group);
1849
+ };
1850
+
1851
+ /**
1852
+ * Get a copy of flags of a group.
1853
+ * @param {object} flags
1854
+ * @param {boolean} all - true to return all flags, false to return only changes from default
1855
+ * @returns {object}
1856
+ */
1857
+ QueryBuilder.prototype.getGroupFlags = function(flags, all) {
1858
+ if (all) {
1859
+ return $.extend({}, flags);
1860
+ }
1861
+ else {
1862
+ var ret = {};
1863
+ $.each(this.settings.default_group_flags, function(key, value) {
1864
+ if (flags[key] !== value) {
1865
+ ret[key] = flags[key];
1866
+ }
1867
+ });
1868
+ return ret;
1869
+ }
1870
+ };
1871
+
1872
+ /**
1873
+ * Translate a label
1874
+ * @param label {string|object}
1875
+ * @return string
1876
+ */
1877
+ QueryBuilder.prototype.translateLabel = function(label) {
1878
+ return typeof label == 'object' ? (label[this.settings.lang_code] || label['en']) : label;
1879
+ };
1880
+
1881
+
1882
+ QueryBuilder.templates.group = '\
1883
+ <dl id="{{= it.group_id }}" class="rules-group-container"> \
1884
+ <dt class="rules-group-header"> \
1885
+ <div class="btn-group pull-right group-actions"> \
1886
+ <button type="button" class="btn btn-xs btn-success" data-add="rule"> \
1887
+ <i class="{{= it.icons.add_rule }}"></i> {{= it.lang.add_rule }} \
1888
+ </button> \
1889
+ {{? it.settings.allow_groups===-1 || it.settings.allow_groups>=it.level }} \
1890
+ <button type="button" class="btn btn-xs btn-success" data-add="group"> \
1891
+ <i class="{{= it.icons.add_group }}"></i> {{= it.lang.add_group }} \
1892
+ </button> \
1893
+ {{?}} \
1894
+ {{? it.level>1 }} \
1895
+ <button type="button" class="btn btn-xs btn-danger" data-delete="group"> \
1896
+ <i class="{{= it.icons.remove_group }}"></i> {{= it.lang.delete_group }} \
1897
+ </button> \
1898
+ {{?}} \
1899
+ </div> \
1900
+ <div class="btn-group group-conditions"> \
1901
+ {{~ it.conditions: condition }} \
1902
+ <label class="btn btn-xs btn-primary"> \
1903
+ <input type="radio" name="{{= it.group_id }}_cond" value="{{= condition }}"> {{= it.lang.conditions[condition] || condition }} \
1904
+ </label> \
1905
+ {{~}} \
1906
+ </div> \
1907
+ {{? it.settings.display_errors }} \
1908
+ <div class="error-container"><i class="{{= it.icons.error }}"></i></div> \
1909
+ {{?}} \
1910
+ </dt> \
1911
+ <dd class=rules-group-body> \
1912
+ <ul class=rules-list></ul> \
1913
+ </dd> \
1914
+ </dl>';
1915
+
1916
+ QueryBuilder.templates.rule = '\
1917
+ <li id="{{= it.rule_id }}" class="rule-container"> \
1918
+ <div class="rule-header"> \
1919
+ <div class="btn-group pull-right rule-actions"> \
1920
+ <button type="button" class="btn btn-xs btn-danger" data-delete="rule"> \
1921
+ <i class="{{= it.icons.remove_rule }}"></i> {{= it.lang.delete_rule }} \
1922
+ </button> \
1923
+ </div> \
1924
+ </div> \
1925
+ {{? it.settings.display_errors }} \
1926
+ <div class="error-container"><i class="{{= it.icons.error }}"></i></div> \
1927
+ {{?}} \
1928
+ <div class="rule-filter-container"></div> \
1929
+ <div class="rule-operator-container"></div> \
1930
+ <div class="rule-value-container"></div> \
1931
+ </li>';
1932
+
1933
+ QueryBuilder.templates.filterSelect = '\
1934
+ {{ var optgroup = null; }} \
1935
+ <select class="form-control" name="{{= it.rule.id }}_filter"> \
1936
+ {{? it.settings.display_empty_filter }} \
1937
+ <option value="-1">{{= it.settings.select_placeholder }}</option> \
1938
+ {{?}} \
1939
+ {{~ it.filters: filter }} \
1940
+ {{? optgroup !== filter.optgroup }} \
1941
+ {{? optgroup !== null }}</optgroup>{{?}} \
1942
+ {{? (optgroup = filter.optgroup) !== null }} \
1943
+ <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
1944
+ {{?}} \
1945
+ {{?}} \
1946
+ <option value="{{= filter.id }}">{{= it.translate(filter.label) }}</option> \
1947
+ {{~}} \
1948
+ {{? optgroup !== null }}</optgroup>{{?}} \
1949
+ </select>';
1950
+
1951
+ QueryBuilder.templates.operatorSelect = '\
1952
+ {{ var optgroup = null; }} \
1953
+ <select class="form-control" name="{{= it.rule.id }}_operator"> \
1954
+ {{~ it.operators: operator }} \
1955
+ {{? optgroup !== operator.optgroup }} \
1956
+ {{? optgroup !== null }}</optgroup>{{?}} \
1957
+ {{? (optgroup = operator.optgroup) !== null }} \
1958
+ <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
1959
+ {{?}} \
1960
+ {{?}} \
1961
+ <option value="{{= operator.type }}">{{= it.lang.operators[operator.type] || operator.type }}</option> \
1962
+ {{~}} \
1963
+ {{? optgroup !== null }}</optgroup>{{?}} \
1964
+ </select>';
1965
+
1966
+ /**
1967
+ * Returns group HTML
1968
+ * @param group_id {string}
1969
+ * @param level {int}
1970
+ * @return {string}
1971
+ */
1972
+ QueryBuilder.prototype.getGroupTemplate = function(group_id, level) {
1973
+ var h = this.templates.group({
1974
+ builder: this,
1975
+ group_id: group_id,
1976
+ level: level,
1977
+ conditions: this.settings.conditions,
1978
+ icons: this.icons,
1979
+ lang: this.lang,
1980
+ settings: this.settings
1981
+ });
1982
+
1983
+ return this.change('getGroupTemplate', h, level);
1984
+ };
1985
+
1986
+ /**
1987
+ * Returns rule HTML
1988
+ * @param rule_id {string}
1989
+ * @return {string}
1990
+ */
1991
+ QueryBuilder.prototype.getRuleTemplate = function(rule_id) {
1992
+ var h = this.templates.rule({
1993
+ builder: this,
1994
+ rule_id: rule_id,
1995
+ icons: this.icons,
1996
+ lang: this.lang,
1997
+ settings: this.settings
1998
+ });
1999
+
2000
+ return this.change('getRuleTemplate', h);
2001
+ };
2002
+
2003
+ /**
2004
+ * Returns rule filter <select> HTML
2005
+ * @param rule {Rule}
2006
+ * @param filters {array}
2007
+ * @return {string}
2008
+ */
2009
+ QueryBuilder.prototype.getRuleFilterSelect = function(rule, filters) {
2010
+ var h = this.templates.filterSelect({
2011
+ builder: this,
2012
+ rule: rule,
2013
+ filters: filters,
2014
+ icons: this.icons,
2015
+ lang: this.lang,
2016
+ settings: this.settings,
2017
+ translate: this.translateLabel
2018
+ });
2019
+
2020
+ return this.change('getRuleFilterSelect', h, rule);
2021
+ };
2022
+
2023
+ /**
2024
+ * Returns rule operator <select> HTML
2025
+ * @param rule {Rule}
2026
+ * @param operators {object}
2027
+ * @return {string}
2028
+ */
2029
+ QueryBuilder.prototype.getRuleOperatorSelect = function(rule, operators) {
2030
+ var h = this.templates.operatorSelect({
2031
+ builder: this,
2032
+ rule: rule,
2033
+ operators: operators,
2034
+ icons: this.icons,
2035
+ lang: this.lang,
2036
+ settings: this.settings,
2037
+ translate: this.translateLabel
2038
+ });
2039
+
2040
+ return this.change('getRuleOperatorSelect', h, rule);
2041
+ };
2042
+
2043
+ /**
2044
+ * Return the rule value HTML
2045
+ * @param rule {Rule}
2046
+ * @param filter {object}
2047
+ * @param value_id {int}
2048
+ * @return {string}
2049
+ */
2050
+ QueryBuilder.prototype.getRuleInput = function(rule, value_id) {
2051
+ var filter = rule.filter;
2052
+ var validation = rule.filter.validation || {};
2053
+ var name = rule.id + '_value_' + value_id;
2054
+ var c = filter.vertical ? ' class=block' : '';
2055
+ var h = '';
2056
+
2057
+ if (typeof filter.input == 'function') {
2058
+ h = filter.input.call(this, rule, name);
2059
+ }
2060
+ else {
2061
+ switch (filter.input) {
2062
+ case 'radio': case 'checkbox':
2063
+ Utils.iterateOptions(filter.values, function(key, val) {
2064
+ h+= '<label' + c + '><input type="' + filter.input + '" name="' + name + '" value="' + key + '"> ' + val + '</label> ';
2065
+ });
2066
+ break;
2067
+
2068
+ case 'select':
2069
+ h+= '<select class="form-control" name="' + name + '"' + (filter.multiple ? ' multiple' : '') + '>';
2070
+ if (filter.placeholder) {
2071
+ h+= '<option value="' + filter.placeholder_value + '" disabled selected>' + filter.placeholder + '</option>';
2072
+ }
2073
+ Utils.iterateOptions(filter.values, function(key, val) {
2074
+ h+= '<option value="' + key + '">' + val + '</option> ';
2075
+ });
2076
+ h+= '</select>';
2077
+ break;
2078
+
2079
+ case 'textarea':
2080
+ h+= '<textarea class="form-control" name="' + name + '"';
2081
+ if (filter.size) h+= ' cols="' + filter.size + '"';
2082
+ if (filter.rows) h+= ' rows="' + filter.rows + '"';
2083
+ if (validation.min !== undefined) h+= ' minlength="' + validation.min + '"';
2084
+ if (validation.max !== undefined) h+= ' maxlength="' + validation.max + '"';
2085
+ if (filter.placeholder) h+= ' placeholder="' + filter.placeholder + '"';
2086
+ h+= '></textarea>';
2087
+ break;
2088
+
2089
+ default:
2090
+ switch (QueryBuilder.types[filter.type]) {
2091
+ case 'number':
2092
+ h+= '<input class="form-control" type="number" name="' + name + '"';
2093
+ if (validation.step !== undefined) h+= ' step="' + validation.step + '"';
2094
+ if (validation.min !== undefined) h+= ' min="' + validation.min + '"';
2095
+ if (validation.max !== undefined) h+= ' max="' + validation.max + '"';
2096
+ if (filter.placeholder) h+= ' placeholder="' + filter.placeholder + '"';
2097
+ if (filter.size) h+= ' size="' + filter.size + '"';
2098
+ h+= '>';
2099
+ break;
2100
+
2101
+ default:
2102
+ h+= '<input class="form-control" type="text" name="' + name + '"';
2103
+ if (filter.placeholder) h+= ' placeholder="' + filter.placeholder + '"';
2104
+ if (filter.type === 'string' && validation.min !== undefined) h+= ' minlength="' + validation.min + '"';
2105
+ if (filter.type === 'string' && validation.max !== undefined) h+= ' maxlength="' + validation.max + '"';
2106
+ if (filter.size) h+= ' size="' + filter.size + '"';
2107
+ h+= '>';
2108
+ }
2109
+ }
2110
+ }
2111
+
2112
+ return this.change('getRuleInput', h, rule, name);
2113
+ };
2114
+
2115
+
2116
+ // Model CLASS
2117
+ // ===============================
2118
+ /**
2119
+ * Main object storing data model and emitting events
2120
+ * ---------
2121
+ * Access Node object stored in jQuery objects
2122
+ * @param el {jQuery|Node}
2123
+ * @return {Node}
2124
+ */
2125
+ function Model(el) {
2126
+ if (!(this instanceof Model)) {
2127
+ return Model.getModel(el);
2128
+ }
2129
+
2130
+ this.root = null;
2131
+ this.$ = $(this);
2132
+ }
2133
+
2134
+ $.extend(Model.prototype, {
2135
+ trigger: function(type) {
2136
+ this.$.triggerHandler(type, Array.prototype.slice.call(arguments, 1));
2137
+ return this;
2138
+ },
2139
+
2140
+ on: function() {
2141
+ this.$.on.apply(this.$, Array.prototype.slice.call(arguments));
2142
+ return this;
2143
+ },
2144
+
2145
+ off: function() {
2146
+ this.$.off.apply(this.$, Array.prototype.slice.call(arguments));
2147
+ return this;
2148
+ },
2149
+
2150
+ once: function() {
2151
+ this.$.one.apply(this.$, Array.prototype.slice.call(arguments));
2152
+ return this;
2153
+ }
2154
+ });
2155
+
2156
+ /**
2157
+ * Access Node object stored in jQuery objects
2158
+ * @param el {jQuery|Node}
2159
+ * @return {Node}
2160
+ */
2161
+ Model.getModel = function(el) {
2162
+ if (!el) {
2163
+ return null;
2164
+ }
2165
+ else if (el instanceof Node) {
2166
+ return el;
2167
+ }
2168
+ else {
2169
+ return $(el).data('queryBuilderModel');
2170
+ }
2171
+ };
2172
+
2173
+ /*
2174
+ * Define Node properties with getter and setter
2175
+ * Update events are emitted in the setter through root Model (if any)
2176
+ */
2177
+ function defineModelProperties(obj, fields) {
2178
+ fields.forEach(function(field) {
2179
+ Object.defineProperty(obj.prototype, field, {
2180
+ enumerable: true,
2181
+ get: function() {
2182
+ return this.__[field];
2183
+ },
2184
+ set: function(value) {
2185
+ var oldValue = (this.__[field] !== null && typeof this.__[field] == 'object') ?
2186
+ $.extend({}, this.__[field]) :
2187
+ this.__[field];
2188
+
2189
+ this.__[field] = value;
2190
+
2191
+ if (this.model !== null) {
2192
+ this.model.trigger('update', this, field, value, oldValue);
2193
+ }
2194
+ }
2195
+ });
2196
+ });
2197
+ }
2198
+
2199
+
2200
+ // Node abstract CLASS
2201
+ // ===============================
2202
+ /**
2203
+ * @param {Node}
2204
+ * @param {jQuery}
2205
+ */
2206
+ var Node = function(parent, $el) {
2207
+ if (!(this instanceof Node)) {
2208
+ return new Node();
2209
+ }
2210
+
2211
+ Object.defineProperty(this, '__', { value: {} });
2212
+
2213
+ $el.data('queryBuilderModel', this);
2214
+
2215
+ this.__.level = 1;
2216
+ this.__.error = null;
2217
+ this.__.data = undefined;
2218
+ this.$el = $el;
2219
+ this.id = $el[0].id;
2220
+ this.model = null;
2221
+ this.parent = parent;
2222
+ };
2223
+
2224
+ defineModelProperties(Node, ['level', 'error', 'data', 'flags']);
2225
+
2226
+ Object.defineProperty(Node.prototype, 'parent', {
2227
+ enumerable: true,
2228
+ get: function() {
2229
+ return this.__.parent;
2230
+ },
2231
+ set: function(value) {
2232
+ this.__.parent = value;
2233
+ this.level = value === null ? 1 : value.level + 1;
2234
+ this.model = value === null ? null : value.model;
2235
+ }
2236
+ });
2237
+
2238
+ /**
2239
+ * Check if this Node is the root
2240
+ * @return {boolean}
2241
+ */
2242
+ Node.prototype.isRoot = function() {
2243
+ return (this.level === 1);
2244
+ };
2245
+
2246
+ /**
2247
+ * Return node position inside parent
2248
+ * @return {int}
2249
+ */
2250
+ Node.prototype.getPos = function() {
2251
+ if (this.isRoot()) {
2252
+ return -1;
2253
+ }
2254
+ else {
2255
+ return this.parent.getNodePos(this);
2256
+ }
2257
+ };
2258
+
2259
+ /**
2260
+ * Delete self
2261
+ */
2262
+ Node.prototype.drop = function() {
2263
+ var model = this.model;
2264
+
2265
+ if (!this.isRoot()) {
2266
+ this.parent._removeNode(this);
2267
+ }
2268
+
2269
+ if (model !== null) {
2270
+ model.trigger('drop', this);
2271
+ }
2272
+ };
2273
+
2274
+ /**
2275
+ * Move itself after another Node
2276
+ * @param {Node}
2277
+ * @return {Node} self
2278
+ */
2279
+ Node.prototype.moveAfter = function(node) {
2280
+ if (this.isRoot()) return;
2281
+
2282
+ this._move(node.parent, node.getPos() + 1);
2283
+
2284
+ return this;
2285
+ };
2286
+
2287
+ /**
2288
+ * Move itself at the beginning of parent or another Group
2289
+ * @param {Group,optional}
2290
+ * @return {Node} self
2291
+ */
2292
+ Node.prototype.moveAtBegin = function(target) {
2293
+ if (this.isRoot()) return;
2294
+
2295
+ if (target === undefined) {
2296
+ target = this.parent;
2297
+ }
2298
+
2299
+ this._move(target, 0);
2300
+
2301
+ return this;
2302
+ };
2303
+
2304
+ /**
2305
+ * Move itself at the end of parent or another Group
2306
+ * @param {Group,optional}
2307
+ * @return {Node} self
2308
+ */
2309
+ Node.prototype.moveAtEnd = function(target) {
2310
+ if (this.isRoot()) return;
2311
+
2312
+ if (target === undefined) {
2313
+ target = this.parent;
2314
+ }
2315
+
2316
+ this._move(target, target.length() - 1);
2317
+
2318
+ return this;
2319
+ };
2320
+
2321
+ /**
2322
+ * Move itself at specific position of Group
2323
+ * @param {Group}
2324
+ * @param {int}
2325
+ */
2326
+ Node.prototype._move = function(group, index) {
2327
+ this.parent._removeNode(this);
2328
+ group._appendNode(this, index, false);
2329
+
2330
+ if (this.model !== null) {
2331
+ this.model.trigger('move', this, group, index);
2332
+ }
2333
+ };
2334
+
2335
+
2336
+ // GROUP CLASS
2337
+ // ===============================
2338
+ /**
2339
+ * @param {Group}
2340
+ * @param {jQuery}
2341
+ */
2342
+ var Group = function(parent, $el) {
2343
+ if (!(this instanceof Group)) {
2344
+ return new Group(parent, $el);
2345
+ }
2346
+
2347
+ Node.call(this, parent, $el);
2348
+
2349
+ this.rules = [];
2350
+ this.__.condition = null;
2351
+ };
2352
+
2353
+ Group.prototype = Object.create(Node.prototype);
2354
+ Group.prototype.constructor = Group;
2355
+
2356
+ defineModelProperties(Group, ['condition']);
2357
+
2358
+ /**
2359
+ * Empty the Group
2360
+ */
2361
+ Group.prototype.empty = function() {
2362
+ this.each('reverse', function(rule) {
2363
+ rule.drop();
2364
+ }, function(group) {
2365
+ group.drop();
2366
+ });
2367
+ };
2368
+
2369
+ /**
2370
+ * Delete self
2371
+ */
2372
+ Group.prototype.drop = function() {
2373
+ this.empty();
2374
+ Node.prototype.drop.call(this);
2375
+ };
2376
+
2377
+ /**
2378
+ * Return the number of children
2379
+ * @return {int}
2380
+ */
2381
+ Group.prototype.length = function() {
2382
+ return this.rules.length;
2383
+ };
2384
+
2385
+ /**
2386
+ * Add a Node at specified index
2387
+ * @param {Node}
2388
+ * @param {int,optional}
2389
+ * @param {boolean,optional}
2390
+ * @return {Node} the inserted node
2391
+ */
2392
+ Group.prototype._appendNode = function(node, index, trigger) {
2393
+ if (index === undefined) {
2394
+ index = this.length();
2395
+ }
2396
+
2397
+ this.rules.splice(index, 0, node);
2398
+ node.parent = this;
2399
+
2400
+ if (trigger && this.model !== null) {
2401
+ this.model.trigger('add', node, index);
2402
+ }
2403
+
2404
+ return node;
2405
+ };
2406
+
2407
+ /**
2408
+ * Add a Group by jQuery element at specified index
2409
+ * @param {jQuery}
2410
+ * @param {int,optional}
2411
+ * @return {Group} the inserted group
2412
+ */
2413
+ Group.prototype.addGroup = function($el, index) {
2414
+ return this._appendNode(new Group(this, $el), index, true);
2415
+ };
2416
+
2417
+ /**
2418
+ * Add a Rule by jQuery element at specified index
2419
+ * @param {jQuery}
2420
+ * @param {int,optional}
2421
+ * @return {Rule} the inserted rule
2422
+ */
2423
+ Group.prototype.addRule = function($el, index) {
2424
+ return this._appendNode(new Rule(this, $el), index, true);
2425
+ };
2426
+
2427
+ /**
2428
+ * Delete a specific Node
2429
+ * @param {Node}
2430
+ * @return {Group} self
2431
+ */
2432
+ Group.prototype._removeNode = function(node) {
2433
+ var index = this.getNodePos(node);
2434
+ if (index !== -1) {
2435
+ node.parent = null;
2436
+ this.rules.splice(index, 1);
2437
+ }
2438
+
2439
+ return this;
2440
+ };
2441
+
2442
+ /**
2443
+ * Return position of a child Node
2444
+ * @param {Node}
2445
+ * @return {int}
2446
+ */
2447
+ Group.prototype.getNodePos = function(node) {
2448
+ return this.rules.indexOf(node);
2449
+ };
2450
+
2451
+ /**
2452
+ * Iterate over all Nodes
2453
+ * @param {boolean,optional} iterate in reverse order, required if you delete nodes
2454
+ * @param {function} callback for Rules
2455
+ * @param {function,optional} callback for Groups
2456
+ * @return {boolean}
2457
+ */
2458
+ Group.prototype.each = function(reverse, cbRule, cbGroup, context) {
2459
+ if (typeof reverse == 'function') {
2460
+ context = cbGroup;
2461
+ cbGroup = cbRule;
2462
+ cbRule = reverse;
2463
+ reverse = false;
2464
+ }
2465
+ context = context === undefined ? null : context;
2466
+
2467
+ var i = reverse ? this.rules.length - 1 : 0;
2468
+ var l = reverse ? 0 : this.rules.length - 1;
2469
+ var c = reverse ? -1 : 1;
2470
+ var next = function() { return reverse ? i >= l : i <= l; };
2471
+ var stop = false;
2472
+
2473
+ for (; next(); i+= c) {
2474
+ if (this.rules[i] instanceof Group) {
2475
+ if (cbGroup !== undefined) {
2476
+ stop = cbGroup.call(context, this.rules[i]) === false;
2477
+ }
2478
+ }
2479
+ else {
2480
+ stop = cbRule.call(context, this.rules[i]) === false;
2481
+ }
2482
+
2483
+ if (stop) {
2484
+ break;
2485
+ }
2486
+ }
2487
+
2488
+ return !stop;
2489
+ };
2490
+
2491
+ /**
2492
+ * Return true if the group contains a particular Node
2493
+ * @param {Node}
2494
+ * @param {boolean,optional} recursive search
2495
+ * @return {boolean}
2496
+ */
2497
+ Group.prototype.contains = function(node, deep) {
2498
+ if (this.getNodePos(node) !== -1) {
2499
+ return true;
2500
+ }
2501
+ else if (!deep) {
2502
+ return false;
2503
+ }
2504
+ else {
2505
+ // the loop will return with false as soon as the Node is found
2506
+ return !this.each(function(rule) {
2507
+ return true;
2508
+ }, function(group) {
2509
+ return !group.contains(node, true);
2510
+ });
2511
+ }
2512
+ };
2513
+
2514
+
2515
+ // RULE CLASS
2516
+ // ===============================
2517
+ /**
2518
+ * @param {Group}
2519
+ * @param {jQuery}
2520
+ */
2521
+ var Rule = function(parent, $el) {
2522
+ if (!(this instanceof Rule)) {
2523
+ return new Rule(parent, $el);
2524
+ }
2525
+
2526
+ Node.call(this, parent, $el);
2527
+
2528
+ this.__.filter = null;
2529
+ this.__.operator = null;
2530
+ this.__.flags = {};
2531
+ this.__.value = undefined;
2532
+ };
2533
+
2534
+ Rule.prototype = Object.create(Node.prototype);
2535
+ Rule.prototype.constructor = Rule;
2536
+
2537
+ defineModelProperties(Rule, ['filter', 'operator', 'value']);
2538
+
2539
+
2540
+ // EXPORT
2541
+ // ===============================
2542
+ QueryBuilder.Group = Group;
2543
+ QueryBuilder.Rule = Rule;
2544
+
2545
+
2546
+ var Utils = QueryBuilder.utils = {};
2547
+
2548
+ /**
2549
+ * Utility to iterate over radio/checkbox/selection options.
2550
+ * it accept three formats: array of values, map, array of 1-element maps
2551
+ *
2552
+ * @param options {object|array}
2553
+ * @param tpl {callable} (takes key and text)
2554
+ */
2555
+ Utils.iterateOptions = function(options, tpl) {
2556
+ if (options) {
2557
+ if ($.isArray(options)) {
2558
+ options.forEach(function(entry) {
2559
+ // array of one-element maps
2560
+ if ($.isPlainObject(entry)) {
2561
+ $.each(entry, function(key, val) {
2562
+ tpl(key, val);
2563
+ return false; // break after first entry
2564
+ });
2565
+ }
2566
+ // array of values
2567
+ else {
2568
+ tpl(entry, entry);
2569
+ }
2570
+ });
2571
+ }
2572
+ // unordered map
2573
+ else {
2574
+ $.each(options, function(key, val) {
2575
+ tpl(key, val);
2576
+ });
2577
+ }
2578
+ }
2579
+ };
2580
+
2581
+ /**
2582
+ * Replaces {0}, {1}, ... in a string
2583
+ * @param str {string}
2584
+ * @param args,... {mixed}
2585
+ * @return {string}
2586
+ */
2587
+ Utils.fmt = function(str/*, args*/) {
2588
+ var args = Array.prototype.slice.call(arguments, 1);
2589
+
2590
+ return str.replace(/{([0-9]+)}/g, function(m, i) {
2591
+ return args[parseInt(i)];
2592
+ });
2593
+ };
2594
+
2595
+ /**
2596
+ * Throw an Error object with custom name
2597
+ * @param type {string}
2598
+ * @param message {string}
2599
+ * @param args,... {mixed}
2600
+ */
2601
+ Utils.error = function(type, message/*, args*/) {
2602
+ var err = new Error(Utils.fmt.apply(null, Array.prototype.slice.call(arguments, 1)));
2603
+ err.name = type + 'Error';
2604
+ err.args = Array.prototype.slice.call(arguments, 2);
2605
+ throw err;
2606
+ };
2607
+
2608
+ /**
2609
+ * Change type of a value to int or float
2610
+ * @param value {mixed}
2611
+ * @param type {string} 'integer', 'double' or anything else
2612
+ * @param boolAsInt {boolean} return 0 or 1 for booleans
2613
+ * @return {mixed}
2614
+ */
2615
+ Utils.changeType = function(value, type, boolAsInt) {
2616
+ switch (type) {
2617
+ case 'integer': return parseInt(value);
2618
+ case 'double': return parseFloat(value);
2619
+ case 'boolean':
2620
+ var bool = value.trim().toLowerCase() === 'true' || value.trim() === '1' || value === 1;
2621
+ return boolAsInt ? (bool ? 1 : 0) : bool;
2622
+ default: return value;
2623
+ }
2624
+ };
2625
+
2626
+ /**
2627
+ * Escape string like mysql_real_escape_string
2628
+ * @param value {string}
2629
+ * @return {string}
2630
+ */
2631
+ Utils.escapeString = function(value) {
2632
+ if (typeof value != 'string') {
2633
+ return value;
2634
+ }
2635
+
2636
+ return value
2637
+ .replace(/[\0\n\r\b\\\'\"]/g, function(s) {
2638
+ switch (s) {
2639
+ case '\0': return '\\0';
2640
+ case '\n': return '\\n';
2641
+ case '\r': return '\\r';
2642
+ case '\b': return '\\b';
2643
+ default: return '\\' + s;
2644
+ }
2645
+ })
2646
+ // uglify compliant
2647
+ .replace(/\t/g, '\\t')
2648
+ .replace(/\x1a/g, '\\Z');
2649
+ };
2650
+
2651
+ /**
2652
+ * Escape value for use in regex
2653
+ * @param value {string}
2654
+ * @return {string}
2655
+ */
2656
+ Utils.escapeRegExp = function(str) {
2657
+ return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
2658
+ };
2659
+
2660
+ /**
2661
+ * Escape HTML element id
2662
+ * @param value {string}
2663
+ * @return {string}
2664
+ */
2665
+ Utils.escapeElementId = function(str) {
2666
+ // Regex based on that suggested by:
2667
+ // https://learn.jquery.com/using-jquery-core/faq/how-do-i-select-an-element-by-an-id-that-has-characters-used-in-css-notation/
2668
+ // - escapes : . [ ] ,
2669
+ // - avoids escaping already escaped values
2670
+ return (str) ? str.replace(/(\\)?([:.\[\],])/g,
2671
+ function( $0, $1, $2 ) { return $1 ? $0 : '\\' + $2; }) : str;
2672
+ };
2673
+
2674
+ /**
2675
+ * Sort objects by grouping them by {key}, preserving initial order when possible
2676
+ * @param {object[]} items
2677
+ * @param {string} key
2678
+ * @returns {object[]}
2679
+ */
2680
+ Utils.groupSort = function(items, key) {
2681
+ var optgroups = [];
2682
+ var newItems = [];
2683
+
2684
+ items.forEach(function(item) {
2685
+ var idx;
2686
+
2687
+ if (item[key]) {
2688
+ idx = optgroups.lastIndexOf(item[key]);
2689
+
2690
+ if (idx == -1) {
2691
+ idx = optgroups.length;
2692
+ }
2693
+ else {
2694
+ idx++;
2695
+ }
2696
+ }
2697
+ else {
2698
+ idx = optgroups.length;
2699
+ }
2700
+
2701
+ optgroups.splice(idx, 0, item[key]);
2702
+ newItems.splice(idx, 0, item);
2703
+ });
2704
+
2705
+ return newItems;
2706
+ };
2707
+
2708
+
2709
+ $.fn.queryBuilder = function(option) {
2710
+ if (this.length > 1) {
2711
+ Utils.error('Config', 'Unable to initialize on multiple target');
2712
+ }
2713
+
2714
+ var data = this.data('queryBuilder');
2715
+ var options = (typeof option == 'object' && option) || {};
2716
+
2717
+ if (!data && option == 'destroy') {
2718
+ return this;
2719
+ }
2720
+ if (!data) {
2721
+ this.data('queryBuilder', new QueryBuilder(this, options));
2722
+ }
2723
+ if (typeof option == 'string') {
2724
+ return data[option].apply(data, Array.prototype.slice.call(arguments, 1));
2725
+ }
2726
+
2727
+ return this;
2728
+ };
2729
+
2730
+ $.fn.queryBuilder.constructor = QueryBuilder;
2731
+ $.fn.queryBuilder.defaults = QueryBuilder.defaults;
2732
+ $.fn.queryBuilder.extend = QueryBuilder.extend;
2733
+ $.fn.queryBuilder.define = QueryBuilder.define;
2734
+ $.fn.queryBuilder.regional = QueryBuilder.regional;
2735
+
2736
+
2737
+ /*!
2738
+ * jQuery QueryBuilder Awesome Bootstrap Checkbox
2739
+ * Applies Awesome Bootstrap Checkbox for checkbox and radio inputs.
2740
+ */
2741
+
2742
+ QueryBuilder.define('bt-checkbox', function(options) {
2743
+ if (options.font == 'glyphicons') {
2744
+ var injectCSS = document.createElement('style');
2745
+ injectCSS.innerHTML = '\
2746
+ .checkbox input[type=checkbox]:checked + label:after { \
2747
+ font-family: "Glyphicons Halflings"; \
2748
+ content: "\\e013"; \
2749
+ } \
2750
+ .checkbox label:after { \
2751
+ padding-left: 4px; \
2752
+ padding-top: 2px; \
2753
+ font-size: 9px; \
2754
+ }';
2755
+ document.body.appendChild(injectCSS);
2756
+ }
2757
+
2758
+ this.on('getRuleInput.filter', function(h, rule, name) {
2759
+ var filter = rule.filter;
2760
+
2761
+ if ((filter.input === 'radio' || filter.input === 'checkbox') && !filter.plugin) {
2762
+ h.value = '';
2763
+
2764
+ if (!filter.colors) {
2765
+ filter.colors = {};
2766
+ }
2767
+ if (filter.color) {
2768
+ filter.colors._def_ = filter.color;
2769
+ }
2770
+
2771
+ var style = filter.vertical ? ' style="display:block"' : '';
2772
+ var i = 0;
2773
+
2774
+ Utils.iterateOptions(filter.values, function(key, val) {
2775
+ var color = filter.colors[key] || filter.colors._def_ || options.color;
2776
+ var id = name + '_' + (i++);
2777
+
2778
+ h.value+= '\
2779
+ <div' + style + ' class="' + filter.input + ' ' + filter.input + '-' + color + '"> \
2780
+ <input type="' + filter.input + '" name="' + name + '" id="' + id + '" value="' + key + '"> \
2781
+ <label for="' + id + '">' + val + '</label> \
2782
+ </div>';
2783
+ });
2784
+ }
2785
+ });
2786
+ }, {
2787
+ font: 'glyphicons',
2788
+ color: 'default'
2789
+ });
2790
+
2791
+
2792
+ /*!
2793
+ * jQuery QueryBuilder Bootstrap Selectpicker
2794
+ * Applies Bootstrap Select on filters and operators combo-boxes.
2795
+ */
2796
+
2797
+ /**
2798
+ * @throws ConfigError
2799
+ */
2800
+ QueryBuilder.define('bt-selectpicker', function(options) {
2801
+ if (!$.fn.selectpicker || !$.fn.selectpicker.Constructor) {
2802
+ Utils.error('MissingLibrary', 'Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select');
2803
+ }
2804
+
2805
+ // init selectpicker
2806
+ this.on('afterCreateRuleFilters', function(e, rule) {
2807
+ rule.$el.find(Selectors.rule_filter).removeClass('form-control').selectpicker(options);
2808
+ });
2809
+
2810
+ this.on('afterCreateRuleOperators', function(e, rule) {
2811
+ rule.$el.find(Selectors.rule_operator).removeClass('form-control').selectpicker(options);
2812
+ });
2813
+
2814
+ // update selectpicker on change
2815
+ this.on('afterUpdateRuleFilter', function(e, rule) {
2816
+ rule.$el.find(Selectors.rule_filter).selectpicker('render');
2817
+ });
2818
+
2819
+ this.on('afterUpdateRuleOperator', function(e, rule) {
2820
+ rule.$el.find(Selectors.rule_operator).selectpicker('render');
2821
+ });
2822
+ }, {
2823
+ container: 'body',
2824
+ style: 'btn-inverse btn-xs',
2825
+ width: 'auto',
2826
+ showIcon: false
2827
+ });
2828
+
2829
+
2830
+ /*!
2831
+ * jQuery QueryBuilder Bootstrap Tooltip errors
2832
+ * Applies Bootstrap Tooltips on validation error messages.
2833
+ */
2834
+
2835
+ /**
2836
+ * @throws ConfigError
2837
+ */
2838
+ QueryBuilder.define('bt-tooltip-errors', function(options) {
2839
+ if (!$.fn.tooltip || !$.fn.tooltip.Constructor || !$.fn.tooltip.Constructor.prototype.fixTitle) {
2840
+ Utils.error('MissingLibrary', 'Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com');
2841
+ }
2842
+
2843
+ var self = this;
2844
+
2845
+ // add BT Tooltip data
2846
+ this.on('getRuleTemplate.filter getGroupTemplate.filter', function(h) {
2847
+ var $h = $(h.value);
2848
+ $h.find(Selectors.error_container).attr('data-toggle', 'tooltip');
2849
+ h.value = $h.prop('outerHTML');
2850
+ });
2851
+
2852
+ // init/refresh tooltip when title changes
2853
+ this.model.on('update', function(e, node, field) {
2854
+ if (field == 'error' && self.settings.display_errors) {
2855
+ node.$el.find(Selectors.error_container).eq(0)
2856
+ .tooltip(options)
2857
+ .tooltip('hide')
2858
+ .tooltip('fixTitle');
2859
+ }
2860
+ });
2861
+ }, {
2862
+ placement: 'right'
2863
+ });
2864
+
2865
+
2866
+ /*!
2867
+ * jQuery QueryBuilder Change Filters
2868
+ * Allows to change available filters after plugin initialization.
2869
+ */
2870
+
2871
+ QueryBuilder.extend({
2872
+ /**
2873
+ * Change the filters of the builder
2874
+ * @throws ChangeFilterError
2875
+ * @param {boolean,optional} delete rules using old filters
2876
+ * @param {object[]} new filters
2877
+ */
2878
+ setFilters: function(delete_orphans, filters) {
2879
+ var self = this;
2880
+
2881
+ if (filters === undefined) {
2882
+ filters = delete_orphans;
2883
+ delete_orphans = false;
2884
+ }
2885
+
2886
+ filters = this.checkFilters(filters);
2887
+ filters = this.change('setFilters', filters);
2888
+
2889
+ var filtersIds = filters.map(function(filter) {
2890
+ return filter.id;
2891
+ });
2892
+
2893
+ // check for orphans
2894
+ if (!delete_orphans) {
2895
+ (function checkOrphans(node) {
2896
+ node.each(
2897
+ function(rule) {
2898
+ if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
2899
+ Utils.error('ChangeFilter', 'A rule is using filter "{0}"', rule.filter.id);
2900
+ }
2901
+ },
2902
+ checkOrphans
2903
+ );
2904
+ }(this.model.root));
2905
+ }
2906
+
2907
+ // replace filters
2908
+ this.filters = filters;
2909
+
2910
+ // apply on existing DOM
2911
+ (function updateBuilder(node) {
2912
+ node.each(true,
2913
+ function(rule) {
2914
+ if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
2915
+ rule.drop();
2916
+ }
2917
+ else {
2918
+ self.createRuleFilters(rule);
2919
+
2920
+ rule.$el.find(Selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
2921
+ }
2922
+ },
2923
+ updateBuilder
2924
+ );
2925
+ }(this.model.root));
2926
+
2927
+ // update plugins
2928
+ if (this.settings.plugins) {
2929
+ if (this.settings.plugins['unique-filter']) {
2930
+ this.updateDisabledFilters();
2931
+ }
2932
+ if (this.settings.plugins['bt-selectpicker']) {
2933
+ this.$el.find(Selectors.rule_filter).selectpicker('render');
2934
+ }
2935
+ }
2936
+
2937
+ // reset the default_filter if does not exist anymore
2938
+ if (this.settings.default_filter) {
2939
+ try {
2940
+ this.getFilterById(this.settings.default_filter);
2941
+ }
2942
+ catch (e) {
2943
+ this.settings.default_filter = null;
2944
+ }
2945
+ }
2946
+
2947
+ this.trigger('afterSetFilters', filters);
2948
+ },
2949
+
2950
+ /**
2951
+ * Adds a new filter to the builder
2952
+ * @param {object|object[]} the new filter
2953
+ * @param {mixed,optional} numeric index or '#start' or '#end'
2954
+ */
2955
+ addFilter: function(new_filters, position) {
2956
+ if (position === undefined || position == '#end') {
2957
+ position = this.filters.length;
2958
+ }
2959
+ else if (position == '#start') {
2960
+ position = 0;
2961
+ }
2962
+
2963
+ if (!$.isArray(new_filters)) {
2964
+ new_filters = [new_filters];
2965
+ }
2966
+
2967
+ var filters = $.extend(true, [], this.filters);
2968
+
2969
+ // numeric position
2970
+ if (parseInt(position) == position) {
2971
+ Array.prototype.splice.apply(filters, [position, 0].concat(new_filters));
2972
+ }
2973
+ else {
2974
+ // after filter by its id
2975
+ if (this.filters.some(function(filter, index) {
2976
+ if (filter.id == position) {
2977
+ position = index + 1;
2978
+ return true;
2979
+ }
2980
+ })) {
2981
+ Array.prototype.splice.apply(filters, [position, 0].concat(new_filters));
2982
+ }
2983
+ // defaults to end of list
2984
+ else {
2985
+ Array.prototype.push.apply(filters, new_filters);
2986
+ }
2987
+ }
2988
+
2989
+ this.setFilters(filters);
2990
+ },
2991
+
2992
+ /**
2993
+ * Removes a filter from the builder
2994
+ * @param {string|string[]} the filter id
2995
+ * @param {boolean,optional} delete rules using old filters
2996
+ */
2997
+ removeFilter: function(filter_ids, delete_orphans) {
2998
+ var filters = $.extend(true, [], this.filters);
2999
+ if (typeof filter_ids === 'string') {
3000
+ filter_ids = [filter_ids];
3001
+ }
3002
+
3003
+ filters = filters.filter(function(filter) {
3004
+ return filter_ids.indexOf(filter.id) === -1;
3005
+ });
3006
+
3007
+ this.setFilters(delete_orphans, filters);
3008
+ }
3009
+ });
3010
+
3011
+
3012
+ /*!
3013
+ * jQuery QueryBuilder Filter Description
3014
+ * Provides three ways to display a description about a filter: inline, Bootsrap Popover or Bootbox.
3015
+ */
3016
+
3017
+ /**
3018
+ * @throws ConfigError
3019
+ */
3020
+ QueryBuilder.define('filter-description', function(options) {
3021
+ /**
3022
+ * INLINE
3023
+ */
3024
+ if (options.mode === 'inline') {
3025
+ this.on('afterUpdateRuleFilter', function(e, rule) {
3026
+ var $p = rule.$el.find('p.filter-description');
3027
+
3028
+ if (!rule.filter || !rule.filter.description) {
3029
+ $p.hide();
3030
+ }
3031
+ else {
3032
+ if ($p.length === 0) {
3033
+ $p = $('<p class="filter-description"></p>');
3034
+ $p.appendTo(rule.$el);
3035
+ }
3036
+ else {
3037
+ $p.show();
3038
+ }
3039
+
3040
+ $p.html('<i class="' + options.icon + '"></i> ' + rule.filter.description);
3041
+ }
3042
+ });
3043
+ }
3044
+ /**
3045
+ * POPOVER
3046
+ */
3047
+ else if (options.mode === 'popover') {
3048
+ if (!$.fn.popover || !$.fn.popover.Constructor || !$.fn.popover.Constructor.prototype.fixTitle) {
3049
+ Utils.error('MissingLibrary', 'Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com');
3050
+ }
3051
+
3052
+ this.on('afterUpdateRuleFilter', function(e, rule) {
3053
+ var $b = rule.$el.find('button.filter-description');
3054
+
3055
+ if (!rule.filter || !rule.filter.description) {
3056
+ $b.hide();
3057
+
3058
+ if ($b.data('bs.popover')) {
3059
+ $b.popover('hide');
3060
+ }
3061
+ }
3062
+ else {
3063
+ if ($b.length === 0) {
3064
+ $b = $('<button type="button" class="btn btn-xs btn-info filter-description" data-toggle="popover"><i class="' + options.icon + '"></i></button>');
3065
+ $b.prependTo(rule.$el.find(Selectors.rule_actions));
3066
+
3067
+ $b.popover({
3068
+ placement: 'left',
3069
+ container: 'body',
3070
+ html: true
3071
+ });
3072
+
3073
+ $b.on('mouseout', function() {
3074
+ $b.popover('hide');
3075
+ });
3076
+ }
3077
+ else {
3078
+ $b.show();
3079
+ }
3080
+
3081
+ $b.data('bs.popover').options.content = rule.filter.description;
3082
+
3083
+ if ($b.attr('aria-describedby')) {
3084
+ $b.popover('show');
3085
+ }
3086
+ }
3087
+ });
3088
+ }
3089
+ /**
3090
+ * BOOTBOX
3091
+ */
3092
+ else if (options.mode === 'bootbox') {
3093
+ if (!('bootbox' in window)) {
3094
+ Utils.error('MissingLibrary', 'Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com');
3095
+ }
3096
+
3097
+ this.on('afterUpdateRuleFilter', function(e, rule) {
3098
+ var $b = rule.$el.find('button.filter-description');
3099
+
3100
+ if (!rule.filter || !rule.filter.description) {
3101
+ $b.hide();
3102
+ }
3103
+ else {
3104
+ if ($b.length === 0) {
3105
+ $b = $('<button type="button" class="btn btn-xs btn-info filter-description" data-toggle="bootbox"><i class="' + options.icon + '"></i></button>');
3106
+ $b.prependTo(rule.$el.find(Selectors.rule_actions));
3107
+
3108
+ $b.on('click', function() {
3109
+ bootbox.alert($b.data('description'));
3110
+ });
3111
+ }
3112
+
3113
+ $b.data('description', rule.filter.description);
3114
+ }
3115
+ });
3116
+ }
3117
+ }, {
3118
+ icon: 'glyphicon glyphicon-info-sign',
3119
+ mode: 'popover'
3120
+ });
3121
+
3122
+
3123
+ /*!
3124
+ * jQuery QueryBuilder Invert
3125
+ * Allows to invert a rule operator, a group condition or the entire builder.
3126
+ */
3127
+
3128
+ QueryBuilder.defaults({
3129
+ operatorOpposites: {
3130
+ 'equal': 'not_equal',
3131
+ 'not_equal': 'equal',
3132
+ 'in': 'not_in',
3133
+ 'not_in': 'in',
3134
+ 'less': 'greater_or_equal',
3135
+ 'less_or_equal': 'greater',
3136
+ 'greater': 'less_or_equal',
3137
+ 'greater_or_equal': 'less',
3138
+ 'between': 'not_between',
3139
+ 'not_between': 'between',
3140
+ 'begins_with': 'not_begins_with',
3141
+ 'not_begins_with': 'begins_with',
3142
+ 'contains': 'not_contains',
3143
+ 'not_contains': 'contains',
3144
+ 'ends_with': 'not_ends_with',
3145
+ 'not_ends_with': 'ends_with',
3146
+ 'is_empty': 'is_not_empty',
3147
+ 'is_not_empty': 'is_empty',
3148
+ 'is_null': 'is_not_null',
3149
+ 'is_not_null': 'is_null'
3150
+ },
3151
+
3152
+ conditionOpposites: {
3153
+ 'AND': 'OR',
3154
+ 'OR': 'AND'
3155
+ }
3156
+ });
3157
+
3158
+ QueryBuilder.define('invert', function(options) {
3159
+ var self = this;
3160
+
3161
+ /**
3162
+ * Bind events
3163
+ */
3164
+ this.on('afterInit', function() {
3165
+ self.$el.on('click.queryBuilder', '[data-invert=group]', function() {
3166
+ var $group = $(this).closest(Selectors.group_container);
3167
+ self.invert(Model($group), options);
3168
+ });
3169
+
3170
+ if (options.display_rules_button && options.invert_rules) {
3171
+ self.$el.on('click.queryBuilder', '[data-invert=rule]', function() {
3172
+ var $rule = $(this).closest(Selectors.rule_container);
3173
+ self.invert(Model($rule), options);
3174
+ });
3175
+ }
3176
+ });
3177
+
3178
+ /**
3179
+ * Modify templates
3180
+ */
3181
+ this.on('getGroupTemplate.filter', function(h, level) {
3182
+ var $h = $(h.value);
3183
+ $h.find(Selectors.condition_container).after('<button type="button" class="btn btn-xs btn-default" data-invert="group"><i class="' + options.icon + '"></i> ' + self.lang.invert + '</button>');
3184
+ h.value = $h.prop('outerHTML');
3185
+ });
3186
+
3187
+ if (options.display_rules_button && options.invert_rules) {
3188
+ this.on('getRuleTemplate.filter', function(h) {
3189
+ var $h = $(h.value);
3190
+ $h.find(Selectors.rule_actions).prepend('<button type="button" class="btn btn-xs btn-default" data-invert="rule"><i class="' + options.icon + '"></i> ' + self.lang.invert + '</button>');
3191
+ h.value = $h.prop('outerHTML');
3192
+ });
3193
+ }
3194
+ }, {
3195
+ icon: 'glyphicon glyphicon-random',
3196
+ recursive: true,
3197
+ invert_rules: true,
3198
+ display_rules_button: false,
3199
+ silent_fail: false
3200
+ });
3201
+
3202
+ QueryBuilder.extend({
3203
+ /**
3204
+ * Invert a Group, a Rule or the whole builder
3205
+ * @throws InvertConditionError, InvertOperatorError
3206
+ * @param {Node,optional}
3207
+ * @param {object,optional}
3208
+ */
3209
+ invert: function(node, options) {
3210
+ if (!(node instanceof Node)) {
3211
+ if (!this.model.root) return;
3212
+ options = node;
3213
+ node = this.model.root;
3214
+ }
3215
+
3216
+ if (typeof options != 'object') options = {};
3217
+ if (options.recursive === undefined) options.recursive = true;
3218
+ if (options.invert_rules === undefined) options.invert_rules = true;
3219
+ if (options.silent_fail === undefined) options.silent_fail = false;
3220
+ if (options.trigger === undefined) options.trigger = true;
3221
+
3222
+ if (node instanceof Group) {
3223
+ // invert group condition
3224
+ if (this.settings.conditionOpposites[node.condition]) {
3225
+ node.condition = this.settings.conditionOpposites[node.condition];
3226
+ }
3227
+ else if (!options.silent_fail) {
3228
+ Utils.error('InvertCondition', 'Unknown inverse of condition "{0}"', node.condition);
3229
+ }
3230
+
3231
+ // recursive call
3232
+ if (options.recursive) {
3233
+ var tempOpts = $.extend({}, options, { trigger: false });
3234
+ node.each(function(rule) {
3235
+ if (options.invert_rules) {
3236
+ this.invert(rule, tempOpts);
3237
+ }
3238
+ }, function(group) {
3239
+ this.invert(group, tempOpts);
3240
+ }, this);
3241
+ }
3242
+ }
3243
+ else if (node instanceof Rule) {
3244
+ if (node.operator && !node.filter.no_invert) {
3245
+ // invert rule operator
3246
+ if (this.settings.operatorOpposites[node.operator.type]) {
3247
+ var invert = this.settings.operatorOpposites[node.operator.type];
3248
+ // check if the invert is "authorized"
3249
+ if (!node.filter.operators || node.filter.operators.indexOf(invert) != -1) {
3250
+ node.operator = this.getOperatorByType(invert);
3251
+ }
3252
+ }
3253
+ else if (!options.silent_fail) {
3254
+ Utils.error('InvertOperator', 'Unknown inverse of operator "{0}"', node.operator.type);
3255
+ }
3256
+ }
3257
+ }
3258
+
3259
+ if (options.trigger) {
3260
+ this.trigger('afterInvert', node, options);
3261
+ }
3262
+ }
3263
+ });
3264
+
3265
+
3266
+ /*!
3267
+ * jQuery QueryBuilder MongoDB Support
3268
+ * Allows to export rules as a MongoDB find object as well as populating the builder from a MongoDB object.
3269
+ */
3270
+
3271
+ // DEFAULT CONFIG
3272
+ // ===============================
3273
+ QueryBuilder.defaults({
3274
+ mongoOperators: {
3275
+ equal: function(v) { return v[0]; },
3276
+ not_equal: function(v) { return { '$ne': v[0] }; },
3277
+ in: function(v) { return { '$in': v }; },
3278
+ not_in: function(v) { return { '$nin': v }; },
3279
+ less: function(v) { return { '$lt': v[0] }; },
3280
+ less_or_equal: function(v) { return { '$lte': v[0] }; },
3281
+ greater: function(v) { return { '$gt': v[0] }; },
3282
+ greater_or_equal: function(v) { return { '$gte': v[0] }; },
3283
+ between: function(v) { return { '$gte': v[0], '$lte': v[1] }; },
3284
+ not_between: function(v) { return { '$lt': v[0], '$gt': v[1] }; },
3285
+ begins_with: function(v) { return { '$regex': '^' + Utils.escapeRegExp(v[0]) }; },
3286
+ not_begins_with: function(v) { return { '$regex': '^(?!' + Utils.escapeRegExp(v[0]) + ')' }; },
3287
+ contains: function(v) { return { '$regex': Utils.escapeRegExp(v[0]) }; },
3288
+ not_contains: function(v) { return { '$regex': '^((?!' + Utils.escapeRegExp(v[0]) + ').)*$', '$options': 's' }; },
3289
+ ends_with: function(v) { return { '$regex': Utils.escapeRegExp(v[0]) + '$' }; },
3290
+ not_ends_with: function(v) { return { '$regex': '(?<!' + Utils.escapeRegExp(v[0]) + ')$' }; },
3291
+ is_empty: function(v) { return ''; },
3292
+ is_not_empty: function(v) { return { '$ne': '' }; },
3293
+ is_null: function(v) { return null; },
3294
+ is_not_null: function(v) { return { '$ne': null }; }
3295
+ },
3296
+
3297
+ mongoRuleOperators: {
3298
+ $ne: function(v) {
3299
+ v = v.$ne;
3300
+ return {
3301
+ 'val': v,
3302
+ 'op': v === null ? 'is_not_null' : (v === '' ? 'is_not_empty' : 'not_equal')
3303
+ };
3304
+ },
3305
+ eq: function(v) {
3306
+ return {
3307
+ 'val': v,
3308
+ 'op': v === null ? 'is_null' : (v === '' ? 'is_empty' : 'equal')
3309
+ };
3310
+ },
3311
+ $regex: function(v) {
3312
+ v = v.$regex;
3313
+ if (v.slice(0, 4) == '^(?!' && v.slice(-1) == ')') {
3314
+ return { 'val': v.slice(4, -1), 'op': 'not_begins_with' };
3315
+ }
3316
+ else if (v.slice(0, 5) == '^((?!' && v.slice(-5) == ').)*$') {
3317
+ return { 'val': v.slice(5, -5), 'op': 'not_contains' };
3318
+ }
3319
+ else if (v.slice(0, 4) == '(?<!' && v.slice(-2) == ')$') {
3320
+ return { 'val': v.slice(4, -2), 'op': 'not_ends_with' };
3321
+ }
3322
+ else if (v.slice(-1) == '$') {
3323
+ return { 'val': v.slice(0, -1), 'op': 'ends_with' };
3324
+ }
3325
+ else if (v.slice(0, 1) == '^') {
3326
+ return { 'val': v.slice(1), 'op': 'begins_with' };
3327
+ }
3328
+ else {
3329
+ return { 'val': v, 'op': 'contains' };
3330
+ }
3331
+ },
3332
+ between: function(v) { return { 'val': [v.$gte, v.$lte], 'op': 'between' }; },
3333
+ not_between: function(v) { return { 'val': [v.$lt, v.$gt], 'op': 'not_between' }; },
3334
+ $in: function(v) { return { 'val': v.$in, 'op': 'in' }; },
3335
+ $nin: function(v) { return { 'val': v.$nin, 'op': 'not_in' }; },
3336
+ $lt: function(v) { return { 'val': v.$lt, 'op': 'less' }; },
3337
+ $lte: function(v) { return { 'val': v.$lte, 'op': 'less_or_equal' }; },
3338
+ $gt: function(v) { return { 'val': v.$gt, 'op': 'greater' }; },
3339
+ $gte: function(v) { return { 'val': v.$gte, 'op': 'greater_or_equal' }; }
3340
+ }
3341
+ });
3342
+
3343
+
3344
+ // PUBLIC METHODS
3345
+ // ===============================
3346
+ QueryBuilder.extend({
3347
+ /**
3348
+ * Get rules as MongoDB query
3349
+ * @throws UndefinedMongoConditionError, UndefinedMongoOperatorError
3350
+ * @param data {object} (optional) rules
3351
+ * @return {object}
3352
+ */
3353
+ getMongo: function(data) {
3354
+ data = (data === undefined) ? this.getRules() : data;
3355
+
3356
+ var self = this;
3357
+
3358
+ return (function parse(data) {
3359
+ if (!data.condition) {
3360
+ data.condition = self.settings.default_condition;
3361
+ }
3362
+ if (['AND', 'OR'].indexOf(data.condition.toUpperCase()) === -1) {
3363
+ Utils.error('UndefinedMongoCondition', 'Unable to build MongoDB query with condition "{0}"', data.condition);
3364
+ }
3365
+
3366
+ if (!data.rules) {
3367
+ return {};
3368
+ }
3369
+
3370
+ var parts = [];
3371
+
3372
+ data.rules.forEach(function(rule) {
3373
+ if (rule.rules && rule.rules.length > 0) {
3374
+ parts.push(parse(rule));
3375
+ }
3376
+ else {
3377
+ var mdb = self.settings.mongoOperators[rule.operator];
3378
+ var ope = self.getOperatorByType(rule.operator);
3379
+ var values = [];
3380
+
3381
+ if (mdb === undefined) {
3382
+ Utils.error('UndefinedMongoOperator', 'Unknown MongoDB operation for operator "{0}"', rule.operator);
3383
+ }
3384
+
3385
+ if (ope.nb_inputs !== 0) {
3386
+ if (!(rule.value instanceof Array)) {
3387
+ rule.value = [rule.value];
3388
+ }
3389
+
3390
+ rule.value.forEach(function(v) {
3391
+ values.push(Utils.changeType(v, rule.type, false));
3392
+ });
3393
+ }
3394
+
3395
+ var part = {};
3396
+ part[rule.field] = mdb.call(self, values);
3397
+ parts.push(part);
3398
+ }
3399
+ });
3400
+
3401
+ var res = {};
3402
+ if (parts.length > 0) {
3403
+ res['$' + data.condition.toLowerCase()] = parts;
3404
+ }
3405
+ return res;
3406
+ }(data));
3407
+ },
3408
+
3409
+ /**
3410
+ * Convert MongoDB object to rules
3411
+ * @throws MongoParseError, UndefinedMongoConditionError, UndefinedMongoOperatorError
3412
+ * @param data {object} query object
3413
+ * @return {object}
3414
+ */
3415
+ getRulesFromMongo: function(data) {
3416
+ if (data === undefined || data === null) {
3417
+ return null;
3418
+ }
3419
+
3420
+ var self = this;
3421
+ var conditions = {
3422
+ '$and': 'AND',
3423
+ '$or': 'OR'
3424
+ };
3425
+
3426
+ return (function parse(data) {
3427
+ var topKeys = Object.keys(data);
3428
+
3429
+ if (topKeys.length > 1) {
3430
+ Utils.error('MongoParse', 'Invalid MongoDB query format');
3431
+ }
3432
+ if (!conditions[topKeys[0].toLowerCase()]) {
3433
+ Utils.error('UndefinedMongoCondition', 'Unable to build MongoDB query with condition "{0}"', topKeys[0]);
3434
+ }
3435
+
3436
+ var rules = data[topKeys[0]];
3437
+ var parts = [];
3438
+
3439
+ rules.forEach(function(rule) {
3440
+ var keys = Object.keys(rule);
3441
+
3442
+ if (conditions[keys[0].toLowerCase()]) {
3443
+ parts.push(parse(rule));
3444
+ }
3445
+ else {
3446
+ var field = keys[0];
3447
+ var value = rule[field];
3448
+
3449
+ var operator = determineMongoOperator(value, field);
3450
+ if (operator === undefined) {
3451
+ Utils.error('MongoParse', 'Invalid MongoDB query format');
3452
+ }
3453
+
3454
+ var mdbrl = self.settings.mongoRuleOperators[operator];
3455
+ if (mdbrl === undefined) {
3456
+ Utils.error('UndefinedMongoOperator', 'JSON Rule operation unknown for operator "{0}"', operator);
3457
+ }
3458
+
3459
+ var opVal = mdbrl.call(self, value);
3460
+ parts.push({
3461
+ id: self.change('getMongoDBFieldID', field, value),
3462
+ field: field,
3463
+ operator: opVal.op,
3464
+ value: opVal.val
3465
+ });
3466
+ }
3467
+ });
3468
+
3469
+ var res = {};
3470
+ if (parts.length > 0) {
3471
+ res.condition = conditions[topKeys[0].toLowerCase()];
3472
+ res.rules = parts;
3473
+ }
3474
+ return res;
3475
+ }(data));
3476
+ },
3477
+
3478
+ /**
3479
+ * Set rules from MongoDB object
3480
+ * @param data {object}
3481
+ */
3482
+ setRulesFromMongo: function(data) {
3483
+ this.setRules(this.getRulesFromMongo(data));
3484
+ }
3485
+ });
3486
+
3487
+ /**
3488
+ * Find which operator is used in a MongoDB sub-object
3489
+ * @param {mixed} value
3490
+ * @param {string} field
3491
+ * @return {string|undefined}
3492
+ */
3493
+ function determineMongoOperator(value, field) {
3494
+ if (value !== null && typeof value == 'object') {
3495
+ var subkeys = Object.keys(value);
3496
+
3497
+ if (subkeys.length === 1) {
3498
+ return subkeys[0];
3499
+ }
3500
+ else {
3501
+ if (value.$gte !== undefined && value.$lte !== undefined) {
3502
+ return 'between';
3503
+ }
3504
+ if (value.$lt !== undefined && value.$gt !== undefined) {
3505
+ return 'not_between';
3506
+ }
3507
+ else if (value.$regex !== undefined) { // optional $options
3508
+ return '$regex';
3509
+ }
3510
+ else {
3511
+ return;
3512
+ }
3513
+ }
3514
+ }
3515
+ else {
3516
+ return 'eq';
3517
+ }
3518
+ }
3519
+
3520
+
3521
+ /*!
3522
+ * jQuery QueryBuilder Sortable
3523
+ * Enables drag & drop sort of rules.
3524
+ */
3525
+
3526
+ Selectors.rule_and_group_containers = Selectors.rule_container + ', ' + Selectors.group_container;
3527
+
3528
+ QueryBuilder.define('sortable', function(options) {
3529
+ /**
3530
+ * Init HTML5 drag and drop
3531
+ */
3532
+ this.on('afterInit', function(e) {
3533
+ // configure jQuery to use dataTransfer
3534
+ $.event.props.push('dataTransfer');
3535
+
3536
+ var placeholder;
3537
+ var src;
3538
+ var self = e.builder;
3539
+
3540
+ // only add "draggable" attribute when hovering drag handle
3541
+ // preventing text select bug in Firefox
3542
+ self.$el.on('mouseover.queryBuilder', '.drag-handle', function() {
3543
+ self.$el.find(Selectors.rule_and_group_containers).attr('draggable', true);
3544
+ });
3545
+ self.$el.on('mouseout.queryBuilder', '.drag-handle', function() {
3546
+ self.$el.find(Selectors.rule_and_group_containers).removeAttr('draggable');
3547
+ });
3548
+
3549
+ // dragstart: create placeholder and hide current element
3550
+ self.$el.on('dragstart.queryBuilder', '[draggable]', function(e) {
3551
+ e.stopPropagation();
3552
+
3553
+ // notify drag and drop (only dummy text)
3554
+ e.dataTransfer.setData('text', 'drag');
3555
+
3556
+ src = Model(e.target);
3557
+
3558
+ // Chrome glitchs
3559
+ // - helper invisible if hidden immediately
3560
+ // - "dragend" is called immediately if we modify the DOM directly
3561
+ setTimeout(function() {
3562
+ var ph = $('<div class="rule-placeholder">&nbsp;</div>');
3563
+ ph.css('min-height', src.$el.height());
3564
+
3565
+ placeholder = src.parent.addRule(ph, src.getPos());
3566
+
3567
+ src.$el.hide();
3568
+ }, 0);
3569
+ });
3570
+
3571
+ // dragenter: move the placeholder
3572
+ self.$el.on('dragenter.queryBuilder', '[draggable]', function(e) {
3573
+ e.preventDefault();
3574
+ e.stopPropagation();
3575
+
3576
+ if (placeholder) {
3577
+ moveSortableToTarget(placeholder, $(e.target));
3578
+ }
3579
+ });
3580
+
3581
+ // dragover: prevent glitches
3582
+ self.$el.on('dragover.queryBuilder', '[draggable]', function(e) {
3583
+ e.preventDefault();
3584
+ e.stopPropagation();
3585
+ });
3586
+
3587
+ // drop: move current element
3588
+ self.$el.on('drop.queryBuilder', function(e) {
3589
+ e.preventDefault();
3590
+ e.stopPropagation();
3591
+
3592
+ moveSortableToTarget(src, $(e.target));
3593
+ });
3594
+
3595
+ // dragend: show current element and delete placeholder
3596
+ self.$el.on('dragend.queryBuilder', '[draggable]', function(e) {
3597
+ e.preventDefault();
3598
+ e.stopPropagation();
3599
+
3600
+ src.$el.show();
3601
+ placeholder.drop();
3602
+
3603
+ self.$el.find(Selectors.rule_and_group_containers).removeAttr('draggable');
3604
+
3605
+ self.trigger('afterMove', src);
3606
+
3607
+ src = placeholder = null;
3608
+ });
3609
+ });
3610
+
3611
+ /**
3612
+ * Remove drag handle from non-sortable rules
3613
+ */
3614
+ this.on('parseRuleFlags.filter', function(flags) {
3615
+ if (flags.value.no_sortable === undefined) {
3616
+ flags.value.no_sortable = options.default_no_sortable;
3617
+ }
3618
+ });
3619
+
3620
+ this.on('afterApplyRuleFlags', function(e, rule) {
3621
+ if (rule.flags.no_sortable) {
3622
+ rule.$el.find('.drag-handle').remove();
3623
+ }
3624
+ });
3625
+
3626
+ /**
3627
+ * Remove drag handle from non-sortable groups
3628
+ */
3629
+ this.on('parseGroupFlags.filter', function(flags) {
3630
+ if (flags.value.no_sortable === undefined) {
3631
+ flags.value.no_sortable = options.default_no_sortable;
3632
+ }
3633
+ });
3634
+
3635
+ this.on('afterApplyGroupFlags', function(e, group) {
3636
+ if (group.flags.no_sortable) {
3637
+ group.$el.find('.drag-handle').remove();
3638
+ }
3639
+ });
3640
+
3641
+ /**
3642
+ * Modify templates
3643
+ */
3644
+ this.on('getGroupTemplate.filter', function(h, level) {
3645
+ if (level > 1) {
3646
+ var $h = $(h.value);
3647
+ $h.find(Selectors.condition_container).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>');
3648
+ h.value = $h.prop('outerHTML');
3649
+ }
3650
+ });
3651
+
3652
+ this.on('getRuleTemplate.filter', function(h) {
3653
+ var $h = $(h.value);
3654
+ $h.find(Selectors.rule_header).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>');
3655
+ h.value = $h.prop('outerHTML');
3656
+ });
3657
+ }, {
3658
+ default_no_sortable: false,
3659
+ icon: 'glyphicon glyphicon-sort'
3660
+ });
3661
+
3662
+ /**
3663
+ * Move an element (placeholder or actual object) depending on active target
3664
+ * @param {Node}
3665
+ * @param {jQuery}
3666
+ */
3667
+ function moveSortableToTarget(element, target) {
3668
+ var parent;
3669
+
3670
+ // on rule
3671
+ parent = target.closest(Selectors.rule_container);
3672
+ if (parent.length) {
3673
+ element.moveAfter(Model(parent));
3674
+ return;
3675
+ }
3676
+
3677
+ // on group header
3678
+ parent = target.closest(Selectors.group_header);
3679
+ if (parent.length) {
3680
+ parent = target.closest(Selectors.group_container);
3681
+ element.moveAtBegin(Model(parent));
3682
+ return;
3683
+ }
3684
+
3685
+ // on group
3686
+ parent = target.closest(Selectors.group_container);
3687
+ if (parent.length) {
3688
+ element.moveAtEnd(Model(parent));
3689
+ return;
3690
+ }
3691
+ }
3692
+
3693
+
3694
+ /*!
3695
+ * jQuery QueryBuilder SQL Support
3696
+ * Allows to export rules as a SQL WHERE statement as well as populating the builder from an SQL query.
3697
+ */
3698
+
3699
+ // DEFAULT CONFIG
3700
+ // ===============================
3701
+ QueryBuilder.defaults({
3702
+ /* operators for internal -> SQL conversion */
3703
+ sqlOperators: {
3704
+ equal: { op: '= ?' },
3705
+ not_equal: { op: '!= ?' },
3706
+ in: { op: 'IN(?)', sep: ', ' },
3707
+ not_in: { op: 'NOT IN(?)', sep: ', ' },
3708
+ less: { op: '< ?' },
3709
+ less_or_equal: { op: '<= ?' },
3710
+ greater: { op: '> ?' },
3711
+ greater_or_equal: { op: '>= ?' },
3712
+ between: { op: 'BETWEEN ?', sep: ' AND ' },
3713
+ not_between: { op: 'NOT BETWEEN ?', sep: ' AND ' },
3714
+ begins_with: { op: 'LIKE(?)', mod: '{0}%' },
3715
+ not_begins_with: { op: 'NOT LIKE(?)', mod: '{0}%' },
3716
+ contains: { op: 'LIKE(?)', mod: '%{0}%' },
3717
+ not_contains: { op: 'NOT LIKE(?)', mod: '%{0}%' },
3718
+ ends_with: { op: 'LIKE(?)', mod: '%{0}' },
3719
+ not_ends_with: { op: 'NOT LIKE(?)', mod: '%{0}' },
3720
+ is_empty: { op: '= \'\'' },
3721
+ is_not_empty: { op: '!= \'\'' },
3722
+ is_null: { op: 'IS NULL' },
3723
+ is_not_null: { op: 'IS NOT NULL' }
3724
+ },
3725
+
3726
+ /* operators for SQL -> internal conversion */
3727
+ sqlRuleOperator: {
3728
+ '=': function(v) {
3729
+ return {
3730
+ val: v,
3731
+ op: v === '' ? 'is_empty' : 'equal'
3732
+ };
3733
+ },
3734
+ '!=': function(v) {
3735
+ return {
3736
+ val: v,
3737
+ op: v === '' ? 'is_not_empty' : 'not_equal'
3738
+ };
3739
+ },
3740
+ 'LIKE': function(v) {
3741
+ if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
3742
+ return {
3743
+ val: v.slice(1, -1),
3744
+ op: 'contains'
3745
+ };
3746
+ }
3747
+ else if (v.slice(0, 1) == '%') {
3748
+ return {
3749
+ val: v.slice(1),
3750
+ op: 'ends_with'
3751
+ };
3752
+ }
3753
+ else if (v.slice(-1) == '%') {
3754
+ return {
3755
+ val: v.slice(0, -1),
3756
+ op: 'begins_with'
3757
+ };
3758
+ }
3759
+ else {
3760
+ Utils.error('SQLParse', 'Invalid value for LIKE operator "{0}"', v);
3761
+ }
3762
+ },
3763
+ 'IN': function(v) { return { val: v, op: 'in' }; },
3764
+ 'NOT IN': function(v) { return { val: v, op: 'not_in' }; },
3765
+ '<': function(v) { return { val: v, op: 'less' }; },
3766
+ '<=': function(v) { return { val: v, op: 'less_or_equal' }; },
3767
+ '>': function(v) { return { val: v, op: 'greater' }; },
3768
+ '>=': function(v) { return { val: v, op: 'greater_or_equal' }; },
3769
+ 'BETWEEN': function(v) { return { val: v, op: 'between' }; },
3770
+ 'NOT BETWEEN': function(v) { return { val: v, op: 'not_between' }; },
3771
+ 'IS': function(v) {
3772
+ if (v !== null) {
3773
+ Utils.error('SQLParse', 'Invalid value for IS operator');
3774
+ }
3775
+ return { val: null, op: 'is_null' };
3776
+ },
3777
+ 'IS NOT': function(v) {
3778
+ if (v !== null) {
3779
+ Utils.error('SQLParse', 'Invalid value for IS operator');
3780
+ }
3781
+ return { val: null, op: 'is_not_null' };
3782
+ }
3783
+ },
3784
+
3785
+ /* statements for internal -> SQL conversion */
3786
+ sqlStatements: {
3787
+ 'question_mark': function() {
3788
+ var params = [];
3789
+ return {
3790
+ add: function(rule, value) {
3791
+ params.push(value);
3792
+ return '?';
3793
+ },
3794
+ run: function() {
3795
+ return params;
3796
+ }
3797
+ };
3798
+ },
3799
+
3800
+ 'numbered': function(char) {
3801
+ if (!char || char.length > 1) char = '$';
3802
+ var index = 0;
3803
+ var params = [];
3804
+ return {
3805
+ add: function(rule, value) {
3806
+ params.push(value);
3807
+ index++;
3808
+ return char + index;
3809
+ },
3810
+ run: function() {
3811
+ return params;
3812
+ }
3813
+ };
3814
+ },
3815
+
3816
+ 'named': function(char) {
3817
+ if (!char || char.length > 1) char = ':';
3818
+ var indexes = {};
3819
+ var params = {};
3820
+ return {
3821
+ add: function(rule, value) {
3822
+ if (!indexes[rule.field]) indexes[rule.field] = 1;
3823
+ var key = rule.field + '_' + (indexes[rule.field]++);
3824
+ params[key] = value;
3825
+ return char + key;
3826
+ },
3827
+ run: function() {
3828
+ return params;
3829
+ }
3830
+ };
3831
+ }
3832
+ },
3833
+
3834
+ /* statements for SQL -> internal conversion */
3835
+ sqlRuleStatement: {
3836
+ 'question_mark': function(values) {
3837
+ var index = 0;
3838
+ return {
3839
+ parse: function(v) {
3840
+ return v == '?' ? values[index++] : v;
3841
+ },
3842
+ esc: function(sql) {
3843
+ return sql.replace(/\?/g, '\'?\'');
3844
+ }
3845
+ };
3846
+ },
3847
+
3848
+ 'numbered': function(values, char) {
3849
+ if (!char || char.length > 1) char = '$';
3850
+ var regex1 = new RegExp('^\\' + char + '[0-9]+$');
3851
+ var regex2 = new RegExp('\\' + char + '([0-9]+)', 'g');
3852
+ return {
3853
+ parse: function(v) {
3854
+ return regex1.test(v) ? values[v.slice(1) - 1] : v;
3855
+ },
3856
+ esc: function(sql) {
3857
+ return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\'');
3858
+ }
3859
+ };
3860
+ },
3861
+
3862
+ 'named': function(values, char) {
3863
+ if (!char || char.length > 1) char = ':';
3864
+ var regex1 = new RegExp('^\\' + char);
3865
+ var regex2 = new RegExp('\\' + char + '(' + Object.keys(values).join('|') + ')', 'g');
3866
+ return {
3867
+ parse: function(v) {
3868
+ return regex1.test(v) ? values[v.slice(1)] : v;
3869
+ },
3870
+ esc: function(sql) {
3871
+ return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\'');
3872
+ }
3873
+ };
3874
+ }
3875
+ }
3876
+ });
3877
+
3878
+
3879
+ // PUBLIC METHODS
3880
+ // ===============================
3881
+ QueryBuilder.extend({
3882
+ /**
3883
+ * Get rules as SQL query
3884
+ * @throws UndefinedSQLConditionError, UndefinedSQLOperatorError
3885
+ * @param stmt {boolean|string} use prepared statements - false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)'
3886
+ * @param nl {bool} output with new lines
3887
+ * @param data {object} (optional) rules
3888
+ * @return {object}
3889
+ */
3890
+ getSQL: function(stmt, nl, data) {
3891
+ data = (data === undefined) ? this.getRules() : data;
3892
+ nl = (nl === true) ? '\n' : ' ';
3893
+
3894
+ if (stmt === true) stmt = 'question_mark';
3895
+ if (typeof stmt == 'string') {
3896
+ var config = getStmtConfig(stmt);
3897
+ stmt = this.settings.sqlStatements[config[1]](config[2]);
3898
+ }
3899
+
3900
+ var self = this;
3901
+
3902
+ var sql = (function parse(data) {
3903
+ if (!data.condition) {
3904
+ data.condition = self.settings.default_condition;
3905
+ }
3906
+ if (['AND', 'OR'].indexOf(data.condition.toUpperCase()) === -1) {
3907
+ Utils.error('UndefinedSQLCondition', 'Unable to build SQL query with condition "{0}"', data.condition);
3908
+ }
3909
+
3910
+ if (!data.rules) {
3911
+ return '';
3912
+ }
3913
+
3914
+ var parts = [];
3915
+
3916
+ data.rules.forEach(function(rule) {
3917
+ if (rule.rules && rule.rules.length > 0) {
3918
+ parts.push('(' + nl + parse(rule) + nl + ')' + nl);
3919
+ }
3920
+ else {
3921
+ var sql = self.settings.sqlOperators[rule.operator];
3922
+ var ope = self.getOperatorByType(rule.operator);
3923
+ var value = '';
3924
+
3925
+ if (sql === undefined) {
3926
+ Utils.error('UndefinedSQLOperator', 'Unknown SQL operation for operator "{0}"', rule.operator);
3927
+ }
3928
+
3929
+ if (ope.nb_inputs !== 0) {
3930
+ if (!(rule.value instanceof Array)) {
3931
+ rule.value = [rule.value];
3932
+ }
3933
+
3934
+ rule.value.forEach(function(v, i) {
3935
+ if (i > 0) {
3936
+ value+= sql.sep;
3937
+ }
3938
+
3939
+ if (rule.type == 'integer' || rule.type == 'double' || rule.type == 'boolean') {
3940
+ v = Utils.changeType(v, rule.type, true);
3941
+ }
3942
+ else if (!stmt) {
3943
+ v = Utils.escapeString(v);
3944
+ }
3945
+
3946
+ if (sql.mod) {
3947
+ v = Utils.fmt(sql.mod, v);
3948
+ }
3949
+
3950
+ if (stmt) {
3951
+ value+= stmt.add(rule, v);
3952
+ }
3953
+ else {
3954
+ if (typeof v == 'string') {
3955
+ v = '\'' + v + '\'';
3956
+ }
3957
+
3958
+ value+= v;
3959
+ }
3960
+ });
3961
+ }
3962
+
3963
+ parts.push(rule.field + ' ' + sql.op.replace(/\?/, value));
3964
+ }
3965
+ });
3966
+
3967
+ return parts.join(' ' + data.condition + nl);
3968
+ }(data));
3969
+
3970
+ if (stmt) {
3971
+ return {
3972
+ sql: sql,
3973
+ params: stmt.run()
3974
+ };
3975
+ }
3976
+ else {
3977
+ return {
3978
+ sql: sql
3979
+ };
3980
+ }
3981
+ },
3982
+
3983
+ /**
3984
+ * Convert SQL to rules
3985
+ * @throws ConfigError, SQLParseError, UndefinedSQLOperatorError
3986
+ * @param data {object} query object
3987
+ * @param stmt {boolean|string} use prepared statements - false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)'
3988
+ * @return {object}
3989
+ */
3990
+ getRulesFromSQL: function(data, stmt) {
3991
+ if (!('SQLParser' in window)) {
3992
+ Utils.error('MissingLibrary', 'SQLParser is required to parse SQL queries. Get it here https://github.com/mistic100/sql-parser');
3993
+ }
3994
+
3995
+ var self = this;
3996
+
3997
+ if (typeof data == 'string') {
3998
+ data = { sql: data };
3999
+ }
4000
+
4001
+ if (stmt === true) stmt = 'question_mark';
4002
+ if (typeof stmt == 'string') {
4003
+ var config = getStmtConfig(stmt);
4004
+ stmt = this.settings.sqlRuleStatement[config[1]](data.params, config[2]);
4005
+ }
4006
+
4007
+ if (stmt) {
4008
+ data.sql = stmt.esc(data.sql);
4009
+ }
4010
+
4011
+ if (data.sql.toUpperCase().indexOf('SELECT') !== 0) {
4012
+ data.sql = 'SELECT * FROM table WHERE ' + data.sql;
4013
+ }
4014
+
4015
+ var parsed = SQLParser.parse(data.sql);
4016
+
4017
+ if (!parsed.where) {
4018
+ Utils.error('SQLParse', 'No WHERE clause found');
4019
+ }
4020
+
4021
+ var out = {
4022
+ condition: this.settings.default_condition,
4023
+ rules: []
4024
+ };
4025
+ var curr = out;
4026
+
4027
+ (function flatten(data, i) {
4028
+ // it's a node
4029
+ if (['AND', 'OR'].indexOf(data.operation.toUpperCase()) !== -1) {
4030
+ // create a sub-group if the condition is not the same and it's not the first level
4031
+ if (i > 0 && curr.condition != data.operation.toUpperCase()) {
4032
+ curr.rules.push({
4033
+ condition: self.settings.default_condition,
4034
+ rules: []
4035
+ });
4036
+
4037
+ curr = curr.rules[curr.rules.length - 1];
4038
+ }
4039
+
4040
+ curr.condition = data.operation.toUpperCase();
4041
+ i++;
4042
+
4043
+ // some magic !
4044
+ var next = curr;
4045
+ flatten(data.left, i);
4046
+
4047
+ curr = next;
4048
+ flatten(data.right, i);
4049
+ }
4050
+ // it's a leaf
4051
+ else {
4052
+ if (data.left.value === undefined || data.right.value === undefined) {
4053
+ Utils.error('SQLParse', 'Missing field and/or value');
4054
+ }
4055
+
4056
+ if ($.isPlainObject(data.right.value)) {
4057
+ Utils.error('SQLParse', 'Value format not supported for {0}.', data.left.value);
4058
+ }
4059
+
4060
+ // convert array
4061
+ var value;
4062
+ if ($.isArray(data.right.value)) {
4063
+ value = data.right.value.map(function(v) {
4064
+ return v.value;
4065
+ });
4066
+ }
4067
+ else {
4068
+ value = data.right.value;
4069
+ }
4070
+
4071
+ // get actual values
4072
+ if (stmt) {
4073
+ if ($.isArray(value)) {
4074
+ value = value.map(stmt.parse);
4075
+ }
4076
+ else {
4077
+ value = stmt.parse(value);
4078
+ }
4079
+ }
4080
+
4081
+ // convert operator
4082
+ var operator = data.operation.toUpperCase();
4083
+ if (operator == '<>') operator = '!=';
4084
+
4085
+ var sqlrl;
4086
+ if (operator == 'NOT LIKE') {
4087
+ sqlrl = self.settings.sqlRuleOperator['LIKE'];
4088
+ }
4089
+ else {
4090
+ sqlrl = self.settings.sqlRuleOperator[operator];
4091
+ }
4092
+
4093
+ if (sqlrl === undefined) {
4094
+ Utils.error('UndefinedSQLOperator', 'Invalid SQL operation "{0}".', data.operation);
4095
+ }
4096
+
4097
+ var opVal = sqlrl.call(this, value, data.operation);
4098
+ if (operator == 'NOT LIKE') opVal.op = 'not_' + opVal.op;
4099
+
4100
+ var left_value = data.left.values.join('.');
4101
+
4102
+ curr.rules.push({
4103
+ id: self.change('getSQLFieldID', left_value, value),
4104
+ field: left_value,
4105
+ operator: opVal.op,
4106
+ value: opVal.val
4107
+ });
4108
+ }
4109
+ }(parsed.where.conditions, 0));
4110
+
4111
+ return out;
4112
+ },
4113
+
4114
+ /**
4115
+ * Set rules from SQL
4116
+ * @param data {object}
4117
+ */
4118
+ setRulesFromSQL: function(data, stmt) {
4119
+ this.setRules(this.getRulesFromSQL(data, stmt));
4120
+ }
4121
+ });
4122
+
4123
+ function getStmtConfig(stmt) {
4124
+ var config = stmt.match(/(question_mark|numbered|named)(?:\((.)\))?/);
4125
+ if (!config) config = [null, 'question_mark', undefined];
4126
+ return config;
4127
+ }
4128
+
4129
+
4130
+ /*!
4131
+ * jQuery QueryBuilder Unique Filter
4132
+ * Allows to define some filters as "unique": ie which can be used for only one rule, globally or in the same group.
4133
+ */
4134
+
4135
+ QueryBuilder.define('unique-filter', function() {
4136
+ this.status.used_filters = {};
4137
+
4138
+ this.on('afterUpdateRuleFilter', this.updateDisabledFilters);
4139
+ this.on('afterDeleteRule', this.updateDisabledFilters);
4140
+ this.on('afterCreateRuleFilters', this.applyDisabledFilters);
4141
+ this.on('afterReset', this.clearDisabledFilters);
4142
+ this.on('afterClear', this.clearDisabledFilters);
4143
+ });
4144
+
4145
+ QueryBuilder.extend({
4146
+ updateDisabledFilters: function(e) {
4147
+ var self = e ? e.builder : this;
4148
+
4149
+ self.status.used_filters = {};
4150
+
4151
+ if (!self.model) {
4152
+ return;
4153
+ }
4154
+
4155
+ // get used filters
4156
+ (function walk(group) {
4157
+ group.each(function(rule) {
4158
+ if (rule.filter && rule.filter.unique) {
4159
+ if (!self.status.used_filters[rule.filter.id]) {
4160
+ self.status.used_filters[rule.filter.id] = [];
4161
+ }
4162
+ if (rule.filter.unique == 'group') {
4163
+ self.status.used_filters[rule.filter.id].push(rule.parent);
4164
+ }
4165
+ }
4166
+ }, function(group) {
4167
+ walk(group);
4168
+ });
4169
+ }(self.model.root));
4170
+
4171
+ self.applyDisabledFilters(e);
4172
+ },
4173
+
4174
+ clearDisabledFilters: function(e) {
4175
+ var self = e ? e.builder : this;
4176
+
4177
+ self.status.used_filters = {};
4178
+
4179
+ self.applyDisabledFilters(e);
4180
+ },
4181
+
4182
+ applyDisabledFilters: function(e) {
4183
+ var self = e ? e.builder : this;
4184
+
4185
+ // re-enable everything
4186
+ self.$el.find(Selectors.filter_container + ' option').prop('disabled', false);
4187
+
4188
+ // disable some
4189
+ $.each(self.status.used_filters, function(filterId, groups) {
4190
+ if (groups.length === 0) {
4191
+ self.$el.find(Selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true);
4192
+ }
4193
+ else {
4194
+ groups.forEach(function(group) {
4195
+ group.each(function(rule) {
4196
+ rule.$el.find(Selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true);
4197
+ });
4198
+ });
4199
+ }
4200
+ });
4201
+
4202
+ // update Selectpicker
4203
+ if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) {
4204
+ self.$el.find(Selectors.rule_filter).selectpicker('render');
4205
+ }
4206
+ }
4207
+ });
4208
+
4209
+
4210
+ /*!
4211
+ * jQuery QueryBuilder 2.3.3
4212
+ * Locale: English (en)
4213
+ * Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr
4214
+ * Licensed under MIT (http://opensource.org/licenses/MIT)
4215
+ */
4216
+
4217
+ QueryBuilder.regional['en'] = {
4218
+ "__locale": "English (en)",
4219
+ "__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr",
4220
+ "add_rule": "Add rule",
4221
+ "add_group": "Add group",
4222
+ "delete_rule": "Delete",
4223
+ "delete_group": "Delete",
4224
+ "conditions": {
4225
+ "AND": "AND",
4226
+ "OR": "OR"
4227
+ },
4228
+ "operators": {
4229
+ "equal": "equal",
4230
+ "not_equal": "not equal",
4231
+ "in": "in",
4232
+ "not_in": "not in",
4233
+ "less": "less",
4234
+ "less_or_equal": "less or equal",
4235
+ "greater": "greater",
4236
+ "greater_or_equal": "greater or equal",
4237
+ "between": "between",
4238
+ "not_between": "not between",
4239
+ "begins_with": "begins with",
4240
+ "not_begins_with": "doesn't begin with",
4241
+ "contains": "contains",
4242
+ "not_contains": "doesn't contain",
4243
+ "ends_with": "ends with",
4244
+ "not_ends_with": "doesn't end with",
4245
+ "is_empty": "is empty",
4246
+ "is_not_empty": "is not empty",
4247
+ "is_null": "is null",
4248
+ "is_not_null": "is not null"
4249
+ },
4250
+ "errors": {
4251
+ "no_filter": "No filter selected",
4252
+ "empty_group": "The group is empty",
4253
+ "radio_empty": "No value selected",
4254
+ "checkbox_empty": "No value selected",
4255
+ "select_empty": "No value selected",
4256
+ "string_empty": "Empty value",
4257
+ "string_exceed_min_length": "Must contain at least {0} characters",
4258
+ "string_exceed_max_length": "Must not contain more than {0} characters",
4259
+ "string_invalid_format": "Invalid format ({0})",
4260
+ "number_nan": "Not a number",
4261
+ "number_not_integer": "Not an integer",
4262
+ "number_not_double": "Not a real number",
4263
+ "number_exceed_min": "Must be greater than {0}",
4264
+ "number_exceed_max": "Must be lower than {0}",
4265
+ "number_wrong_step": "Must be a multiple of {0}",
4266
+ "datetime_empty": "Empty value",
4267
+ "datetime_invalid": "Invalid date format ({0})",
4268
+ "datetime_exceed_min": "Must be after {0}",
4269
+ "datetime_exceed_max": "Must be before {0}",
4270
+ "boolean_not_valid": "Not a boolean",
4271
+ "operator_not_multiple": "Operator {0} cannot accept multiple values"
4272
+ },
4273
+ "invert": "Invert"
4274
+ };
4275
+
4276
+ QueryBuilder.defaults({ lang_code: 'en' });
4277
+ }));