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.
- checksums.yaml +7 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/LICENSE.txt +21 -0
- data/README.md +56 -0
- data/lib/jquery_query_builder-rails.rb +9 -0
- data/lib/jquery_query_builder/rails/version.rb +5 -0
- data/vendor/assets/javascripts/doT.js +140 -0
- data/vendor/assets/javascripts/jquery.extendext.js +132 -0
- data/vendor/assets/javascripts/query-builder.js +4277 -0
- data/vendor/assets/stylesheets/query-builder.dark.css +128 -0
- data/vendor/assets/stylesheets/query-builder.default.css +128 -0
- metadata +125 -0
checksums.yaml
ADDED
@@ -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
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -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/
|
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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,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 = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "/": "/" },
|
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"> </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
|
+
}));
|