diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..7715ae28
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,18 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+[*.json]
+indent_size = 2
+insert_final_newline = false
+
+[*.scss]
+indent_size = 2
+
+[*.html]
+indent_size = 2
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 00000000..be3320fd
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,21 @@
+# Guidelines for contributing
+
+## Work on `dev`
+Any merge request should be created from and issued to the `dev` branch.
+
+Do not add the `dist` files to your pull request. The directory is ignored for a reason: it is generated and pushed only when doing a release on `master`.
+
+## Core vs Plugins
+I want to keep the core clean of extra (and certainly awesome) functionalities. That includes, but is not limited to, export/import plugins, visual aids, etc.
+
+Check the doc about [creating plugins](http://querybuilder.js.org/dev/plugins.html) and [use events](http://querybuilder.js.org/dev/events.html).
+
+I reserve the right to refuse any plugin I think is not useful for many people. Particularly, only import/export plugins for mainstream data storages will be integrated in the main repository. Others should be in a separated repository. But it's totally possible to add a link to your repository in the documentation.
+
+## Unit tests
+Any big feature must have it's own QUnit tests suite. Of course existing tests must still pass after changes.
+
+I won't merge any branch not passing the TravisCI build, including JShint/JSCS/SCSSlint compliance.
+
+## Translations
+Source language files are plain JSON files which will be converted to executable JS files by the build task. The `__locale` key must be filled with the international name of the language + 2-chars code and the `__author` key can be used to give information about the translator.
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 00000000..7a8c011b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,6 @@
+**Issues guidelines**
+
+- Please search in the [documentation](http://querybuilder.js.org) before asking.
+- Any issue without enough details won't get any answer and will be closed.
+- Help requests must be exhaustive, precise and come with some code explaining the need (use Markdown code highlight).
+- Bug reports must come with a simple test case, preferably on jsFiddle, Plunker, etc. (QueryBuilder is available on [jsDelivr](https://cdn.jsdelivr.net/npm/jQuery-QueryBuilder/dist/) and [unpkg](https://unpkg.com/jQuery-QueryBuilder/dist/) to be used on such platforms).
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..1131297f
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,7 @@
+**Merge request checklist**
+
+- [ ] I read the [guidelines for contributing](https://github.com/mistic100/jQuery-QueryBuilder/blob/master/.github/CONTRIBUTING.md)
+- [ ] I created my branch from `dev` and I am issuing the PR to `dev`
+- [ ] I didn't pushed the `dist` directory
+- [ ] If it's a new feature, I added the necessary unit tests
+- [ ] If it's a new language, I filled the `__locale` and `__author` fields
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 00000000..0648b398
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,21 @@
+name: CI
+
+on: [push, pull_request]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: actions/setup-node@v3
+ with:
+ node-version: '16'
+ cache: 'yarn'
+
+ - name: build
+ run: |
+ yarn install
+ yarn build
diff --git a/.gitignore b/.gitignore
index 7bf6eb18..a952958a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,6 @@
-bower_components
node_modules
+doc
+.sass-cache
+.idea
+*.iml
+package-lock.json
diff --git a/.jsdoc.json b/.jsdoc.json
new file mode 100644
index 00000000..d8a3fd67
--- /dev/null
+++ b/.jsdoc.json
@@ -0,0 +1,39 @@
+{
+ "opts": {
+ "private": false,
+ "template": "node_modules/foodoc/template",
+ "readme": "build/jsdoc.md"
+ },
+ "plugins": [
+ "plugins/markdown"
+ ],
+ "templates": {
+ "systemName": "jQuery QueryBuilder API",
+ "systemSummary": "jQuery plugin for user friendly query/filter creator",
+ "systemColor": "#004482",
+ "copyright": "Licensed under MIT License, documentation under CC BY 3.0.",
+ "includeDate": false,
+ "inverseNav": false,
+ "cleverLinks": true,
+ "sort": "longname, version, since",
+ "analytics": {
+ "ua": "UA-28192323-3",
+ "domain": "auto"
+ },
+ "navMembers": [
+ {"kind": "class", "title": "Classes", "summary": "All documented classes."},
+ {"kind": "external", "title": "Externals", "summary": "All documented external members."},
+ {"kind": "global", "title": "Globals", "summary": "All documented globals."},
+ {"kind": "mixin", "title": "Mixins", "summary": "All documented mixins."},
+ {"kind": "interface", "title": "Interfaces", "summary": "All documented interfaces."},
+ {"kind": "module", "title": "Modules", "summary": "All documented modules."},
+ {"kind": "event", "title": "Events", "summary": "All documented events."},
+ {"kind": "namespace", "title": "Namespaces", "summary": "All documented namespaces."},
+ {"kind": "tutorial", "title": "Tutorials", "summary": "All available tutorials."}
+ ],
+ "scripts": [
+ "https://cdnjs.cloudflare.com/ajax/libs/trianglify/1.0.1/trianglify.min.js",
+ "js/custom.js"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/Gruntfile.js b/Gruntfile.js
deleted file mode 100644
index 0a209781..00000000
--- a/Gruntfile.js
+++ /dev/null
@@ -1,158 +0,0 @@
-module.exports = function(grunt) {
- // list available modules and languages
- var modules = {
- 'sql': 'src/query-builder-sql-support.js'
- },
-
- langs = {},
- files = [];
-
- grunt.file.expandMapping('src/i18n/*.js', '', {
- flatten: true, ext: ''
- })
- .forEach(function(f) {
- langs[f.dest] = f.src[0];
- });
-
- // parse 'modules' parameter
- var arg_modules = grunt.option('modules');
- if (typeof arg_modules === 'string') {
- arg_modules.split(',').forEach(function(m) {
- if (modules[m]) {
- files.push(modules[m]);
- }
- else {
- grunt.fail.warn('Module '+ m +' unknown');
- }
- });
- }
- else if (arg_modules === undefined) {
- for (var m in modules) {
- files.push(modules[m]);
- }
- }
-
- // parse 'lang' parameter
- var arg_lang = grunt.option('lang');
- if (typeof arg_lang === 'string') {
- if (langs[arg_lang]) {
- if (arg_lang != 'en') {
- files.push(langs[arg_lang]);
- }
- }
- else {
- grunt.fail.warn('Lang '+ arg_lang +' unknown');
- }
- }
-
- grunt.log.writeln('Merged files: ['+ files.join(', ') +']');
-
-
- grunt.initConfig({
- pkg: grunt.file.readJSON('package.json'),
-
- banner:
- '/*!\n'+
- ' * jQuery QueryBuilder <%= pkg.version %>\n'+
- ' * Copyright <%= grunt.template.today("yyyy") %> Damien "Mistic" Sorel (http://www.strangeplanet.fr)\n'+
- ' * Licensed under MIT (http://opensource.org/licenses/MIT)\n'+
- ' */',
-
- // copy i18n
- copy: {
- i18n: {
- files: [{
- expand: true,
- flatten: true,
- src: ['src/i18n/*.js'],
- dest: 'dist/i18n'
- }]
- }
- },
-
- // copy src
- concat: {
- options: {
- banner: '<%= banner %>\n',
- stripBanners: {
- block: true
- }
- },
- src: {
- files: {
- 'dist/query-builder.css': [
- 'src/query-builder.css'
- ],
- 'dist/query-builder.js': [
- 'src/query-builder.js'
- ].concat(files)
- }
- }
- },
-
- // compress js
- uglify: {
- options: {
- banner: '<%= banner %>\n'
- },
- dist: {
- files: {
- 'dist/query-builder.min.js': [
- 'dist/query-builder.js'
- ]
- }
- }
- },
-
- // compress css
- cssmin: {
- options: {
- banner: '<%= banner %>',
- keepSpecialComments: 0
- },
- dist: {
- files: {
- 'dist/query-builder.min.css': [
- 'dist/query-builder.css'
- ]
- }
- }
- },
-
- // jshint tests
- jshint: {
- lib: {
- files: {
- src: [
- 'src/query-builder.js',
- 'src/query-builder-sql-support.js'
- ]
- }
- }
- },
-
- // qunit test suite
- qunit: {
- all: ['tests/*.html']
- }
- });
-
- grunt.loadNpmTasks('grunt-contrib-uglify');
- grunt.loadNpmTasks('grunt-contrib-copy');
- grunt.loadNpmTasks('grunt-contrib-cssmin');
- grunt.loadNpmTasks('grunt-contrib-concat');
- grunt.loadNpmTasks('grunt-contrib-qunit');
- grunt.loadNpmTasks('grunt-contrib-jshint');
-
- grunt.registerTask('default', [
- 'copy',
- 'concat',
- 'uglify',
- 'cssmin'
- ]);
-
- grunt.registerTask('test', [
- 'qunit',
- 'jshint'
- ]);
-};
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index 0e7b28a6..2558fa6a 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2014 Damien Sorel
+Copyright (c) 2014-2018 Damien Sorel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
\ No newline at end of file
+SOFTWARE.
diff --git a/README.md b/README.md
index b8766293..f0fd8ab7 100644
--- a/README.md
+++ b/README.md
@@ -1,46 +1,61 @@
# jQuery QueryBuilder
-[](http://badge.fury.io/bo/jQuery-QueryBuilder)
+[](https://www.npmjs.com/package/jQuery-QueryBuilder)
+[](https://www.jsdelivr.com/package/npm/jQuery-QueryBuilder)
+[](https://github.com/mistic100/jQuery-QueryBuilder/actions)
+[](https://gitlocalize.com/repo/5259/whole_project?utm_source=badge)
+
+jQuery plugin offering an simple interface to create complex queries.
+
+[](https://querybuilder.js.org)
+
-jQuery plugin offering an interface to create complex queries.
## Documentation
-http://mistic100.github.io/jQuery-QueryBuilder
+[querybuilder.js.org](https://querybuilder.js.org)
-### Requirements
- * Bootstrap 3.x CSS
- * jQuery >= 1.9
- * (optional) MomentJS
- * (optional) any widgets library like jQuery UI
-### Browser support
- * Internet Explorer >= 10
- * Mozilla FireFox ??
- * Google Chrome ??
- * Opera ??
- * Safari ??
-### Build
-Run `grunt` in root directory to generate minified files inside `dist`.
+## Install
-You can choose which modules to include with `--modules`
-```bash
-# include "sql" module
-grunt --modules=sql
+#### Manually
-# disable all modules
-grunt --modules=false
-```
-All modules are included by default.
+[Download the latest release](https://github.com/mistic100/jQuery-QueryBuilder/releases)
+
+#### With npm
-You can also include ONE language with `--lang`
```bash
-# include French translation
-grunt --lang=fr
+$ npm install jQuery-QueryBuilder
```
-Run `grunt test` to run jsHint and the Mocha test suite.
+#### Via CDN
+
+jQuery-QueryBuilder is available on [jsDelivr](https://www.jsdelivr.com/package/npm/jQuery-QueryBuilder).
+### Dependencies
+ * [jQuery 3](https://jquery.com)
+ * [Bootstrap 5](https://getbootstrap.com/docs/5.3/) CSS and bundle.js which includes `Popper` for tooltips and popovers
+ * [Bootstrap Icons](https://icons.getbootstrap.com/)
+ * [jQuery.extendext](https://github.com/mistic100/jQuery.extendext)
+ * [MomentJS](https://momentjs.com) (optional, for Date/Time validation)
+ * [SQL Parser](https://github.com/mistic100/sql-parser) (optional, for SQL methods)
+ * Other Bootstrap/jQuery plugins used by plugins
+
+($.extendext is directly included in the [standalone](https://github.com/mistic100/jQuery-QueryBuilder/blob/master/dist/js/query-builder.standalone.js) file)
+
+
+
+## Developement
+
+Install Node dependencies with `npm install`.
+
+#### Build
+
+Run `npm run build` in the root directory to generate production files inside `dist`.
+
+#### Serve
+
+Run `npm run serve` to open the example page with automatic build and livereload.
+
-### Inspiration
- * [Knockout Query Builder](http://kindohm.com/posts/2013/09/25/knockout-query-builder/)
- * [jui_filter_rules](http://www.pontikis.net/labs/jui_filter_rules/)
+## License
+This library is available under the MIT license.
diff --git a/bower.json b/bower.json
deleted file mode 100644
index 35fde906..00000000
--- a/bower.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "name": "jQuery-QueryBuilder",
- "version": "1.3.0-SNAPSHOT",
- "authors": [{
- "name": "Damien \"Mistic\" Sorel",
- "homepage": "http://www.strangeplanet.fr"
- }],
- "description": "jQuery plugin for user friendly query/filter creator",
- "main": [
- "dist/query-builder.js",
- "dist/query-builder.css"
- ],
- "dependencies" : {
- "jquery": ">= 1.9.0",
- "bootstrap": "^3.1.0",
- "momentjs": "^2.6.0"
- },
- "keywords": [
- "jquery",
- "query",
- "builder",
- "filter"
- ],
- "license": "MIT",
- "homepage": "https://github.com/mistic100/jQuery-QueryBuilder",
- "repository": {
- "type": "git",
- "url": "git://github.com/mistic100/jQuery-QueryBuilder.git"
- },
- "ignore": [
- "**/.*",
- "node_modules",
- "bower_components",
- "test",
- "tests"
- ]
-}
diff --git a/build/dist.mjs b/build/dist.mjs
new file mode 100644
index 00000000..e413853b
--- /dev/null
+++ b/build/dist.mjs
@@ -0,0 +1,210 @@
+import fs from 'fs';
+import path from 'path';
+import { globSync } from 'glob';
+import * as sass from 'sass';
+import pkg from '../package.json' assert { type: 'json' };
+
+const DEV = process.argv[2] === '--dev';
+
+const DIST = 'dist/';
+
+const CORE_JS = [
+ 'src/main.js',
+ 'src/defaults.js',
+ 'src/plugins.js',
+ 'src/core.js',
+ 'src/public.js',
+ 'src/data.js',
+ 'src/template.js',
+ 'src/utils.js',
+ 'src/model.js',
+ 'src/jquery.js',
+];
+
+const CORE_SASS = [
+ 'src/scss/dark.scss',
+ 'src/scss/default.scss',
+];
+
+const STANDALONE_JS = {
+ 'jquery-extendext': 'node_modules/jquery-extendext/jquery-extendext.js',
+ 'query-builder': `${DIST}js/query-builder.js`,
+};
+
+const BANNER = () => `/*!
+ * jQuery QueryBuilder ${pkg.version}
+ * Copyright 2014-${new Date().getFullYear()} Damien "Mistic" Sorel (http://www.strangeplanet.fr)
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */`;
+
+const LANG_BANNER = (locale, author) => `/*!
+ * jQuery QueryBuilder ${pkg.version}
+ * Locale: ${locale}
+ * Author: ${author}
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */`;
+
+const ALL_PLUGINS_JS = glob('src/plugins/*/plugin.js')
+ .sort()
+ .reduce((all, p) => {
+ const n = p.split('/')[2];
+ all[n] = p;
+ return all;
+ }, {});
+
+const ALL_PLUGINS_SASS = glob('src/plugins/*/plugin.scss')
+ .sort()
+ .reduce((all, p) => {
+ const n = p.split('/')[2];
+ all[n] = p;
+ return all;
+ }, {});
+
+const ALL_LANGS = glob('src/i18n/*.json')
+ .map(p => p.split(/[\/\.]/)[2])
+ .sort();
+
+function glob(pattern) {
+ return globSync(pattern)
+ .map(p => p.split(path.sep).join('/'));
+}
+
+/**
+ * Build lang files
+ */
+function buildLangs() {
+ const wrapper = fs.readFileSync('src/i18n/.wrapper.js', { encoding: 'utf8' })
+ .split('@@js\n');
+
+ ALL_LANGS.forEach(lang => {
+ const outpath = `${DIST}i18n/query-builder.${lang}.js`;
+ console.log(`LANG ${lang} (${outpath})`);
+ fs.writeFileSync(outpath, getLang(lang, wrapper));
+ });
+}
+
+/**
+ * Get the content of a single lang
+ */
+function getLang(lang, wrapper = ['', '']) {
+ const corepath = `src/i18n/${lang}.json`;
+ const content = JSON.parse(fs.readFileSync(corepath, { encoding: 'utf8' }));
+
+ Object.keys(ALL_PLUGINS_JS).forEach(plugin => {
+ const pluginpath = `src/plugins/${plugin}/i18n/${lang}.json`;
+ try {
+ const plugincontent = JSON.parse(fs.readFileSync(pluginpath, { encoding: 'utf8' }));
+ Object.assign(content, plugincontent);
+ } catch { }
+ });
+
+ return LANG_BANNER(content.__locale || lang, content.__author || '')
+ + '\n\n'
+ + wrapper[0]
+ + `QueryBuilder.regional['${lang}'] = `
+ + JSON.stringify(content, null, 2)
+ + ';\n\n'
+ + `QueryBuilder.defaults({ lang_code: '${lang}' });`
+ + wrapper[1];
+}
+
+/**
+ * Build main JS file
+ */
+function buildMain() {
+ const wrapper = fs.readFileSync('src/.wrapper.js', { encoding: 'utf8' })
+ .split('@@js\n');
+
+ const files_to_load = [
+ ...CORE_JS,
+ ...Object.values(ALL_PLUGINS_JS),
+ ];
+
+ const output = BANNER()
+ + '\n\n'
+ + wrapper[0]
+ + files_to_load.map(f => fs.readFileSync(f, { encoding: 'utf8' })).join('\n\n')
+ + '\n\n'
+ + getLang('en')
+ + wrapper[1];
+
+ const outpath = `${DIST}js/query-builder.js`;
+ console.log(`MAIN (${outpath})`);
+ fs.writeFileSync(outpath, output);
+}
+
+/**
+ * Build standalone JS file
+ */
+function buildStandalone() {
+ const output = Object.entries(STANDALONE_JS)
+ .map(([name, file]) => {
+ return fs.readFileSync(file, { encoding: 'utf8' })
+ .replace(/define\((.*?)\);/, `define('${name}', $1);`);
+ })
+ .join('\n\n');
+
+ const outpath = `${DIST}js/query-builder.standalone.js`;
+ console.log(`STANDALONE (${outpath})`);
+ fs.writeFileSync(outpath, output);
+}
+
+/**
+ * Copy SASS files
+ */
+function copySass() {
+ Object.entries(ALL_PLUGINS_SASS).forEach(([plugin, path]) => {
+ const outpath = `${DIST}scss/plugins/${plugin}.scss`;
+ console.log(`SASS ${plugin} (${path})`);
+ fs.copyFileSync(path, outpath);
+ });
+
+ CORE_SASS.forEach(path => {
+ const name = path.split('/').pop();
+
+ const content = fs.readFileSync(path, { encoding: 'utf8' });
+
+ let output = BANNER()
+ + '\n'
+ + content;
+ if (name === 'default.scss') {
+ output += '\n'
+ + Object.keys(ALL_PLUGINS_SASS).map(p => `@import "plugins/${p}";`).join('\n');
+ }
+
+ const outpath = `${DIST}scss/${name}`;
+ console.log(`SASS (${path})`);
+ fs.writeFileSync(outpath, output);
+ });
+}
+
+/**
+ * Build CSS files
+ */
+function buildCss() {
+ CORE_SASS.forEach(p => {
+ const path = p.replace('src/', DIST);
+ const name = path.split('/').pop();
+
+ const output = sass.compile(path);
+
+ const outpath = `${DIST}css/query-builder.${name.split('.').shift()}.css`;
+ console.log(`CSS (${path})`);
+ fs.writeFileSync(outpath, output.css);
+ });
+}
+
+if (!DEV) {
+ fs.rmSync(DIST, { recursive: true, force: true });
+}
+fs.mkdirSync(DIST + 'css', { recursive: true });
+fs.mkdirSync(DIST + 'i18n', { recursive: true });
+fs.mkdirSync(DIST + 'js', { recursive: true });
+fs.mkdirSync(DIST + 'scss', { recursive: true });
+fs.mkdirSync(DIST + 'scss/plugins', { recursive: true });
+
+buildLangs();
+buildMain();
+buildStandalone();
+copySass();
+buildCss();
diff --git a/build/jsdoc.js b/build/jsdoc.js
new file mode 100644
index 00000000..edbad8d4
--- /dev/null
+++ b/build/jsdoc.js
@@ -0,0 +1,12 @@
+(function() {
+ var header = $('.page-header');
+ var pattern = Trianglify({
+ width: window.screen.width | header.outerWidth(),
+ height: header.outerHeight(),
+ cell_size: 90,
+ seed: 'jQuery QueryBuilder',
+ x_colors: ['#0074d9', '#001224']
+ });
+
+ header.css('background-image', 'url(' + pattern.png() + ')');
+}());
diff --git a/build/jsdoc.md b/build/jsdoc.md
new file mode 100644
index 00000000..e329af92
--- /dev/null
+++ b/build/jsdoc.md
@@ -0,0 +1,11 @@
+# [Main documentation](..)
+
+# Entry point: [$.fn.QueryBuilder](external-_jQuery.fn_.html)
+
+# [QueryBuilder](QueryBuilder.html)
+
+# [Rule](Rule.html) & [Group](Group.html)
+
+# [Events](list_event.html)
+
+# [Plugins](module-plugins.html)
diff --git a/build/liveserver.mjs b/build/liveserver.mjs
new file mode 100644
index 00000000..3d60e23c
--- /dev/null
+++ b/build/liveserver.mjs
@@ -0,0 +1,20 @@
+import liveServer from 'alive-server';
+import path from 'path';
+
+const rootDir = process.cwd();
+
+const EXAMPLES_DIR = 'examples';
+const DIST_DIR = 'dist';
+
+liveServer.start({
+ open: true,
+ root: path.join(rootDir, EXAMPLES_DIR),
+ watch: [
+ path.join(rootDir, EXAMPLES_DIR),
+ path.join(rootDir, DIST_DIR),
+ ],
+ mount: [
+ ['/node_modules', path.join(rootDir, 'node_modules')],
+ ['/dist', path.join(rootDir, DIST_DIR)],
+ ],
+});
diff --git a/composer.json b/composer.json
deleted file mode 100644
index 8f3c9352..00000000
--- a/composer.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "name": "mistic100/jquery-querybuilder",
- "description": "jQuery plugin for user friendly query/filter creator",
- "keywords": [ "jquery", "query", "builder", "filter"],
- "homepage": "https://github.com/mistic100/jQuery-QueryBuilder",
- "authors": [
- {
- "name": "Damien \"Mistic\" Sorel",
- "homepage": "http://www.strangeplanet.fr"
- }
- ],
- "support": {
- "issues": "https://github.com/mistic100/jQuery-QueryBuilder/issues"
- },
- "require": {
- "yiisoft/jquery": "2.* | 1.9.*",
- "moment/moment": "dev-master",
- "twbs/bootstrap": "3.2.*@dev"
- },
- "license": "MIT"
-}
diff --git a/dist/css/query-builder.dark.css b/dist/css/query-builder.dark.css
new file mode 100644
index 00000000..90b5fbaa
--- /dev/null
+++ b/dist/css/query-builder.dark.css
@@ -0,0 +1,150 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Copyright 2014-2024 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Copyright 2014-2024 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+.query-builder .rule-placeholder, .query-builder .rule-container, .query-builder .rules-group-container {
+ position: relative;
+ margin: 4px 0;
+ border-radius: 5px;
+ padding: 5px;
+ border: 1px solid #111;
+ background: rgba(40, 40, 40, 0.9);
+}
+
+.query-builder .drag-handle, .query-builder .error-container, .query-builder .rule-container .rule-filter-container,
+.query-builder .rule-container .rule-operator-container,
+.query-builder .rule-container .rule-value-container {
+ display: inline-block;
+ margin: 0 5px 0 0;
+ vertical-align: middle;
+}
+
+.query-builder .rules-group-container {
+ padding: 10px;
+ padding-bottom: 6px;
+ border: 1px solid #00164A;
+ background: rgba(50, 70, 80, 0.5);
+}
+.query-builder .rules-group-header {
+ margin-bottom: 10px;
+}
+.query-builder .rules-group-header .group-conditions .btn.readonly:not(.active),
+.query-builder .rules-group-header .group-conditions input[name$=_cond] {
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+ white-space: nowrap;
+}
+.query-builder .rules-group-header .group-conditions .btn.readonly {
+ border-radius: 3px;
+}
+.query-builder .rules-list {
+ list-style: none;
+ padding: 0 0 0 15px;
+ margin: 0;
+}
+.query-builder .rule-value-container {
+ border-left: 1px solid #DDD;
+ padding-left: 5px;
+}
+.query-builder .rule-value-container label {
+ margin-bottom: 0;
+ font-weight: normal;
+}
+.query-builder .rule-value-container label.block {
+ display: block;
+}
+.query-builder .error-container {
+ display: none;
+ cursor: help;
+ color: #F00;
+}
+.query-builder .has-error {
+ background-color: #322;
+ border-color: #800;
+}
+.query-builder .has-error .error-container {
+ display: inline-block !important;
+}
+.query-builder .rules-list > *::before, .query-builder .rules-list > *::after {
+ content: "";
+ position: absolute;
+ left: -10px;
+ width: 10px;
+ height: calc(50% + 4px);
+ border-color: #222;
+ border-style: solid;
+}
+.query-builder .rules-list > *::before {
+ top: -4px;
+ border-width: 0 0 2px 2px;
+}
+.query-builder .rules-list > *::after {
+ top: 50%;
+ border-width: 0 0 0 2px;
+}
+.query-builder .rules-list > *:first-child::before {
+ top: -12px;
+ height: calc(50% + 14px);
+}
+.query-builder .rules-list > *:last-child::before {
+ border-radius: 0 0 0 4px;
+}
+.query-builder .rules-list > *:last-child::after {
+ display: none;
+}
+
+.query-builder.bt-checkbox-bootstrap-icons .checkbox input[type=checkbox] + label::before {
+ outline: 0;
+}
+.query-builder.bt-checkbox-bootstrap-icons .checkbox input[type=checkbox]:checked + label::after {
+ font-family: "bootstrap-icons";
+ content: "\f633";
+}
+
+.query-builder .error-container + .tooltip .tooltip-inner {
+ color: #F22 !important;
+}
+
+.query-builder p.filter-description {
+ margin: 5px 0 0 0;
+ background: rgba(0, 170, 255, 0.2);
+ border: 1px solid #346F7B;
+ color: #AAD1E4;
+ border-radius: 5px;
+ padding: 2.5px 5px;
+ font-size: 0.8em;
+}
+
+.query-builder .rules-group-header [data-invert] {
+ margin-left: 5px;
+}
+
+.query-builder .drag-handle {
+ cursor: move;
+ vertical-align: middle;
+ margin-left: 5px;
+}
+.query-builder .dragging {
+ position: fixed;
+ opacity: 0.5;
+ z-index: 100;
+}
+.query-builder .dragging::before, .query-builder .dragging::after {
+ display: none;
+}
+.query-builder .rule-placeholder {
+ border: 1px dashed #BBB;
+ opacity: 0.7;
+}
\ No newline at end of file
diff --git a/dist/css/query-builder.default.css b/dist/css/query-builder.default.css
new file mode 100644
index 00000000..b5edf68e
--- /dev/null
+++ b/dist/css/query-builder.default.css
@@ -0,0 +1,145 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Copyright 2014-2024 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+.query-builder .rule-placeholder, .query-builder .rule-container, .query-builder .rules-group-container {
+ position: relative;
+ margin: 4px 0;
+ border-radius: 5px;
+ padding: 5px;
+ border: 1px solid #EEE;
+ background: rgba(255, 255, 255, 0.9);
+}
+
+.query-builder .drag-handle, .query-builder .error-container, .query-builder .rule-container .rule-filter-container,
+.query-builder .rule-container .rule-operator-container,
+.query-builder .rule-container .rule-value-container {
+ display: inline-block;
+ margin: 0 5px 0 0;
+ vertical-align: middle;
+}
+
+.query-builder .rules-group-container {
+ padding: 10px;
+ padding-bottom: 6px;
+ border: 1px solid #DCC896;
+ background: rgba(250, 240, 210, 0.5);
+}
+.query-builder .rules-group-header {
+ margin-bottom: 10px;
+}
+.query-builder .rules-group-header .group-conditions .btn.readonly:not(.active),
+.query-builder .rules-group-header .group-conditions input[name$=_cond] {
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+ white-space: nowrap;
+}
+.query-builder .rules-group-header .group-conditions .btn.readonly {
+ border-radius: 3px;
+}
+.query-builder .rules-list {
+ list-style: none;
+ padding: 0 0 0 15px;
+ margin: 0;
+}
+.query-builder .rule-value-container {
+ border-left: 1px solid #DDD;
+ padding-left: 5px;
+}
+.query-builder .rule-value-container label {
+ margin-bottom: 0;
+ font-weight: normal;
+}
+.query-builder .rule-value-container label.block {
+ display: block;
+}
+.query-builder .error-container {
+ display: none;
+ cursor: help;
+ color: #F00;
+}
+.query-builder .has-error {
+ background-color: #FDD;
+ border-color: #F99;
+}
+.query-builder .has-error .error-container {
+ display: inline-block !important;
+}
+.query-builder .rules-list > *::before, .query-builder .rules-list > *::after {
+ content: "";
+ position: absolute;
+ left: -10px;
+ width: 10px;
+ height: calc(50% + 4px);
+ border-color: #CCC;
+ border-style: solid;
+}
+.query-builder .rules-list > *::before {
+ top: -4px;
+ border-width: 0 0 2px 2px;
+}
+.query-builder .rules-list > *::after {
+ top: 50%;
+ border-width: 0 0 0 2px;
+}
+.query-builder .rules-list > *:first-child::before {
+ top: -12px;
+ height: calc(50% + 14px);
+}
+.query-builder .rules-list > *:last-child::before {
+ border-radius: 0 0 0 4px;
+}
+.query-builder .rules-list > *:last-child::after {
+ display: none;
+}
+
+.query-builder.bt-checkbox-bootstrap-icons .checkbox input[type=checkbox] + label::before {
+ outline: 0;
+}
+.query-builder.bt-checkbox-bootstrap-icons .checkbox input[type=checkbox]:checked + label::after {
+ font-family: "bootstrap-icons";
+ content: "\f633";
+}
+
+.query-builder .error-container + .tooltip .tooltip-inner {
+ color: #F99 !important;
+}
+
+.query-builder p.filter-description {
+ margin: 5px 0 0 0;
+ background: #D9EDF7;
+ border: 1px solid #BCE8F1;
+ color: #31708F;
+ border-radius: 5px;
+ padding: 2.5px 5px;
+ font-size: 0.8em;
+}
+
+.query-builder .rules-group-header [data-invert] {
+ margin-left: 5px;
+}
+
+.query-builder .drag-handle {
+ cursor: move;
+ vertical-align: middle;
+ margin-left: 5px;
+}
+.query-builder .dragging {
+ position: fixed;
+ opacity: 0.5;
+ z-index: 100;
+}
+.query-builder .dragging::before, .query-builder .dragging::after {
+ display: none;
+}
+.query-builder .rule-placeholder {
+ border: 1px dashed #BBB;
+ opacity: 0.7;
+}
\ No newline at end of file
diff --git a/dist/i18n/da.js b/dist/i18n/da.js
deleted file mode 100644
index f4c53fbc..00000000
--- a/dist/i18n/da.js
+++ /dev/null
@@ -1,35 +0,0 @@
-/*!
- * jQuery QueryBuilder
- * Oversat af Jna Borup Coyle, github@coyle.dk
- */
-
-jQuery.fn.queryBuilder.defaults.set({ lang: {
- "add_rule": "Tilføj regel",
- "add_group": "Tilføj gruppe",
- "delete_rule": "Slet regel",
- "delete_group": "Slet gruppe",
-
- "condition_and": "OG",
- "condition_or": "ELLER",
-
- "filter_select_placeholder": "------",
-
- "operator_equal": "lig med",
- "operator_not_equal": "ikke lige med",
- "operator_in": "i",
- "operator_not_in": "ikke i",
- "operator_less": "mindre",
- "operator_less_or_equal": "mindre eller lig med",
- "operator_greater": "større",
- "operator_greater_or_equal": "større eller lig med",
- "operator_begins_with": "begynder med",
- "operator_not_begins_with": "begynder ikke med",
- "operator_contains": "indeholder",
- "operator_not_contains": "indeholder ikke",
- "operator_ends_with": "slutter med",
- "operator_not_ends_with": "slutter ikke med",
- "operator_is_empty": "er tom",
- "operator_is_not_empty": "er ikke tom",
- "operator_is_null": "er null",
- "operator_is_not_null": "er ikke null"
-}});
\ No newline at end of file
diff --git a/dist/i18n/de.js b/dist/i18n/de.js
deleted file mode 100644
index 6cf2b3fa..00000000
--- a/dist/i18n/de.js
+++ /dev/null
@@ -1,35 +0,0 @@
-/*!
- * jQuery QueryBuilder
- * German translation
- */
-
-jQuery.fn.queryBuilder.defaults.set({ lang: {
- "add_rule": "neue Regel",
- "add_group": "neue Gruppe",
- "delete_rule": "löschen",
- "delete_group": "löschen",
-
- "condition_and": "UND",
- "condition_or": "ODER",
-
- "filter_select_placeholder": "------",
-
- "operator_equal": "gleich",
- "operator_not_equal": "ungleich",
- "operator_in": "in",
- "operator_not_in": "nicht in",
- "operator_less": "kleiner",
- "operator_less_or_equal": "kleiner gleich",
- "operator_greater": "größer",
- "operator_greater_or_equal": "größer gleich",
- "operator_begins_with": "beginnt mit",
- "operator_not_begins_with": "beginnt nicht mit",
- "operator_contains": "enthält",
- "operator_not_contains": "enthält nicht",
- "operator_ends_with": "endet mit",
- "operator_not_ends_with": "endet nicht mit",
- "operator_is_empty": "ist leer",
- "operator_is_not_empty": "ist nicht leer",
- "operator_is_null": "ist null",
- "operator_is_not_null": "ist nicht null"
-}});
\ No newline at end of file
diff --git a/dist/i18n/en.js b/dist/i18n/en.js
deleted file mode 100644
index b22a9d9f..00000000
--- a/dist/i18n/en.js
+++ /dev/null
@@ -1,35 +0,0 @@
-/*!
- * jQuery QueryBuilder
- * Reference language file
- */
-
-jQuery.fn.queryBuilder.defaults.set({ lang: {
- "add_rule": "Add rule",
- "add_group": "Add group",
- "delete_rule": "Delete",
- "delete_group": "Delete",
-
- "condition_and": "AND",
- "condition_or": "OR",
-
- "filter_select_placeholder": "------",
-
- "operator_equal": "equal",
- "operator_not_equal": "not equal",
- "operator_in": "in",
- "operator_not_in": "not in",
- "operator_less": "less",
- "operator_less_or_equal": "less or equal",
- "operator_greater": "greater",
- "operator_greater_or_equal": "greater or equal",
- "operator_begins_with": "begins with",
- "operator_not_begins_with": "doesn't begin with",
- "operator_contains": "contains",
- "operator_not_contains": "doesn't contain",
- "operator_ends_with": "ends with",
- "operator_not_ends_with": "doesn't end with",
- "operator_is_empty": "is empty",
- "operator_is_not_empty": "is not empty",
- "operator_is_null": "is null",
- "operator_is_not_null": "is not null"
-}});
\ No newline at end of file
diff --git a/dist/i18n/fr.js b/dist/i18n/fr.js
deleted file mode 100644
index a2e9e7a3..00000000
--- a/dist/i18n/fr.js
+++ /dev/null
@@ -1,35 +0,0 @@
-/*!
- * jQuery QueryBuilder
- * French translation by Damien "Mistic" Sorel
- */
-
-jQuery.fn.queryBuilder.defaults.set({ lang: {
- "add_rule": "Ajouter une règle",
- "add_group": "Ajouter un groupe",
- "delete_rule": "Supprimer",
- "delete_group": "Supprimer",
-
- "condition_and": "ET",
- "condition_or": "OU",
-
- "filter_select_placeholder": "------",
-
- "operator_equal": "égal",
- "operator_not_equal": "non égal",
- "operator_in": "dans",
- "operator_not_in": "pas dans",
- "operator_less": "inférieur",
- "operator_less_or_equal": "inférieur ou égal",
- "operator_greater": "supérieur",
- "operator_greater_or_equal": "supérieur ou égal",
- "operator_begins_with": "commence par",
- "operator_not_begins_with": "ne commence pas par",
- "operator_contains": "contient",
- "operator_not_contains": "ne contient pas",
- "operator_ends_with": "finit par",
- "operator_not_ends_with": "ne finit pas par",
- "operator_is_empty": "est vide",
- "operator_is_not_empty": "n'est pas vide",
- "operator_is_null": "est nul",
- "operator_is_not_null": "n'est pas nul"
-}});
\ No newline at end of file
diff --git a/dist/i18n/query-builder.ar.js b/dist/i18n/query-builder.ar.js
new file mode 100644
index 00000000..edb2917e
--- /dev/null
+++ b/dist/i18n/query-builder.ar.js
@@ -0,0 +1,80 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Arabic (ar)
+ * Author: Mohamed YOUNES, https://github.com/MedYOUNES
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['ar'] = {
+ "__locale": "Arabic (ar)",
+ "__author": "Mohamed YOUNES, https://github.com/MedYOUNES",
+ "add_rule": "إضافة حُكم",
+ "add_group": "إضافة زُمْرَة",
+ "delete_rule": "حذف",
+ "delete_group": "حذف",
+ "conditions": {
+ "AND": "و",
+ "OR": "أو"
+ },
+ "operators": {
+ "equal": "يساوي",
+ "not_equal": "غير مساوٍ",
+ "in": "في",
+ "not_in": "ليس في",
+ "less": "أقل من",
+ "less_or_equal": "أصغر أو مساو",
+ "greater": "أكبر",
+ "greater_or_equal": "أكبر أو مساو",
+ "between": "محصور بين",
+ "not_between": "غير محصور بين",
+ "begins_with": "يبدأ بـ",
+ "not_begins_with": "لا يبدأ بـ",
+ "contains": "يحتوي على",
+ "not_contains": "لا يحتوي على",
+ "ends_with": "ينتهي بـ",
+ "not_ends_with": "لا ينتهي بـ",
+ "is_empty": "فارغ",
+ "is_not_empty": "غير فارغ",
+ "is_null": "صفر",
+ "is_not_null": "ليس صفرا"
+ },
+ "errors": {
+ "no_filter": "لم تحدد أي مرشح",
+ "empty_group": "الزمرة فارغة",
+ "radio_empty": "لم تحدد أي قيمة",
+ "checkbox_empty": "لم تحدد أي قيمة",
+ "select_empty": "لم تحدد أي قيمة",
+ "string_empty": "النص فارغ",
+ "string_exceed_min_length": "النص دون الأدنى المسموح به",
+ "string_exceed_max_length": "النص فوق الأقصى المسموح به",
+ "string_invalid_format": "تركيبة غير صحيحة",
+ "number_nan": "ليس عددا",
+ "number_not_integer": "ليس عددا صحيحا",
+ "number_not_double": "ليس عددا كسريا",
+ "number_exceed_min": "العدد أصغر من الأدنى المسموح به",
+ "number_exceed_max": "العدد أكبر من الأقصى المسموح به",
+ "number_wrong_step": "أخطأت في حساب مضاعفات العدد",
+ "datetime_empty": "لم تحدد التاريخ",
+ "datetime_invalid": "صيغة التاريخ غير صحيحة",
+ "datetime_exceed_min": "التاريخ دون الأدنى المسموح به",
+ "datetime_exceed_max": "التاريخ أكبر من الأقصى المسموح به",
+ "boolean_not_valid": "ليست قيمة منطقية ثنائية",
+ "operator_not_multiple": "العامل ليس متعدد القيَم"
+ },
+ "invert": "قَلْبُ"
+};
+
+QueryBuilder.defaults({ lang_code: 'ar' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.az.js b/dist/i18n/query-builder.az.js
new file mode 100644
index 00000000..3820f42e
--- /dev/null
+++ b/dist/i18n/query-builder.az.js
@@ -0,0 +1,79 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Azerbaijan (az)
+ * Author: Megaplan, mborisv
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['az'] = {
+ "__locale": "Azerbaijan (az)",
+ "__author": "Megaplan, mborisv ",
+ "add_rule": "Əlavə etmək",
+ "add_group": "Qrup əlavə etmək",
+ "delete_rule": "Silmək",
+ "delete_group": "Silmək",
+ "conditions": {
+ "AND": "VƏ",
+ "OR": "VƏ YA"
+ },
+ "operators": {
+ "equal": "bərabərdir",
+ "not_equal": "bərabər deyil",
+ "in": "qeyd edilmişlərdən",
+ "not_in": "qeyd olunmamışlardan",
+ "less": "daha az",
+ "less_or_equal": "daha az və ya bərabər",
+ "greater": "daha çox",
+ "greater_or_equal": "daha çox və ya bərabər",
+ "between": "arasında",
+ "begins_with": "başlayır",
+ "not_begins_with": "başlamır",
+ "contains": "ibarətdir",
+ "not_contains": "yoxdur",
+ "ends_with": "başa çatır",
+ "not_ends_with": "başa çatmır",
+ "is_empty": "boş sətir",
+ "is_not_empty": "boş olmayan sətir",
+ "is_null": "boşdur",
+ "is_not_null": "boş deyil"
+ },
+ "errors": {
+ "no_filter": "Filterlər seçilməyib",
+ "empty_group": "Qrup boşdur",
+ "radio_empty": "Məna seçilməyib",
+ "checkbox_empty": "Məna seçilməyib",
+ "select_empty": "Məna seçilməyib",
+ "string_empty": "Doldurulmayıb",
+ "string_exceed_min_length": "{0} daha çox simvol olmalıdır",
+ "string_exceed_max_length": "{0} daha az simvol olmalıdır",
+ "string_invalid_format": "Yanlış format ({0})",
+ "number_nan": "Rəqəm deyil",
+ "number_not_integer": "Rəqəm deyil",
+ "number_not_double": "Rəqəm deyil",
+ "number_exceed_min": "{0} daha çox olmalıdır",
+ "number_exceed_max": "{0} daha az olmalıdır",
+ "number_wrong_step": "{0} bölünən olmalıdır",
+ "datetime_empty": "Doldurulmayıb",
+ "datetime_invalid": "Yanlış tarix formatı ({0})",
+ "datetime_exceed_min": "{0} sonra olmalıdır",
+ "datetime_exceed_max": "{0} əvvəl olmalıdır",
+ "boolean_not_valid": "Loqik olmayan",
+ "operator_not_multiple": "\"{1}\" operatoru çoxlu məna daşımır"
+ },
+ "invert": "invert"
+};
+
+QueryBuilder.defaults({ lang_code: 'az' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.bg.js b/dist/i18n/query-builder.bg.js
new file mode 100644
index 00000000..3fa9dc74
--- /dev/null
+++ b/dist/i18n/query-builder.bg.js
@@ -0,0 +1,79 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Bulgarian (bg)
+ * Author: Valentin Hristov
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['bg'] = {
+ "__locale": "Bulgarian (bg)",
+ "__author": "Valentin Hristov",
+ "add_rule": "Добави правило",
+ "add_group": "Добави група",
+ "delete_rule": "Изтрий",
+ "delete_group": "Изтрий",
+ "conditions": {
+ "AND": "И",
+ "OR": "ИЛИ"
+ },
+ "operators": {
+ "equal": "равно",
+ "not_equal": "различно",
+ "in": "в",
+ "not_in": "не е в",
+ "less": "по-малко",
+ "less_or_equal": "по-малко или равно",
+ "greater": "по-голям",
+ "greater_or_equal": "по-голям или равно",
+ "between": "между",
+ "not_between": "не е между",
+ "begins_with": "започва с",
+ "not_begins_with": "не започва с",
+ "contains": "съдържа",
+ "not_contains": "не съдържа",
+ "ends_with": "завършва с",
+ "not_ends_with": "не завършва с",
+ "is_empty": "е празно",
+ "is_not_empty": "не е празно",
+ "is_null": "е нищо",
+ "is_not_null": "различно от нищо"
+ },
+ "errors": {
+ "no_filter": "Не е избран филтър",
+ "empty_group": "Групата е празна",
+ "radio_empty": "Не е селектирана стойност",
+ "checkbox_empty": "Не е селектирана стойност",
+ "select_empty": "Не е селектирана стойност",
+ "string_empty": "Празна стойност",
+ "string_exceed_min_length": "Необходимо е да съдържа поне {0} символа",
+ "string_exceed_max_length": "Необходимо е да съдържа повече от {0} символа",
+ "string_invalid_format": "Невалиден формат ({0})",
+ "number_nan": "Не е число",
+ "number_not_integer": "Не е цяло число",
+ "number_not_double": "Не е реално число",
+ "number_exceed_min": "Трябва да е по-голямо от {0}",
+ "number_exceed_max": "Трябва да е по-малко от {0}",
+ "number_wrong_step": "Трябва да е кратно на {0}",
+ "datetime_empty": "Празна стойност",
+ "datetime_invalid": "Невалиден формат на дата ({0})",
+ "datetime_exceed_min": "Трябва да е след {0}",
+ "datetime_exceed_max": "Трябва да е преди {0}",
+ "boolean_not_valid": "Не е булева",
+ "operator_not_multiple": "Оператора \"{1}\" не може да приеме множество стойности"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'bg' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.cs.js b/dist/i18n/query-builder.cs.js
new file mode 100644
index 00000000..551bee28
--- /dev/null
+++ b/dist/i18n/query-builder.cs.js
@@ -0,0 +1,79 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Čeština (cs)
+ * Author: Megaplan, mborisv
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['cs'] = {
+ "__locale": "Čeština (cs)",
+ "__author": "Megaplan, mborisv ",
+ "add_rule": "Přidat",
+ "add_group": "Přidat skupinu",
+ "delete_rule": "Odstranit",
+ "delete_group": "Odstranit skupinu",
+ "conditions": {
+ "AND": "I",
+ "OR": "NEBO"
+ },
+ "operators": {
+ "equal": "stejně",
+ "not_equal": "liší se",
+ "in": "z uvedených",
+ "not_in": "ne z uvedených",
+ "less": "méně",
+ "less_or_equal": "méně nebo stejně",
+ "greater": "více",
+ "greater_or_equal": "více nebo stejně",
+ "between": "mezi",
+ "begins_with": "začíná z",
+ "not_begins_with": "nezačíná z",
+ "contains": "obsahuje",
+ "not_contains": "neobsahuje",
+ "ends_with": "končí na",
+ "not_ends_with": "nekončí na",
+ "is_empty": "prázdný řádek",
+ "is_not_empty": "neprázdný řádek",
+ "is_null": "prázdno",
+ "is_not_null": "plno"
+ },
+ "errors": {
+ "no_filter": "není vybraný filtr",
+ "empty_group": "prázdná skupina",
+ "radio_empty": "Není udaná hodnota",
+ "checkbox_empty": "Není udaná hodnota",
+ "select_empty": "Není udaná hodnota",
+ "string_empty": "Nevyplněno",
+ "string_exceed_min_length": "Musí obsahovat více {0} symbolů",
+ "string_exceed_max_length": "Musí obsahovat méně {0} symbolů",
+ "string_invalid_format": "Nesprávný formát ({0})",
+ "number_nan": "Žádné číslo",
+ "number_not_integer": "Žádné číslo",
+ "number_not_double": "Žádné číslo",
+ "number_exceed_min": "Musí být více {0}",
+ "number_exceed_max": "Musí být méně {0}",
+ "number_wrong_step": "Musí být násobkem {0}",
+ "datetime_empty": "Nevyplněno",
+ "datetime_invalid": "Nesprávný formát datumu ({0})",
+ "datetime_exceed_min": "Musí být po {0}",
+ "datetime_exceed_max": "Musí být do {0}",
+ "boolean_not_valid": "Nelogické",
+ "operator_not_multiple": "Operátor \"{1}\" nepodporuje mnoho hodnot"
+ },
+ "invert": "invertní"
+};
+
+QueryBuilder.defaults({ lang_code: 'cs' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.da.js b/dist/i18n/query-builder.da.js
new file mode 100644
index 00000000..ff6d3f77
--- /dev/null
+++ b/dist/i18n/query-builder.da.js
@@ -0,0 +1,56 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Danish (da)
+ * Author: Jna Borup Coyle, github@coyle.dk
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['da'] = {
+ "__locale": "Danish (da)",
+ "__author": "Jna Borup Coyle, github@coyle.dk",
+ "add_rule": "Tilføj regel",
+ "add_group": "Tilføj gruppe",
+ "delete_rule": "Slet regel",
+ "delete_group": "Slet gruppe",
+ "conditions": {
+ "AND": "OG",
+ "OR": "ELLER"
+ },
+ "condition_and": "OG",
+ "condition_or": "ELLER",
+ "operators": {
+ "equal": "lig med",
+ "not_equal": "ikke lige med",
+ "in": "i",
+ "not_in": "ikke i",
+ "less": "mindre",
+ "less_or_equal": "mindre eller lig med",
+ "greater": "større",
+ "greater_or_equal": "større eller lig med",
+ "begins_with": "begynder med",
+ "not_begins_with": "begynder ikke med",
+ "contains": "indeholder",
+ "not_contains": "indeholder ikke",
+ "ends_with": "slutter med",
+ "not_ends_with": "slutter ikke med",
+ "is_empty": "er tom",
+ "is_not_empty": "er ikke tom",
+ "is_null": "er null",
+ "is_not_null": "er ikke null"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'da' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.de.js b/dist/i18n/query-builder.de.js
new file mode 100644
index 00000000..d92c6679
--- /dev/null
+++ b/dist/i18n/query-builder.de.js
@@ -0,0 +1,76 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: German (de)
+ * Author: "raimu"
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['de'] = {
+ "__locale": "German (de)",
+ "__author": "\"raimu\"",
+ "add_rule": "neue Regel",
+ "add_group": "neue Gruppe",
+ "delete_rule": "löschen",
+ "delete_group": "löschen",
+ "conditions": {
+ "AND": "UND",
+ "OR": "ODER"
+ },
+ "operators": {
+ "equal": "gleich",
+ "not_equal": "ungleich",
+ "in": "in",
+ "not_in": "nicht in",
+ "less": "kleiner",
+ "less_or_equal": "kleiner gleich",
+ "greater": "größer",
+ "greater_or_equal": "größer gleich",
+ "between": "zwischen",
+ "not_between": "nicht zwischen",
+ "begins_with": "beginnt mit",
+ "not_begins_with": "beginnt nicht mit",
+ "contains": "enthält",
+ "not_contains": "enthält nicht",
+ "ends_with": "endet mit",
+ "not_ends_with": "endet nicht mit",
+ "is_empty": "ist leer",
+ "is_not_empty": "ist nicht leer",
+ "is_null": "ist null",
+ "is_not_null": "ist nicht null"
+ },
+ "errors": {
+ "no_filter": "Kein Filter ausgewählt",
+ "empty_group": "Die Gruppe ist leer",
+ "radio_empty": "Kein Wert ausgewählt",
+ "checkbox_empty": "Kein Wert ausgewählt",
+ "select_empty": "Kein Wert ausgewählt",
+ "string_empty": "Leerer Wert",
+ "string_exceed_min_length": "Muss mindestens {0} Zeichen enthalten",
+ "string_exceed_max_length": "Darf nicht mehr als {0} Zeichen enthalten",
+ "string_invalid_format": "Ungültiges Format ({0})",
+ "number_nan": "Keine Zahl",
+ "number_not_integer": "Keine Ganzzahl",
+ "number_not_double": "Keine Dezimalzahl",
+ "number_exceed_min": "Muss größer als {0} sein",
+ "number_exceed_max": "Muss kleiner als {0} sein",
+ "number_wrong_step": "Muss ein Vielfaches von {0} sein",
+ "datetime_invalid": "Ungültiges Datumsformat ({0})",
+ "datetime_exceed_min": "Muss nach dem {0} sein",
+ "datetime_exceed_max": "Muss vor dem {0} sein"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'de' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.el.js b/dist/i18n/query-builder.el.js
new file mode 100644
index 00000000..701d2045
--- /dev/null
+++ b/dist/i18n/query-builder.el.js
@@ -0,0 +1,80 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Greek (el)
+ * Author: Stelios Patsatzis, https://www.linkedin.com/in/stelios-patsatzis-89841561
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['el'] = {
+ "__locale": "Greek (el)",
+ "__author": "Stelios Patsatzis, https://www.linkedin.com/in/stelios-patsatzis-89841561",
+ "add_rule": "Προσθήκη Συνθήκης",
+ "add_group": "Προσθήκη Ομάδας",
+ "delete_rule": "Διαγραφή",
+ "delete_group": "Διαγραφή",
+ "conditions": {
+ "AND": "Λογικό ΚΑΙ",
+ "OR": "Λογικό Η"
+ },
+ "operators": {
+ "equal": "Ισούται με",
+ "not_equal": "Διάφορο από ",
+ "in": "Περιέχει",
+ "not_in": "Δεν Περιέχει",
+ "less": "Λιγότερο από",
+ "less_or_equal": "Λιγότερο ή Ίσο",
+ "greater": "Μεγαλύτερο από",
+ "greater_or_equal": "Μεγαλύτερο ή Ίσο",
+ "between": "Μεταξύ",
+ "not_between": "Εκτός",
+ "begins_with": "Αρχίζει με",
+ "not_begins_with": "Δεν αρχίζει με",
+ "contains": "Περιέχει",
+ "not_contains": "Δεν περιέχει",
+ "ends_with": "Τελειώνει σε",
+ "not_ends_with": "Δεν τελειώνει σε",
+ "is_empty": "Είναι άδειο",
+ "is_not_empty": "Δεν είναι άδειο",
+ "is_null": "Είναι NULL",
+ "is_not_null": "Δεν είναι NULL"
+ },
+ "errors": {
+ "no_filter": "Χωρίς φίλτρα",
+ "empty_group": "Άδεια ομάδα",
+ "radio_empty": "Χωρίς τιμή",
+ "checkbox_empty": "Χωρίς τιμή",
+ "select_empty": "Χωρίς τιμή",
+ "string_empty": "Χωρίς τιμή",
+ "string_exceed_min_length": "Ελάχιστο όριο {0} χαρακτήρων",
+ "string_exceed_max_length": "Μέγιστο όριο {0} χαρακτήρων",
+ "string_invalid_format": "Λανθασμένη μορφή ({0})",
+ "number_nan": "Δεν είναι αριθμός",
+ "number_not_integer": "Δεν είναι ακέραιος αριθμός",
+ "number_not_double": "Δεν είναι πραγματικός αριθμός",
+ "number_exceed_min": "Πρέπει να είναι μεγαλύτερο απο {0}",
+ "number_exceed_max": "Πρέπει να είναι μικρότερο απο {0}",
+ "number_wrong_step": "Πρέπει να είναι πολλαπλάσιο του {0}",
+ "datetime_empty": "Χωρίς τιμή",
+ "datetime_invalid": "Λανθασμένη μορφή ημερομηνίας ({0})",
+ "datetime_exceed_min": "Νεότερο από {0}",
+ "datetime_exceed_max": "Παλαιότερο από {0}",
+ "boolean_not_valid": "Δεν είναι BOOLEAN",
+ "operator_not_multiple": "Η συνθήκη \"{1}\" δεν μπορεί να δεχθεί πολλαπλές τιμές"
+ },
+ "invert": "Εναλλαγή"
+};
+
+QueryBuilder.defaults({ lang_code: 'el' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.en.js b/dist/i18n/query-builder.en.js
new file mode 100644
index 00000000..a6ce0f66
--- /dev/null
+++ b/dist/i18n/query-builder.en.js
@@ -0,0 +1,83 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: English (en)
+ * Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['en'] = {
+ "__locale": "English (en)",
+ "__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr",
+ "add_rule": "Add rule",
+ "add_group": "Add group",
+ "delete_rule": "Delete",
+ "delete_group": "Delete",
+ "conditions": {
+ "AND": "AND",
+ "OR": "OR"
+ },
+ "operators": {
+ "equal": "equal",
+ "not_equal": "not equal",
+ "in": "in",
+ "not_in": "not in",
+ "less": "less",
+ "less_or_equal": "less or equal",
+ "greater": "greater",
+ "greater_or_equal": "greater or equal",
+ "between": "between",
+ "not_between": "not between",
+ "begins_with": "begins with",
+ "not_begins_with": "doesn't begin with",
+ "contains": "contains",
+ "not_contains": "doesn't contain",
+ "ends_with": "ends with",
+ "not_ends_with": "doesn't end with",
+ "is_empty": "is empty",
+ "is_not_empty": "is not empty",
+ "is_null": "is null",
+ "is_not_null": "is not null"
+ },
+ "errors": {
+ "no_filter": "No filter selected",
+ "empty_group": "The group is empty",
+ "radio_empty": "No value selected",
+ "checkbox_empty": "No value selected",
+ "select_empty": "No value selected",
+ "string_empty": "Empty value",
+ "string_exceed_min_length": "Must contain at least {0} characters",
+ "string_exceed_max_length": "Must not contain more than {0} characters",
+ "string_invalid_format": "Invalid format ({0})",
+ "number_nan": "Not a number",
+ "number_not_integer": "Not an integer",
+ "number_not_double": "Not a real number",
+ "number_exceed_min": "Must be greater than {0}",
+ "number_exceed_max": "Must be lower than {0}",
+ "number_wrong_step": "Must be a multiple of {0}",
+ "number_between_invalid": "Invalid values, {0} is greater than {1}",
+ "datetime_empty": "Empty value",
+ "datetime_invalid": "Invalid date format ({0})",
+ "datetime_exceed_min": "Must be after {0}",
+ "datetime_exceed_max": "Must be before {0}",
+ "datetime_between_invalid": "Invalid values, {0} is greater than {1}",
+ "boolean_not_valid": "Not a boolean",
+ "operator_not_multiple": "Operator \"{1}\" cannot accept multiple values"
+ },
+ "invert": "Invert",
+ "NOT": "NOT"
+};
+
+QueryBuilder.defaults({ lang_code: 'en' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.eo.js b/dist/i18n/query-builder.eo.js
new file mode 100644
index 00000000..6e66521d
--- /dev/null
+++ b/dist/i18n/query-builder.eo.js
@@ -0,0 +1,83 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Esperanto (eo)
+ * Author: Robin van der Vliet, https://robinvandervliet.com/
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['eo'] = {
+ "__locale": "Esperanto (eo)",
+ "__author": "Robin van der Vliet, https://robinvandervliet.com/",
+ "add_rule": "Aldoni regulon",
+ "add_group": "Aldoni grupon",
+ "delete_rule": "Forigi",
+ "delete_group": "Forigi",
+ "conditions": {
+ "AND": "KAJ",
+ "OR": "AŬ"
+ },
+ "operators": {
+ "equal": "estas egala al",
+ "not_equal": "ne estas egala al",
+ "in": "estas en",
+ "not_in": "ne estas en",
+ "less": "estas malpli ol",
+ "less_or_equal": "estas malpli ol aŭ egala al",
+ "greater": "estas pli ol",
+ "greater_or_equal": "estas pli ol aŭ egala al",
+ "between": "estas inter",
+ "not_between": "ne estas inter",
+ "begins_with": "komenciĝas per",
+ "not_begins_with": "ne komenciĝas per",
+ "contains": "enhavas",
+ "not_contains": "ne enhavas",
+ "ends_with": "finiĝas per",
+ "not_ends_with": "ne finiĝas per",
+ "is_empty": "estas malplena",
+ "is_not_empty": "ne estas malplena",
+ "is_null": "estas senvalora",
+ "is_not_null": "ne estas senvalora"
+ },
+ "errors": {
+ "no_filter": "Neniu filtrilo elektita",
+ "empty_group": "La grupo estas malplena",
+ "radio_empty": "Neniu valoro elektita",
+ "checkbox_empty": "Neniu valoro elektita",
+ "select_empty": "Neniu valoro elektita",
+ "string_empty": "Malplena valoro",
+ "string_exceed_min_length": "Devas enhavi pli ol {0} signojn",
+ "string_exceed_max_length": "Devas ne enhavi pli ol {0} signojn",
+ "string_invalid_format": "Nevalida strukturo ({0})",
+ "number_nan": "Ne estas nombro",
+ "number_not_integer": "Ne estas entjera nombro",
+ "number_not_double": "Ne estas reela nombro",
+ "number_exceed_min": "Devas esti pli ol {0}",
+ "number_exceed_max": "Devas esti malpli ol {0}",
+ "number_wrong_step": "Devas esti oblo de {0}",
+ "number_between_invalid": "Nevalidaj valoroj, {0} estas pli ol {1}",
+ "datetime_empty": "Malplena valoro",
+ "datetime_invalid": "Nevalida dato ({0})",
+ "datetime_exceed_min": "Devas esti post {0}",
+ "datetime_exceed_max": "Devas esti antaŭ {0}",
+ "datetime_between_invalid": "Nevalidaj valoroj, {0} estas post {1}",
+ "boolean_not_valid": "Ne estas bulea valoro",
+ "operator_not_multiple": "La operacio \"{1}\" ne akceptas plurajn valorojn"
+ },
+ "invert": "Inversigi",
+ "NOT": "NE"
+};
+
+QueryBuilder.defaults({ lang_code: 'eo' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.es.js b/dist/i18n/query-builder.es.js
new file mode 100644
index 00000000..ea94a3fd
--- /dev/null
+++ b/dist/i18n/query-builder.es.js
@@ -0,0 +1,81 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Spanish (es)
+ * Author: "pyarza", "kddlb"
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['es'] = {
+ "__locale": "Spanish (es)",
+ "__author": "\"pyarza\", \"kddlb\"",
+ "add_rule": "Añadir regla",
+ "add_group": "Añadir grupo",
+ "delete_rule": "Borrar",
+ "delete_group": "Borrar",
+ "conditions": {
+ "AND": "Y",
+ "OR": "O"
+ },
+ "operators": {
+ "equal": "igual",
+ "not_equal": "distinto",
+ "in": "en",
+ "not_in": "no en",
+ "less": "menor",
+ "less_or_equal": "menor o igual",
+ "greater": "mayor",
+ "greater_or_equal": "mayor o igual",
+ "between": "entre",
+ "not_between": "no está entre",
+ "begins_with": "empieza por",
+ "not_begins_with": "no empieza por",
+ "contains": "contiene",
+ "not_contains": "no contiene",
+ "ends_with": "acaba con",
+ "not_ends_with": "no acaba con",
+ "is_empty": "está vacío",
+ "is_not_empty": "no está vacío",
+ "is_null": "es nulo",
+ "is_not_null": "no es nulo"
+ },
+ "errors": {
+ "no_filter": "No se ha seleccionado ningún filtro",
+ "empty_group": "El grupo está vacío",
+ "radio_empty": "Ningún valor seleccionado",
+ "checkbox_empty": "Ningún valor seleccionado",
+ "select_empty": "Ningún valor seleccionado",
+ "string_empty": "Cadena vacía",
+ "string_exceed_min_length": "Debe contener al menos {0} caracteres",
+ "string_exceed_max_length": "No debe contener más de {0} caracteres",
+ "string_invalid_format": "Formato inválido ({0})",
+ "number_nan": "No es un número",
+ "number_not_integer": "No es un número entero",
+ "number_not_double": "No es un número real",
+ "number_exceed_min": "Debe ser mayor que {0}",
+ "number_exceed_max": "Debe ser menor que {0}",
+ "number_wrong_step": "Debe ser múltiplo de {0}",
+ "datetime_invalid": "Formato de fecha inválido ({0})",
+ "datetime_exceed_min": "Debe ser posterior a {0}",
+ "datetime_exceed_max": "Debe ser anterior a {0}",
+ "number_between_invalid": "Valores Inválidos, {0} es mayor que {1}",
+ "datetime_empty": "Campo vacio",
+ "datetime_between_invalid": "Valores Inválidos, {0} es mayor que {1}",
+ "boolean_not_valid": "No es booleano",
+ "operator_not_multiple": "El operador \"{1}\" no puede aceptar valores multiples"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'es' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.fa-IR.js b/dist/i18n/query-builder.fa-IR.js
new file mode 100644
index 00000000..011758fd
--- /dev/null
+++ b/dist/i18n/query-builder.fa-IR.js
@@ -0,0 +1,79 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Farsi (fa-ir)
+ * Author: Behzad Sedighzade, behzad.sedighzade@gmail.com
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['fa-IR'] = {
+ "__locale": "Farsi (fa-ir)",
+ "__author": "Behzad Sedighzade, behzad.sedighzade@gmail.com",
+ "add_rule": "افزودن قاعده",
+ "add_group": "افزودن گروه",
+ "delete_rule": "حذف قاعده",
+ "delete_group": "حذف گروه",
+ "conditions": {
+ "AND": "و",
+ "OR": "یا"
+ },
+ "operators": {
+ "equal": "برابر با",
+ "not_equal": "مخالف",
+ "in": "شامل مجموعه شود",
+ "not_in": "شامل مجموعه نشود",
+ "less": "کمتر از",
+ "less_or_equal": "کمتر یا مساوی با",
+ "greater": "بزرگتر از",
+ "greater_or_equal": "بزرگتر یا مساوی با",
+ "between": "مابین",
+ "not_between": "مابین نباشد",
+ "begins_with": "شروع شود با",
+ "not_begins_with": "شروع نشود با",
+ "contains": "شامل شود",
+ "not_contains": "شامل نشود",
+ "ends_with": "خاتمه یابد با",
+ "not_ends_with": "خاتمه نیابد با",
+ "is_empty": "خالی باشد",
+ "is_not_empty": "خالی نباشد",
+ "is_null": "باشد ( null ) پوچ",
+ "is_not_null": "نباشد( null ) پوچ "
+ },
+ "errors": {
+ "no_filter": "هیچ قاعده ای انتخاب نشده است",
+ "empty_group": "گروه خالی است",
+ "radio_empty": "مقداری انتخاب نشده است",
+ "checkbox_empty": "مقداری انتخاب نشده است",
+ "select_empty": "مقداری انتخاب نشده است",
+ "string_empty": "مقدار متنی خالی است",
+ "string_exceed_min_length": "رشته حداقل باید {0} عدد حرف داشته باشد",
+ "string_exceed_max_length": "رشته حداکثر {0} عدد حرف می تواند قبول کند",
+ "string_invalid_format": "قالب رشته {0} نامعتبر ست",
+ "number_nan": "عدد وارد کنید",
+ "number_not_integer": "مقدار صحیح وارد کنید",
+ "number_not_double": "مقدار اعشاری وارد کنید",
+ "number_exceed_min": "باید از {0} بزرگتر باشد",
+ "number_exceed_max": "باید از {0} کمتر باشد",
+ "number_wrong_step": "باید مضربی از {0} باشد",
+ "datetime_empty": "مقدار تاریخ خالی وارد شده!",
+ "datetime_invalid": "قالب تاریخ ( {0} ) اشتباه است",
+ "datetime_exceed_min": "باید بعد از {0} باشد",
+ "datetime_exceed_max": "باید قبل از {0} باشد",
+ "boolean_not_valid": "مقدار دودویی وارد کنید",
+ "operator_not_multiple": "اپراتور \"{1}\" نمی تواند چند مقدار قبول کند"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'fa-IR' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.fr.js b/dist/i18n/query-builder.fr.js
new file mode 100644
index 00000000..39dc7303
--- /dev/null
+++ b/dist/i18n/query-builder.fr.js
@@ -0,0 +1,83 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: French (fr)
+ * Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['fr'] = {
+ "__locale": "French (fr)",
+ "__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr",
+ "add_rule": "Ajouter une règle",
+ "add_group": "Ajouter un groupe",
+ "delete_rule": "Supprimer",
+ "delete_group": "Supprimer",
+ "conditions": {
+ "AND": "ET",
+ "OR": "OU"
+ },
+ "operators": {
+ "equal": "est égal à",
+ "not_equal": "n'est pas égal à",
+ "in": "est compris dans",
+ "not_in": "n'est pas compris dans",
+ "less": "est inférieur à",
+ "less_or_equal": "est inférieur ou égal à",
+ "greater": "est supérieur à",
+ "greater_or_equal": "est supérieur ou égal à",
+ "between": "est entre",
+ "not_between": "n'est pas entre",
+ "begins_with": "commence par",
+ "not_begins_with": "ne commence pas par",
+ "contains": "contient",
+ "not_contains": "ne contient pas",
+ "ends_with": "finit par",
+ "not_ends_with": "ne finit pas par",
+ "is_empty": "est vide",
+ "is_not_empty": "n'est pas vide",
+ "is_null": "est nul",
+ "is_not_null": "n'est pas nul"
+ },
+ "errors": {
+ "no_filter": "Aucun filtre sélectionné",
+ "empty_group": "Le groupe est vide",
+ "radio_empty": "Pas de valeur selectionnée",
+ "checkbox_empty": "Pas de valeur selectionnée",
+ "select_empty": "Pas de valeur selectionnée",
+ "string_empty": "Valeur vide",
+ "string_exceed_min_length": "Doit contenir au moins {0} caractères",
+ "string_exceed_max_length": "Ne doit pas contenir plus de {0} caractères",
+ "string_invalid_format": "Format invalide ({0})",
+ "number_nan": "N'est pas un nombre",
+ "number_not_integer": "N'est pas un entier",
+ "number_not_double": "N'est pas un nombre réel",
+ "number_exceed_min": "Doit être plus grand que {0}",
+ "number_exceed_max": "Doit être plus petit que {0}",
+ "number_wrong_step": "Doit être un multiple de {0}",
+ "number_between_invalid": "Valeurs invalides, {0} est plus grand que {1}",
+ "datetime_empty": "Valeur vide",
+ "datetime_invalid": "Fomat de date invalide ({0})",
+ "datetime_exceed_min": "Doit être après {0}",
+ "datetime_exceed_max": "Doit être avant {0}",
+ "datetime_between_invalid": "Valeurs invalides, {0} est plus grand que {1}",
+ "boolean_not_valid": "N'est pas un booléen",
+ "operator_not_multiple": "L'opérateur \"{1}\" ne peut utiliser plusieurs valeurs"
+ },
+ "invert": "Inverser",
+ "NOT": "NON"
+};
+
+QueryBuilder.defaults({ lang_code: 'fr' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.he.js b/dist/i18n/query-builder.he.js
new file mode 100644
index 00000000..2e453775
--- /dev/null
+++ b/dist/i18n/query-builder.he.js
@@ -0,0 +1,81 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Hebrew (he)
+ * Author: Kfir Stri https://github.com/kfirstri
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['he'] = {
+ "__locale": "Hebrew (he)",
+ "__author": "Kfir Stri https://github.com/kfirstri",
+ "add_rule": "הוסף כלל",
+ "add_group": "הוסף קבוצה",
+ "delete_rule": "מחק",
+ "delete_group": "מחק",
+ "conditions": {
+ "AND": "וגם",
+ "OR": "או"
+ },
+ "operators": {
+ "equal": "שווה ל",
+ "not_equal": "שונה מ",
+ "in": "חלק מ",
+ "not_in": "לא חלק מ",
+ "less": "פחות מ",
+ "less_or_equal": "פחות או שווה ל",
+ "greater": "גדול מ",
+ "greater_or_equal": "גדול או שווה ל",
+ "between": "בין",
+ "not_between": "לא בין",
+ "begins_with": "מתחיל ב",
+ "not_begins_with": "לא מתחיל ב",
+ "contains": "מכיל",
+ "not_contains": "לא מכיל",
+ "ends_with": "מסתיים ב",
+ "not_ends_with": "לא מסתיים ב",
+ "is_empty": "ריק",
+ "is_not_empty": "לא ריק",
+ "is_null": "חסר ערך",
+ "is_not_null": "לא חסר ערך"
+ },
+ "errors": {
+ "no_filter": "לא נבחרו מסננים",
+ "empty_group": "הקבוצה רירקה",
+ "radio_empty": "לא נבחר אף ערך",
+ "checkbox_empty": "לא נבחר אף ערך",
+ "select_empty": "לא נבחר אף ערך",
+ "string_empty": "חסר ערך",
+ "string_exceed_min_length": "המחרוזת חייבת להכיל לפחות {0} תווים",
+ "string_exceed_max_length": "המחרוזת לא יכולה להכיל יותר מ{0} תווים",
+ "string_invalid_format": "המחרוזת בפורמט שגוי ({0})",
+ "number_nan": "זהו לא מספר",
+ "number_not_integer": "המספר אינו מספר שלם",
+ "number_not_double": "המספר אינו מספר עשרוני",
+ "number_exceed_min": "המספר צריך להיות גדול מ {0}",
+ "number_exceed_max": "המספר צריך להיות קטן מ{0}",
+ "number_wrong_step": "המספר צריך להיות כפולה של {0}",
+ "datetime_empty": "תאריך ריק",
+ "datetime_invalid": "פורמט תאריך שגוי ({0})",
+ "datetime_exceed_min": "התאריך חייב להיות אחרי {0}",
+ "datetime_exceed_max": "התאריך חייב להיות לפני {0}",
+ "boolean_not_valid": "זהו לא בוליאני",
+ "operator_not_multiple": "האופרטור \"{1}\" לא יכול לקבל ערכים מרובים"
+ },
+ "invert": "הפוך שאילתא",
+ "NOT": "לא"
+};
+
+QueryBuilder.defaults({ lang_code: 'he' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.hu.js b/dist/i18n/query-builder.hu.js
new file mode 100644
index 00000000..323a9adc
--- /dev/null
+++ b/dist/i18n/query-builder.hu.js
@@ -0,0 +1,83 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Hungarian - Magyar (hu)
+ * Author: Szabó Attila "Tailor993", https://www.tailor993.hu
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['hu'] = {
+ "__locale": "Hungarian - Magyar (hu)",
+ "__author": "Szabó Attila \"Tailor993\", https://www.tailor993.hu",
+ "add_rule": "Feltétel hozzáadása",
+ "add_group": "Csoport hozzáadása",
+ "delete_rule": "Feltétel törlése",
+ "delete_group": "Csoport törlése",
+ "conditions": {
+ "AND": "ÉS",
+ "OR": "VAGY"
+ },
+ "operators": {
+ "equal": "egyenlő",
+ "not_equal": "nem egyenlő",
+ "in": "bennevan",
+ "not_in": "nincs benne",
+ "less": "kisebb",
+ "less_or_equal": "kisebb vagy egyenlő",
+ "greater": "nagyobb",
+ "greater_or_equal": "nagyobb vagy egyenlő",
+ "between": "közötte",
+ "not_between": "nincs közötte",
+ "begins_with": "ezzel kezdődik",
+ "not_begins_with": "ezzel nem kezdődik",
+ "contains": "tartalmazza",
+ "not_contains": "nem tartalmazza",
+ "ends_with": "erre végződik",
+ "not_ends_with": "errre nem végződik",
+ "is_empty": "üres",
+ "is_not_empty": "nem üres",
+ "is_null": "null",
+ "is_not_null": "nem null"
+ },
+ "errors": {
+ "no_filter": "Nincs kiválasztott feltétel",
+ "empty_group": "A csoport üres",
+ "radio_empty": "Nincs kiválasztott érték",
+ "checkbox_empty": "Nincs kiválasztott érték",
+ "select_empty": "Nincs kiválasztott érték",
+ "string_empty": "Üres érték",
+ "string_exceed_min_length": "A megadott szöveg rövidebb a várt {0} karakternél",
+ "string_exceed_max_length": "A megadott szöveg nem tartalmazhat többet, mint {0} karaktert",
+ "string_invalid_format": "Nem megfelelő formátum ({0})",
+ "number_nan": "Nem szám",
+ "number_not_integer": "Nem egész szám (integer)",
+ "number_not_double": "Nem valós szám",
+ "number_exceed_min": "Nagyobbnak kell lennie, mint {0}",
+ "number_exceed_max": "Kisebbnek kell lennie, mint {0}",
+ "number_wrong_step": "{0} többszörösének kell lennie.",
+ "number_between_invalid": "INem megfelelő érték, {0} nagyobb, mint {1}",
+ "datetime_empty": "Üres érték",
+ "datetime_invalid": "nem megfelelő dátum formátum ({0})",
+ "datetime_exceed_min": "A dátumnak későbbinek kell lennie, mint{0}",
+ "datetime_exceed_max": "A dátumnak korábbinak kell lennie, mint {0}",
+ "datetime_between_invalid": "Nem megfelelő értékek, {0} nagyobb, mint {1}",
+ "boolean_not_valid": "Nem igaz/hamis (boolean)",
+ "operator_not_multiple": "Ez a művelet: \"{1}\" nem fogadhat el több értéket"
+ },
+ "invert": "Megfordítás (Invertálás)",
+ "NOT": "NEM"
+};
+
+QueryBuilder.defaults({ lang_code: 'hu' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.it.js b/dist/i18n/query-builder.it.js
new file mode 100644
index 00000000..c4565e96
--- /dev/null
+++ b/dist/i18n/query-builder.it.js
@@ -0,0 +1,81 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Italian (it)
+ * Author: davegraziosi, Giuseppe Lodi Rizzini
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['it'] = {
+ "__locale": "Italian (it)",
+ "__author": "davegraziosi, Giuseppe Lodi Rizzini",
+ "add_rule": "Aggiungi regola",
+ "add_group": "Aggiungi gruppo",
+ "delete_rule": "Elimina",
+ "delete_group": "Elimina",
+ "conditions": {
+ "AND": "E",
+ "OR": "O"
+ },
+ "operators": {
+ "equal": "uguale",
+ "not_equal": "non uguale",
+ "in": "in",
+ "not_in": "non in",
+ "less": "minore",
+ "less_or_equal": "minore o uguale",
+ "greater": "maggiore",
+ "greater_or_equal": "maggiore o uguale",
+ "between": "compreso tra",
+ "not_between": "non compreso tra",
+ "begins_with": "inizia con",
+ "not_begins_with": "non inizia con",
+ "contains": "contiene",
+ "not_contains": "non contiene",
+ "ends_with": "finisce con",
+ "not_ends_with": "non finisce con",
+ "is_empty": "è vuoto",
+ "is_not_empty": "non è vuoto",
+ "is_null": "è nullo",
+ "is_not_null": "non è nullo"
+ },
+ "errors": {
+ "no_filter": "Nessun filtro selezionato",
+ "empty_group": "Il gruppo è vuoto",
+ "radio_empty": "No value selected",
+ "checkbox_empty": "Nessun valore selezionato",
+ "select_empty": "Nessun valore selezionato",
+ "string_empty": "Valore vuoto",
+ "string_exceed_min_length": "Deve contenere almeno {0} caratteri",
+ "string_exceed_max_length": "Non deve contenere più di {0} caratteri",
+ "string_invalid_format": "Formato non valido ({0})",
+ "number_nan": "Non è un numero",
+ "number_not_integer": "Non è un intero",
+ "number_not_double": "Non è un numero con la virgola",
+ "number_exceed_min": "Deve essere maggiore di {0}",
+ "number_exceed_max": "Deve essere minore di {0}",
+ "number_wrong_step": "Deve essere multiplo di {0}",
+ "number_between_invalid": "Valori non validi, {0} è maggiore di {1}",
+ "datetime_empty": "Valore vuoto",
+ "datetime_invalid": "Formato data non valido ({0})",
+ "datetime_exceed_min": "Deve essere successivo a {0}",
+ "datetime_exceed_max": "Deve essere precedente a {0}",
+ "datetime_between_invalid": "Valori non validi, {0} è maggiore di {1}",
+ "boolean_not_valid": "Non è un booleano",
+ "operator_not_multiple": "L'Operatore {0} non può accettare valori multipli"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'it' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.lt.js b/dist/i18n/query-builder.lt.js
new file mode 100644
index 00000000..5d0324ab
--- /dev/null
+++ b/dist/i18n/query-builder.lt.js
@@ -0,0 +1,83 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Lithuanian (lt)
+ * Author: Dalius Guzauskas (aka Tichij), https://lt.linkedin.com/in/daliusg
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['lt'] = {
+ "__locale": "Lithuanian (lt)",
+ "__author": "Dalius Guzauskas (aka Tichij), https://lt.linkedin.com/in/daliusg",
+ "add_rule": "Pridėti taisyklę",
+ "add_group": "Pridėti grupę",
+ "delete_rule": "Ištrinti",
+ "delete_group": "Ištrinti",
+ "conditions": {
+ "AND": "IR",
+ "OR": "ARBA"
+ },
+ "operators": {
+ "equal": "lygu",
+ "not_equal": "nėra lygu",
+ "in": "iš nurodytų",
+ "not_in": "ne iš nurodytų",
+ "less": "mažiau",
+ "less_or_equal": "mažiau arba lygu",
+ "greater": "daugiau",
+ "greater_or_equal": "daugiau arba lygu",
+ "between": "tarp",
+ "not_between": "nėra tarp",
+ "begins_with": "prasideda",
+ "not_begins_with": "neprasideda",
+ "contains": "turi",
+ "not_contains": "neturi",
+ "ends_with": "baigiasi",
+ "not_ends_with": "nesibaigia",
+ "is_empty": "tuščia",
+ "is_not_empty": "ne tuščia",
+ "is_null": "neapibrėžta",
+ "is_not_null": "nėra neapibrėžta"
+ },
+ "errors": {
+ "no_filter": "Nepasirinktas filtras",
+ "empty_group": "Grupė tuščia",
+ "radio_empty": "Nepasirinkta reikšmė",
+ "checkbox_empty": "Nepasirinkta reikšmė",
+ "select_empty": "Nepasirinkta reikšmė",
+ "string_empty": "Tuščia reikšmė",
+ "string_exceed_min_length": "Turi būti bent {0} simbolių",
+ "string_exceed_max_length": "Turi būti ne daugiau kaip {0} simbolių",
+ "string_invalid_format": "Klaidingas formatas ({0})",
+ "number_nan": "Nėra skaičius",
+ "number_not_integer": "Ne sveikasis skaičius",
+ "number_not_double": "Ne realusis skaičius",
+ "number_exceed_min": "Turi būti daugiau už {0}",
+ "number_exceed_max": "Turi būti mažiau už {0}",
+ "number_wrong_step": "Turi būti {0} kartotinis",
+ "number_between_invalid": "Klaidingos reikšmės, {0} yra daugiau už {1}",
+ "datetime_empty": "Tuščia reikšmė",
+ "datetime_invalid": "Klaidingas datos formatas ({0})",
+ "datetime_exceed_min": "Turi būti po {0}",
+ "datetime_exceed_max": "Turi būti prieš {0}",
+ "datetime_between_invalid": "Klaidingos reikšmės, {0} yra daugiau už {1}",
+ "boolean_not_valid": "Nėra loginis tipas",
+ "operator_not_multiple": "Operatorius \"{1}\" negali priimti kelių reikšmių"
+ },
+ "invert": "Invertuoti",
+ "NOT": "NE"
+};
+
+QueryBuilder.defaults({ lang_code: 'lt' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.nl.js b/dist/i18n/query-builder.nl.js
new file mode 100644
index 00000000..8e88ac56
--- /dev/null
+++ b/dist/i18n/query-builder.nl.js
@@ -0,0 +1,76 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Dutch (nl)
+ * Author: "Roywcm"
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['nl'] = {
+ "__locale": "Dutch (nl)",
+ "__author": "\"Roywcm\"",
+ "add_rule": "Nieuwe regel",
+ "add_group": "Nieuwe groep",
+ "delete_rule": "Verwijder",
+ "delete_group": "Verwijder",
+ "conditions": {
+ "AND": "EN",
+ "OR": "OF"
+ },
+ "operators": {
+ "equal": "gelijk",
+ "not_equal": "niet gelijk",
+ "in": "in",
+ "not_in": "niet in",
+ "less": "minder",
+ "less_or_equal": "minder of gelijk",
+ "greater": "groter",
+ "greater_or_equal": "groter of gelijk",
+ "between": "tussen",
+ "not_between": "niet tussen",
+ "begins_with": "begint met",
+ "not_begins_with": "begint niet met",
+ "contains": "bevat",
+ "not_contains": "bevat niet",
+ "ends_with": "eindigt met",
+ "not_ends_with": "eindigt niet met",
+ "is_empty": "is leeg",
+ "is_not_empty": "is niet leeg",
+ "is_null": "is null",
+ "is_not_null": "is niet null"
+ },
+ "errors": {
+ "no_filter": "Geen filter geselecteerd",
+ "empty_group": "De groep is leeg",
+ "radio_empty": "Geen waarde geselecteerd",
+ "checkbox_empty": "Geen waarde geselecteerd",
+ "select_empty": "Geen waarde geselecteerd",
+ "string_empty": "Lege waarde",
+ "string_exceed_min_length": "Dient minstens {0} karakters te bevatten",
+ "string_exceed_max_length": "Dient niet meer dan {0} karakters te bevatten",
+ "string_invalid_format": "Ongeldig format ({0})",
+ "number_nan": "Niet een nummer",
+ "number_not_integer": "Geen geheel getal",
+ "number_not_double": "Geen echt nummer",
+ "number_exceed_min": "Dient groter te zijn dan {0}",
+ "number_exceed_max": "Dient lager te zijn dan {0}",
+ "number_wrong_step": "Dient een veelvoud te zijn van {0}",
+ "datetime_invalid": "Ongeldige datumformat ({0})",
+ "datetime_exceed_min": "Dient na {0}",
+ "datetime_exceed_max": "Dient voor {0}"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'nl' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.no.js b/dist/i18n/query-builder.no.js
new file mode 100644
index 00000000..1ec2d013
--- /dev/null
+++ b/dist/i18n/query-builder.no.js
@@ -0,0 +1,54 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Norwegian (no)
+ * Author: Jna Borup Coyle, github@coyle.dk
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['no'] = {
+ "__locale": "Norwegian (no)",
+ "__author": "Jna Borup Coyle, github@coyle.dk",
+ "add_rule": "Legg til regel",
+ "add_group": "Legg til gruppe",
+ "delete_rule": "Slett regel",
+ "delete_group": "Slett gruppe",
+ "conditions": {
+ "AND": "OG",
+ "OR": "ELLER"
+ },
+ "operators": {
+ "equal": "er lik",
+ "not_equal": "er ikke lik",
+ "in": "finnes i",
+ "not_in": "finnes ikke i",
+ "less": "er mindre enn",
+ "less_or_equal": "er mindre eller lik",
+ "greater": "er større enn",
+ "greater_or_equal": "er større eller lik",
+ "begins_with": "begynner med",
+ "not_begins_with": "begynner ikke med",
+ "contains": "inneholder",
+ "not_contains": "inneholder ikke",
+ "ends_with": "slutter med",
+ "not_ends_with": "slutter ikke med",
+ "is_empty": "er tom",
+ "is_not_empty": "er ikke tom",
+ "is_null": "er null",
+ "is_not_null": "er ikke null"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'no' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.pl.js b/dist/i18n/query-builder.pl.js
new file mode 100644
index 00000000..c89d0316
--- /dev/null
+++ b/dist/i18n/query-builder.pl.js
@@ -0,0 +1,80 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Polish (pl)
+ * Author: Artur Smolarek
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['pl'] = {
+ "__locale": "Polish (pl)",
+ "__author": "Artur Smolarek",
+ "add_rule": "Dodaj regułę",
+ "add_group": "Dodaj grupę",
+ "delete_rule": "Usuń",
+ "delete_group": "Usuń",
+ "conditions": {
+ "AND": "ORAZ",
+ "OR": "LUB"
+ },
+ "operators": {
+ "equal": "równa się",
+ "not_equal": "jest różne od",
+ "in": "zawiera",
+ "not_in": "nie zawiera",
+ "less": "mniejsze",
+ "less_or_equal": "mniejsze lub równe",
+ "greater": "większe",
+ "greater_or_equal": "większe lub równe",
+ "between": "pomiędzy",
+ "not_between": "nie jest pomiędzy",
+ "begins_with": "rozpoczyna się od",
+ "not_begins_with": "nie rozpoczyna się od",
+ "contains": "zawiera",
+ "not_contains": "nie zawiera",
+ "ends_with": "kończy się na",
+ "not_ends_with": "nie kończy się na",
+ "is_empty": "jest puste",
+ "is_not_empty": "nie jest puste",
+ "is_null": "jest niezdefiniowane",
+ "is_not_null": "nie jest niezdefiniowane"
+ },
+ "errors": {
+ "no_filter": "Nie wybrano żadnego filtra",
+ "empty_group": "Grupa jest pusta",
+ "radio_empty": "Nie wybrano wartości",
+ "checkbox_empty": "Nie wybrano wartości",
+ "select_empty": "Nie wybrano wartości",
+ "string_empty": "Nie wpisano wartości",
+ "string_exceed_min_length": "Minimalna długość to {0} znaków",
+ "string_exceed_max_length": "Maksymalna długość to {0} znaków",
+ "string_invalid_format": "Nieprawidłowy format ({0})",
+ "number_nan": "To nie jest liczba",
+ "number_not_integer": "To nie jest liczba całkowita",
+ "number_not_double": "To nie jest liczba rzeczywista",
+ "number_exceed_min": "Musi być większe niż {0}",
+ "number_exceed_max": "Musi być mniejsze niż {0}",
+ "number_wrong_step": "Musi być wielokrotnością {0}",
+ "datetime_empty": "Nie wybrano wartości",
+ "datetime_invalid": "Nieprawidłowy format daty ({0})",
+ "datetime_exceed_min": "Musi być po {0}",
+ "datetime_exceed_max": "Musi być przed {0}",
+ "boolean_not_valid": "Niepoprawna wartość logiczna",
+ "operator_not_multiple": "Operator \"{1}\" nie przyjmuje wielu wartości"
+ },
+ "invert": "Odwróć"
+};
+
+QueryBuilder.defaults({ lang_code: 'pl' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.pt-BR.js b/dist/i18n/query-builder.pt-BR.js
new file mode 100644
index 00000000..a9b4fe59
--- /dev/null
+++ b/dist/i18n/query-builder.pt-BR.js
@@ -0,0 +1,80 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Brazilian Portuguese (pr-BR)
+ * Author: Leandro Gehlen, leandrogehlen@gmail.com; Marcos Ferretti, marcosvferretti@gmail.com
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['pt-BR'] = {
+ "__locale": "Brazilian Portuguese (pr-BR)",
+ "__author": "Leandro Gehlen, leandrogehlen@gmail.com; Marcos Ferretti, marcosvferretti@gmail.com",
+ "add_rule": "Nova Regra",
+ "add_group": "Novo Grupo",
+ "delete_rule": "Excluir",
+ "delete_group": "Excluir",
+ "conditions": {
+ "AND": "E",
+ "OR": "OU"
+ },
+ "operators": {
+ "equal": "Igual",
+ "not_equal": "Diferente",
+ "in": "Contido",
+ "not_in": "Não contido",
+ "less": "Menor",
+ "less_or_equal": "Menor ou igual",
+ "greater": "Maior",
+ "greater_or_equal": "Maior ou igual",
+ "between": "Entre",
+ "not_between": "Não entre",
+ "begins_with": "Iniciando com",
+ "not_begins_with": "Não iniciando com",
+ "contains": "Contém",
+ "not_contains": "Não contém",
+ "ends_with": "Terminando com",
+ "not_ends_with": "Terminando sem",
+ "is_empty": "É vazio",
+ "is_not_empty": "Não é vazio",
+ "is_null": "É nulo",
+ "is_not_null": "Não é nulo"
+ },
+ "errors": {
+ "no_filter": "Nenhum filtro selecionado",
+ "empty_group": "O grupo está vazio",
+ "radio_empty": "Nenhum valor selecionado",
+ "checkbox_empty": "Nenhum valor selecionado",
+ "select_empty": "Nenhum valor selecionado",
+ "string_empty": "Valor vazio",
+ "string_exceed_min_length": "É necessário conter pelo menos {0} caracteres",
+ "string_exceed_max_length": "É necessário conter mais de {0} caracteres",
+ "string_invalid_format": "Formato inválido ({0})",
+ "number_nan": "Não é um número",
+ "number_not_integer": "Não é um número inteiro",
+ "number_not_double": "Não é um número real",
+ "number_exceed_min": "É necessário ser maior que {0}",
+ "number_exceed_max": "É necessário ser menor que {0}",
+ "number_wrong_step": "É necessário ser múltiplo de {0}",
+ "datetime_invalid": "Formato de data inválido ({0})",
+ "datetime_exceed_min": "É necessário ser superior a {0}",
+ "datetime_exceed_max": "É necessário ser inferior a {0}",
+ "datetime_empty": "Nenhuma data selecionada",
+ "boolean_not_valid": "Não é um valor booleano",
+ "operator_not_multiple": "O operador \"{1}\" não aceita valores múltiplos"
+ },
+ "invert": "Inverter"
+};
+
+QueryBuilder.defaults({ lang_code: 'pt-BR' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.pt-PT.js b/dist/i18n/query-builder.pt-PT.js
new file mode 100644
index 00000000..1e0aa77f
--- /dev/null
+++ b/dist/i18n/query-builder.pt-PT.js
@@ -0,0 +1,75 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Portuguese (pt-PT)
+ * Author: Miguel Guerreiro, migas.csi@gmail.com
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['pt-PT'] = {
+ "__locale": "Portuguese (pt-PT)",
+ "__author": "Miguel Guerreiro, migas.csi@gmail.com",
+ "add_rule": "Nova Regra",
+ "add_group": "Novo Grupo",
+ "delete_rule": "Excluir",
+ "delete_group": "Excluir",
+ "conditions": {
+ "AND": "E",
+ "OR": "OU"
+ },
+ "operators": {
+ "equal": "Igual a",
+ "not_equal": "Diferente de",
+ "in": "Contido",
+ "not_in": "Não contido",
+ "less": "Menor que",
+ "less_or_equal": "Menor ou igual a",
+ "greater": "Maior que",
+ "greater_or_equal": "Maior ou igual que",
+ "between": "Entre",
+ "begins_with": "Começar por",
+ "not_begins_with": "Não a começar por",
+ "contains": "Contém",
+ "not_contains": "Não contém",
+ "ends_with": "Terminando com",
+ "not_ends_with": "Terminando sem",
+ "is_empty": "É vazio",
+ "is_not_empty": "Não é vazio",
+ "is_null": "É nulo",
+ "is_not_null": "Não é nulo"
+ },
+ "errors": {
+ "no_filter": "Nenhum filtro selecionado",
+ "empty_group": "O grupo está vazio",
+ "radio_empty": "Nenhum valor selecionado",
+ "checkbox_empty": "Nenhum valor selecionado",
+ "select_empty": "Nenhum valor selecionado",
+ "string_empty": "Valor vazio",
+ "string_exceed_min_length": "É necessário conter pelo menos {0} caracteres",
+ "string_exceed_max_length": "É necessário conter mais de {0} caracteres",
+ "string_invalid_format": "Formato inválido ({0})",
+ "number_nan": "Não é um número",
+ "number_not_integer": "Não é um número inteiro",
+ "number_not_double": "Não é um número real",
+ "number_exceed_min": "É necessário ser maior que {0}",
+ "number_exceed_max": "É necessário ser menor que {0}",
+ "number_wrong_step": "É necessário ser múltiplo de {0}",
+ "datetime_invalid": "Formato de data inválido ({0})",
+ "datetime_exceed_min": "É necessário ser superior a {0}",
+ "datetime_exceed_max": "É necessário ser inferior a {0}"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'pt-PT' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.ro.js b/dist/i18n/query-builder.ro.js
new file mode 100644
index 00000000..a1ba7eda
--- /dev/null
+++ b/dist/i18n/query-builder.ro.js
@@ -0,0 +1,81 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Romanian (ro)
+ * Author: ArianServ, totpero
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['ro'] = {
+ "__locale": "Romanian (ro)",
+ "__author": "ArianServ, totpero",
+ "add_rule": "Adaugă regulă",
+ "add_group": "Adaugă grup",
+ "delete_rule": "Şterge",
+ "delete_group": "Şterge",
+ "conditions": {
+ "AND": "ŞI",
+ "OR": "SAU"
+ },
+ "operators": {
+ "equal": "egal",
+ "not_equal": "diferit",
+ "in": "în",
+ "not_in": "nu în",
+ "less": "mai mic",
+ "less_or_equal": "mai mic sau egal",
+ "greater": "mai mare",
+ "greater_or_equal": "mai mare sau egal",
+ "between": "între",
+ "not_between": "nu între",
+ "begins_with": "începe cu",
+ "not_begins_with": "nu începe cu",
+ "contains": "conţine",
+ "not_contains": "nu conţine",
+ "ends_with": "se termină cu",
+ "not_ends_with": "nu se termină cu",
+ "is_empty": "este gol",
+ "is_not_empty": "nu este gol",
+ "is_null": "e nul",
+ "is_not_null": "nu e nul"
+ },
+ "errors": {
+ "no_filter": "Nici un filtru selectat",
+ "empty_group": "Grupul este gol",
+ "radio_empty": "Nici o valoare nu este selectată",
+ "checkbox_empty": "Nici o valoare nu este selectată",
+ "select_empty": "Nici o valoare nu este selectată",
+ "string_empty": "Valoare goală",
+ "string_exceed_min_length": "Trebuie să conţină mai puţin de {0} caractere",
+ "string_exceed_max_length": "Trebuie să conţină mai mult de {0} caractere",
+ "string_invalid_format": "Format invalid ({0})",
+ "number_nan": "Nu este număr",
+ "number_not_integer": "Nu este număr întreg",
+ "number_not_double": "Nu este număr real",
+ "number_exceed_min": "Trebuie să fie mai mare decât {0}",
+ "number_exceed_max": "Trebuie să fie mai mic decât {0}",
+ "number_wrong_step": "Trebuie să fie multiplu de {0}",
+ "number_between_invalid": "Valori invalide, {0} este mai mare decât {1}",
+ "datetime_empty": "Valoare goală",
+ "datetime_invalid": "Format dată invalid ({0})",
+ "datetime_exceed_min": "Trebuie să fie după {0}",
+ "datetime_exceed_max": "Trebuie să fie înainte {0}",
+ "datetime_between_invalid": "Valori invalide, {0} este mai mare decât {1}",
+ "boolean_not_valid": "Nu este boolean",
+ "operator_not_multiple": "Operatorul \"{1}\" nu poate accepta mai multe valori"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'ro' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.ru.js b/dist/i18n/query-builder.ru.js
new file mode 100644
index 00000000..03cc3b7c
--- /dev/null
+++ b/dist/i18n/query-builder.ru.js
@@ -0,0 +1,82 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Russian (ru)
+ * Author:
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['ru'] = {
+ "__locale": "Russian (ru)",
+ "add_rule": "Добавить",
+ "add_group": "Добавить группу",
+ "delete_rule": "Удалить",
+ "delete_group": "Удалить",
+ "conditions": {
+ "AND": "И",
+ "OR": "ИЛИ"
+ },
+ "operators": {
+ "equal": "равно",
+ "not_equal": "не равно",
+ "in": "из указанных",
+ "not_in": "не из указанных",
+ "less": "меньше",
+ "less_or_equal": "меньше или равно",
+ "greater": "больше",
+ "greater_or_equal": "больше или равно",
+ "between": "между",
+ "not_between": "не между",
+ "begins_with": "начинается с",
+ "not_begins_with": "не начинается с",
+ "contains": "содержит",
+ "not_contains": "не содержит",
+ "ends_with": "оканчивается на",
+ "not_ends_with": "не оканчивается на",
+ "is_empty": "пустая строка",
+ "is_not_empty": "не пустая строка",
+ "is_null": "пусто",
+ "is_not_null": "не пусто"
+ },
+ "errors": {
+ "no_filter": "Фильтр не выбран",
+ "empty_group": "Группа пуста",
+ "radio_empty": "Не выбрано значение",
+ "checkbox_empty": "Не выбрано значение",
+ "select_empty": "Не выбрано значение",
+ "string_empty": "Не заполнено",
+ "string_exceed_min_length": "Должен содержать больше {0} символов",
+ "string_exceed_max_length": "Должен содержать меньше {0} символов",
+ "string_invalid_format": "Неверный формат ({0})",
+ "number_nan": "Не число",
+ "number_not_integer": "Не число",
+ "number_not_double": "Не число",
+ "number_exceed_min": "Должно быть больше {0}",
+ "number_exceed_max": "Должно быть меньше, чем {0}",
+ "number_wrong_step": "Должно быть кратно {0}",
+ "number_between_invalid": "Недопустимые значения, {0} больше {1}",
+ "datetime_empty": "Не заполнено",
+ "datetime_invalid": "Неверный формат даты ({0})",
+ "datetime_exceed_min": "Должно быть, после {0}",
+ "datetime_exceed_max": "Должно быть, до {0}",
+ "datetime_between_invalid": "Недопустимые значения, {0} больше {1}",
+ "boolean_not_valid": "Не логическое",
+ "operator_not_multiple": "Оператор \"{1}\" не поддерживает много значений"
+ },
+ "invert": "Инвертировать",
+ "NOT": "НЕ"
+};
+
+QueryBuilder.defaults({ lang_code: 'ru' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.sk.js b/dist/i18n/query-builder.sk.js
new file mode 100644
index 00000000..d8ca2623
--- /dev/null
+++ b/dist/i18n/query-builder.sk.js
@@ -0,0 +1,83 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Slovensky (sk)
+ * Author: k2s
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['sk'] = {
+ "__locale": "Slovensky (sk)",
+ "__author": "k2s",
+ "add_rule": "Pridať podmienku",
+ "add_group": "Pridať skupinu",
+ "delete_rule": "Zmazať",
+ "delete_group": "Zmazať",
+ "conditions": {
+ "AND": "A",
+ "OR": "ALEBO"
+ },
+ "operators": {
+ "equal": "rovné",
+ "not_equal": "nerovné",
+ "in": "v",
+ "not_in": "nie v",
+ "less": "menej",
+ "less_or_equal": "menej alebo rovné",
+ "greater": "väčšie",
+ "greater_or_equal": "väčšie alebo rovné",
+ "between": "medzi",
+ "not_between": "nie medzi",
+ "begins_with": "začína na",
+ "not_begins_with": "nezačína na",
+ "contains": "obsahuje",
+ "not_contains": "neobsahuje",
+ "ends_with": "končí na",
+ "not_ends_with": "nekončí na",
+ "is_empty": "je prázdne",
+ "is_not_empty": "nie je prázdne",
+ "is_null": "je null",
+ "is_not_null": "nie je null"
+ },
+ "errors": {
+ "no_filter": "Nie je zvolený filter",
+ "empty_group": "Skupina je prázdna",
+ "radio_empty": "Nie je označená hodnota",
+ "checkbox_empty": "Nie je označená hodnota",
+ "select_empty": "Nie je označená hodnota",
+ "string_empty": "Prázdna hodnota",
+ "string_exceed_min_length": "Musí obsahovať aspon {0} znakov",
+ "string_exceed_max_length": "Nesmie obsahovať viac ako {0} znakov",
+ "string_invalid_format": "Chybný formát ({0})",
+ "number_nan": "Nie je číslo",
+ "number_not_integer": "Nie je celé číslo",
+ "number_not_double": "Nie je desatinné číslo",
+ "number_exceed_min": "Musí byť väčšie ako {0}",
+ "number_exceed_max": "Musí byť menšie ako {0}",
+ "number_wrong_step": "Musí byť násobkom čísla {0}",
+ "number_between_invalid": "Chybné hodnoty, {0} je väčšie ako {1}",
+ "datetime_empty": "Prázdna hodnota",
+ "datetime_invalid": "Chybný formát dátumu ({0})",
+ "datetime_exceed_min": "Musí byť neskôr ako {0}",
+ "datetime_exceed_max": "Musí byť skôr ako {0}",
+ "datetime_between_invalid": "Chybné hodnoty, {0} je neskôr ako {1}",
+ "boolean_not_valid": "Neplatné áno/nie",
+ "operator_not_multiple": "Operátor '{1}' nepodporuje viacero hodnôt"
+ },
+ "invert": "Invertný",
+ "NOT": "NIE"
+};
+
+QueryBuilder.defaults({ lang_code: 'sk' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.sq.js b/dist/i18n/query-builder.sq.js
new file mode 100644
index 00000000..f991b12a
--- /dev/null
+++ b/dist/i18n/query-builder.sq.js
@@ -0,0 +1,78 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Albanian (sq)
+ * Author: Tomor Pupovci
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['sq'] = {
+ "__locale": "Albanian (sq)",
+ "__author": "Tomor Pupovci",
+ "add_rule": "Shto rregull",
+ "add_group": "Shto grup",
+ "delete_rule": "Fshij",
+ "delete_group": "Fshij",
+ "conditions": {
+ "AND": "DHE",
+ "OR": "OSE"
+ },
+ "operators": {
+ "equal": "barabartë",
+ "not_equal": "e ndryshme prej",
+ "in": "në",
+ "not_in": "jo në",
+ "less": "më e vogël",
+ "less_or_equal": "më e vogël ose e barabartë me",
+ "greater": "më e madhe",
+ "greater_or_equal": "më e madhe ose e barabartë",
+ "between": "në mes",
+ "begins_with": "fillon me",
+ "not_begins_with": "nuk fillon me",
+ "contains": "përmban",
+ "not_contains": "nuk përmban",
+ "ends_with": "mbaron me",
+ "not_ends_with": "nuk mbaron me",
+ "is_empty": "është e zbrazët",
+ "is_not_empty": "nuk është e zbrazët",
+ "is_null": "është null",
+ "is_not_null": "nuk është null"
+ },
+ "errors": {
+ "no_filter": "Nuk ka filter të zgjedhur",
+ "empty_group": "Grupi është i zbrazët",
+ "radio_empty": "Nuk ka vlerë të zgjedhur",
+ "checkbox_empty": "Nuk ka vlerë të zgjedhur",
+ "select_empty": "Nuk ka vlerë të zgjedhur",
+ "string_empty": "Vlerë e zbrazët",
+ "string_exceed_min_length": "Duhet të përmbajë së paku {0} karaktere",
+ "string_exceed_max_length": "Nuk duhet të përmbajë më shumë se {0} karaktere",
+ "string_invalid_format": "Format i pasaktë ({0})",
+ "number_nan": "Nuk është numër",
+ "number_not_integer": "Nuk është numër i plotë",
+ "number_not_double": "Nuk është numër me presje",
+ "number_exceed_min": "Duhet të jetë më i madh se {0}",
+ "number_exceed_max": "Duhet të jetë më i vogël se {0}",
+ "number_wrong_step": "Duhet të jetë shumëfish i {0}",
+ "datetime_empty": "Vlerë e zbrazët",
+ "datetime_invalid": "Format i pasaktë i datës ({0})",
+ "datetime_exceed_min": "Duhet të jetë pas {0}",
+ "datetime_exceed_max": "Duhet të jetë para {0}",
+ "boolean_not_valid": "Nuk është boolean",
+ "operator_not_multiple": "Operatori \"{1}\" nuk mund të pranojë vlera të shumëfishta"
+ }
+};
+
+QueryBuilder.defaults({ lang_code: 'sq' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.sv.js b/dist/i18n/query-builder.sv.js
new file mode 100644
index 00000000..715cebb5
--- /dev/null
+++ b/dist/i18n/query-builder.sv.js
@@ -0,0 +1,83 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Svenska (sv)
+ * Author: hekin1
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['sv'] = {
+ "__locale": "Svenska (sv)",
+ "__author": "hekin1",
+ "add_rule": "Lägg till regel",
+ "add_group": "Lägg till grupp",
+ "delete_rule": "Ta bort",
+ "delete_group": "Ta bort",
+ "conditions": {
+ "AND": "OCH",
+ "OR": "ELLER"
+ },
+ "operators": {
+ "equal": "lika med",
+ "not_equal": "ej lika med",
+ "in": "en av",
+ "not_in": "ej en av",
+ "less": "mindre",
+ "less_or_equal": "mindre eller lika med",
+ "greater": "större",
+ "greater_or_equal": "större eller lika med",
+ "between": "mellan",
+ "not_between": "ej mellan",
+ "begins_with": "börjar med",
+ "not_begins_with": "börjar inte med",
+ "contains": "innehåller",
+ "not_contains": "innehåller inte",
+ "ends_with": "slutar med",
+ "not_ends_with": "slutar inte med",
+ "is_empty": "är tom",
+ "is_not_empty": "är inte tom",
+ "is_null": "är null",
+ "is_not_null": "är inte null"
+ },
+ "errors": {
+ "no_filter": "Inget filter valt",
+ "empty_group": "Gruppen är tom",
+ "radio_empty": "Inget värde valt",
+ "checkbox_empty": "Inget värde valt",
+ "select_empty": "Inget värde valt",
+ "string_empty": "Tomt värde",
+ "string_exceed_min_length": "Måste innehålla minst {0} tecken",
+ "string_exceed_max_length": "Får ej innehålla fler än {0} tecken",
+ "string_invalid_format": "Felaktigt format ({0})",
+ "number_nan": "Inte numeriskt",
+ "number_not_integer": "Inte en siffra",
+ "number_not_double": "Inte ett decimaltal",
+ "number_exceed_min": "Måste vara större än {0}",
+ "number_exceed_max": "Måste vara lägre än {0}",
+ "number_wrong_step": "Måste vara en mutipel av {0}",
+ "number_between_invalid": "Felaktiga värden, {0} är större än {1}",
+ "datetime_empty": "Tomt värde",
+ "datetime_invalid": "Felaktigt datumformat ({0})",
+ "datetime_exceed_min": "Måste vara efter {0}",
+ "datetime_exceed_max": "Måste vara före {0}",
+ "datetime_between_invalid": "Felaktiga värden, {0} är större än {1}",
+ "boolean_not_valid": "Inte en boolean",
+ "operator_not_multiple": "Operatorn \"{1}\" accepterar inte flera värden"
+ },
+ "invert": "Invertera",
+ "NOT": "INTE"
+};
+
+QueryBuilder.defaults({ lang_code: 'sv' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.sw.js b/dist/i18n/query-builder.sw.js
new file mode 100644
index 00000000..8829fc60
--- /dev/null
+++ b/dist/i18n/query-builder.sw.js
@@ -0,0 +1,83 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Swahili (sw)
+ * Author: Timothy Anyona
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['sw'] = {
+ "__locale": "Swahili (sw)",
+ "__author": "Timothy Anyona",
+ "add_rule": "Ongeza kanuni",
+ "add_group": "Ongeza kikundi",
+ "delete_rule": "Futa",
+ "delete_group": "Futa",
+ "conditions": {
+ "AND": "NA",
+ "OR": "AU"
+ },
+ "operators": {
+ "equal": "ni",
+ "not_equal": "sio",
+ "in": "mojawapo ya",
+ "not_in": "sio mojawapo ya",
+ "less": "isiyozidi",
+ "less_or_equal": "isiyozidi au ni sawa na",
+ "greater": "inayozidi",
+ "greater_or_equal": "inayozidi au ni sawa na",
+ "between": "kati ya",
+ "not_between": "isiyo kati ya",
+ "begins_with": "inaanza na",
+ "not_begins_with": "isiyoanza na",
+ "contains": "ina",
+ "not_contains": "haina",
+ "ends_with": "inaisha na",
+ "not_ends_with": "isiyoisha na",
+ "is_empty": "ni tupu",
+ "is_not_empty": "sio tupu",
+ "is_null": "ni batili",
+ "is_not_null": "sio batili"
+ },
+ "errors": {
+ "no_filter": "Chujio halijachaguliwa",
+ "empty_group": "Kikundi ki tupu",
+ "radio_empty": "Thamani haijachaguliwa",
+ "checkbox_empty": "Thamani haijachaguliwa",
+ "select_empty": "Thamani haijachaguliwa",
+ "string_empty": "Thamani tupu",
+ "string_exceed_min_length": "Lazima iwe na vibambo visiopungua {0}",
+ "string_exceed_max_length": "Haifai kuwa na vibambo zaidi ya {0}",
+ "string_invalid_format": "Fomati batili ({0})",
+ "number_nan": "Sio nambari",
+ "number_not_integer": "Sio namba kamili",
+ "number_not_double": "Sio namba desimali",
+ "number_exceed_min": "Lazima iwe zaidi ya {0}",
+ "number_exceed_max": "Lazima iwe chini ya {0}",
+ "number_wrong_step": "Lazima iwe kigawe cha {0}",
+ "number_between_invalid": "Thamani batili, {0} ni kubwa kuliko {1}",
+ "datetime_empty": "Thamani tupu",
+ "datetime_invalid": "Fomati tarehe batili ({0})",
+ "datetime_exceed_min": "Lazima iwe baada ya {0}",
+ "datetime_exceed_max": "Lazima iwe kabla ya {0}",
+ "datetime_between_invalid": "Thamani batili, {0} ni baada ya {1}",
+ "boolean_not_valid": "Sio buleani",
+ "operator_not_multiple": "Opereta \"{1}\" haikubali thamani nyingi"
+ },
+ "invert": "Pindua",
+ "NOT": "SIO"
+};
+
+QueryBuilder.defaults({ lang_code: 'sw' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.tr.js b/dist/i18n/query-builder.tr.js
new file mode 100644
index 00000000..7751e423
--- /dev/null
+++ b/dist/i18n/query-builder.tr.js
@@ -0,0 +1,82 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Turkish (tr)
+ * Author: Aykut Alpgiray Ateş
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['tr'] = {
+ "__locale": "Turkish (tr)",
+ "__author": "Aykut Alpgiray Ateş",
+ "add_rule": "Kural Ekle",
+ "add_group": "Grup Ekle",
+ "delete_rule": "Sil",
+ "delete_group": "Sil",
+ "conditions": {
+ "AND": "Ve",
+ "OR": "Veya"
+ },
+ "operators": {
+ "equal": "eşit",
+ "not_equal": "eşit değil",
+ "in": "içinde",
+ "not_in": "içinde değil",
+ "less": "küçük",
+ "less_or_equal": "küçük veya eşit",
+ "greater": "büyük",
+ "greater_or_equal": "büyük veya eşit",
+ "between": "arasında",
+ "not_between": "arasında değil",
+ "begins_with": "ile başlayan",
+ "not_begins_with": "ile başlamayan",
+ "contains": "içeren",
+ "not_contains": "içermeyen",
+ "ends_with": "ile biten",
+ "not_ends_with": "ile bitmeyen",
+ "is_empty": "boş ise",
+ "is_not_empty": "boş değil ise",
+ "is_null": "var ise",
+ "is_not_null": "yok ise"
+ },
+ "errors": {
+ "no_filter": "Bir filtre seçili değil",
+ "empty_group": "Grup bir eleman içermiyor",
+ "radio_empty": "Seçim yapılmalı",
+ "checkbox_empty": "Seçim yapılmalı",
+ "select_empty": "Seçim yapılmalı",
+ "string_empty": "Bir metin girilmeli",
+ "string_exceed_min_length": "En az {0} karakter girilmeli",
+ "string_exceed_max_length": "En fazla {0} karakter girilebilir",
+ "string_invalid_format": "Uyumsuz format ({0})",
+ "number_nan": "Sayı değil",
+ "number_not_integer": "Tam sayı değil",
+ "number_not_double": "Ondalıklı sayı değil",
+ "number_exceed_min": "Sayı {0}'den/dan daha büyük olmalı",
+ "number_exceed_max": "Sayı {0}'den/dan daha küçük olmalı",
+ "number_wrong_step": "{0} veya katı olmalı",
+ "number_between_invalid": "Geçersiz değerler, {0} değeri {1} değerinden büyük",
+ "datetime_empty": "Tarih Seçilmemiş",
+ "datetime_invalid": "Uygun olmayan tarih formatı ({0})",
+ "datetime_exceed_min": "{0} Tarihinden daha sonrası olmalı.",
+ "datetime_exceed_max": "{0} Tarihinden daha öncesi olmalı.",
+ "datetime_between_invalid": "Geçersiz değerler, {0} değeri {1} değerinden büyük",
+ "boolean_not_valid": "Değer Doğru/Yanlış(bool) olmalı",
+ "operator_not_multiple": "Operatör \"{1}\" birden fazla değer kabul etmiyor"
+ },
+ "invert": "Ters Çevir"
+};
+
+QueryBuilder.defaults({ lang_code: 'tr' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.ua.js b/dist/i18n/query-builder.ua.js
new file mode 100644
index 00000000..1238b26e
--- /dev/null
+++ b/dist/i18n/query-builder.ua.js
@@ -0,0 +1,79 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Ukrainian (ua)
+ * Author: Megaplan, mborisv
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['ua'] = {
+ "__locale": "Ukrainian (ua)",
+ "__author": "Megaplan, mborisv ",
+ "add_rule": "Додати",
+ "add_group": "Додати групу",
+ "delete_rule": "Видалити",
+ "delete_group": "Видалити",
+ "conditions": {
+ "AND": "І",
+ "OR": "АБО"
+ },
+ "operators": {
+ "equal": "дорівнює",
+ "not_equal": "не дорівнює",
+ "in": "з вказаних",
+ "not_in": "не з вказаних",
+ "less": "менше",
+ "less_or_equal": "менше або дорівнюж",
+ "greater": "більше",
+ "greater_or_equal": "більше або дорівнює",
+ "between": "між",
+ "begins_with": "починається з",
+ "not_begins_with": "не починається з",
+ "contains": "містить",
+ "not_contains": "не містить",
+ "ends_with": "закінчується на",
+ "not_ends_with": "не не закінчується на",
+ "is_empty": "порожній рядок",
+ "is_not_empty": "не порожній рядок",
+ "is_null": "порожньо",
+ "is_not_null": "не порожньо"
+ },
+ "errors": {
+ "no_filter": "Фільтр не вибраний",
+ "empty_group": "Група порожня",
+ "radio_empty": "Значення не вибрано",
+ "checkbox_empty": "Значення не вибрано",
+ "select_empty": "Значення не вибрано",
+ "string_empty": "Не заповнено",
+ "string_exceed_min_length": "Повинен містити більше {0} символів",
+ "string_exceed_max_length": "Повинен містити менше {0} символів",
+ "string_invalid_format": "Невірний формат ({0})",
+ "number_nan": "Не число",
+ "number_not_integer": "Не число",
+ "number_not_double": "Не число",
+ "number_exceed_min": "Повинне бути більше {0}",
+ "number_exceed_max": "Повинне бути менше, ніж {0}",
+ "number_wrong_step": "Повинне бути кратне {0}",
+ "datetime_empty": "Не заповнено",
+ "datetime_invalid": "Невірний формат дати ({0})",
+ "datetime_exceed_min": "Повинне бути, після {0}",
+ "datetime_exceed_max": "Повинне бути, до {0}",
+ "boolean_not_valid": "Не логічне",
+ "operator_not_multiple": "Оператор \"{1}\" не підтримує багато значень"
+ },
+ "invert": "інвертувати"
+};
+
+QueryBuilder.defaults({ lang_code: 'ua' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/query-builder.zh-CN.js b/dist/i18n/query-builder.zh-CN.js
new file mode 100644
index 00000000..d908f33a
--- /dev/null
+++ b/dist/i18n/query-builder.zh-CN.js
@@ -0,0 +1,80 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: Simplified Chinese (zh_CN)
+ * Author: shadowwind, shatteredwindgo@gmail.com
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'query-builder'], factory);
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+var QueryBuilder = $.fn.queryBuilder;
+
+QueryBuilder.regional['zh-CN'] = {
+ "__locale": "Simplified Chinese (zh_CN)",
+ "__author": "shadowwind, shatteredwindgo@gmail.com",
+ "add_rule": "添加规则",
+ "add_group": "添加组",
+ "delete_rule": "删除",
+ "delete_group": "删除组",
+ "conditions": {
+ "AND": "和",
+ "OR": "或"
+ },
+ "operators": {
+ "equal": "等于",
+ "not_equal": "不等于",
+ "in": "在...之內",
+ "not_in": "不在...之內",
+ "less": "小于",
+ "less_or_equal": "小于或等于",
+ "greater": "大于",
+ "greater_or_equal": "大于或等于",
+ "between": "在...之间",
+ "not_between": "不在...之间",
+ "begins_with": "以...开始",
+ "not_begins_with": "不以...开始",
+ "contains": "包含以下内容",
+ "not_contains": "不包含以下内容",
+ "ends_with": "以...结束",
+ "not_ends_with": "不以...结束",
+ "is_empty": "为空",
+ "is_not_empty": "不为空",
+ "is_null": "为 null",
+ "is_not_null": "不为 null"
+ },
+ "errors": {
+ "no_filter": "没有选择过滤器",
+ "empty_group": "该组为空",
+ "radio_empty": "没有选中项",
+ "checkbox_empty": "没有选中项",
+ "select_empty": "没有选中项",
+ "string_empty": "没有输入值",
+ "string_exceed_min_length": "必须至少包含{0}个字符",
+ "string_exceed_max_length": "必须不超过{0}个字符",
+ "string_invalid_format": "无效格式({0})",
+ "number_nan": "值不是数字",
+ "number_not_integer": "不是整数",
+ "number_not_double": "不是浮点数",
+ "number_exceed_min": "必须大于{0}",
+ "number_exceed_max": "必须小于{0}",
+ "number_wrong_step": "必须是{0}的倍数",
+ "datetime_empty": "值为空",
+ "datetime_invalid": "不是有效日期({0})",
+ "datetime_exceed_min": "必须在{0}之后",
+ "datetime_exceed_max": "必须在{0}之前",
+ "boolean_not_valid": "不是布尔值",
+ "operator_not_multiple": "选项\"{1}\"无法接受多个值"
+ },
+ "invert": "倒置"
+};
+
+QueryBuilder.defaults({ lang_code: 'zh-CN' });
+}));
\ No newline at end of file
diff --git a/dist/i18n/ro.js b/dist/i18n/ro.js
deleted file mode 100644
index 3523ad13..00000000
--- a/dist/i18n/ro.js
+++ /dev/null
@@ -1,35 +0,0 @@
-/*!
- * jQuery QueryBuilder
- * Romanian translation by ArianServ
- */
-
-jQuery.fn.queryBuilder.defaults.set({ lang: {
- "add_rule": "Adaugă regulă",
- "add_group": "Adaugă grup",
- "delete_rule": "Şterge",
- "delete_group": "Şterge",
-
- "condition_and": "ŞI",
- "condition_or": "SAU",
-
- "filter_select_placeholder": "------",
-
- "operator_equal": "egal",
- "operator_not_equal": "diferit",
- "operator_in": "în",
- "operator_not_in": "nu în",
- "operator_less": "mai puţin",
- "operator_less_or_equal": "mai puţin sau egal",
- "operator_greater": "mai mare",
- "operator_greater_or_equal": "mai mare sau egal",
- "operator_begins_with": "începe cu",
- "operator_not_begins_with": "nu începe cu",
- "operator_contains": "conţine",
- "operator_not_contains": "nu conţine",
- "operator_ends_with": "se termină cu",
- "operator_not_ends_with": "nu se termină cu",
- "operator_is_empty": "este gol",
- "operator_is_not_empty": "nu este gol",
- "operator_is_null": "e nul",
- "operator_is_not_null": "nu e nul"
-}});
\ No newline at end of file
diff --git a/dist/js/query-builder.js b/dist/js/query-builder.js
new file mode 100644
index 00000000..b5b30837
--- /dev/null
+++ b/dist/js/query-builder.js
@@ -0,0 +1,6174 @@
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Copyright 2014-2024 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define(['jquery', 'jquery-extendext'], factory);
+ }
+ else if (typeof module === 'object' && module.exports) {
+ module.exports = factory(require('jquery'), require('jquery-extendext'));
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+/**
+ * @typedef {object} Filter
+ * @memberof QueryBuilder
+ * @description See {@link http://querybuilder.js.org/index.html#filters}
+ */
+
+/**
+ * @typedef {object} Operator
+ * @memberof QueryBuilder
+ * @description See {@link http://querybuilder.js.org/index.html#operators}
+ */
+
+/**
+ * @param {jQuery} $el
+ * @param {object} options - see {@link http://querybuilder.js.org/#options}
+ * @constructor
+ */
+var QueryBuilder = function($el, options) {
+ $el[0].queryBuilder = this;
+
+ /**
+ * Element container
+ * @member {jQuery}
+ * @readonly
+ */
+ this.$el = $el;
+
+ /**
+ * Configuration object
+ * @member {object}
+ * @readonly
+ */
+ this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options);
+
+ /**
+ * Internal model
+ * @member {Model}
+ * @readonly
+ */
+ this.model = new Model();
+
+ /**
+ * Internal status
+ * @member {object}
+ * @property {string} id - id of the container
+ * @property {boolean} generated_id - if the container id has been generated
+ * @property {int} group_id - current group id
+ * @property {int} rule_id - current rule id
+ * @property {boolean} has_optgroup - if filters have optgroups
+ * @property {boolean} has_operator_optgroup - if operators have optgroups
+ * @readonly
+ * @private
+ */
+ this.status = {
+ id: null,
+ generated_id: false,
+ group_id: 0,
+ rule_id: 0,
+ has_optgroup: false,
+ has_operator_optgroup: false
+ };
+
+ /**
+ * List of filters
+ * @member {QueryBuilder.Filter[]}
+ * @readonly
+ */
+ this.filters = this.settings.filters;
+
+ /**
+ * List of icons
+ * @member {object.}
+ * @readonly
+ */
+ this.icons = this.settings.icons;
+
+ /**
+ * List of operators
+ * @member {QueryBuilder.Operator[]}
+ * @readonly
+ */
+ this.operators = this.settings.operators;
+
+ /**
+ * List of templates
+ * @member {object.}
+ * @readonly
+ */
+ this.templates = this.settings.templates;
+
+ /**
+ * Plugins configuration
+ * @member {object.}
+ * @readonly
+ */
+ this.plugins = this.settings.plugins;
+
+ /**
+ * Translations object
+ * @member {object}
+ * @readonly
+ */
+ this.lang = null;
+
+ // translations : english << 'lang_code' << custom
+ if (QueryBuilder.regional['en'] === undefined) {
+ Utils.error('Config', '"i18n/en.js" not loaded.');
+ }
+ this.lang = $.extendext(true, 'replace', {}, QueryBuilder.regional['en'], QueryBuilder.regional[this.settings.lang_code], this.settings.lang);
+
+ // "allow_groups" can be boolean or int
+ if (this.settings.allow_groups === false) {
+ this.settings.allow_groups = 0;
+ }
+ else if (this.settings.allow_groups === true) {
+ this.settings.allow_groups = -1;
+ }
+
+ // init templates
+ Object.keys(this.templates).forEach(function(tpl) {
+ if (!this.templates[tpl]) {
+ this.templates[tpl] = QueryBuilder.templates[tpl];
+ }
+ if (typeof this.templates[tpl] !== 'function') {
+ throw new Error(`Template ${tpl} must be a function`);
+ }
+ }, this);
+
+ // ensure we have a container id
+ if (!this.$el.attr('id')) {
+ this.$el.attr('id', 'qb_' + Math.floor(Math.random() * 99999));
+ this.status.generated_id = true;
+ }
+ this.status.id = this.$el.attr('id');
+
+ // INIT
+ this.$el.addClass('query-builder');
+
+ this.filters = this.checkFilters(this.filters);
+ this.operators = this.checkOperators(this.operators);
+ this.bindEvents();
+ this.initPlugins();
+};
+
+$.extend(QueryBuilder.prototype, /** @lends QueryBuilder.prototype */ {
+ /**
+ * Triggers an event on the builder container
+ * @param {string} type
+ * @returns {$.Event}
+ */
+ trigger: function(type) {
+ var event = new $.Event(this._tojQueryEvent(type), {
+ builder: this
+ });
+
+ this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 1));
+
+ return event;
+ },
+
+ /**
+ * Triggers an event on the builder container and returns the modified value
+ * @param {string} type
+ * @param {*} value
+ * @returns {*}
+ */
+ change: function(type, value) {
+ var event = new $.Event(this._tojQueryEvent(type, true), {
+ builder: this,
+ value: value
+ });
+
+ this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 2));
+
+ return event.value;
+ },
+
+ /**
+ * Attaches an event listener on the builder container
+ * @param {string} type
+ * @param {function} cb
+ * @returns {QueryBuilder}
+ */
+ on: function(type, cb) {
+ this.$el.on(this._tojQueryEvent(type), cb);
+ return this;
+ },
+
+ /**
+ * Removes an event listener from the builder container
+ * @param {string} type
+ * @param {function} [cb]
+ * @returns {QueryBuilder}
+ */
+ off: function(type, cb) {
+ this.$el.off(this._tojQueryEvent(type), cb);
+ return this;
+ },
+
+ /**
+ * Attaches an event listener called once on the builder container
+ * @param {string} type
+ * @param {function} cb
+ * @returns {QueryBuilder}
+ */
+ once: function(type, cb) {
+ this.$el.one(this._tojQueryEvent(type), cb);
+ return this;
+ },
+
+ /**
+ * Appends `.queryBuilder` and optionally `.filter` to the events names
+ * @param {string} name
+ * @param {boolean} [filter=false]
+ * @returns {string}
+ * @private
+ */
+ _tojQueryEvent: function(name, filter) {
+ return name.split(' ').map(function(type) {
+ return type + '.queryBuilder' + (filter ? '.filter' : '');
+ }).join(' ');
+ }
+});
+
+
+/**
+ * Allowed types and their internal representation
+ * @type {object.}
+ * @readonly
+ * @private
+ */
+QueryBuilder.types = {
+ 'string': 'string',
+ 'integer': 'number',
+ 'double': 'number',
+ 'date': 'datetime',
+ 'time': 'datetime',
+ 'datetime': 'datetime',
+ 'boolean': 'boolean'
+};
+
+/**
+ * Allowed inputs
+ * @type {string[]}
+ * @readonly
+ * @private
+ */
+QueryBuilder.inputs = [
+ 'text',
+ 'number',
+ 'textarea',
+ 'radio',
+ 'checkbox',
+ 'select'
+];
+
+/**
+ * Runtime modifiable options with `setOptions` method
+ * @type {string[]}
+ * @readonly
+ * @private
+ */
+QueryBuilder.modifiable_options = [
+ 'display_errors',
+ 'allow_groups',
+ 'allow_empty',
+ 'default_condition',
+ 'default_filter'
+];
+
+/**
+ * CSS selectors for common components
+ * @type {object.}
+ * @readonly
+ */
+QueryBuilder.selectors = {
+ group_container: '.rules-group-container',
+ rule_container: '.rule-container',
+ filter_container: '.rule-filter-container',
+ operator_container: '.rule-operator-container',
+ value_container: '.rule-value-container',
+ error_container: '.error-container',
+ condition_container: '.rules-group-header .group-conditions',
+
+ rule_header: '.rule-header',
+ group_header: '.rules-group-header',
+ group_actions: '.group-actions',
+ rule_actions: '.rule-actions',
+
+ rules_list: '.rules-group-body>.rules-list',
+
+ group_condition: '.rules-group-header [name$=_cond]',
+ rule_filter: '.rule-filter-container [name$=_filter]',
+ rule_operator: '.rule-operator-container [name$=_operator]',
+ rule_value: '.rule-value-container [name*=_value_]',
+
+ add_rule: '[data-add=rule]',
+ delete_rule: '[data-delete=rule]',
+ add_group: '[data-add=group]',
+ delete_group: '[data-delete=group]'
+};
+
+/**
+ * Template strings (see template.js)
+ * @type {object.}
+ * @readonly
+ */
+QueryBuilder.templates = {};
+
+/**
+ * Localized strings (see i18n/)
+ * @type {object.}
+ * @readonly
+ */
+QueryBuilder.regional = {};
+
+/**
+ * Default operators
+ * @type {object.}
+ * @readonly
+ */
+QueryBuilder.OPERATORS = {
+ equal: { type: 'equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] },
+ not_equal: { type: 'not_equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] },
+ in: { type: 'in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime'] },
+ not_in: { type: 'not_in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime'] },
+ less: { type: 'less', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
+ less_or_equal: { type: 'less_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
+ greater: { type: 'greater', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
+ greater_or_equal: { type: 'greater_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
+ between: { type: 'between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] },
+ not_between: { type: 'not_between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] },
+ begins_with: { type: 'begins_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
+ not_begins_with: { type: 'not_begins_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
+ contains: { type: 'contains', nb_inputs: 1, multiple: false, apply_to: ['string'] },
+ not_contains: { type: 'not_contains', nb_inputs: 1, multiple: false, apply_to: ['string'] },
+ ends_with: { type: 'ends_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
+ not_ends_with: { type: 'not_ends_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
+ is_empty: { type: 'is_empty', nb_inputs: 0, multiple: false, apply_to: ['string'] },
+ is_not_empty: { type: 'is_not_empty', nb_inputs: 0, multiple: false, apply_to: ['string'] },
+ is_null: { type: 'is_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] },
+ is_not_null: { type: 'is_not_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] }
+};
+
+/**
+ * Default configuration
+ * @type {object}
+ * @readonly
+ */
+QueryBuilder.DEFAULTS = {
+ filters: [],
+ plugins: [],
+
+ sort_filters: false,
+ display_errors: true,
+ allow_groups: -1,
+ allow_empty: false,
+ conditions: ['AND', 'OR'],
+ default_condition: 'AND',
+ inputs_separator: ' , ',
+ select_placeholder: '------',
+ display_empty_filter: true,
+ default_filter: null,
+ optgroups: {},
+
+ default_rule_flags: {
+ filter_readonly: false,
+ operator_readonly: false,
+ value_readonly: false,
+ no_delete: false
+ },
+
+ default_group_flags: {
+ condition_readonly: false,
+ no_add_rule: false,
+ no_add_group: false,
+ no_delete: false
+ },
+
+ templates: {
+ group: null,
+ rule: null,
+ filterSelect: null,
+ operatorSelect: null,
+ ruleValueSelect: null
+ },
+
+ lang_code: 'en',
+ lang: {},
+
+ operators: [
+ 'equal',
+ 'not_equal',
+ 'in',
+ 'not_in',
+ 'less',
+ 'less_or_equal',
+ 'greater',
+ 'greater_or_equal',
+ 'between',
+ 'not_between',
+ 'begins_with',
+ 'not_begins_with',
+ 'contains',
+ 'not_contains',
+ 'ends_with',
+ 'not_ends_with',
+ 'is_empty',
+ 'is_not_empty',
+ 'is_null',
+ 'is_not_null'
+ ],
+
+ icons: {
+ add_group: 'bi-plus-circle-fill',
+ add_rule: 'bi-plus-lg',
+ remove_group: 'bi-x-lg',
+ remove_rule: 'bi-x-lg',
+ error: 'bi-exclamation-triangle'
+ }
+};
+
+
+/**
+ * @module plugins
+ */
+
+/**
+ * Definition of available plugins
+ * @type {object.}
+ */
+QueryBuilder.plugins = {};
+
+/**
+ * Gets or extends the default configuration
+ * @param {object} [options] - new configuration
+ * @returns {undefined|object} nothing or configuration object (copy)
+ */
+QueryBuilder.defaults = function(options) {
+ if (typeof options == 'object') {
+ $.extendext(true, 'replace', QueryBuilder.DEFAULTS, options);
+ }
+ else if (typeof options == 'string') {
+ if (typeof QueryBuilder.DEFAULTS[options] == 'object') {
+ return $.extend(true, {}, QueryBuilder.DEFAULTS[options]);
+ }
+ else {
+ return QueryBuilder.DEFAULTS[options];
+ }
+ }
+ else {
+ return $.extend(true, {}, QueryBuilder.DEFAULTS);
+ }
+};
+
+/**
+ * Registers a new plugin
+ * @param {string} name
+ * @param {function} fct - init function
+ * @param {object} [def] - default options
+ */
+QueryBuilder.define = function(name, fct, def) {
+ QueryBuilder.plugins[name] = {
+ fct: fct,
+ def: def || {}
+ };
+};
+
+/**
+ * Adds new methods to QueryBuilder prototype
+ * @param {object.} methods
+ */
+QueryBuilder.extend = function(methods) {
+ $.extend(QueryBuilder.prototype, methods);
+};
+
+/**
+ * Initializes plugins for an instance
+ * @throws ConfigError
+ * @private
+ */
+QueryBuilder.prototype.initPlugins = function() {
+ if (!this.plugins) {
+ return;
+ }
+
+ if ($.isArray(this.plugins)) {
+ var tmp = {};
+ this.plugins.forEach(function(plugin) {
+ tmp[plugin] = null;
+ });
+ this.plugins = tmp;
+ }
+
+ Object.keys(this.plugins).forEach(function(plugin) {
+ if (plugin in QueryBuilder.plugins) {
+ this.plugins[plugin] = $.extend(true, {},
+ QueryBuilder.plugins[plugin].def,
+ this.plugins[plugin] || {}
+ );
+
+ QueryBuilder.plugins[plugin].fct.call(this, this.plugins[plugin]);
+ }
+ else {
+ Utils.error('Config', 'Unable to find plugin "{0}"', plugin);
+ }
+ }, this);
+};
+
+/**
+ * Returns the config of a plugin, if the plugin is not loaded, returns the default config.
+ * @param {string} name
+ * @param {string} [property]
+ * @throws ConfigError
+ * @returns {*}
+ */
+QueryBuilder.prototype.getPluginOptions = function(name, property) {
+ var plugin;
+ if (this.plugins && this.plugins[name]) {
+ plugin = this.plugins[name];
+ }
+ else if (QueryBuilder.plugins[name]) {
+ plugin = QueryBuilder.plugins[name].def;
+ }
+
+ if (plugin) {
+ if (property) {
+ return plugin[property];
+ }
+ else {
+ return plugin;
+ }
+ }
+ else {
+ Utils.error('Config', 'Unable to find plugin "{0}"', name);
+ }
+};
+
+
+/**
+ * Final initialisation of the builder
+ * @param {object} [rules]
+ * @fires QueryBuilder.afterInit
+ * @private
+ */
+QueryBuilder.prototype.init = function(rules) {
+ /**
+ * When the initilization is done, just before creating the root group
+ * @event afterInit
+ * @memberof QueryBuilder
+ */
+ this.trigger('afterInit');
+
+ if (rules) {
+ this.setRules(rules);
+ delete this.settings.rules;
+ }
+ else {
+ this.setRoot(true);
+ }
+};
+
+/**
+ * Checks the configuration of each filter
+ * @param {QueryBuilder.Filter[]} filters
+ * @returns {QueryBuilder.Filter[]}
+ * @throws ConfigError
+ */
+QueryBuilder.prototype.checkFilters = function(filters) {
+ var definedFilters = [];
+
+ if (!filters || filters.length === 0) {
+ Utils.error('Config', 'Missing filters list');
+ }
+
+ filters.forEach(function(filter, i) {
+ if (!filter.id) {
+ Utils.error('Config', 'Missing filter {0} id', i);
+ }
+ if (definedFilters.indexOf(filter.id) != -1) {
+ Utils.error('Config', 'Filter "{0}" already defined', filter.id);
+ }
+ definedFilters.push(filter.id);
+
+ if (!filter.type) {
+ filter.type = 'string';
+ }
+ else if (!QueryBuilder.types[filter.type]) {
+ Utils.error('Config', 'Invalid type "{0}"', filter.type);
+ }
+
+ if (!filter.input) {
+ filter.input = QueryBuilder.types[filter.type] === 'number' ? 'number' : 'text';
+ }
+ else if (typeof filter.input != 'function' && QueryBuilder.inputs.indexOf(filter.input) == -1) {
+ Utils.error('Config', 'Invalid input "{0}"', filter.input);
+ }
+
+ if (filter.operators) {
+ filter.operators.forEach(function(operator) {
+ if (typeof operator != 'string') {
+ Utils.error('Config', 'Filter operators must be global operators types (string)');
+ }
+ });
+ }
+
+ if (!filter.field) {
+ filter.field = filter.id;
+ }
+ if (!filter.label) {
+ filter.label = filter.field;
+ }
+
+ if (!filter.optgroup) {
+ filter.optgroup = null;
+ }
+ else {
+ this.status.has_optgroup = true;
+
+ // register optgroup if needed
+ if (!this.settings.optgroups[filter.optgroup]) {
+ this.settings.optgroups[filter.optgroup] = filter.optgroup;
+ }
+ }
+
+ switch (filter.input) {
+ case 'radio':
+ case 'checkbox':
+ if (!filter.values || filter.values.length < 1) {
+ Utils.error('Config', 'Missing filter "{0}" values', filter.id);
+ }
+ break;
+
+ case 'select':
+ var cleanValues = [];
+ filter.has_optgroup = false;
+
+ Utils.iterateOptions(filter.values, function(value, label, optgroup) {
+ cleanValues.push({
+ value: value,
+ label: label,
+ optgroup: optgroup || null
+ });
+
+ if (optgroup) {
+ filter.has_optgroup = true;
+
+ // register optgroup if needed
+ if (!this.settings.optgroups[optgroup]) {
+ this.settings.optgroups[optgroup] = optgroup;
+ }
+ }
+ }.bind(this));
+
+ if (filter.has_optgroup) {
+ filter.values = Utils.groupSort(cleanValues, 'optgroup');
+ }
+ else {
+ filter.values = cleanValues;
+ }
+
+ if (filter.placeholder) {
+ if (filter.placeholder_value === undefined) {
+ filter.placeholder_value = -1;
+ }
+
+ filter.values.forEach(function(entry) {
+ if (entry.value == filter.placeholder_value) {
+ Utils.error('Config', 'Placeholder of filter "{0}" overlaps with one of its values', filter.id);
+ }
+ });
+ }
+ break;
+ }
+ }, this);
+
+ if (this.settings.sort_filters) {
+ if (typeof this.settings.sort_filters == 'function') {
+ filters.sort(this.settings.sort_filters);
+ }
+ else {
+ var self = this;
+ filters.sort(function(a, b) {
+ return self.translate(a.label).localeCompare(self.translate(b.label));
+ });
+ }
+ }
+
+ if (this.status.has_optgroup) {
+ filters = Utils.groupSort(filters, 'optgroup');
+ }
+
+ return filters;
+};
+
+/**
+ * Checks the configuration of each operator
+ * @param {QueryBuilder.Operator[]} operators
+ * @returns {QueryBuilder.Operator[]}
+ * @throws ConfigError
+ */
+QueryBuilder.prototype.checkOperators = function(operators) {
+ var definedOperators = [];
+
+ operators.forEach(function(operator, i) {
+ if (typeof operator == 'string') {
+ if (!QueryBuilder.OPERATORS[operator]) {
+ Utils.error('Config', 'Unknown operator "{0}"', operator);
+ }
+
+ operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator]);
+ }
+ else {
+ if (!operator.type) {
+ Utils.error('Config', 'Missing "type" for operator {0}', i);
+ }
+
+ if (QueryBuilder.OPERATORS[operator.type]) {
+ operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator.type], operator);
+ }
+
+ if (operator.nb_inputs === undefined || operator.apply_to === undefined) {
+ Utils.error('Config', 'Missing "nb_inputs" and/or "apply_to" for operator "{0}"', operator.type);
+ }
+ }
+
+ if (definedOperators.indexOf(operator.type) != -1) {
+ Utils.error('Config', 'Operator "{0}" already defined', operator.type);
+ }
+ definedOperators.push(operator.type);
+
+ if (!operator.optgroup) {
+ operator.optgroup = null;
+ }
+ else {
+ this.status.has_operator_optgroup = true;
+
+ // register optgroup if needed
+ if (!this.settings.optgroups[operator.optgroup]) {
+ this.settings.optgroups[operator.optgroup] = operator.optgroup;
+ }
+ }
+ }, this);
+
+ if (this.status.has_operator_optgroup) {
+ operators = Utils.groupSort(operators, 'optgroup');
+ }
+
+ return operators;
+};
+
+/**
+ * Adds all events listeners to the builder
+ * @private
+ */
+QueryBuilder.prototype.bindEvents = function() {
+ var self = this;
+ var Selectors = QueryBuilder.selectors;
+
+ // group condition change
+ this.$el.on('change.queryBuilder', Selectors.group_condition, function() {
+ if ($(this).is(':checked')) {
+ var $group = $(this).closest(Selectors.group_container);
+ self.getModel($group).condition = $(this).val();
+ }
+ });
+
+ // rule filter change
+ this.$el.on('change.queryBuilder', Selectors.rule_filter, function() {
+ var $rule = $(this).closest(Selectors.rule_container);
+ self.getModel($rule).filter = self.getFilterById($(this).val());
+ });
+
+ // rule operator change
+ this.$el.on('change.queryBuilder', Selectors.rule_operator, function() {
+ var $rule = $(this).closest(Selectors.rule_container);
+ self.getModel($rule).operator = self.getOperatorByType($(this).val());
+ });
+
+ // add rule button
+ this.$el.on('click.queryBuilder', Selectors.add_rule, function() {
+ var $group = $(this).closest(Selectors.group_container);
+ self.addRule(self.getModel($group));
+ });
+
+ // delete rule button
+ this.$el.on('click.queryBuilder', Selectors.delete_rule, function() {
+ var $rule = $(this).closest(Selectors.rule_container);
+ self.deleteRule(self.getModel($rule));
+ });
+
+ if (this.settings.allow_groups !== 0) {
+ // add group button
+ this.$el.on('click.queryBuilder', Selectors.add_group, function() {
+ var $group = $(this).closest(Selectors.group_container);
+ self.addGroup(self.getModel($group));
+ });
+
+ // delete group button
+ this.$el.on('click.queryBuilder', Selectors.delete_group, function() {
+ var $group = $(this).closest(Selectors.group_container);
+ self.deleteGroup(self.getModel($group));
+ });
+ }
+
+ // model events
+ this.model.on({
+ 'drop': function(e, node) {
+ node.$el.remove();
+ self.refreshGroupsConditions();
+ },
+ 'add': function(e, parent, node, index) {
+ if (index === 0) {
+ node.$el.prependTo(parent.$el.find('>' + QueryBuilder.selectors.rules_list));
+ }
+ else {
+ node.$el.insertAfter(parent.rules[index - 1].$el);
+ }
+ self.refreshGroupsConditions();
+ },
+ 'move': function(e, node, group, index) {
+ node.$el.detach();
+
+ if (index === 0) {
+ node.$el.prependTo(group.$el.find('>' + QueryBuilder.selectors.rules_list));
+ }
+ else {
+ node.$el.insertAfter(group.rules[index - 1].$el);
+ }
+ self.refreshGroupsConditions();
+ },
+ 'update': function(e, node, field, value, oldValue) {
+ if (node instanceof Rule) {
+ switch (field) {
+ case 'error':
+ self.updateError(node);
+ break;
+
+ case 'flags':
+ self.applyRuleFlags(node);
+ break;
+
+ case 'filter':
+ self.updateRuleFilter(node, oldValue);
+ break;
+
+ case 'operator':
+ self.updateRuleOperator(node, oldValue);
+ break;
+
+ case 'value':
+ self.updateRuleValue(node, oldValue);
+ break;
+ }
+ }
+ else {
+ switch (field) {
+ case 'error':
+ self.updateError(node);
+ break;
+
+ case 'flags':
+ self.applyGroupFlags(node);
+ break;
+
+ case 'condition':
+ self.updateGroupCondition(node, oldValue);
+ break;
+ }
+ }
+ }
+ });
+};
+
+/**
+ * Creates the root group
+ * @param {boolean} [addRule=true] - adds a default empty rule
+ * @param {object} [data] - group custom data
+ * @param {object} [flags] - flags to apply to the group
+ * @returns {Group} root group
+ * @fires QueryBuilder.afterAddGroup
+ */
+QueryBuilder.prototype.setRoot = function(addRule, data, flags) {
+ addRule = (addRule === undefined || addRule === true);
+
+ var group_id = this.nextGroupId();
+ var $group = $($.parseHTML(this.getGroupTemplate(group_id, 1)));
+
+ this.$el.append($group);
+ this.model.root = new Group(null, $group);
+ this.model.root.model = this.model;
+
+ this.model.root.data = data;
+ this.model.root.flags = $.extend({}, this.settings.default_group_flags, flags);
+ this.model.root.condition = this.settings.default_condition;
+
+ this.trigger('afterAddGroup', this.model.root);
+
+ if (addRule) {
+ this.addRule(this.model.root);
+ }
+
+ return this.model.root;
+};
+
+/**
+ * Adds a new group
+ * @param {Group} parent
+ * @param {boolean} [addRule=true] - adds a default empty rule
+ * @param {object} [data] - group custom data
+ * @param {object} [flags] - flags to apply to the group
+ * @returns {Group}
+ * @fires QueryBuilder.beforeAddGroup
+ * @fires QueryBuilder.afterAddGroup
+ */
+QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) {
+ addRule = (addRule === undefined || addRule === true);
+
+ var level = parent.level + 1;
+
+ /**
+ * Just before adding a group, can be prevented.
+ * @event beforeAddGroup
+ * @memberof QueryBuilder
+ * @param {Group} parent
+ * @param {boolean} addRule - if an empty rule will be added in the group
+ * @param {int} level - nesting level of the group, 1 is the root group
+ */
+ var e = this.trigger('beforeAddGroup', parent, addRule, level);
+ if (e.isDefaultPrevented()) {
+ return null;
+ }
+
+ var group_id = this.nextGroupId();
+ var $group = $(this.getGroupTemplate(group_id, level));
+ var model = parent.addGroup($group);
+
+ model.data = data;
+ model.flags = $.extend({}, this.settings.default_group_flags, flags);
+ model.condition = this.settings.default_condition;
+
+ /**
+ * Just after adding a group
+ * @event afterAddGroup
+ * @memberof QueryBuilder
+ * @param {Group} group
+ */
+ this.trigger('afterAddGroup', model);
+
+ /**
+ * After any change in the rules
+ * @event rulesChanged
+ * @memberof QueryBuilder
+ */
+ this.trigger('rulesChanged');
+
+ if (addRule) {
+ this.addRule(model);
+ }
+
+ return model;
+};
+
+/**
+ * Tries to delete a group. The group is not deleted if at least one rule is flagged `no_delete`.
+ * @param {Group} group
+ * @returns {boolean} if the group has been deleted
+ * @fires QueryBuilder.beforeDeleteGroup
+ * @fires QueryBuilder.afterDeleteGroup
+ */
+QueryBuilder.prototype.deleteGroup = function(group) {
+ if (group.isRoot()) {
+ return false;
+ }
+
+ /**
+ * Just before deleting a group, can be prevented
+ * @event beforeDeleteGroup
+ * @memberof QueryBuilder
+ * @param {Group} parent
+ */
+ var e = this.trigger('beforeDeleteGroup', group);
+ if (e.isDefaultPrevented()) {
+ return false;
+ }
+
+ var del = true;
+
+ group.each('reverse', function(rule) {
+ del &= this.deleteRule(rule);
+ }, function(group) {
+ del &= this.deleteGroup(group);
+ }, this);
+
+ if (del) {
+ group.drop();
+
+ /**
+ * Just after deleting a group
+ * @event afterDeleteGroup
+ * @memberof QueryBuilder
+ */
+ this.trigger('afterDeleteGroup');
+
+ this.trigger('rulesChanged');
+ }
+
+ return del;
+};
+
+/**
+ * Performs actions when a group's condition changes
+ * @param {Group} group
+ * @param {object} previousCondition
+ * @fires QueryBuilder.afterUpdateGroupCondition
+ * @private
+ */
+QueryBuilder.prototype.updateGroupCondition = function(group, previousCondition) {
+ group.$el.find('>' + QueryBuilder.selectors.group_condition).each(function() {
+ var $this = $(this);
+ $this.prop('checked', $this.val() === group.condition);
+ $this.parent().toggleClass('active', $this.val() === group.condition);
+ });
+
+ /**
+ * After the group condition has been modified
+ * @event afterUpdateGroupCondition
+ * @memberof QueryBuilder
+ * @param {Group} group
+ * @param {object} previousCondition
+ */
+ this.trigger('afterUpdateGroupCondition', group, previousCondition);
+
+ this.trigger('rulesChanged');
+};
+
+/**
+ * Updates the visibility of conditions based on number of rules inside each group
+ * @private
+ */
+QueryBuilder.prototype.refreshGroupsConditions = function() {
+ (function walk(group) {
+ if (!group.flags || (group.flags && !group.flags.condition_readonly)) {
+ group.$el.find('>' + QueryBuilder.selectors.group_condition).prop('disabled', group.rules.length <= 1)
+ .parent().toggleClass('disabled', group.rules.length <= 1);
+ }
+
+ group.each(null, function(group) {
+ walk(group);
+ }, this);
+ }(this.model.root));
+};
+
+/**
+ * Adds a new rule
+ * @param {Group} parent
+ * @param {object} [data] - rule custom data
+ * @param {object} [flags] - flags to apply to the rule
+ * @returns {Rule}
+ * @fires QueryBuilder.beforeAddRule
+ * @fires QueryBuilder.afterAddRule
+ * @fires QueryBuilder.changer:getDefaultFilter
+ */
+QueryBuilder.prototype.addRule = function(parent, data, flags) {
+ /**
+ * Just before adding a rule, can be prevented
+ * @event beforeAddRule
+ * @memberof QueryBuilder
+ * @param {Group} parent
+ */
+ var e = this.trigger('beforeAddRule', parent);
+ if (e.isDefaultPrevented()) {
+ return null;
+ }
+
+ var rule_id = this.nextRuleId();
+ var $rule = $($.parseHTML(this.getRuleTemplate(rule_id)));
+ var model = parent.addRule($rule);
+
+ model.data = data;
+ model.flags = $.extend({}, this.settings.default_rule_flags, flags);
+
+ /**
+ * Just after adding a rule
+ * @event afterAddRule
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ */
+ this.trigger('afterAddRule', model);
+
+ this.trigger('rulesChanged');
+
+ this.createRuleFilters(model);
+
+ if (this.settings.default_filter || !this.settings.display_empty_filter) {
+ /**
+ * Modifies the default filter for a rule
+ * @event changer:getDefaultFilter
+ * @memberof QueryBuilder
+ * @param {QueryBuilder.Filter} filter
+ * @param {Rule} rule
+ * @returns {QueryBuilder.Filter}
+ */
+ model.filter = this.change('getDefaultFilter',
+ this.getFilterById(this.settings.default_filter || this.filters[0].id),
+ model
+ );
+ }
+
+ return model;
+};
+
+/**
+ * Tries to delete a rule
+ * @param {Rule} rule
+ * @returns {boolean} if the rule has been deleted
+ * @fires QueryBuilder.beforeDeleteRule
+ * @fires QueryBuilder.afterDeleteRule
+ */
+QueryBuilder.prototype.deleteRule = function(rule) {
+ if (rule.flags.no_delete) {
+ return false;
+ }
+
+ /**
+ * Just before deleting a rule, can be prevented
+ * @event beforeDeleteRule
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ */
+ var e = this.trigger('beforeDeleteRule', rule);
+ if (e.isDefaultPrevented()) {
+ return false;
+ }
+
+ rule.drop();
+
+ /**
+ * Just after deleting a rule
+ * @event afterDeleteRule
+ * @memberof QueryBuilder
+ */
+ this.trigger('afterDeleteRule');
+
+ this.trigger('rulesChanged');
+
+ return true;
+};
+
+/**
+ * Creates the filters for a rule
+ * @param {Rule} rule
+ * @fires QueryBuilder.changer:getRuleFilters
+ * @fires QueryBuilder.afterCreateRuleFilters
+ * @private
+ */
+QueryBuilder.prototype.createRuleFilters = function(rule) {
+ /**
+ * Modifies the list a filters available for a rule
+ * @event changer:getRuleFilters
+ * @memberof QueryBuilder
+ * @param {QueryBuilder.Filter[]} filters
+ * @param {Rule} rule
+ * @returns {QueryBuilder.Filter[]}
+ */
+ var filters = this.change('getRuleFilters', this.filters, rule);
+ var $filterSelect = $($.parseHTML(this.getRuleFilterSelect(rule, filters)));
+
+ rule.$el.find(QueryBuilder.selectors.filter_container).html($filterSelect);
+
+ /**
+ * After creating the dropdown for filters
+ * @event afterCreateRuleFilters
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ */
+ this.trigger('afterCreateRuleFilters', rule);
+
+ this.applyRuleFlags(rule);
+};
+
+/**
+ * Creates the operators for a rule and init the rule operator
+ * @param {Rule} rule
+ * @fires QueryBuilder.afterCreateRuleOperators
+ * @private
+ */
+QueryBuilder.prototype.createRuleOperators = function(rule) {
+ var $operatorContainer = rule.$el.find(QueryBuilder.selectors.operator_container).empty();
+
+ if (!rule.filter) {
+ return;
+ }
+
+ var operators = this.getOperators(rule.filter);
+ var $operatorSelect = $($.parseHTML(this.getRuleOperatorSelect(rule, operators)));
+
+ $operatorContainer.html($operatorSelect);
+
+ // set the operator without triggering update event
+ if (rule.filter.default_operator) {
+ rule.__.operator = this.getOperatorByType(rule.filter.default_operator);
+ }
+ else {
+ rule.__.operator = operators[0];
+ }
+
+ rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
+
+ /**
+ * After creating the dropdown for operators
+ * @event afterCreateRuleOperators
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ * @param {QueryBuilder.Operator[]} operators - allowed operators for this rule
+ */
+ this.trigger('afterCreateRuleOperators', rule, operators);
+
+ this.applyRuleFlags(rule);
+};
+
+/**
+ * Creates the main input for a rule
+ * @param {Rule} rule
+ * @fires QueryBuilder.afterCreateRuleInput
+ * @private
+ */
+QueryBuilder.prototype.createRuleInput = function(rule) {
+ var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container).empty();
+
+ rule.__.value = undefined;
+
+ if (!rule.filter || !rule.operator || rule.operator.nb_inputs === 0) {
+ return;
+ }
+
+ var self = this;
+ var $inputs = $();
+ var filter = rule.filter;
+
+ for (var i = 0; i < rule.operator.nb_inputs; i++) {
+ var $ruleInput = $($.parseHTML($.trim(this.getRuleInput(rule, i))));
+ if (i > 0) $valueContainer.append(this.settings.inputs_separator);
+ $valueContainer.append($ruleInput);
+ $inputs = $inputs.add($ruleInput);
+ }
+
+ $valueContainer.css('display', '');
+
+ $inputs.on('change ' + (filter.input_event || ''), function() {
+ if (!rule._updating_input) {
+ rule._updating_value = true;
+ rule.value = self.getRuleInputValue(rule);
+ rule._updating_value = false;
+ }
+ });
+
+ if (filter.plugin) {
+ $inputs[filter.plugin](filter.plugin_config || {});
+ }
+
+ /**
+ * After creating the input for a rule and initializing optional plugin
+ * @event afterCreateRuleInput
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ */
+ this.trigger('afterCreateRuleInput', rule);
+
+ if (filter.default_value !== undefined) {
+ rule.value = filter.default_value;
+ }
+ else {
+ rule._updating_value = true;
+ rule.value = self.getRuleInputValue(rule);
+ rule._updating_value = false;
+ }
+
+ this.applyRuleFlags(rule);
+};
+
+/**
+ * Performs action when a rule's filter changes
+ * @param {Rule} rule
+ * @param {object} previousFilter
+ * @fires QueryBuilder.afterUpdateRuleFilter
+ * @private
+ */
+QueryBuilder.prototype.updateRuleFilter = function(rule, previousFilter) {
+ this.createRuleOperators(rule);
+ this.createRuleInput(rule);
+
+ rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
+
+ // clear rule data if the filter changed
+ if (previousFilter && rule.filter && previousFilter.id !== rule.filter.id) {
+ rule.data = undefined;
+ }
+
+ /**
+ * After the filter has been updated and the operators and input re-created
+ * @event afterUpdateRuleFilter
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ * @param {object} previousFilter
+ */
+ this.trigger('afterUpdateRuleFilter', rule, previousFilter);
+
+ this.trigger('rulesChanged');
+};
+
+/**
+ * Performs actions when a rule's operator changes
+ * @param {Rule} rule
+ * @param {object} previousOperator
+ * @fires QueryBuilder.afterUpdateRuleOperator
+ * @private
+ */
+QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) {
+ var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container);
+
+ if (!rule.operator || rule.operator.nb_inputs === 0) {
+ $valueContainer.hide();
+
+ rule.__.value = undefined;
+ }
+ else {
+ $valueContainer.css('display', '');
+
+ if ($valueContainer.is(':empty') || !previousOperator ||
+ rule.operator.nb_inputs !== previousOperator.nb_inputs ||
+ rule.operator.optgroup !== previousOperator.optgroup
+ ) {
+ this.createRuleInput(rule);
+ }
+ }
+
+ if (rule.operator) {
+ rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
+
+ // refresh value if the format changed for this operator
+ rule.__.value = this.getRuleInputValue(rule);
+ }
+
+ /**
+ * After the operator has been updated and the input optionally re-created
+ * @event afterUpdateRuleOperator
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ * @param {object} previousOperator
+ */
+ this.trigger('afterUpdateRuleOperator', rule, previousOperator);
+
+ this.trigger('rulesChanged');
+};
+
+/**
+ * Performs actions when rule's value changes
+ * @param {Rule} rule
+ * @param {object} previousValue
+ * @fires QueryBuilder.afterUpdateRuleValue
+ * @private
+ */
+QueryBuilder.prototype.updateRuleValue = function(rule, previousValue) {
+ if (!rule._updating_value) {
+ this.setRuleInputValue(rule, rule.value);
+ }
+
+ /**
+ * After the rule value has been modified
+ * @event afterUpdateRuleValue
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ * @param {*} previousValue
+ */
+ this.trigger('afterUpdateRuleValue', rule, previousValue);
+
+ this.trigger('rulesChanged');
+};
+
+/**
+ * Changes a rule's properties depending on its flags
+ * @param {Rule} rule
+ * @fires QueryBuilder.afterApplyRuleFlags
+ * @private
+ */
+QueryBuilder.prototype.applyRuleFlags = function(rule) {
+ var flags = rule.flags;
+ var Selectors = QueryBuilder.selectors;
+
+ rule.$el.find(Selectors.rule_filter).prop('disabled', flags.filter_readonly);
+ rule.$el.find(Selectors.rule_operator).prop('disabled', flags.operator_readonly);
+ rule.$el.find(Selectors.rule_value).prop('disabled', flags.value_readonly);
+
+ if (flags.no_delete) {
+ rule.$el.find(Selectors.delete_rule).remove();
+ }
+
+ /**
+ * After rule's flags has been applied
+ * @event afterApplyRuleFlags
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ */
+ this.trigger('afterApplyRuleFlags', rule);
+};
+
+/**
+ * Changes group's properties depending on its flags
+ * @param {Group} group
+ * @fires QueryBuilder.afterApplyGroupFlags
+ * @private
+ */
+QueryBuilder.prototype.applyGroupFlags = function(group) {
+ var flags = group.flags;
+ var Selectors = QueryBuilder.selectors;
+
+ group.$el.find('>' + Selectors.group_condition).prop('disabled', flags.condition_readonly)
+ .parent().toggleClass('readonly', flags.condition_readonly);
+
+ if (flags.no_add_rule) {
+ group.$el.find(Selectors.add_rule).remove();
+ }
+ if (flags.no_add_group) {
+ group.$el.find(Selectors.add_group).remove();
+ }
+ if (flags.no_delete) {
+ group.$el.find(Selectors.delete_group).remove();
+ }
+
+ /**
+ * After group's flags has been applied
+ * @event afterApplyGroupFlags
+ * @memberof QueryBuilder
+ * @param {Group} group
+ */
+ this.trigger('afterApplyGroupFlags', group);
+};
+
+/**
+ * Clears all errors markers
+ * @param {Node} [node] default is root Group
+ */
+QueryBuilder.prototype.clearErrors = function(node) {
+ node = node || this.model.root;
+
+ if (!node) {
+ return;
+ }
+
+ node.error = null;
+
+ if (node instanceof Group) {
+ node.each(function(rule) {
+ rule.error = null;
+ }, function(group) {
+ this.clearErrors(group);
+ }, this);
+ }
+};
+
+/**
+ * Adds/Removes error on a Rule or Group
+ * @param {Node} node
+ * @fires QueryBuilder.changer:displayError
+ * @private
+ */
+QueryBuilder.prototype.updateError = function(node) {
+ if (this.settings.display_errors) {
+ if (node.error === null) {
+ node.$el.removeClass('has-error');
+ }
+ else {
+ var errorMessage = this.translate('errors', node.error[0]);
+ errorMessage = Utils.fmt(errorMessage, node.error.slice(1));
+
+ /**
+ * Modifies an error message before display
+ * @event changer:displayError
+ * @memberof QueryBuilder
+ * @param {string} errorMessage - the error message (translated and formatted)
+ * @param {array} error - the raw error array (error code and optional arguments)
+ * @param {Node} node
+ * @returns {string}
+ */
+ errorMessage = this.change('displayError', errorMessage, node.error, node);
+
+ node.$el.addClass('has-error')
+ .find(QueryBuilder.selectors.error_container).eq(0)
+ .attr('title', errorMessage);
+ }
+ }
+};
+
+/**
+ * Triggers a validation error event
+ * @param {Node} node
+ * @param {string|array} error
+ * @param {*} value
+ * @fires QueryBuilder.validationError
+ * @private
+ */
+QueryBuilder.prototype.triggerValidationError = function(node, error, value) {
+ if (!$.isArray(error)) {
+ error = [error];
+ }
+
+ /**
+ * Fired when a validation error occurred, can be prevented
+ * @event validationError
+ * @memberof QueryBuilder
+ * @param {Node} node
+ * @param {string} error
+ * @param {*} value
+ */
+ var e = this.trigger('validationError', node, error, value);
+ if (!e.isDefaultPrevented()) {
+ node.error = error;
+ }
+};
+
+
+/**
+ * Destroys the builder
+ * @fires QueryBuilder.beforeDestroy
+ */
+QueryBuilder.prototype.destroy = function() {
+ /**
+ * Before the {@link QueryBuilder#destroy} method
+ * @event beforeDestroy
+ * @memberof QueryBuilder
+ */
+ this.trigger('beforeDestroy');
+
+ if (this.status.generated_id) {
+ this.$el.removeAttr('id');
+ }
+
+ this.clear();
+ this.model = null;
+
+ this.$el
+ .off('.queryBuilder')
+ .removeClass('query-builder')
+ .removeData('queryBuilder');
+
+ delete this.$el[0].queryBuilder;
+};
+
+/**
+ * Clear all rules and resets the root group
+ * @fires QueryBuilder.beforeReset
+ * @fires QueryBuilder.afterReset
+ */
+QueryBuilder.prototype.reset = function() {
+ /**
+ * Before the {@link QueryBuilder#reset} method, can be prevented
+ * @event beforeReset
+ * @memberof QueryBuilder
+ */
+ var e = this.trigger('beforeReset');
+ if (e.isDefaultPrevented()) {
+ return;
+ }
+
+ this.status.group_id = 1;
+ this.status.rule_id = 0;
+
+ this.model.root.empty();
+
+ this.model.root.data = undefined;
+ this.model.root.flags = $.extend({}, this.settings.default_group_flags);
+ this.model.root.condition = this.settings.default_condition;
+
+ this.addRule(this.model.root);
+
+ /**
+ * After the {@link QueryBuilder#reset} method
+ * @event afterReset
+ * @memberof QueryBuilder
+ */
+ this.trigger('afterReset');
+
+ this.trigger('rulesChanged');
+};
+
+/**
+ * Clears all rules and removes the root group
+ * @fires QueryBuilder.beforeClear
+ * @fires QueryBuilder.afterClear
+ */
+QueryBuilder.prototype.clear = function() {
+ /**
+ * Before the {@link QueryBuilder#clear} method, can be prevented
+ * @event beforeClear
+ * @memberof QueryBuilder
+ */
+ var e = this.trigger('beforeClear');
+ if (e.isDefaultPrevented()) {
+ return;
+ }
+
+ this.status.group_id = 0;
+ this.status.rule_id = 0;
+
+ if (this.model.root) {
+ this.model.root.drop();
+ this.model.root = null;
+ }
+
+ /**
+ * After the {@link QueryBuilder#clear} method
+ * @event afterClear
+ * @memberof QueryBuilder
+ */
+ this.trigger('afterClear');
+
+ this.trigger('rulesChanged');
+};
+
+/**
+ * Modifies the builder configuration.
+ * Only options defined in QueryBuilder.modifiable_options are modifiable
+ * @param {object} options
+ */
+QueryBuilder.prototype.setOptions = function(options) {
+ $.each(options, function(opt, value) {
+ if (QueryBuilder.modifiable_options.indexOf(opt) !== -1) {
+ this.settings[opt] = value;
+ }
+ }.bind(this));
+};
+
+/**
+ * Returns the model associated to a DOM object, or the root model
+ * @param {jQuery} [target]
+ * @returns {Node}
+ */
+QueryBuilder.prototype.getModel = function(target) {
+ if (!target) {
+ return this.model.root;
+ }
+ else if (target instanceof Node) {
+ return target;
+ }
+ else {
+ return $(target).data('queryBuilderModel');
+ }
+};
+
+/**
+ * Validates the whole builder
+ * @param {object} [options]
+ * @param {boolean} [options.skip_empty=false] - skips validating rules that have no filter selected
+ * @returns {boolean}
+ * @fires QueryBuilder.changer:validate
+ */
+QueryBuilder.prototype.validate = function(options) {
+ options = $.extend({
+ skip_empty: false
+ }, options);
+
+ this.clearErrors();
+
+ var self = this;
+
+ var valid = (function parse(group) {
+ var done = 0;
+ var errors = 0;
+
+ group.each(function(rule) {
+ if (!rule.filter && options.skip_empty) {
+ return;
+ }
+
+ if (!rule.filter) {
+ self.triggerValidationError(rule, 'no_filter', null);
+ errors++;
+ return;
+ }
+
+ if (!rule.operator) {
+ self.triggerValidationError(rule, 'no_operator', null);
+ errors++;
+ return;
+ }
+
+ if (rule.operator.nb_inputs !== 0) {
+ var valid = self.validateValue(rule, rule.value);
+
+ if (valid !== true) {
+ self.triggerValidationError(rule, valid, rule.value);
+ errors++;
+ return;
+ }
+ }
+
+ done++;
+
+ }, function(group) {
+ var res = parse(group);
+ if (res === true) {
+ done++;
+ }
+ else if (res === false) {
+ errors++;
+ }
+ });
+
+ if (errors > 0) {
+ return false;
+ }
+ else if (done === 0 && !group.isRoot() && options.skip_empty) {
+ return null;
+ }
+ else if (done === 0 && (!self.settings.allow_empty || !group.isRoot())) {
+ self.triggerValidationError(group, 'empty_group', null);
+ return false;
+ }
+
+ return true;
+
+ }(this.model.root));
+
+ /**
+ * Modifies the result of the {@link QueryBuilder#validate} method
+ * @event changer:validate
+ * @memberof QueryBuilder
+ * @param {boolean} valid
+ * @returns {boolean}
+ */
+ return this.change('validate', valid);
+};
+
+/**
+ * Gets an object representing current rules
+ * @param {object} [options]
+ * @param {boolean|string} [options.get_flags=false] - export flags, true: only changes from default flags or 'all'
+ * @param {boolean} [options.allow_invalid=false] - returns rules even if they are invalid
+ * @param {boolean} [options.skip_empty=false] - remove rules that have no filter selected
+ * @returns {object}
+ * @fires QueryBuilder.changer:ruleToJson
+ * @fires QueryBuilder.changer:groupToJson
+ * @fires QueryBuilder.changer:getRules
+ */
+QueryBuilder.prototype.getRules = function(options) {
+ options = $.extend({
+ get_flags: false,
+ allow_invalid: false,
+ skip_empty: false
+ }, options);
+
+ var valid = this.validate(options);
+ if (!valid && !options.allow_invalid) {
+ return null;
+ }
+
+ var self = this;
+
+ var out = (function parse(group) {
+ var groupData = {
+ condition: group.condition,
+ rules: []
+ };
+
+ if (group.data) {
+ groupData.data = $.extendext(true, 'replace', {}, group.data);
+ }
+
+ if (options.get_flags) {
+ var flags = self.getGroupFlags(group.flags, options.get_flags === 'all');
+ if (!$.isEmptyObject(flags)) {
+ groupData.flags = flags;
+ }
+ }
+
+ group.each(function(rule) {
+ if (!rule.filter && options.skip_empty) {
+ return;
+ }
+
+ var value = null;
+ if (!rule.operator || rule.operator.nb_inputs !== 0) {
+ value = rule.value;
+ }
+
+ var ruleData = {
+ id: rule.filter ? rule.filter.id : null,
+ field: rule.filter ? rule.filter.field : null,
+ type: rule.filter ? rule.filter.type : null,
+ input: rule.filter ? rule.filter.input : null,
+ operator: rule.operator ? rule.operator.type : null,
+ value: value
+ };
+
+ if (rule.filter && rule.filter.data || rule.data) {
+ ruleData.data = $.extendext(true, 'replace', {}, rule.filter ? rule.filter.data : {}, rule.data);
+ }
+
+ if (options.get_flags) {
+ var flags = self.getRuleFlags(rule.flags, options.get_flags === 'all');
+ if (!$.isEmptyObject(flags)) {
+ ruleData.flags = flags;
+ }
+ }
+
+ /**
+ * Modifies the JSON generated from a Rule object
+ * @event changer:ruleToJson
+ * @memberof QueryBuilder
+ * @param {object} json
+ * @param {Rule} rule
+ * @returns {object}
+ */
+ groupData.rules.push(self.change('ruleToJson', ruleData, rule));
+
+ }, function(model) {
+ var data = parse(model);
+ if (data.rules.length !== 0 || !options.skip_empty) {
+ groupData.rules.push(data);
+ }
+ }, this);
+
+ /**
+ * Modifies the JSON generated from a Group object
+ * @event changer:groupToJson
+ * @memberof QueryBuilder
+ * @param {object} json
+ * @param {Group} group
+ * @returns {object}
+ */
+ return self.change('groupToJson', groupData, group);
+
+ }(this.model.root));
+
+ out.valid = valid;
+
+ /**
+ * Modifies the result of the {@link QueryBuilder#getRules} method
+ * @event changer:getRules
+ * @memberof QueryBuilder
+ * @param {object} json
+ * @returns {object}
+ */
+ return this.change('getRules', out);
+};
+
+/**
+ * Sets rules from object
+ * @param {object} data
+ * @param {object} [options]
+ * @param {boolean} [options.allow_invalid=false] - silent-fail if the data are invalid
+ * @throws RulesError, UndefinedConditionError
+ * @fires QueryBuilder.changer:setRules
+ * @fires QueryBuilder.changer:jsonToRule
+ * @fires QueryBuilder.changer:jsonToGroup
+ * @fires QueryBuilder.afterSetRules
+ */
+QueryBuilder.prototype.setRules = function(data, options) {
+ options = $.extend({
+ allow_invalid: false
+ }, options);
+
+ if ($.isArray(data)) {
+ data = {
+ condition: this.settings.default_condition,
+ rules: data
+ };
+ }
+
+ if (!data || !data.rules || (data.rules.length === 0 && !this.settings.allow_empty)) {
+ Utils.error('RulesParse', 'Incorrect data object passed');
+ }
+
+ this.clear();
+ this.setRoot(false, data.data, this.parseGroupFlags(data));
+
+ /**
+ * Modifies data before the {@link QueryBuilder#setRules} method
+ * @event changer:setRules
+ * @memberof QueryBuilder
+ * @param {object} json
+ * @param {object} options
+ * @returns {object}
+ */
+ data = this.change('setRules', data, options);
+
+ var self = this;
+
+ (function add(data, group) {
+ if (group === null) {
+ return;
+ }
+
+ if (data.condition === undefined) {
+ data.condition = self.settings.default_condition;
+ }
+ else if (self.settings.conditions.indexOf(data.condition) == -1) {
+ Utils.error(!options.allow_invalid, 'UndefinedCondition', 'Invalid condition "{0}"', data.condition);
+ data.condition = self.settings.default_condition;
+ }
+
+ group.condition = data.condition;
+
+ data.rules.forEach(function(item) {
+ var model;
+
+ if (item.rules !== undefined) {
+ if (self.settings.allow_groups !== -1 && self.settings.allow_groups < group.level) {
+ Utils.error(!options.allow_invalid, 'RulesParse', 'No more than {0} groups are allowed', self.settings.allow_groups);
+ self.reset();
+ }
+ else {
+ model = self.addGroup(group, false, item.data, self.parseGroupFlags(item));
+ if (model === null) {
+ return;
+ }
+
+ add(item, model);
+ }
+ }
+ else {
+ if (!item.empty) {
+ if (item.id === undefined) {
+ Utils.error(!options.allow_invalid, 'RulesParse', 'Missing rule field id');
+ item.empty = true;
+ }
+ if (item.operator === undefined) {
+ item.operator = 'equal';
+ }
+ }
+
+ model = self.addRule(group, item.data, self.parseRuleFlags(item));
+ if (model === null) {
+ return;
+ }
+
+ if (!item.empty) {
+ model.filter = self.getFilterById(item.id, !options.allow_invalid);
+ }
+
+ if (model.filter) {
+ model.operator = self.getOperatorByType(item.operator, !options.allow_invalid);
+
+ if (!model.operator) {
+ model.operator = self.getOperators(model.filter)[0];
+ }
+ }
+
+ if (model.operator && model.operator.nb_inputs !== 0) {
+ if (item.value !== undefined) {
+ model.value = item.value;
+ }
+ else if (model.filter.default_value !== undefined) {
+ model.value = model.filter.default_value;
+ }
+ }
+
+ /**
+ * Modifies the Rule object generated from the JSON
+ * @event changer:jsonToRule
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ * @param {object} json
+ * @returns {Rule} the same rule
+ */
+ if (self.change('jsonToRule', model, item) != model) {
+ Utils.error('RulesParse', 'Plugin tried to change rule reference');
+ }
+ }
+ });
+
+ /**
+ * Modifies the Group object generated from the JSON
+ * @event changer:jsonToGroup
+ * @memberof QueryBuilder
+ * @param {Group} group
+ * @param {object} json
+ * @returns {Group} the same group
+ */
+ if (self.change('jsonToGroup', group, data) != group) {
+ Utils.error('RulesParse', 'Plugin tried to change group reference');
+ }
+
+ }(data, this.model.root));
+
+ /**
+ * After the {@link QueryBuilder#setRules} method
+ * @event afterSetRules
+ * @memberof QueryBuilder
+ */
+ this.trigger('afterSetRules');
+};
+
+
+/**
+ * Performs value validation
+ * @param {Rule} rule
+ * @param {string|string[]} value
+ * @returns {array|boolean} true or error array
+ * @fires QueryBuilder.changer:validateValue
+ */
+QueryBuilder.prototype.validateValue = function(rule, value) {
+ var validation = rule.filter.validation || {};
+ var result = true;
+
+ if (validation.callback) {
+ result = validation.callback.call(this, value, rule);
+ }
+ else {
+ result = this._validateValue(rule, value);
+ }
+
+ /**
+ * Modifies the result of the rule validation method
+ * @event changer:validateValue
+ * @memberof QueryBuilder
+ * @param {array|boolean} result - true or an error array
+ * @param {*} value
+ * @param {Rule} rule
+ * @returns {array|boolean}
+ */
+ return this.change('validateValue', result, value, rule);
+};
+
+/**
+ * Default validation function
+ * @param {Rule} rule
+ * @param {string|string[]} value
+ * @returns {array|boolean} true or error array
+ * @throws ConfigError
+ * @private
+ */
+QueryBuilder.prototype._validateValue = function(rule, value) {
+ var filter = rule.filter;
+ var operator = rule.operator;
+ var validation = filter.validation || {};
+ var result = true;
+ var tmp, tempValue;
+
+ if (rule.operator.nb_inputs === 1) {
+ value = [value];
+ }
+
+ for (var i = 0; i < operator.nb_inputs; i++) {
+ if (!operator.multiple && $.isArray(value[i]) && value[i].length > 1) {
+ result = ['operator_not_multiple', operator.type, this.translate('operators', operator.type)];
+ break;
+ }
+
+ switch (filter.input) {
+ case 'radio':
+ if (value[i] === undefined || value[i].length === 0) {
+ if (!validation.allow_empty_value) {
+ result = ['radio_empty'];
+ }
+ break;
+ }
+ break;
+
+ case 'checkbox':
+ if (value[i] === undefined || value[i].length === 0) {
+ if (!validation.allow_empty_value) {
+ result = ['checkbox_empty'];
+ }
+ break;
+ }
+ break;
+
+ case 'select':
+ if (value[i] === undefined || value[i].length === 0 || (filter.placeholder && value[i] == filter.placeholder_value)) {
+ if (!validation.allow_empty_value) {
+ result = ['select_empty'];
+ }
+ break;
+ }
+ break;
+
+ default:
+ tempValue = $.isArray(value[i]) ? value[i] : [value[i]];
+
+ for (var j = 0; j < tempValue.length; j++) {
+ switch (QueryBuilder.types[filter.type]) {
+ case 'string':
+ if (tempValue[j] === undefined || tempValue[j].length === 0) {
+ if (!validation.allow_empty_value) {
+ result = ['string_empty'];
+ }
+ break;
+ }
+ if (validation.min !== undefined) {
+ if (tempValue[j].length < parseInt(validation.min)) {
+ result = [this.getValidationMessage(validation, 'min', 'string_exceed_min_length'), validation.min];
+ break;
+ }
+ }
+ if (validation.max !== undefined) {
+ if (tempValue[j].length > parseInt(validation.max)) {
+ result = [this.getValidationMessage(validation, 'max', 'string_exceed_max_length'), validation.max];
+ break;
+ }
+ }
+ if (validation.format) {
+ if (typeof validation.format == 'string') {
+ validation.format = new RegExp(validation.format);
+ }
+ if (!validation.format.test(tempValue[j])) {
+ result = [this.getValidationMessage(validation, 'format', 'string_invalid_format'), validation.format];
+ break;
+ }
+ }
+ break;
+
+ case 'number':
+ if (tempValue[j] === undefined || tempValue[j].length === 0) {
+ if (!validation.allow_empty_value) {
+ result = ['number_nan'];
+ }
+ break;
+ }
+ if (isNaN(tempValue[j])) {
+ result = ['number_nan'];
+ break;
+ }
+ if (filter.type == 'integer') {
+ if (parseInt(tempValue[j]) != tempValue[j]) {
+ result = ['number_not_integer'];
+ break;
+ }
+ }
+ else {
+ if (parseFloat(tempValue[j]) != tempValue[j]) {
+ result = ['number_not_double'];
+ break;
+ }
+ }
+ if (validation.min !== undefined) {
+ if (tempValue[j] < parseFloat(validation.min)) {
+ result = [this.getValidationMessage(validation, 'min', 'number_exceed_min'), validation.min];
+ break;
+ }
+ }
+ if (validation.max !== undefined) {
+ if (tempValue[j] > parseFloat(validation.max)) {
+ result = [this.getValidationMessage(validation, 'max', 'number_exceed_max'), validation.max];
+ break;
+ }
+ }
+ if (validation.step !== undefined && validation.step !== 'any') {
+ var v = (tempValue[j] / validation.step).toPrecision(14);
+ if (parseInt(v) != v) {
+ result = [this.getValidationMessage(validation, 'step', 'number_wrong_step'), validation.step];
+ break;
+ }
+ }
+ break;
+
+ case 'datetime':
+ if (tempValue[j] === undefined || tempValue[j].length === 0) {
+ if (!validation.allow_empty_value) {
+ result = ['datetime_empty'];
+ }
+ break;
+ }
+
+ // we need MomentJS
+ if (validation.format) {
+ if (!('moment' in window)) {
+ Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
+ }
+
+ var datetime = moment(tempValue[j], validation.format);
+ if (!datetime.isValid()) {
+ result = [this.getValidationMessage(validation, 'format', 'datetime_invalid'), validation.format];
+ break;
+ }
+ else {
+ if (validation.min) {
+ if (datetime < moment(validation.min, validation.format)) {
+ result = [this.getValidationMessage(validation, 'min', 'datetime_exceed_min'), validation.min];
+ break;
+ }
+ }
+ if (validation.max) {
+ if (datetime > moment(validation.max, validation.format)) {
+ result = [this.getValidationMessage(validation, 'max', 'datetime_exceed_max'), validation.max];
+ break;
+ }
+ }
+ }
+ }
+ break;
+
+ case 'boolean':
+ if (tempValue[j] === undefined || tempValue[j].length === 0) {
+ if (!validation.allow_empty_value) {
+ result = ['boolean_not_valid'];
+ }
+ break;
+ }
+ tmp = ('' + tempValue[j]).trim().toLowerCase();
+ if (tmp !== 'true' && tmp !== 'false' && tmp !== '1' && tmp !== '0' && tempValue[j] !== 1 && tempValue[j] !== 0) {
+ result = ['boolean_not_valid'];
+ break;
+ }
+ }
+
+ if (result !== true) {
+ break;
+ }
+ }
+ }
+
+ if (result !== true) {
+ break;
+ }
+ }
+
+ if ((rule.operator.type === 'between' || rule.operator.type === 'not_between') && value.length === 2) {
+ switch (QueryBuilder.types[filter.type]) {
+ case 'number':
+ if (value[0] > value[1]) {
+ result = ['number_between_invalid', value[0], value[1]];
+ }
+ break;
+
+ case 'datetime':
+ // we need MomentJS
+ if (validation.format) {
+ if (!('moment' in window)) {
+ Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
+ }
+
+ if (moment(value[0], validation.format).isAfter(moment(value[1], validation.format))) {
+ result = ['datetime_between_invalid', value[0], value[1]];
+ }
+ }
+ break;
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Returns an incremented group ID
+ * @returns {string}
+ * @private
+ */
+QueryBuilder.prototype.nextGroupId = function() {
+ return this.status.id + '_group_' + (this.status.group_id++);
+};
+
+/**
+ * Returns an incremented rule ID
+ * @returns {string}
+ * @private
+ */
+QueryBuilder.prototype.nextRuleId = function() {
+ return this.status.id + '_rule_' + (this.status.rule_id++);
+};
+
+/**
+ * Returns the operators for a filter
+ * @param {string|object} filter - filter id or filter object
+ * @returns {object[]}
+ * @fires QueryBuilder.changer:getOperators
+ */
+QueryBuilder.prototype.getOperators = function(filter) {
+ if (typeof filter == 'string') {
+ filter = this.getFilterById(filter);
+ }
+
+ var result = [];
+
+ for (var i = 0, l = this.operators.length; i < l; i++) {
+ // filter operators check
+ if (filter.operators) {
+ if (filter.operators.indexOf(this.operators[i].type) == -1) {
+ continue;
+ }
+ }
+ // type check
+ else if (this.operators[i].apply_to.indexOf(QueryBuilder.types[filter.type]) == -1) {
+ continue;
+ }
+
+ result.push(this.operators[i]);
+ }
+
+ // keep sort order defined for the filter
+ if (filter.operators) {
+ result.sort(function(a, b) {
+ return filter.operators.indexOf(a.type) - filter.operators.indexOf(b.type);
+ });
+ }
+
+ /**
+ * Modifies the operators available for a filter
+ * @event changer:getOperators
+ * @memberof QueryBuilder
+ * @param {QueryBuilder.Operator[]} operators
+ * @param {QueryBuilder.Filter} filter
+ * @returns {QueryBuilder.Operator[]}
+ */
+ return this.change('getOperators', result, filter);
+};
+
+/**
+ * Returns a particular filter by its id
+ * @param {string} id
+ * @param {boolean} [doThrow=true]
+ * @returns {object|null}
+ * @throws UndefinedFilterError
+ */
+QueryBuilder.prototype.getFilterById = function(id, doThrow) {
+ if (id == '-1') {
+ return null;
+ }
+
+ for (var i = 0, l = this.filters.length; i < l; i++) {
+ if (this.filters[i].id == id) {
+ return this.filters[i];
+ }
+ }
+
+ Utils.error(doThrow !== false, 'UndefinedFilter', 'Undefined filter "{0}"', id);
+
+ return null;
+};
+
+/**
+ * Returns a particular operator by its type
+ * @param {string} type
+ * @param {boolean} [doThrow=true]
+ * @returns {object|null}
+ * @throws UndefinedOperatorError
+ */
+QueryBuilder.prototype.getOperatorByType = function(type, doThrow) {
+ if (type == '-1') {
+ return null;
+ }
+
+ for (var i = 0, l = this.operators.length; i < l; i++) {
+ if (this.operators[i].type == type) {
+ return this.operators[i];
+ }
+ }
+
+ Utils.error(doThrow !== false, 'UndefinedOperator', 'Undefined operator "{0}"', type);
+
+ return null;
+};
+
+/**
+ * Returns rule's current input value
+ * @param {Rule} rule
+ * @returns {*}
+ * @fires QueryBuilder.changer:getRuleValue
+ * @private
+ */
+QueryBuilder.prototype.getRuleInputValue = function(rule) {
+ var filter = rule.filter;
+ var operator = rule.operator;
+ var value = [];
+
+ if (filter.valueGetter) {
+ value = filter.valueGetter.call(this, rule);
+ }
+ else {
+ var $value = rule.$el.find(QueryBuilder.selectors.value_container);
+
+ for (var i = 0; i < operator.nb_inputs; i++) {
+ var name = Utils.escapeElementId(rule.id + '_value_' + i);
+ var tmp;
+
+ switch (filter.input) {
+ case 'radio':
+ value.push($value.find('[name=' + name + ']:checked').val());
+ break;
+
+ case 'checkbox':
+ tmp = [];
+ $value.find('[name=' + name + ']:checked').each(function() {
+ tmp.push($(this).val());
+ });
+ value.push(tmp);
+ break;
+
+ case 'select':
+ if (filter.multiple) {
+ tmp = [];
+ $value.find('[name=' + name + '] option:selected').each(function() {
+ tmp.push($(this).val());
+ });
+ value.push(tmp);
+ }
+ else {
+ value.push($value.find('[name=' + name + '] option:selected').val());
+ }
+ break;
+
+ default:
+ value.push($value.find('[name=' + name + ']').val());
+ }
+ }
+
+ value = value.map(function(val) {
+ if (operator.multiple && filter.value_separator && typeof val == 'string') {
+ val = val.split(filter.value_separator);
+ }
+
+ if ($.isArray(val)) {
+ return val.map(function(subval) {
+ return Utils.changeType(subval, filter.type);
+ });
+ }
+ else {
+ return Utils.changeType(val, filter.type);
+ }
+ });
+
+ if (operator.nb_inputs === 1) {
+ value = value[0];
+ }
+
+ // @deprecated
+ if (filter.valueParser) {
+ value = filter.valueParser.call(this, rule, value);
+ }
+ }
+
+ /**
+ * Modifies the rule's value grabbed from the DOM
+ * @event changer:getRuleValue
+ * @memberof QueryBuilder
+ * @param {*} value
+ * @param {Rule} rule
+ * @returns {*}
+ */
+ return this.change('getRuleValue', value, rule);
+};
+
+/**
+ * Sets the value of a rule's input
+ * @param {Rule} rule
+ * @param {*} value
+ * @private
+ */
+QueryBuilder.prototype.setRuleInputValue = function(rule, value) {
+ var filter = rule.filter;
+ var operator = rule.operator;
+
+ if (!filter || !operator) {
+ return;
+ }
+
+ rule._updating_input = true;
+
+ if (filter.valueSetter) {
+ filter.valueSetter.call(this, rule, value);
+ }
+ else {
+ var $value = rule.$el.find(QueryBuilder.selectors.value_container);
+
+ if (operator.nb_inputs == 1) {
+ value = [value];
+ }
+
+ for (var i = 0; i < operator.nb_inputs; i++) {
+ var name = Utils.escapeElementId(rule.id + '_value_' + i);
+
+ switch (filter.input) {
+ case 'radio':
+ $value.find('[name=' + name + '][value="' + value[i] + '"]').prop('checked', true).trigger('change');
+ break;
+
+ case 'checkbox':
+ if (!$.isArray(value[i])) {
+ value[i] = [value[i]];
+ }
+ value[i].forEach(function(value) {
+ $value.find('[name=' + name + '][value="' + value + '"]').prop('checked', true).trigger('change');
+ });
+ break;
+
+ default:
+ if (operator.multiple && filter.value_separator && $.isArray(value[i])) {
+ value[i] = value[i].join(filter.value_separator);
+ }
+ $value.find('[name=' + name + ']').val(value[i]).trigger('change');
+ break;
+ }
+ }
+ }
+
+ rule._updating_input = false;
+};
+
+/**
+ * Parses rule flags
+ * @param {object} rule
+ * @returns {object}
+ * @fires QueryBuilder.changer:parseRuleFlags
+ * @private
+ */
+QueryBuilder.prototype.parseRuleFlags = function(rule) {
+ var flags = $.extend({}, this.settings.default_rule_flags);
+
+ if (rule.readonly) {
+ $.extend(flags, {
+ filter_readonly: true,
+ operator_readonly: true,
+ value_readonly: true,
+ no_delete: true
+ });
+ }
+
+ if (rule.flags) {
+ $.extend(flags, rule.flags);
+ }
+
+ /**
+ * Modifies the consolidated rule's flags
+ * @event changer:parseRuleFlags
+ * @memberof QueryBuilder
+ * @param {object} flags
+ * @param {object} rule - not a Rule object
+ * @returns {object}
+ */
+ return this.change('parseRuleFlags', flags, rule);
+};
+
+/**
+ * Gets a copy of flags of a rule
+ * @param {object} flags
+ * @param {boolean} [all=false] - return all flags or only changes from default flags
+ * @returns {object}
+ * @private
+ */
+QueryBuilder.prototype.getRuleFlags = function(flags, all) {
+ if (all) {
+ return $.extend({}, flags);
+ }
+ else {
+ var ret = {};
+ $.each(this.settings.default_rule_flags, function(key, value) {
+ if (flags[key] !== value) {
+ ret[key] = flags[key];
+ }
+ });
+ return ret;
+ }
+};
+
+/**
+ * Parses group flags
+ * @param {object} group
+ * @returns {object}
+ * @fires QueryBuilder.changer:parseGroupFlags
+ * @private
+ */
+QueryBuilder.prototype.parseGroupFlags = function(group) {
+ var flags = $.extend({}, this.settings.default_group_flags);
+
+ if (group.readonly) {
+ $.extend(flags, {
+ condition_readonly: true,
+ no_add_rule: true,
+ no_add_group: true,
+ no_delete: true
+ });
+ }
+
+ if (group.flags) {
+ $.extend(flags, group.flags);
+ }
+
+ /**
+ * Modifies the consolidated group's flags
+ * @event changer:parseGroupFlags
+ * @memberof QueryBuilder
+ * @param {object} flags
+ * @param {object} group - not a Group object
+ * @returns {object}
+ */
+ return this.change('parseGroupFlags', flags, group);
+};
+
+/**
+ * Gets a copy of flags of a group
+ * @param {object} flags
+ * @param {boolean} [all=false] - return all flags or only changes from default flags
+ * @returns {object}
+ * @private
+ */
+QueryBuilder.prototype.getGroupFlags = function(flags, all) {
+ if (all) {
+ return $.extend({}, flags);
+ }
+ else {
+ var ret = {};
+ $.each(this.settings.default_group_flags, function(key, value) {
+ if (flags[key] !== value) {
+ ret[key] = flags[key];
+ }
+ });
+ return ret;
+ }
+};
+
+/**
+ * Translate a label either by looking in the `lang` object or in itself if it's an object where keys are language codes
+ * @param {string} [category]
+ * @param {string|object} key
+ * @returns {string}
+ * @fires QueryBuilder.changer:translate
+ */
+QueryBuilder.prototype.translate = function(category, key) {
+ if (!key) {
+ key = category;
+ category = undefined;
+ }
+
+ var translation;
+ if (typeof key === 'object') {
+ translation = key[this.settings.lang_code] || key['en'];
+ }
+ else {
+ translation = (category ? this.lang[category] : this.lang)[key] || key;
+ }
+
+ /**
+ * Modifies the translated label
+ * @event changer:translate
+ * @memberof QueryBuilder
+ * @param {string} translation
+ * @param {string|object} key
+ * @param {string} [category]
+ * @returns {string}
+ */
+ return this.change('translate', translation, key, category);
+};
+
+/**
+ * Returns a validation message
+ * @param {object} validation
+ * @param {string} type
+ * @param {string} def
+ * @returns {string}
+ * @private
+ */
+QueryBuilder.prototype.getValidationMessage = function(validation, type, def) {
+ return validation.messages && validation.messages[type] || def;
+};
+
+
+QueryBuilder.templates.group = ({ group_id, level, conditions, icons, settings, translate, builder }) => {
+ return `
+`;
+};
+
+QueryBuilder.templates.rule = ({ rule_id, icons, settings, translate, builder }) => {
+ return `
+
+
+ ${settings.display_errors ? `
+
+ ` : ''}
+
+
+
+
`;
+};
+
+QueryBuilder.templates.filterSelect = ({ rule, filters, icons, settings, translate, builder }) => {
+ let optgroup = null;
+ return `
+`;
+};
+
+QueryBuilder.templates.operatorSelect = ({ rule, operators, icons, settings, translate, builder }) => {
+ let optgroup = null;
+ return `
+${operators.length === 1 ? `
+
+${translate("operators", operators[0].type)}
+
+` : ''}
+`;
+};
+
+QueryBuilder.templates.ruleValueSelect = ({ name, rule, icons, settings, translate, builder }) => {
+ let optgroup = null;
+ return `
+`;
+};
+
+/**
+ * Returns group's HTML
+ * @param {string} group_id
+ * @param {int} level
+ * @returns {string}
+ * @fires QueryBuilder.changer:getGroupTemplate
+ * @private
+ */
+QueryBuilder.prototype.getGroupTemplate = function (group_id, level) {
+ var h = this.templates.group({
+ builder: this,
+ group_id: group_id,
+ level: level,
+ conditions: this.settings.conditions,
+ icons: this.icons,
+ settings: this.settings,
+ translate: this.translate.bind(this)
+ }).trim();
+
+ /**
+ * Modifies the raw HTML of a group
+ * @event changer:getGroupTemplate
+ * @memberof QueryBuilder
+ * @param {string} html
+ * @param {int} level
+ * @returns {string}
+ */
+ return this.change('getGroupTemplate', h, level);
+};
+
+/**
+ * Returns rule's HTML
+ * @param {string} rule_id
+ * @returns {string}
+ * @fires QueryBuilder.changer:getRuleTemplate
+ * @private
+ */
+QueryBuilder.prototype.getRuleTemplate = function (rule_id) {
+ var h = this.templates.rule({
+ builder: this,
+ rule_id: rule_id,
+ icons: this.icons,
+ settings: this.settings,
+ translate: this.translate.bind(this)
+ }).trim();
+
+ /**
+ * Modifies the raw HTML of a rule
+ * @event changer:getRuleTemplate
+ * @memberof QueryBuilder
+ * @param {string} html
+ * @returns {string}
+ */
+ return this.change('getRuleTemplate', h);
+};
+
+/**
+ * Returns rule's filter HTML
+ * @param {Rule} rule
+ * @param {object[]} filters
+ * @returns {string}
+ * @fires QueryBuilder.changer:getRuleFilterTemplate
+ * @private
+ */
+QueryBuilder.prototype.getRuleFilterSelect = function (rule, filters) {
+ var h = this.templates.filterSelect({
+ builder: this,
+ rule: rule,
+ filters: filters,
+ icons: this.icons,
+ settings: this.settings,
+ translate: this.translate.bind(this)
+ }).trim();
+
+ /**
+ * Modifies the raw HTML of the rule's filter dropdown
+ * @event changer:getRuleFilterSelect
+ * @memberof QueryBuilder
+ * @param {string} html
+ * @param {Rule} rule
+ * @param {QueryBuilder.Filter[]} filters
+ * @returns {string}
+ */
+ return this.change('getRuleFilterSelect', h, rule, filters);
+};
+
+/**
+ * Returns rule's operator HTML
+ * @param {Rule} rule
+ * @param {object[]} operators
+ * @returns {string}
+ * @fires QueryBuilder.changer:getRuleOperatorTemplate
+ * @private
+ */
+QueryBuilder.prototype.getRuleOperatorSelect = function (rule, operators) {
+ var h = this.templates.operatorSelect({
+ builder: this,
+ rule: rule,
+ operators: operators,
+ icons: this.icons,
+ settings: this.settings,
+ translate: this.translate.bind(this)
+ }).trim();
+
+ /**
+ * Modifies the raw HTML of the rule's operator dropdown
+ * @event changer:getRuleOperatorSelect
+ * @memberof QueryBuilder
+ * @param {string} html
+ * @param {Rule} rule
+ * @param {QueryBuilder.Operator[]} operators
+ * @returns {string}
+ */
+ return this.change('getRuleOperatorSelect', h, rule, operators);
+};
+
+/**
+ * Returns the rule's value select HTML
+ * @param {string} name
+ * @param {Rule} rule
+ * @returns {string}
+ * @fires QueryBuilder.changer:getRuleValueSelect
+ * @private
+ */
+QueryBuilder.prototype.getRuleValueSelect = function (name, rule) {
+ var h = this.templates.ruleValueSelect({
+ builder: this,
+ name: name,
+ rule: rule,
+ icons: this.icons,
+ settings: this.settings,
+ translate: this.translate.bind(this)
+ }).trim();
+
+ /**
+ * Modifies the raw HTML of the rule's value dropdown (in case of a "select filter)
+ * @event changer:getRuleValueSelect
+ * @memberof QueryBuilder
+ * @param {string} html
+ * @param [string} name
+ * @param {Rule} rule
+ * @returns {string}
+ */
+ return this.change('getRuleValueSelect', h, name, rule);
+};
+
+/**
+ * Returns the rule's value HTML
+ * @param {Rule} rule
+ * @param {int} value_id
+ * @returns {string}
+ * @fires QueryBuilder.changer:getRuleInput
+ * @private
+ */
+QueryBuilder.prototype.getRuleInput = function (rule, value_id) {
+ var filter = rule.filter;
+ var validation = rule.filter.validation || {};
+ var name = rule.id + '_value_' + value_id;
+ var c = filter.vertical ? ' class=block' : '';
+ var h = '';
+ var placeholder = Array.isArray(filter.placeholder) ? filter.placeholder[value_id] : filter.placeholder;
+
+ if (typeof filter.input == 'function') {
+ h = filter.input.call(this, rule, name);
+ }
+ else {
+ switch (filter.input) {
+ case 'radio':
+ case 'checkbox':
+ Utils.iterateOptions(filter.values, function (key, val) {
+ h += ' ';
+ });
+ break;
+
+ case 'select':
+ h = this.getRuleValueSelect(name, rule);
+ break;
+
+ case 'textarea':
+ h += '" ; + break + + case number : + h +='" ; + break + + default + h +='" ; + } + } + + + * Modifies the raw HTML of the rule s input + * @event changer:getRuleInput + * @memberof QueryBuilder + * @param {string html + * @param {Rule rule + * @param {string name - the name that the input must have + * @returns {string + * + return this.change getRuleInput , h rule name + + + + + * @namespace + * +var Utils="{};" + + + * @member {object + * @memberof QueryBuilder + * @see Utils + * +QueryBuilder.utils="Utils;" + + + * @callback Utils#OptionsIteratee + * @param {string key + * @param {string value + * @param {string [optgroup + * + + + * Iterates over radio/checkbox/selection options it accept four formats + * + * @example + * array of values + * options="['one'," two , three ] + * @example + * simple key-value map + * options="{1:" one , 2 two , 3 three } + * @example + * array of 1-element maps + * options="[{1:" one } {2 two } {3 three } + * @example + * array of elements + * options="[{value:" 1 label one , optgroup group } {value 2 label two } + * + * @param {object|array options + * @param {Utils#OptionsIteratee tpl + * +Utils.iterateOptions="function(options," tpl { + if (options { + if ($.isArray(options { + options.forEach(function(entry { + if ($.isPlainObject(entry { + array of elements + if ( value in entry { + tpl(entry.value entry.label | entry.value entry.optgroup + } + array of one-element maps + else { + $.each(entry function(key val { + tpl(key val + return false break after first entry + } + } + } + array of values + else { + tpl(entry entry + } + } + } + unordered map + else { + $.each(options function(key val { + tpl(key val + } + } + } + + + + * Replaces {0 {1 . in a string + * @param {string str + * @param { args + * @returns {string + * +Utils.fmt="function(str," args { + if (!Array.isArray(args { + args="Array.prototype.slice.call(arguments," 1 + } + + return str.replace(/{([0-9]+)}/g function(m i { + return args[parseInt(i + } + + + + * Throws an Error object with custom name or logs an error + * @param {boolean [doThrow="true]" + * @param {string type + * @param {string message + * @param { args + * +Utils.error="function()" { + var i="0;" + var doThrow="typeof" arguments[i="==" boolean ? arguments[i : true + var type="arguments[i++];" + var message="arguments[i++];" + var args="Array.isArray(arguments[i])" ? arguments[i : Array.prototype.slice.call(arguments i + + if (doThrow { + var err="new" Error(Utils.fmt(message args + err.name="type" + Error ; + err.args="args;" + throw err + } + else { + console.error(type + Error + Utils.fmt(message args + } + + + + * Changes the type of a value to int float or bool + * @param { value + * @param {string type - integer , double , boolean or anything else (passthrough + * @returns { + * +Utils.changeType="function(value," type { + if (value="==" | value="==" undefined { + return undefined + } + + switch (type { + @formatter:off + case integer : + if (typeof value="==" string & !/^-?\d+$/.test(value { + return value + } + return parseInt(value + case double : + if (typeof value="==" string & !/^-?\d+\.?\d*$/.test(value { + return value + } + return parseFloat(value + case boolean : + if (typeof value="==" string & !/^(0|1|true|false){1}$/i.test(value { + return value + } + return value="==" true | value="==" 1 | value.toLowerCase="==" true | value="==" 1 ; + default return value + @formatter:on + } + + + + * Escapes a string like PHP s mysql_real_escape_string does + * @param {string value + * @param {string [additionalEscape additionnal chars to escape + * @returns {string + * +Utils.escapeString="function(value," additionalEscape { + if (typeof value !="string" ) { + return value + } + + var escaped="value" + .replace(/[\0\n\r\b \ ]/g function(s { + switch (s { + @formatter:off + case \0 : return \\0 ; + case \n : return \\n ; + case \r : return \\r ; + case \b : return \\b ; + case \ : return \ \ ; + default return \ + s + @formatter:off + } + } + uglify compliant + .replace(/\t/g \\t ) + .replace(/\x1a/g \\Z ) + + if (additionalEscape { + escaped="escaped" + .replace(new RegExp [ + additionalEscape + ] , g ) function(s { + return \ + s + } + } + + return escaped + + + + * Escapes a string for use in regex + * @param {string str + * @returns {string + * +Utils.escapeRegExp="function(str)" { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g \ ) + + + + * Escapes a string for use in HTML element id + * @param {string str + * @returns {string + * +Utils.escapeElementId="function(str)" { + Regex based on that suggested by + https://learn.jquery.com/using-jquery-core/faq/how-do-i-select-an-element-by-an-id-that-has-characters-used-in-css-notation + - escapes : . [ ] , + - avoids escaping already escaped values + return (str ? str.replace(/(\\)?([:.\[\],])/g + function $0 $1 $2 ) { return $1 ? $0 : \ + $2 } : str + + + + * Sorts objects by grouping them by `key preserving initial order when possible + * @param {object items + * @param {string key + * @returns {object + * +Utils.groupSort="function(items," key { + var optgroups="[];" + var newItems="[];" + + items.forEach(function(item { + var idx + + if (item[key { + idx="optgroups.lastIndexOf(item[key]);" + + if (idx="=" -1 { + idx="optgroups.length;" + } + else { + idx + } + } + else { + idx="optgroups.length;" + } + + optgroups.splice(idx 0 item[key + newItems.splice(idx 0 item + } + + return newItems + + + + * Defines properties on an Node prototype with getter and setter />
+ * Update events are emitted in the setter through root Model (if any).
+ * The object must have a `__` object, non enumerable property to store values.
+ * @param {function} obj
+ * @param {string[]} fields
+ */
+Utils.defineModelProperties = function(obj, fields) {
+ fields.forEach(function(field) {
+ Object.defineProperty(obj.prototype, field, {
+ enumerable: true,
+ get: function() {
+ return this.__[field];
+ },
+ set: function(value) {
+ var previousValue = (this.__[field] !== null && typeof this.__[field] == 'object') ?
+ $.extend({}, this.__[field]) :
+ this.__[field];
+
+ this.__[field] = value;
+
+ if (this.model !== null) {
+ /**
+ * After a value of the model changed
+ * @event model:update
+ * @memberof Model
+ * @param {Node} node
+ * @param {string} field
+ * @param {*} value
+ * @param {*} previousValue
+ */
+ this.model.trigger('update', this, field, value, previousValue);
+ }
+ }
+ });
+ });
+};
+
+
+/**
+ * Main object storing data model and emitting model events
+ * @constructor
+ */
+function Model() {
+ /**
+ * @member {Group}
+ * @readonly
+ */
+ this.root = null;
+
+ /**
+ * Base for event emitting
+ * @member {jQuery}
+ * @readonly
+ * @private
+ */
+ this.$ = $(this);
+}
+
+$.extend(Model.prototype, /** @lends Model.prototype */ {
+ /**
+ * Triggers an event on the model
+ * @param {string} type
+ * @returns {$.Event}
+ */
+ trigger: function(type) {
+ var event = new $.Event(type);
+ this.$.triggerHandler(event, Array.prototype.slice.call(arguments, 1));
+ return event;
+ },
+
+ /**
+ * Attaches an event listener on the model
+ * @param {string} type
+ * @param {function} cb
+ * @returns {Model}
+ */
+ on: function() {
+ this.$.on.apply(this.$, Array.prototype.slice.call(arguments));
+ return this;
+ },
+
+ /**
+ * Removes an event listener from the model
+ * @param {string} type
+ * @param {function} [cb]
+ * @returns {Model}
+ */
+ off: function() {
+ this.$.off.apply(this.$, Array.prototype.slice.call(arguments));
+ return this;
+ },
+
+ /**
+ * Attaches an event listener called once on the model
+ * @param {string} type
+ * @param {function} cb
+ * @returns {Model}
+ */
+ once: function() {
+ this.$.one.apply(this.$, Array.prototype.slice.call(arguments));
+ return this;
+ }
+});
+
+
+/**
+ * Root abstract object
+ * @constructor
+ * @param {Node} [parent]
+ * @param {jQuery} $el
+ */
+var Node = function(parent, $el) {
+ if (!(this instanceof Node)) {
+ return new Node(parent, $el);
+ }
+
+ Object.defineProperty(this, '__', { value: {} });
+
+ $el.data('queryBuilderModel', this);
+
+ /**
+ * @name level
+ * @member {int}
+ * @memberof Node
+ * @instance
+ * @readonly
+ */
+ this.__.level = 1;
+
+ /**
+ * @name error
+ * @member {string}
+ * @memberof Node
+ * @instance
+ */
+ this.__.error = null;
+
+ /**
+ * @name flags
+ * @member {object}
+ * @memberof Node
+ * @instance
+ * @readonly
+ */
+ this.__.flags = {};
+
+ /**
+ * @name data
+ * @member {object}
+ * @memberof Node
+ * @instance
+ */
+ this.__.data = undefined;
+
+ /**
+ * @member {jQuery}
+ * @readonly
+ */
+ this.$el = $el;
+
+ /**
+ * @member {string}
+ * @readonly
+ */
+ this.id = $el[0].id;
+
+ /**
+ * @member {Model}
+ * @readonly
+ */
+ this.model = null;
+
+ /**
+ * @member {Group}
+ * @readonly
+ */
+ this.parent = parent;
+};
+
+Utils.defineModelProperties(Node, ['level', 'error', 'data', 'flags']);
+
+Object.defineProperty(Node.prototype, 'parent', {
+ enumerable: true,
+ get: function() {
+ return this.__.parent;
+ },
+ set: function(value) {
+ this.__.parent = value;
+ this.level = value === null ? 1 : value.level + 1;
+ this.model = value === null ? null : value.model;
+ }
+});
+
+/**
+ * Checks if this Node is the root
+ * @returns {boolean}
+ */
+Node.prototype.isRoot = function() {
+ return (this.level === 1);
+};
+
+/**
+ * Returns the node position inside its parent
+ * @returns {int}
+ */
+Node.prototype.getPos = function() {
+ if (this.isRoot()) {
+ return -1;
+ }
+ else {
+ return this.parent.getNodePos(this);
+ }
+};
+
+/**
+ * Deletes self
+ * @fires Model.model:drop
+ */
+Node.prototype.drop = function() {
+ var model = this.model;
+
+ if (!!this.parent) {
+ this.parent.removeNode(this);
+ }
+
+ this.$el.removeData('queryBuilderModel');
+
+ if (model !== null) {
+ /**
+ * After a node of the model has been removed
+ * @event model:drop
+ * @memberof Model
+ * @param {Node} node
+ */
+ model.trigger('drop', this);
+ }
+};
+
+/**
+ * Moves itself after another Node
+ * @param {Node} target
+ * @fires Model.model:move
+ */
+Node.prototype.moveAfter = function(target) {
+ if (!this.isRoot()) {
+ this.move(target.parent, target.getPos() + 1);
+ }
+};
+
+/**
+ * Moves itself at the beginning of parent or another Group
+ * @param {Group} [target]
+ * @fires Model.model:move
+ */
+Node.prototype.moveAtBegin = function(target) {
+ if (!this.isRoot()) {
+ if (target === undefined) {
+ target = this.parent;
+ }
+
+ this.move(target, 0);
+ }
+};
+
+/**
+ * Moves itself at the end of parent or another Group
+ * @param {Group} [target]
+ * @fires Model.model:move
+ */
+Node.prototype.moveAtEnd = function(target) {
+ if (!this.isRoot()) {
+ if (target === undefined) {
+ target = this.parent;
+ }
+
+ this.move(target, target.length() === 0 ? 0 : target.length() - 1);
+ }
+};
+
+/**
+ * Moves itself at specific position of Group
+ * @param {Group} target
+ * @param {int} index
+ * @fires Model.model:move
+ */
+Node.prototype.move = function(target, index) {
+ if (!this.isRoot()) {
+ if (typeof target === 'number') {
+ index = target;
+ target = this.parent;
+ }
+
+ this.parent.removeNode(this);
+ target.insertNode(this, index, false);
+
+ if (this.model !== null) {
+ /**
+ * After a node of the model has been moved
+ * @event model:move
+ * @memberof Model
+ * @param {Node} node
+ * @param {Node} target
+ * @param {int} index
+ */
+ this.model.trigger('move', this, target, index);
+ }
+ }
+};
+
+
+/**
+ * Group object
+ * @constructor
+ * @extends Node
+ * @param {Group} [parent]
+ * @param {jQuery} $el
+ */
+var Group = function(parent, $el) {
+ if (!(this instanceof Group)) {
+ return new Group(parent, $el);
+ }
+
+ Node.call(this, parent, $el);
+
+ /**
+ * @member {object[]}
+ * @readonly
+ */
+ this.rules = [];
+
+ /**
+ * @name condition
+ * @member {string}
+ * @memberof Group
+ * @instance
+ */
+ this.__.condition = null;
+};
+
+Group.prototype = Object.create(Node.prototype);
+Group.prototype.constructor = Group;
+
+Utils.defineModelProperties(Group, ['condition']);
+
+/**
+ * Removes group's content
+ */
+Group.prototype.empty = function() {
+ this.each('reverse', function(rule) {
+ rule.drop();
+ }, function(group) {
+ group.drop();
+ });
+};
+
+/**
+ * Deletes self
+ */
+Group.prototype.drop = function() {
+ this.empty();
+ Node.prototype.drop.call(this);
+};
+
+/**
+ * Returns the number of children
+ * @returns {int}
+ */
+Group.prototype.length = function() {
+ return this.rules.length;
+};
+
+/**
+ * Adds a Node at specified index
+ * @param {Node} node
+ * @param {int} [index=end]
+ * @param {boolean} [trigger=false] - fire 'add' event
+ * @returns {Node} the inserted node
+ * @fires Model.model:add
+ */
+Group.prototype.insertNode = function(node, index, trigger) {
+ if (index === undefined) {
+ index = this.length();
+ }
+
+ this.rules.splice(index, 0, node);
+ node.parent = this;
+
+ if (trigger && this.model !== null) {
+ /**
+ * After a node of the model has been added
+ * @event model:add
+ * @memberof Model
+ * @param {Node} parent
+ * @param {Node} node
+ * @param {int} index
+ */
+ this.model.trigger('add', this, node, index);
+ }
+
+ return node;
+};
+
+/**
+ * Adds a new Group at specified index
+ * @param {jQuery} $el
+ * @param {int} [index=end]
+ * @returns {Group}
+ * @fires Model.model:add
+ */
+Group.prototype.addGroup = function($el, index) {
+ return this.insertNode(new Group(this, $el), index, true);
+};
+
+/**
+ * Adds a new Rule at specified index
+ * @param {jQuery} $el
+ * @param {int} [index=end]
+ * @returns {Rule}
+ * @fires Model.model:add
+ */
+Group.prototype.addRule = function($el, index) {
+ return this.insertNode(new Rule(this, $el), index, true);
+};
+
+/**
+ * Deletes a specific Node
+ * @param {Node} node
+ */
+Group.prototype.removeNode = function(node) {
+ var index = this.getNodePos(node);
+ if (index !== -1) {
+ node.parent = null;
+ this.rules.splice(index, 1);
+ }
+};
+
+/**
+ * Returns the position of a child Node
+ * @param {Node} node
+ * @returns {int}
+ */
+Group.prototype.getNodePos = function(node) {
+ return this.rules.indexOf(node);
+};
+
+/**
+ * @callback Model#GroupIteratee
+ * @param {Node} node
+ * @returns {boolean} stop the iteration
+ */
+
+/**
+ * Iterate over all Nodes
+ * @param {boolean} [reverse=false] - iterate in reverse order, required if you delete nodes
+ * @param {Model#GroupIteratee} cbRule - callback for Rules (can be `null` but not omitted)
+ * @param {Model#GroupIteratee} [cbGroup] - callback for Groups
+ * @param {object} [context] - context for callbacks
+ * @returns {boolean} if the iteration has been stopped by a callback
+ */
+Group.prototype.each = function(reverse, cbRule, cbGroup, context) {
+ if (typeof reverse !== 'boolean' && typeof reverse !== 'string') {
+ context = cbGroup;
+ cbGroup = cbRule;
+ cbRule = reverse;
+ reverse = false;
+ }
+ context = context === undefined ? null : context;
+
+ var i = reverse ? this.rules.length - 1 : 0;
+ var l = reverse ? 0 : this.rules.length - 1;
+ var c = reverse ? -1 : 1;
+ var next = function() {
+ return reverse ? i >= l : i <= l;
+ };
+ var stop = false;
+
+ for (; next(); i += c) {
+ if (this.rules[i] instanceof Group) {
+ if (!!cbGroup) {
+ stop = cbGroup.call(context, this.rules[i]) === false;
+ }
+ }
+ else if (!!cbRule) {
+ stop = cbRule.call(context, this.rules[i]) === false;
+ }
+
+ if (stop) {
+ break;
+ }
+ }
+
+ return !stop;
+};
+
+/**
+ * Checks if the group contains a particular Node
+ * @param {Node} node
+ * @param {boolean} [recursive=false]
+ * @returns {boolean}
+ */
+Group.prototype.contains = function(node, recursive) {
+ if (this.getNodePos(node) !== -1) {
+ return true;
+ }
+ else if (!recursive) {
+ return false;
+ }
+ else {
+ // the loop will return with false as soon as the Node is found
+ return !this.each(function() {
+ return true;
+ }, function(group) {
+ return !group.contains(node, true);
+ });
+ }
+};
+
+
+/**
+ * Rule object
+ * @constructor
+ * @extends Node
+ * @param {Group} parent
+ * @param {jQuery} $el
+ */
+var Rule = function(parent, $el) {
+ if (!(this instanceof Rule)) {
+ return new Rule(parent, $el);
+ }
+
+ Node.call(this, parent, $el);
+
+ this._updating_value = false;
+ this._updating_input = false;
+
+ /**
+ * @name filter
+ * @member {QueryBuilder.Filter}
+ * @memberof Rule
+ * @instance
+ */
+ this.__.filter = null;
+
+ /**
+ * @name operator
+ * @member {QueryBuilder.Operator}
+ * @memberof Rule
+ * @instance
+ */
+ this.__.operator = null;
+
+ /**
+ * @name value
+ * @member {*}
+ * @memberof Rule
+ * @instance
+ */
+ this.__.value = undefined;
+};
+
+Rule.prototype = Object.create(Node.prototype);
+Rule.prototype.constructor = Rule;
+
+Utils.defineModelProperties(Rule, ['filter', 'operator', 'value']);
+
+/**
+ * Checks if this Node is the root
+ * @returns {boolean} always false
+ */
+Rule.prototype.isRoot = function() {
+ return false;
+};
+
+
+/**
+ * @member {function}
+ * @memberof QueryBuilder
+ * @see Group
+ */
+QueryBuilder.Group = Group;
+
+/**
+ * @member {function}
+ * @memberof QueryBuilder
+ * @see Rule
+ */
+QueryBuilder.Rule = Rule;
+
+
+/**
+ * The {@link http://learn.jquery.com/plugins/|jQuery Plugins} namespace
+ * @external "jQuery.fn"
+ */
+
+/**
+ * Instanciates or accesses the {@link QueryBuilder} on an element
+ * @function
+ * @memberof external:"jQuery.fn"
+ * @param {*} option - initial configuration or method name
+ * @param {...*} args - method arguments
+ *
+ * @example
+ * $('#builder').queryBuilder({ /** configuration object *\/ });
+ * @example
+ * $('#builder').queryBuilder('methodName', methodParam1, methodParam2);
+ */
+$.fn.queryBuilder = function(option) {
+ if (this.length === 0) {
+ Utils.error('Config', 'No target defined');
+ }
+ if (this.length > 1) {
+ Utils.error('Config', 'Unable to initialize on multiple target');
+ }
+
+ var data = this.data('queryBuilder');
+ var options = (typeof option == 'object' && option) || {};
+
+ if (!data && option == 'destroy') {
+ return this;
+ }
+ if (!data) {
+ var builder = new QueryBuilder(this, options);
+ this.data('queryBuilder', builder);
+ builder.init(options.rules);
+ }
+ if (typeof option == 'string') {
+ return data[option].apply(data, Array.prototype.slice.call(arguments, 1));
+ }
+
+ return this;
+};
+
+/**
+ * @function
+ * @memberof external:"jQuery.fn"
+ * @see QueryBuilder
+ */
+$.fn.queryBuilder.constructor = QueryBuilder;
+
+/**
+ * @function
+ * @memberof external:"jQuery.fn"
+ * @see QueryBuilder.defaults
+ */
+$.fn.queryBuilder.defaults = QueryBuilder.defaults;
+
+/**
+ * @function
+ * @memberof external:"jQuery.fn"
+ * @see QueryBuilder.defaults
+ */
+$.fn.queryBuilder.extend = QueryBuilder.extend;
+
+/**
+ * @function
+ * @memberof external:"jQuery.fn"
+ * @see QueryBuilder.define
+ */
+$.fn.queryBuilder.define = QueryBuilder.define;
+
+/**
+ * @function
+ * @memberof external:"jQuery.fn"
+ * @see QueryBuilder.regional
+ */
+$.fn.queryBuilder.regional = QueryBuilder.regional;
+
+
+/**
+ * @class BtCheckbox
+ * @memberof module:plugins
+ * @description Applies Awesome Bootstrap Checkbox for checkbox and radio inputs.
+ * @param {object} [options]
+ * @param {string} [options.font='bootstrap-icons']
+ * @param {string} [options.color='default']
+ */
+QueryBuilder.define('bt-checkbox', function(options) {
+ if (options.font === 'bootstrap-icons') {
+ this.$el.addClass('bt-checkbox-bootstrap-icons');
+ }
+
+ this.on('getRuleInput.filter', function(h, rule, name) {
+ var filter = rule.filter;
+
+ if ((filter.input === 'radio' || filter.input === 'checkbox') && !filter.plugin) {
+ h.value = '';
+
+ if (!filter.colors) {
+ filter.colors = {};
+ }
+ if (filter.color) {
+ filter.colors._def_ = filter.color;
+ }
+
+ var style = filter.vertical ? ' style="display:block"' : '';
+ var i = 0;
+
+ Utils.iterateOptions(filter.values, function(key, val) {
+ var color = filter.colors[key] || filter.colors._def_ || options.color;
+ var id = name + '_' + (i++);
+
+ h.value += `
`;
+ });
+ }
+ });
+}, {
+ font: 'bootstrap-icons',
+ color: 'default'
+});
+
+
+/**
+ * @class BtTooltipErrors
+ * @memberof module:plugins
+ * @description Applies Bootstrap Tooltips on validation error messages.
+ * @param {object} [options]
+ * @param {string} [options.placement='right']
+ * @throws MissingLibraryError
+ */
+QueryBuilder.define('bt-tooltip-errors', function(options) {
+ if (! typeof bootstrap.Tooltip === "function") {
+ alert(typeof bootstrap.Tooltip );
+ Utils.error('MissingLibrary', 'Bootstrap Popper is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com');
+ }
+
+ var self = this;
+
+ // add BT Tooltip data
+ this.on('getRuleTemplate.filter getGroupTemplate.filter', function(h) {
+ var $h = $($.parseHTML(h.value));
+ $h.find(QueryBuilder.selectors.error_container).attr('data-bs-toggle', 'tooltip');
+ h.value = $h.prop('outerHTML');
+ });
+
+ // init/refresh tooltip when title changes
+ this.model.on('update', function(e, node, field) {
+ if (field == 'error' && self.settings.display_errors) {
+ node.$el.find(QueryBuilder.selectors.error_container).eq(0)
+ .attr('data-bs-original-title',options).attr('data-bs-title',options).tooltip();
+ }
+ });
+}, {
+ placement: 'right'
+});
+
+
+/**
+ * @class ChangeFilters
+ * @memberof module:plugins
+ * @description Allows to change available filters after plugin initialization.
+ */
+
+QueryBuilder.extend(/** @lends module:plugins.ChangeFilters.prototype */ {
+ /**
+ * Change the filters of the builder
+ * @param {boolean} [deleteOrphans=false] - delete rules using old filters
+ * @param {QueryBuilder[]} filters
+ * @fires module:plugins.ChangeFilters.changer:setFilters
+ * @fires module:plugins.ChangeFilters.afterSetFilters
+ * @throws ChangeFilterError
+ */
+ setFilters: function(deleteOrphans, filters) {
+ var self = this;
+
+ if (filters === undefined) {
+ filters = deleteOrphans;
+ deleteOrphans = false;
+ }
+
+ filters = this.checkFilters(filters);
+
+ /**
+ * Modifies the filters before {@link module:plugins.ChangeFilters.setFilters} method
+ * @event changer:setFilters
+ * @memberof module:plugins.ChangeFilters
+ * @param {QueryBuilder.Filter[]} filters
+ * @returns {QueryBuilder.Filter[]}
+ */
+ filters = this.change('setFilters', filters);
+
+ var filtersIds = filters.map(function(filter) {
+ return filter.id;
+ });
+
+ // check for orphans
+ if (!deleteOrphans) {
+ (function checkOrphans(node) {
+ node.each(
+ function(rule) {
+ if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
+ Utils.error('ChangeFilter', 'A rule is using filter "{0}"', rule.filter.id);
+ }
+ },
+ checkOrphans
+ );
+ }(this.model.root));
+ }
+
+ // replace filters
+ this.filters = filters;
+
+ // apply on existing DOM
+ (function updateBuilder(node) {
+ node.each(true,
+ function(rule) {
+ if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
+ rule.drop();
+
+ self.trigger('rulesChanged');
+ }
+ else {
+ self.createRuleFilters(rule);
+
+ rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
+ self.trigger('afterUpdateRuleFilter', rule);
+ }
+ },
+ updateBuilder
+ );
+ }(this.model.root));
+
+ // update plugins
+ if (this.settings.plugins) {
+ if (this.settings.plugins['unique-filter']) {
+ this.updateDisabledFilters();
+ }
+ if (this.settings.plugins['bt-selectpicker']) {
+ this.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render');
+ }
+ }
+
+ // reset the default_filter if does not exist anymore
+ if (this.settings.default_filter) {
+ try {
+ this.getFilterById(this.settings.default_filter);
+ }
+ catch (e) {
+ this.settings.default_filter = null;
+ }
+ }
+
+ /**
+ * After {@link module:plugins.ChangeFilters.setFilters} method
+ * @event afterSetFilters
+ * @memberof module:plugins.ChangeFilters
+ * @param {QueryBuilder.Filter[]} filters
+ */
+ this.trigger('afterSetFilters', filters);
+ },
+
+ /**
+ * Adds a new filter to the builder
+ * @param {QueryBuilder.Filter|Filter[]} newFilters
+ * @param {int|string} [position=#end] - index or '#start' or '#end'
+ * @fires module:plugins.ChangeFilters.changer:setFilters
+ * @fires module:plugins.ChangeFilters.afterSetFilters
+ * @throws ChangeFilterError
+ */
+ addFilter: function(newFilters, position) {
+ if (position === undefined || position == '#end') {
+ position = this.filters.length;
+ }
+ else if (position == '#start') {
+ position = 0;
+ }
+
+ if (!$.isArray(newFilters)) {
+ newFilters = [newFilters];
+ }
+
+ var filters = $.extend(true, [], this.filters);
+
+ // numeric position
+ if (parseInt(position) == position) {
+ Array.prototype.splice.apply(filters, [position, 0].concat(newFilters));
+ }
+ else {
+ // after filter by its id
+ if (this.filters.some(function(filter, index) {
+ if (filter.id == position) {
+ position = index + 1;
+ return true;
+ }
+ })
+ ) {
+ Array.prototype.splice.apply(filters, [position, 0].concat(newFilters));
+ }
+ // defaults to end of list
+ else {
+ Array.prototype.push.apply(filters, newFilters);
+ }
+ }
+
+ this.setFilters(filters);
+ },
+
+ /**
+ * Removes a filter from the builder
+ * @param {string|string[]} filterIds
+ * @param {boolean} [deleteOrphans=false] delete rules using old filters
+ * @fires module:plugins.ChangeFilters.changer:setFilters
+ * @fires module:plugins.ChangeFilters.afterSetFilters
+ * @throws ChangeFilterError
+ */
+ removeFilter: function(filterIds, deleteOrphans) {
+ var filters = $.extend(true, [], this.filters);
+ if (typeof filterIds === 'string') {
+ filterIds = [filterIds];
+ }
+
+ filters = filters.filter(function(filter) {
+ return filterIds.indexOf(filter.id) === -1;
+ });
+
+ this.setFilters(deleteOrphans, filters);
+ }
+});
+
+
+/**
+ * @class ChosenSelectpicker
+ * @memberof module:plugins
+ * @descriptioon Applies chosen-js Select on filters and operators combo-boxes.
+ * @param {object} [options] Supports all the options for chosen
+ * @throws MissingLibraryError
+ */
+QueryBuilder.define('chosen-selectpicker', function(options) {
+
+ if (!$.fn.chosen) {
+ Utils.error('MissingLibrary', 'chosen is required to use "chosen-selectpicker" plugin. Get it here: https://github.com/harvesthq/chosen');
+ }
+
+ if (this.settings.plugins['bt-selectpicker']) {
+ Utils.error('Conflict', 'bt-selectpicker is already selected as the dropdown plugin. Please remove chosen-selectpicker from the plugin list');
+ }
+
+ var Selectors = QueryBuilder.selectors;
+
+ // init selectpicker
+ this.on('afterCreateRuleFilters', function(e, rule) {
+ rule.$el.find(Selectors.rule_filter).removeClass('form-control').chosen(options);
+ });
+
+ this.on('afterCreateRuleOperators', function(e, rule) {
+ if (e.builder.getOperators(rule.filter).length > 1) {
+ rule.$el.find(Selectors.rule_operator).removeClass('form-control').chosen(options);
+ }
+ });
+
+ // update selectpicker on change
+ this.on('afterUpdateRuleFilter', function(e, rule) {
+ rule.$el.find(Selectors.rule_filter).trigger('chosen:updated');
+ });
+
+ this.on('afterUpdateRuleOperator', function(e, rule) {
+ rule.$el.find(Selectors.rule_operator).trigger('chosen:updated');
+ });
+
+ this.on('beforeDeleteRule', function(e, rule) {
+ rule.$el.find(Selectors.rule_filter).chosen('destroy');
+ rule.$el.find(Selectors.rule_operator).chosen('destroy');
+ });
+});
+
+
+/**
+ * @class FilterDescription
+ * @memberof module:plugins
+ * @description Provides three ways to display a description about a filter: inline, Bootsrap Popover or Bootbox.
+ * @param {object} [options]
+ * @param {string} [options.icon='bi-info-circle-fill']
+ * @param {string} [options.mode='popover'] - inline, popover or bootbox
+ * @throws ConfigError
+ */
+QueryBuilder.define('filter-description', function(options) {
+ // INLINE
+ if (options.mode === 'inline') {
+ this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
+ var $p = rule.$el.find('p.filter-description');
+ var description = e.builder.getFilterDescription(rule.filter, rule);
+
+ if (!description) {
+ $p.hide();
+ }
+ else {
+ if ($p.length === 0) {
+ $p = $($.parseHTML(''));
+ $p.appendTo(rule.$el);
+ }
+ else {
+ $p.css('display', '');
+ }
+
+ $p.html(' ' + description);
+ }
+ });
+ }
+ // POPOVER
+ else if (options.mode === 'popover') {
+ if (!$.fn.popover || !$.fn.popover.Constructor || !$.fn.popover.Constructor.prototype.fixTitle) {
+ Utils.error('MissingLibrary', 'Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com');
+ }
+
+ this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
+ var $b = rule.$el.find('button.filter-description');
+ var description = e.builder.getFilterDescription(rule.filter, rule);
+
+ if (!description) {
+ $b.hide();
+
+ if ($b.data('bs-popover')) {
+ $b.popover('hide');
+ }
+ }
+ else {
+ if ($b.length === 0) {
+ $b = $($.parseHTML(''));
+ $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions));
+ const popover = new bootstrap.Popover($b.get(0), {
+ placement: 'left',
+ container: 'body',
+ html: true
+ })
+ $b.on('mouseout', function() {
+ popover('hide');
+ });
+ }
+ else {
+ $b.css('display', '');
+ }
+
+ $b.data('bs-popover').options.content = description;
+
+ if ($b.attr('aria-describedby')) {
+ $b.popover('show');
+ }
+ }
+ });
+ }
+ // BOOTBOX
+ else if (options.mode === 'bootbox') {
+ if (!('bootbox' in window)) {
+ Utils.error('MissingLibrary', 'Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com');
+ }
+
+ this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
+ var $b = rule.$el.find('button.filter-description');
+ var description = e.builder.getFilterDescription(rule.filter, rule);
+
+ if (!description) {
+ $b.hide();
+ }
+ else {
+ if ($b.length === 0) {
+ $b = $($.parseHTML(''));
+ $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions));
+
+ $b.on('click', function() {
+ bootbox.alert($b.data('description'));
+ });
+ }
+ else {
+ $b.css('display', '');
+ }
+
+ $b.data('description', description);
+ }
+ });
+ }
+}, {
+ icon: 'bi-info-circle-fill',
+ mode: 'popover'
+});
+
+QueryBuilder.extend(/** @lends module:plugins.FilterDescription.prototype */ {
+ /**
+ * Returns the description of a filter for a particular rule (if present)
+ * @param {object} filter
+ * @param {Rule} [rule]
+ * @returns {string}
+ * @private
+ */
+ getFilterDescription: function(filter, rule) {
+ if (!filter) {
+ return undefined;
+ }
+ else if (typeof filter.description == 'function') {
+ return filter.description.call(this, rule);
+ }
+ else {
+ return filter.description;
+ }
+ }
+});
+
+
+/**
+ * @class Invert
+ * @memberof module:plugins
+ * @description Allows to invert a rule operator, a group condition or the entire builder.
+ * @param {object} [options]
+ * @param {string} [options.icon='bi-shuffle']
+ * @param {boolean} [options.recursive=true]
+ * @param {boolean} [options.invert_rules=true]
+ * @param {boolean} [options.display_rules_button=false]
+ * @param {boolean} [options.silent_fail=false]
+ */
+QueryBuilder.define('invert', function(options) {
+ var self = this;
+ var Selectors = QueryBuilder.selectors;
+
+ // Bind events
+ this.on('afterInit', function() {
+ self.$el.on('click.queryBuilder', '[data-invert=group]', function() {
+ var $group = $(this).closest(Selectors.group_container);
+ self.invert(self.getModel($group), options);
+ });
+
+ if (options.display_rules_button && options.invert_rules) {
+ self.$el.on('click.queryBuilder', '[data-invert=rule]', function() {
+ var $rule = $(this).closest(Selectors.rule_container);
+ self.invert(self.getModel($rule), options);
+ });
+ }
+ });
+
+ // Modify templates
+ if (!options.disable_template) {
+ this.on('getGroupTemplate.filter', function(h) {
+ var $h = $($.parseHTML(h.value));
+ $h.find(Selectors.condition_container).after(
+ ''
+ );
+ h.value = $h.prop('outerHTML');
+ });
+
+ if (options.display_rules_button && options.invert_rules) {
+ this.on('getRuleTemplate.filter', function(h) {
+ var $h = $($.parseHTML(h.value));
+ $h.find(Selectors.rule_actions).prepend(
+ ''
+ );
+ h.value = $h.prop('outerHTML');
+ });
+ }
+ }
+}, {
+ icon: 'bi-shuffle',
+ recursive: true,
+ invert_rules: true,
+ display_rules_button: false,
+ silent_fail: false,
+ disable_template: false
+});
+
+QueryBuilder.defaults({
+ operatorOpposites: {
+ 'equal': 'not_equal',
+ 'not_equal': 'equal',
+ 'in': 'not_in',
+ 'not_in': 'in',
+ 'less': 'greater_or_equal',
+ 'less_or_equal': 'greater',
+ 'greater': 'less_or_equal',
+ 'greater_or_equal': 'less',
+ 'between': 'not_between',
+ 'not_between': 'between',
+ 'begins_with': 'not_begins_with',
+ 'not_begins_with': 'begins_with',
+ 'contains': 'not_contains',
+ 'not_contains': 'contains',
+ 'ends_with': 'not_ends_with',
+ 'not_ends_with': 'ends_with',
+ 'is_empty': 'is_not_empty',
+ 'is_not_empty': 'is_empty',
+ 'is_null': 'is_not_null',
+ 'is_not_null': 'is_null'
+ },
+
+ conditionOpposites: {
+ 'AND': 'OR',
+ 'OR': 'AND'
+ }
+});
+
+QueryBuilder.extend(/** @lends module:plugins.Invert.prototype */ {
+ /**
+ * Invert a Group, a Rule or the whole builder
+ * @param {Node} [node]
+ * @param {object} [options] {@link module:plugins.Invert}
+ * @fires module:plugins.Invert.afterInvert
+ * @throws InvertConditionError, InvertOperatorError
+ */
+ invert: function(node, options) {
+ if (!(node instanceof Node)) {
+ if (!this.model.root) return;
+ options = node;
+ node = this.model.root;
+ }
+
+ if (typeof options != 'object') options = {};
+ if (options.recursive === undefined) options.recursive = true;
+ if (options.invert_rules === undefined) options.invert_rules = true;
+ if (options.silent_fail === undefined) options.silent_fail = false;
+ if (options.trigger === undefined) options.trigger = true;
+
+ if (node instanceof Group) {
+ // invert group condition
+ if (this.settings.conditionOpposites[node.condition]) {
+ node.condition = this.settings.conditionOpposites[node.condition];
+ }
+ else if (!options.silent_fail) {
+ Utils.error('InvertCondition', 'Unknown inverse of condition "{0}"', node.condition);
+ }
+
+ // recursive call
+ if (options.recursive) {
+ var tempOpts = $.extend({}, options, { trigger: false });
+ node.each(function(rule) {
+ if (options.invert_rules) {
+ this.invert(rule, tempOpts);
+ }
+ }, function(group) {
+ this.invert(group, tempOpts);
+ }, this);
+ }
+ }
+ else if (node instanceof Rule) {
+ if (node.operator && !node.filter.no_invert) {
+ // invert rule operator
+ if (this.settings.operatorOpposites[node.operator.type]) {
+ var invert = this.settings.operatorOpposites[node.operator.type];
+ // check if the invert is "authorized"
+ if (!node.filter.operators || node.filter.operators.indexOf(invert) != -1) {
+ node.operator = this.getOperatorByType(invert);
+ }
+ }
+ else if (!options.silent_fail) {
+ Utils.error('InvertOperator', 'Unknown inverse of operator "{0}"', node.operator.type);
+ }
+ }
+ }
+
+ if (options.trigger) {
+ /**
+ * After {@link module:plugins.Invert.invert} method
+ * @event afterInvert
+ * @memberof module:plugins.Invert
+ * @param {Node} node - the main group or rule that has been modified
+ * @param {object} options
+ */
+ this.trigger('afterInvert', node, options);
+
+ this.trigger('rulesChanged');
+ }
+ }
+});
+
+
+/**
+ * @class MongoDbSupport
+ * @memberof module:plugins
+ * @description Allows to export rules as a MongoDB find object as well as populating the builder from a MongoDB object.
+ */
+
+QueryBuilder.defaults({
+ mongoOperators: {
+ // @formatter:off
+ equal: function(v) { return v[0]; },
+ not_equal: function(v) { return { '$ne': v[0] }; },
+ in: function(v) { return { '$in': v }; },
+ not_in: function(v) { return { '$nin': v }; },
+ less: function(v) { return { '$lt': v[0] }; },
+ less_or_equal: function(v) { return { '$lte': v[0] }; },
+ greater: function(v) { return { '$gt': v[0] }; },
+ greater_or_equal: function(v) { return { '$gte': v[0] }; },
+ between: function(v) { return { '$gte': v[0], '$lte': v[1] }; },
+ not_between: function(v) { return { '$lt': v[0], '$gt': v[1] }; },
+ begins_with: function(v) { return { '$regex': '^' + Utils.escapeRegExp(v[0]) }; },
+ not_begins_with: function(v) { return { '$regex': '^(?!' + Utils.escapeRegExp(v[0]) + ')' }; },
+ contains: function(v) { return { '$regex': Utils.escapeRegExp(v[0]) }; },
+ not_contains: function(v) { return { '$regex': '^((?!' + Utils.escapeRegExp(v[0]) + ').)*$', '$options': 's' }; },
+ ends_with: function(v) { return { '$regex': Utils.escapeRegExp(v[0]) + '$' }; },
+ not_ends_with: function(v) { return { '$regex': '(? 0) {
+ parts.push(parse(rule));
+ }
+ else {
+ var mdb = self.settings.mongoOperators[rule.operator];
+ var ope = self.getOperatorByType(rule.operator);
+
+ if (mdb === undefined) {
+ Utils.error('UndefinedMongoOperator', 'Unknown MongoDB operation for operator "{0}"', rule.operator);
+ }
+
+ if (ope.nb_inputs !== 0) {
+ if (!(rule.value instanceof Array)) {
+ rule.value = [rule.value];
+ }
+ }
+
+ /**
+ * Modifies the MongoDB field used by a rule
+ * @event changer:getMongoDBField
+ * @memberof module:plugins.MongoDbSupport
+ * @param {string} field
+ * @param {Rule} rule
+ * @returns {string}
+ */
+ var field = self.change('getMongoDBField', rule.field, rule);
+
+ var ruleExpression = {};
+ ruleExpression[field] = mdb.call(self, rule.value);
+
+ /**
+ * Modifies the MongoDB expression generated for a rul
+ * @event changer:ruleToMongo
+ * @memberof module:plugins.MongoDbSupport
+ * @param {object} expression
+ * @param {Rule} rule
+ * @param {*} value
+ * @param {function} valueWrapper - function that takes the value and adds the operator
+ * @returns {object}
+ */
+ parts.push(self.change('ruleToMongo', ruleExpression, rule, rule.value, mdb));
+ }
+ });
+
+ var groupExpression = {};
+ groupExpression['$' + group.condition.toLowerCase()] = parts;
+
+ /**
+ * Modifies the MongoDB expression generated for a group
+ * @event changer:groupToMongo
+ * @memberof module:plugins.MongoDbSupport
+ * @param {object} expression
+ * @param {Group} group
+ * @returns {object}
+ */
+ return self.change('groupToMongo', groupExpression, group);
+ }(data));
+ },
+
+ /**
+ * Converts a MongoDB query to rules
+ * @param {object} query
+ * @returns {object}
+ * @fires module:plugins.MongoDbSupport.changer:parseMongoNode
+ * @fires module:plugins.MongoDbSupport.changer:getMongoDBFieldID
+ * @fires module:plugins.MongoDbSupport.changer:mongoToRule
+ * @fires module:plugins.MongoDbSupport.changer:mongoToGroup
+ * @throws MongoParseError, UndefinedMongoConditionError, UndefinedMongoOperatorError
+ */
+ getRulesFromMongo: function(query) {
+ if (query === undefined || query === null) {
+ return null;
+ }
+
+ var self = this;
+
+ /**
+ * Custom parsing of a MongoDB expression, you can return a sub-part of the expression, or a well formed group or rule JSON
+ * @event changer:parseMongoNode
+ * @memberof module:plugins.MongoDbSupport
+ * @param {object} expression
+ * @returns {object} expression, rule or group
+ */
+ query = self.change('parseMongoNode', query);
+
+ // a plugin returned a group
+ if ('rules' in query && 'condition' in query) {
+ return query;
+ }
+
+ // a plugin returned a rule
+ if ('id' in query && 'operator' in query && 'value' in query) {
+ return {
+ condition: this.settings.default_condition,
+ rules: [query]
+ };
+ }
+
+ var key = self.getMongoCondition(query);
+ if (!key) {
+ Utils.error('MongoParse', 'Invalid MongoDB query format');
+ }
+
+ return (function parse(data, topKey) {
+ var rules = data[topKey];
+ var parts = [];
+
+ rules.forEach(function(data) {
+ // allow plugins to manually parse or handle special cases
+ data = self.change('parseMongoNode', data);
+
+ // a plugin returned a group
+ if ('rules' in data && 'condition' in data) {
+ parts.push(data);
+ return;
+ }
+
+ // a plugin returned a rule
+ if ('id' in data && 'operator' in data && 'value' in data) {
+ parts.push(data);
+ return;
+ }
+
+ var key = self.getMongoCondition(data);
+ if (key) {
+ parts.push(parse(data, key));
+ }
+ else {
+ var field = Object.keys(data)[0];
+ var value = data[field];
+
+ var operator = self.getMongoOperator(value);
+ if (operator === undefined) {
+ Utils.error('MongoParse', 'Invalid MongoDB query format');
+ }
+
+ var mdbrl = self.settings.mongoRuleOperators[operator];
+ if (mdbrl === undefined) {
+ Utils.error('UndefinedMongoOperator', 'JSON Rule operation unknown for operator "{0}"', operator);
+ }
+
+ var opVal = mdbrl.call(self, value);
+
+ var id = self.getMongoDBFieldID(field, value);
+
+ /**
+ * Modifies the rule generated from the MongoDB expression
+ * @event changer:mongoToRule
+ * @memberof module:plugins.MongoDbSupport
+ * @param {object} rule
+ * @param {object} expression
+ * @returns {object}
+ */
+ var rule = self.change('mongoToRule', {
+ id: id,
+ field: field,
+ operator: opVal.op,
+ value: opVal.val
+ }, data);
+
+ parts.push(rule);
+ }
+ });
+
+ /**
+ * Modifies the group generated from the MongoDB expression
+ * @event changer:mongoToGroup
+ * @memberof module:plugins.MongoDbSupport
+ * @param {object} group
+ * @param {object} expression
+ * @returns {object}
+ */
+ return self.change('mongoToGroup', {
+ condition: topKey.replace('$', '').toUpperCase(),
+ rules: parts
+ }, data);
+ }(query, key));
+ },
+
+ /**
+ * Sets rules a from MongoDB query
+ * @see module:plugins.MongoDbSupport.getRulesFromMongo
+ */
+ setRulesFromMongo: function(query) {
+ this.setRules(this.getRulesFromMongo(query));
+ },
+
+ /**
+ * Returns a filter identifier from the MongoDB field.
+ * Automatically use the only one filter with a matching field, fires a changer otherwise.
+ * @param {string} field
+ * @param {*} value
+ * @fires module:plugins.MongoDbSupport:changer:getMongoDBFieldID
+ * @returns {string}
+ * @private
+ */
+ getMongoDBFieldID: function(field, value) {
+ var matchingFilters = this.filters.filter(function(filter) {
+ return filter.field === field;
+ });
+
+ var id;
+ if (matchingFilters.length === 1) {
+ id = matchingFilters[0].id;
+ }
+ else {
+ /**
+ * Returns a filter identifier from the MongoDB field
+ * @event changer:getMongoDBFieldID
+ * @memberof module:plugins.MongoDbSupport
+ * @param {string} field
+ * @param {*} value
+ * @returns {string}
+ */
+ id = this.change('getMongoDBFieldID', field, value);
+ }
+
+ return id;
+ },
+
+ /**
+ * Finds which operator is used in a MongoDB sub-object
+ * @param {*} data
+ * @returns {string|undefined}
+ * @private
+ */
+ getMongoOperator: function(data) {
+ if (data !== null && typeof data === 'object') {
+ if (data.$gte !== undefined && data.$lte !== undefined) {
+ return 'between';
+ }
+ if (data.$lt !== undefined && data.$gt !== undefined) {
+ return 'not_between';
+ }
+
+ var knownKeys = Object.keys(data).filter(function(key) {
+ return !!this.settings.mongoRuleOperators[key];
+ }.bind(this));
+
+ if (knownKeys.length === 1) {
+ return knownKeys[0];
+ }
+ }
+ else {
+ return '$eq';
+ }
+ },
+
+
+ /**
+ * Returns the key corresponding to "$or" or "$and"
+ * @param {object} data
+ * @returns {string|undefined}
+ * @private
+ */
+ getMongoCondition: function(data) {
+ var keys = Object.keys(data);
+
+ for (var i = 0, l = keys.length; i < l; i++) {
+ if (keys[i].toLowerCase() === '$or' || keys[i].toLowerCase() === '$and') {
+ return keys[i];
+ }
+ }
+ }
+});
+
+
+/**
+ * @class NotGroup
+ * @memberof module:plugins
+ * @description Adds a "Not" checkbox in front of group conditions.
+ * @param {object} [options]
+ * @param {string} [options.icon_checked='bi-check2-square']
+ * @param {string} [options.icon_unchecked='bi-square']
+ */
+QueryBuilder.define('not-group', function(options) {
+ var self = this;
+
+ // Bind events
+ this.on('afterInit', function() {
+ self.$el.on('click.queryBuilder', '[data-not=group]', function() {
+ var $group = $(this).closest(QueryBuilder.selectors.group_container);
+ var group = self.getModel($group);
+ group.not = !group.not;
+ });
+
+ self.model.on('update', function(e, node, field) {
+ if (node instanceof Group && field === 'not') {
+ self.updateGroupNot(node);
+ }
+ });
+ });
+
+ // Init "not" property
+ this.on('afterAddGroup', function(e, group) {
+ group.__.not = false;
+ });
+
+ // Modify templates
+ if (!options.disable_template) {
+ this.on('getGroupTemplate.filter', function(h) {
+ var $h = $($.parseHTML(h.value));
+ $h.find(QueryBuilder.selectors.condition_container).prepend(
+ ''
+ );
+ h.value = $h.prop('outerHTML');
+ });
+ }
+
+ // Export "not" to JSON
+ this.on('groupToJson.filter', function(e, group) {
+ e.value.not = group.not;
+ });
+
+ // Read "not" from JSON
+ this.on('jsonToGroup.filter', function(e, json) {
+ e.value.not = !!json.not;
+ });
+
+ // Export "not" to SQL
+ this.on('groupToSQL.filter', function(e, group) {
+ if (group.not) {
+ e.value = 'NOT ( ' + e.value + ' )';
+ }
+ });
+
+ // Parse "NOT" function from sqlparser
+ this.on('parseSQLNode.filter', function(e) {
+ if (e.value.name && e.value.name.toUpperCase() == 'NOT') {
+ e.value = e.value.arguments.value[0];
+
+ // if the there is no sub-group, create one
+ if (['AND', 'OR'].indexOf(e.value.operation.toUpperCase()) === -1) {
+ e.value = new SQLParser.nodes.Op(
+ self.settings.default_condition,
+ e.value,
+ null
+ );
+ }
+
+ e.value.not = true;
+ }
+ });
+
+ // Request to create sub-group if the "not" flag is set
+ this.on('sqlGroupsDistinct.filter', function(e, group, data, i) {
+ if (data.not && i > 0) {
+ e.value = true;
+ }
+ });
+
+ // Read "not" from parsed SQL
+ this.on('sqlToGroup.filter', function(e, data) {
+ e.value.not = !!data.not;
+ });
+
+ // Export "not" to Mongo
+ this.on('groupToMongo.filter', function(e, group) {
+ var key = '$' + group.condition.toLowerCase();
+ if (group.not && e.value[key]) {
+ e.value = { '$nor': [e.value] };
+ }
+ });
+
+ // Parse "$nor" operator from Mongo
+ this.on('parseMongoNode.filter', function(e) {
+ var keys = Object.keys(e.value);
+
+ if (keys[0] == '$nor') {
+ e.value = e.value[keys[0]][0];
+ e.value.not = true;
+ }
+ });
+
+ // Read "not" from parsed Mongo
+ this.on('mongoToGroup.filter', function(e, data) {
+ e.value.not = !!data.not;
+ });
+}, {
+ icon_unchecked: 'bi-square',
+ icon_checked: 'bi-check2-square',
+ disable_template: false
+});
+
+/**
+ * From {@link module:plugins.NotGroup}
+ * @name not
+ * @member {boolean}
+ * @memberof Group
+ * @instance
+ */
+Utils.defineModelProperties(Group, ['not']);
+
+QueryBuilder.selectors.group_not = QueryBuilder.selectors.group_header + ' [data-not=group]';
+
+QueryBuilder.extend(/** @lends module:plugins.NotGroup.prototype */ {
+ /**
+ * Performs actions when a group's not changes
+ * @param {Group} group
+ * @fires module:plugins.NotGroup.afterUpdateGroupNot
+ * @private
+ */
+ updateGroupNot: function(group) {
+ var options = this.plugins['not-group'];
+ group.$el.find('>' + QueryBuilder.selectors.group_not)
+ .toggleClass('active', group.not)
+ .find('i').attr('class', group.not ? options.icon_checked : options.icon_unchecked);
+
+ /**
+ * After the group's not flag has been modified
+ * @event afterUpdateGroupNot
+ * @memberof module:plugins.NotGroup
+ * @param {Group} group
+ */
+ this.trigger('afterUpdateGroupNot', group);
+
+ this.trigger('rulesChanged');
+ }
+});
+
+
+/**
+ * @class Sortable
+ * @memberof module:plugins
+ * @description Enables drag & drop sort of rules.
+ * @param {object} [options]
+ * @param {boolean} [options.inherit_no_drop=true]
+ * @param {boolean} [options.inherit_no_sortable=true]
+ * @param {string} [options.icon='bi-sort-down']
+ * @throws MissingLibraryError, ConfigError
+ */
+QueryBuilder.define('sortable', function(options) {
+ if (!('interact' in window)) {
+ Utils.error('MissingLibrary', 'interact.js is required to use "sortable" plugin. Get it here: http://interactjs.io');
+ }
+
+ if (options.default_no_sortable !== undefined) {
+ Utils.error(false, 'Config', 'Sortable plugin : "default_no_sortable" options is deprecated, use standard "default_rule_flags" and "default_group_flags" instead');
+ this.settings.default_rule_flags.no_sortable = this.settings.default_group_flags.no_sortable = options.default_no_sortable;
+ }
+
+ // recompute drop-zones during drag (when a rule is hidden)
+ interact.dynamicDrop(true);
+
+ // set move threshold to 10px
+ interact.pointerMoveTolerance(10);
+
+ var placeholder;
+ var ghost;
+ var src;
+ var moved;
+
+ // Init drag and drop
+ this.on('afterAddRule afterAddGroup', function(e, node) {
+ if (node == placeholder) {
+ return;
+ }
+
+ var self = e.builder;
+
+ // Inherit flags
+ if (options.inherit_no_sortable && node.parent && node.parent.flags.no_sortable) {
+ node.flags.no_sortable = true;
+ }
+ if (options.inherit_no_drop && node.parent && node.parent.flags.no_drop) {
+ node.flags.no_drop = true;
+ }
+
+ // Configure drag
+ if (!node.flags.no_sortable) {
+ interact(node.$el[0])
+ .draggable({
+ allowFrom: QueryBuilder.selectors.drag_handle,
+ onstart: function(event) {
+ moved = false;
+
+ // get model of dragged element
+ src = self.getModel(event.target);
+
+ // create ghost
+ ghost = src.$el.clone()
+ .appendTo(src.$el.parent())
+ .width(src.$el.outerWidth())
+ .addClass('dragging');
+
+ // create drop placeholder
+ var ph = $($.parseHTML('
'))
+ .height(src.$el.outerHeight());
+
+ placeholder = src.parent.addRule(ph, src.getPos());
+
+ // hide dragged element
+ src.$el.hide();
+ },
+ onmove: function(event) {
+ // make the ghost follow the cursor
+ ghost[0].style.top = event.clientY - 15 + 'px';
+ ghost[0].style.left = event.clientX - 15 + 'px';
+ },
+ onend: function(event) {
+ // starting from Interact 1.3.3, onend is called before ondrop
+ if (event.dropzone) {
+ moveSortableToTarget(src, $(event.relatedTarget), self);
+ moved = true;
+ }
+
+ // remove ghost
+ ghost.remove();
+ ghost = undefined;
+
+ // remove placeholder
+ placeholder.drop();
+ placeholder = undefined;
+
+ // show element
+ src.$el.css('display', '');
+
+ /**
+ * After a node has been moved with {@link module:plugins.Sortable}
+ * @event afterMove
+ * @memberof module:plugins.Sortable
+ * @param {Node} node
+ */
+ self.trigger('afterMove', src);
+
+ self.trigger('rulesChanged');
+ }
+ });
+ }
+
+ if (!node.flags.no_drop) {
+ // Configure drop on groups and rules
+ interact(node.$el[0])
+ .dropzone({
+ accept: QueryBuilder.selectors.rule_and_group_containers,
+ ondragenter: function(event) {
+ moveSortableToTarget(placeholder, $(event.target), self);
+ },
+ ondrop: function(event) {
+ if (!moved) {
+ moveSortableToTarget(src, $(event.target), self);
+ }
+ }
+ });
+
+ // Configure drop on group headers
+ if (node instanceof Group) {
+ interact(node.$el.find(QueryBuilder.selectors.group_header)[0])
+ .dropzone({
+ accept: QueryBuilder.selectors.rule_and_group_containers,
+ ondragenter: function(event) {
+ moveSortableToTarget(placeholder, $(event.target), self);
+ },
+ ondrop: function(event) {
+ if (!moved) {
+ moveSortableToTarget(src, $(event.target), self);
+ }
+ }
+ });
+ }
+ }
+ });
+
+ // Detach interactables
+ this.on('beforeDeleteRule beforeDeleteGroup', function(e, node) {
+ if (!e.isDefaultPrevented()) {
+ interact(node.$el[0]).unset();
+
+ if (node instanceof Group) {
+ interact(node.$el.find(QueryBuilder.selectors.group_header)[0]).unset();
+ }
+ }
+ });
+
+ // Remove drag handle from non-sortable items
+ this.on('afterApplyRuleFlags afterApplyGroupFlags', function(e, node) {
+ if (node.flags.no_sortable) {
+ node.$el.find('.drag-handle').remove();
+ }
+ });
+
+ // Modify templates
+ if (!options.disable_template) {
+ this.on('getGroupTemplate.filter', function(h, level) {
+ if (level > 1) {
+ var $h = $($.parseHTML(h.value));
+ $h.find(QueryBuilder.selectors.condition_container).after('
');
+ h.value = $h.prop('outerHTML');
+ }
+ });
+
+ this.on('getRuleTemplate.filter', function(h) {
+ var $h = $($.parseHTML(h.value));
+ $h.find(QueryBuilder.selectors.rule_header).after('
');
+ h.value = $h.prop('outerHTML');
+ });
+ }
+}, {
+ inherit_no_sortable: true,
+ inherit_no_drop: true,
+ icon: 'bi-sort-down',
+ disable_template: false
+});
+
+QueryBuilder.selectors.rule_and_group_containers = QueryBuilder.selectors.rule_container + ', ' + QueryBuilder.selectors.group_container;
+QueryBuilder.selectors.drag_handle = '.drag-handle';
+
+QueryBuilder.defaults({
+ default_rule_flags: {
+ no_sortable: false,
+ no_drop: false
+ },
+ default_group_flags: {
+ no_sortable: false,
+ no_drop: false
+ }
+});
+
+/**
+ * Moves an element (placeholder or actual object) depending on active target
+ * @memberof module:plugins.Sortable
+ * @param {Node} node
+ * @param {jQuery} target
+ * @param {QueryBuilder} [builder]
+ * @private
+ */
+function moveSortableToTarget(node, target, builder) {
+ var parent, method;
+ var Selectors = QueryBuilder.selectors;
+
+ // on rule
+ parent = target.closest(Selectors.rule_container);
+ if (parent.length) {
+ method = 'moveAfter';
+ }
+
+ // on group header
+ if (!method) {
+ parent = target.closest(Selectors.group_header);
+ if (parent.length) {
+ parent = target.closest(Selectors.group_container);
+ method = 'moveAtBegin';
+ }
+ }
+
+ // on group
+ if (!method) {
+ parent = target.closest(Selectors.group_container);
+ if (parent.length) {
+ method = 'moveAtEnd';
+ }
+ }
+
+ if (method) {
+ node[method](builder.getModel(parent));
+
+ // refresh radio value
+ if (builder && node instanceof Rule) {
+ builder.setRuleInputValue(node, node.value);
+ }
+ }
+}
+
+
+/**
+ * @class SqlSupport
+ * @memberof module:plugins
+ * @description Allows to export rules as a SQL WHERE statement as well as populating the builder from an SQL query.
+ * @param {object} [options]
+ * @param {boolean} [options.boolean_as_integer=true] - `true` to convert boolean values to integer in the SQL output
+ */
+QueryBuilder.define('sql-support', function(options) {
+
+}, {
+ boolean_as_integer: true
+});
+
+QueryBuilder.defaults({
+ // operators for internal -> SQL conversion
+ sqlOperators: {
+ equal: { op: '= ?' },
+ not_equal: { op: '!= ?' },
+ in: { op: 'IN(?)', sep: ', ' },
+ not_in: { op: 'NOT IN(?)', sep: ', ' },
+ less: { op: '< ?' },
+ less_or_equal: { op: '<= ?' },
+ greater: { op: '> ?' },
+ greater_or_equal: { op: '>= ?' },
+ between: { op: 'BETWEEN ?', sep: ' AND ' },
+ not_between: { op: 'NOT BETWEEN ?', sep: ' AND ' },
+ begins_with: { op: 'LIKE ?', mod: '{0}%', escape: '%_' },
+ not_begins_with: { op: 'NOT LIKE ?', mod: '{0}%', escape: '%_' },
+ contains: { op: 'LIKE ?', mod: '%{0}%', escape: '%_' },
+ not_contains: { op: 'NOT LIKE ?', mod: '%{0}%', escape: '%_' },
+ ends_with: { op: 'LIKE ?', mod: '%{0}', escape: '%_' },
+ not_ends_with: { op: 'NOT LIKE ?', mod: '%{0}', escape: '%_' },
+ is_empty: { op: '= \'\'' },
+ is_not_empty: { op: '!= \'\'' },
+ is_null: { op: 'IS NULL' },
+ is_not_null: { op: 'IS NOT NULL' }
+ },
+
+ // operators for SQL -> internal conversion
+ sqlRuleOperator: {
+ '=': function(v) {
+ return {
+ val: v,
+ op: v === '' ? 'is_empty' : 'equal'
+ };
+ },
+ '!=': function(v) {
+ return {
+ val: v,
+ op: v === '' ? 'is_not_empty' : 'not_equal'
+ };
+ },
+ 'LIKE': function(v) {
+ if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
+ return {
+ val: v.slice(1, -1),
+ op: 'contains'
+ };
+ }
+ else if (v.slice(0, 1) == '%') {
+ return {
+ val: v.slice(1),
+ op: 'ends_with'
+ };
+ }
+ else if (v.slice(-1) == '%') {
+ return {
+ val: v.slice(0, -1),
+ op: 'begins_with'
+ };
+ }
+ else {
+ Utils.error('SQLParse', 'Invalid value for LIKE operator "{0}"', v);
+ }
+ },
+ 'NOT LIKE': function(v) {
+ if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
+ return {
+ val: v.slice(1, -1),
+ op: 'not_contains'
+ };
+ }
+ else if (v.slice(0, 1) == '%') {
+ return {
+ val: v.slice(1),
+ op: 'not_ends_with'
+ };
+ }
+ else if (v.slice(-1) == '%') {
+ return {
+ val: v.slice(0, -1),
+ op: 'not_begins_with'
+ };
+ }
+ else {
+ Utils.error('SQLParse', 'Invalid value for NOT LIKE operator "{0}"', v);
+ }
+ },
+ 'IN': function(v) {
+ return { val: v, op: 'in' };
+ },
+ 'NOT IN': function(v) {
+ return { val: v, op: 'not_in' };
+ },
+ '<': function(v) {
+ return { val: v, op: 'less' };
+ },
+ '<=': function(v) {
+ return { val: v, op: 'less_or_equal' };
+ },
+ '>': function(v) {
+ return { val: v, op: 'greater' };
+ },
+ '>=': function(v) {
+ return { val: v, op: 'greater_or_equal' };
+ },
+ 'BETWEEN': function(v) {
+ return { val: v, op: 'between' };
+ },
+ 'NOT BETWEEN': function(v) {
+ return { val: v, op: 'not_between' };
+ },
+ 'IS': function(v) {
+ if (v !== null) {
+ Utils.error('SQLParse', 'Invalid value for IS operator');
+ }
+ return { val: null, op: 'is_null' };
+ },
+ 'IS NOT': function(v) {
+ if (v !== null) {
+ Utils.error('SQLParse', 'Invalid value for IS operator');
+ }
+ return { val: null, op: 'is_not_null' };
+ }
+ },
+
+ // statements for internal -> SQL conversion
+ sqlStatements: {
+ 'question_mark': function() {
+ var params = [];
+ return {
+ add: function(rule, value) {
+ params.push(value);
+ return '?';
+ },
+ run: function() {
+ return params;
+ }
+ };
+ },
+
+ 'numbered': function(char) {
+ if (!char || char.length > 1) char = '$';
+ var index = 0;
+ var params = [];
+ return {
+ add: function(rule, value) {
+ params.push(value);
+ index++;
+ return char + index;
+ },
+ run: function() {
+ return params;
+ }
+ };
+ },
+
+ 'named': function(char) {
+ if (!char || char.length > 1) char = ':';
+ var indexes = {};
+ var params = {};
+ return {
+ add: function(rule, value) {
+ if (!indexes[rule.field]) indexes[rule.field] = 1;
+ var key = rule.field + '_' + (indexes[rule.field]++);
+ params[key] = value;
+ return char + key;
+ },
+ run: function() {
+ return params;
+ }
+ };
+ }
+ },
+
+ // statements for SQL -> internal conversion
+ sqlRuleStatement: {
+ 'question_mark': function(values) {
+ var index = 0;
+ return {
+ parse: function(v) {
+ return v == '?' ? values[index++] : v;
+ },
+ esc: function(sql) {
+ return sql.replace(/\?/g, '\'?\'');
+ }
+ };
+ },
+
+ 'numbered': function(values, char) {
+ if (!char || char.length > 1) char = '$';
+ var regex1 = new RegExp('^\\' + char + '[0-9]+$');
+ var regex2 = new RegExp('\\' + char + '([0-9]+)', 'g');
+ return {
+ parse: function(v) {
+ return regex1.test(v) ? values[v.slice(1) - 1] : v;
+ },
+ esc: function(sql) {
+ return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\'');
+ }
+ };
+ },
+
+ 'named': function(values, char) {
+ if (!char || char.length > 1) char = ':';
+ var regex1 = new RegExp('^\\' + char);
+ var regex2 = new RegExp('\\' + char + '(' + Object.keys(values).join('|') + ')\\b', 'g');
+ return {
+ parse: function(v) {
+ return regex1.test(v) ? values[v.slice(1)] : v;
+ },
+ esc: function(sql) {
+ return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\'');
+ }
+ };
+ }
+ }
+});
+
+/**
+ * @typedef {object} SqlQuery
+ * @memberof module:plugins.SqlSupport
+ * @property {string} sql
+ * @property {object} params
+ */
+
+QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ {
+ /**
+ * Returns rules as a SQL query
+ * @param {boolean|string} [stmt] - use prepared statements: false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)'
+ * @param {boolean} [nl=false] output with new lines
+ * @param {object} [data] - current rules by default
+ * @returns {module:plugins.SqlSupport.SqlQuery}
+ * @fires module:plugins.SqlSupport.changer:getSQLField
+ * @fires module:plugins.SqlSupport.changer:ruleToSQL
+ * @fires module:plugins.SqlSupport.changer:groupToSQL
+ * @throws UndefinedSQLConditionError, UndefinedSQLOperatorError
+ */
+ getSQL: function(stmt, nl, data) {
+ data = (data === undefined) ? this.getRules() : data;
+
+ if (!data) {
+ return null;
+ }
+
+ nl = !!nl ? '\n' : ' ';
+ var boolean_as_integer = this.getPluginOptions('sql-support', 'boolean_as_integer');
+
+ if (stmt === true) {
+ stmt = 'question_mark';
+ }
+ if (typeof stmt == 'string') {
+ var config = getStmtConfig(stmt);
+ stmt = this.settings.sqlStatements[config[1]](config[2]);
+ }
+
+ var self = this;
+
+ var sql = (function parse(group) {
+ if (!group.condition) {
+ group.condition = self.settings.default_condition;
+ }
+ if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) {
+ Utils.error('UndefinedSQLCondition', 'Unable to build SQL query with condition "{0}"', group.condition);
+ }
+
+ if (!group.rules) {
+ return '';
+ }
+
+ var parts = [];
+
+ group.rules.forEach(function(rule) {
+ if (rule.rules && rule.rules.length > 0) {
+ parts.push('(' + nl + parse(rule) + nl + ')' + nl);
+ }
+ else {
+ var sql = self.settings.sqlOperators[rule.operator];
+ var ope = self.getOperatorByType(rule.operator);
+ var value = '';
+
+ if (sql === undefined) {
+ Utils.error('UndefinedSQLOperator', 'Unknown SQL operation for operator "{0}"', rule.operator);
+ }
+
+ if (ope.nb_inputs !== 0) {
+ if (!(rule.value instanceof Array)) {
+ rule.value = [rule.value];
+ }
+
+ rule.value.forEach(function(v, i) {
+ if (i > 0) {
+ value += sql.sep;
+ }
+
+ if (rule.type == 'boolean' && boolean_as_integer) {
+ v = v ? 1 : 0;
+ }
+ else if (!stmt && rule.type !== 'integer' && rule.type !== 'double' && rule.type !== 'boolean') {
+ v = Utils.escapeString(v, sql.escape);
+ }
+
+ if (sql.mod) {
+ v = Utils.fmt(sql.mod, v);
+ }
+
+ if (stmt) {
+ value += stmt.add(rule, v);
+ }
+ else {
+ if (typeof v == 'string') {
+ v = '\'' + v + '\'';
+ }
+
+ value += v;
+ }
+ });
+ }
+
+ var sqlFn = function(v) {
+ return sql.op.replace('?', function() {
+ return v;
+ });
+ };
+
+ /**
+ * Modifies the SQL field used by a rule
+ * @event changer:getSQLField
+ * @memberof module:plugins.SqlSupport
+ * @param {string} field
+ * @param {Rule} rule
+ * @returns {string}
+ */
+ var field = self.change('getSQLField', rule.field, rule);
+
+ var ruleExpression = field + ' ' + sqlFn(value);
+
+ /**
+ * Modifies the SQL generated for a rule
+ * @event changer:ruleToSQL
+ * @memberof module:plugins.SqlSupport
+ * @param {string} expression
+ * @param {Rule} rule
+ * @param {*} value
+ * @param {function} valueWrapper - function that takes the value and adds the operator
+ * @returns {string}
+ */
+ parts.push(self.change('ruleToSQL', ruleExpression, rule, value, sqlFn));
+ }
+ });
+
+ var groupExpression = parts.join(' ' + group.condition + nl);
+
+ /**
+ * Modifies the SQL generated for a group
+ * @event changer:groupToSQL
+ * @memberof module:plugins.SqlSupport
+ * @param {string} expression
+ * @param {Group} group
+ * @returns {string}
+ */
+ return self.change('groupToSQL', groupExpression, group);
+ }(data));
+
+ if (stmt) {
+ return {
+ sql: sql,
+ params: stmt.run()
+ };
+ }
+ else {
+ return {
+ sql: sql
+ };
+ }
+ },
+
+ /**
+ * Convert a SQL query to rules
+ * @param {string|module:plugins.SqlSupport.SqlQuery} query
+ * @param {boolean|string} stmt
+ * @returns {object}
+ * @fires module:plugins.SqlSupport.changer:parseSQLNode
+ * @fires module:plugins.SqlSupport.changer:getSQLFieldID
+ * @fires module:plugins.SqlSupport.changer:sqlToRule
+ * @fires module:plugins.SqlSupport.changer:sqlToGroup
+ * @throws MissingLibraryError, SQLParseError, UndefinedSQLOperatorError
+ */
+ getRulesFromSQL: function(query, stmt) {
+ if (!('SQLParser' in window)) {
+ Utils.error('MissingLibrary', 'SQLParser is required to parse SQL queries. Get it here https://github.com/mistic100/sql-parser');
+ }
+
+ var self = this;
+
+ if (typeof query == 'string') {
+ query = { sql: query };
+ }
+
+ if (stmt === true) stmt = 'question_mark';
+ if (typeof stmt == 'string') {
+ var config = getStmtConfig(stmt);
+ stmt = this.settings.sqlRuleStatement[config[1]](query.params, config[2]);
+ }
+
+ if (stmt) {
+ query.sql = stmt.esc(query.sql);
+ }
+
+ if (query.sql.toUpperCase().indexOf('SELECT') !== 0) {
+ query.sql = 'SELECT * FROM table WHERE ' + query.sql;
+ }
+
+ var parsed = SQLParser.parse(query.sql);
+
+ if (!parsed.where) {
+ Utils.error('SQLParse', 'No WHERE clause found');
+ }
+
+ /**
+ * Custom parsing of an AST node generated by SQLParser, you can return a sub-part of the tree, or a well formed group or rule JSON
+ * @event changer:parseSQLNode
+ * @memberof module:plugins.SqlSupport
+ * @param {object} AST node
+ * @returns {object} tree, rule or group
+ */
+ var data = self.change('parseSQLNode', parsed.where.conditions);
+
+ // a plugin returned a group
+ if ('rules' in data && 'condition' in data) {
+ return data;
+ }
+
+ // a plugin returned a rule
+ if ('id' in data && 'operator' in data && 'value' in data) {
+ return {
+ condition: this.settings.default_condition,
+ rules: [data]
+ };
+ }
+
+ // create root group
+ var out = self.change('sqlToGroup', {
+ condition: this.settings.default_condition,
+ rules: []
+ }, data);
+
+ // keep track of current group
+ var curr = out;
+
+ (function flatten(data, i) {
+ if (data === null) {
+ return;
+ }
+
+ // allow plugins to manually parse or handle special cases
+ data = self.change('parseSQLNode', data);
+
+ // a plugin returned a group
+ if ('rules' in data && 'condition' in data) {
+ curr.rules.push(data);
+ return;
+ }
+
+ // a plugin returned a rule
+ if ('id' in data && 'operator' in data && 'value' in data) {
+ curr.rules.push(data);
+ return;
+ }
+
+ // data must be a SQL parser node
+ if (!('left' in data) || !('right' in data) || !('operation' in data)) {
+ Utils.error('SQLParse', 'Unable to parse WHERE clause');
+ }
+
+ // it's a node
+ if (['AND', 'OR'].indexOf(data.operation.toUpperCase()) !== -1) {
+ // create a sub-group if the condition is not the same and it's not the first level
+
+ /**
+ * Given an existing group and an AST node, determines if a sub-group must be created
+ * @event changer:sqlGroupsDistinct
+ * @memberof module:plugins.SqlSupport
+ * @param {boolean} create - true by default if the group condition is different
+ * @param {object} group
+ * @param {object} AST
+ * @param {int} current group level
+ * @returns {boolean}
+ */
+ var createGroup = self.change('sqlGroupsDistinct', i > 0 && curr.condition != data.operation.toUpperCase(), curr, data, i);
+
+ if (createGroup) {
+ /**
+ * Modifies the group generated from the SQL expression (this is called before the group is filled with rules)
+ * @event changer:sqlToGroup
+ * @memberof module:plugins.SqlSupport
+ * @param {object} group
+ * @param {object} AST
+ * @returns {object}
+ */
+ var group = self.change('sqlToGroup', {
+ condition: self.settings.default_condition,
+ rules: []
+ }, data);
+
+ curr.rules.push(group);
+ curr = group;
+ }
+
+ curr.condition = data.operation.toUpperCase();
+ i++;
+
+ // some magic !
+ var next = curr;
+ flatten(data.left, i);
+
+ curr = next;
+ flatten(data.right, i);
+ }
+ // it's a leaf
+ else {
+ if ($.isPlainObject(data.right.value)) {
+ Utils.error('SQLParse', 'Value format not supported for {0}.', data.left.value);
+ }
+
+ // convert array
+ var value;
+ if ($.isArray(data.right.value)) {
+ value = data.right.value.map(function(v) {
+ return v.value;
+ });
+ }
+ else {
+ value = data.right.value;
+ }
+
+ // get actual values
+ if (stmt) {
+ if ($.isArray(value)) {
+ value = value.map(stmt.parse);
+ }
+ else {
+ value = stmt.parse(value);
+ }
+ }
+
+ // convert operator
+ var operator = data.operation.toUpperCase();
+ if (operator == '<>') {
+ operator = '!=';
+ }
+
+ var sqlrl = self.settings.sqlRuleOperator[operator];
+ if (sqlrl === undefined) {
+ Utils.error('UndefinedSQLOperator', 'Invalid SQL operation "{0}".', data.operation);
+ }
+
+ var opVal = sqlrl.call(this, value, data.operation);
+
+ // find field name
+ var field;
+ if ('values' in data.left) {
+ field = data.left.values.join('.');
+ }
+ else if ('value' in data.left) {
+ field = data.left.value;
+ }
+ else {
+ Utils.error('SQLParse', 'Cannot find field name in {0}', JSON.stringify(data.left));
+ }
+
+ // unescape chars declared by the operator
+ var finalValue = opVal.val;
+ var sql = self.settings.sqlOperators[opVal.op];
+ if (!stmt && sql && sql.escape) {
+ var searchChars = sql.escape.split('').map(function(c) {
+ return '\\\\' + c;
+ }).join('|');
+ finalValue = finalValue
+ .replace(new RegExp('(' + searchChars + ')', 'g'), function(s) {
+ return s[1];
+ });
+ }
+
+ var id = self.getSQLFieldID(field, value);
+
+ /**
+ * Modifies the rule generated from the SQL expression
+ * @event changer:sqlToRule
+ * @memberof module:plugins.SqlSupport
+ * @param {object} rule
+ * @param {object} AST
+ * @returns {object}
+ */
+ var rule = self.change('sqlToRule', {
+ id: id,
+ field: field,
+ operator: opVal.op,
+ value: finalValue
+ }, data);
+
+ curr.rules.push(rule);
+ }
+ }(data, 0));
+
+ return out;
+ },
+
+ /**
+ * Sets the builder's rules from a SQL query
+ * @see module:plugins.SqlSupport.getRulesFromSQL
+ */
+ setRulesFromSQL: function(query, stmt) {
+ this.setRules(this.getRulesFromSQL(query, stmt));
+ },
+
+ /**
+ * Returns a filter identifier from the SQL field.
+ * Automatically use the only one filter with a matching field, fires a changer otherwise.
+ * @param {string} field
+ * @param {*} value
+ * @fires module:plugins.SqlSupport:changer:getSQLFieldID
+ * @returns {string}
+ * @private
+ */
+ getSQLFieldID: function(field, value) {
+ var matchingFilters = this.filters.filter(function(filter) {
+ return filter.field.toLowerCase() === field.toLowerCase();
+ });
+
+ var id;
+ if (matchingFilters.length === 1) {
+ id = matchingFilters[0].id;
+ }
+ else {
+ /**
+ * Returns a filter identifier from the SQL field
+ * @event changer:getSQLFieldID
+ * @memberof module:plugins.SqlSupport
+ * @param {string} field
+ * @param {*} value
+ * @returns {string}
+ */
+ id = this.change('getSQLFieldID', field, value);
+ }
+
+ return id;
+ }
+});
+
+/**
+ * Parses the statement configuration
+ * @memberof module:plugins.SqlSupport
+ * @param {string} stmt
+ * @returns {Array} null, mode, option
+ * @private
+ */
+function getStmtConfig(stmt) {
+ var config = stmt.match(/(question_mark|numbered|named)(?:\((.)\))?/);
+ if (!config) config = [null, 'question_mark', undefined];
+ return config;
+}
+
+
+/**
+ * @class UniqueFilter
+ * @memberof module:plugins
+ * @description Allows to define some filters as "unique": ie which can be used for only one rule, globally or in the same group.
+ */
+QueryBuilder.define('unique-filter', function() {
+ this.status.used_filters = {};
+
+ this.on('afterUpdateRuleFilter', this.updateDisabledFilters);
+ this.on('afterDeleteRule', this.updateDisabledFilters);
+ this.on('afterCreateRuleFilters', this.applyDisabledFilters);
+ this.on('afterReset', this.clearDisabledFilters);
+ this.on('afterClear', this.clearDisabledFilters);
+
+ // Ensure that the default filter is not already used if unique
+ this.on('getDefaultFilter.filter', function(e, model) {
+ var self = e.builder;
+
+ self.updateDisabledFilters();
+
+ if (e.value.id in self.status.used_filters) {
+ var found = self.filters.some(function(filter) {
+ if (!(filter.id in self.status.used_filters) || self.status.used_filters[filter.id].length > 0 && self.status.used_filters[filter.id].indexOf(model.parent) === -1) {
+ e.value = filter;
+ return true;
+ }
+ });
+
+ if (!found) {
+ Utils.error(false, 'UniqueFilter', 'No more non-unique filters available');
+ e.value = undefined;
+ }
+ }
+ });
+});
+
+QueryBuilder.extend(/** @lends module:plugins.UniqueFilter.prototype */ {
+ /**
+ * Updates the list of used filters
+ * @param {$.Event} [e]
+ * @private
+ */
+ updateDisabledFilters: function(e) {
+ var self = e ? e.builder : this;
+
+ self.status.used_filters = {};
+
+ if (!self.model) {
+ return;
+ }
+
+ // get used filters
+ (function walk(group) {
+ group.each(function(rule) {
+ if (rule.filter && rule.filter.unique) {
+ if (!self.status.used_filters[rule.filter.id]) {
+ self.status.used_filters[rule.filter.id] = [];
+ }
+ if (rule.filter.unique == 'group') {
+ self.status.used_filters[rule.filter.id].push(rule.parent);
+ }
+ }
+ }, function(group) {
+ walk(group);
+ });
+ }(self.model.root));
+
+ self.applyDisabledFilters(e);
+ },
+
+ /**
+ * Clear the list of used filters
+ * @param {$.Event} [e]
+ * @private
+ */
+ clearDisabledFilters: function(e) {
+ var self = e ? e.builder : this;
+
+ self.status.used_filters = {};
+
+ self.applyDisabledFilters(e);
+ },
+
+ /**
+ * Disabled filters depending on the list of used ones
+ * @param {$.Event} [e]
+ * @private
+ */
+ applyDisabledFilters: function(e) {
+ var self = e ? e.builder : this;
+
+ // re-enable everything
+ self.$el.find(QueryBuilder.selectors.filter_container + ' option').prop('disabled', false);
+
+ // disable some
+ $.each(self.status.used_filters, function(filterId, groups) {
+ if (groups.length === 0) {
+ self.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true);
+ }
+ else {
+ groups.forEach(function(group) {
+ group.each(function(rule) {
+ rule.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true);
+ });
+ });
+ }
+ });
+
+ // update Selectpicker
+ if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) {
+ self.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render');
+ }
+ }
+});
+
+
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: English (en)
+ * Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+QueryBuilder.regional['en'] = {
+ "__locale": "English (en)",
+ "__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr",
+ "add_rule": "Add rule",
+ "add_group": "Add group",
+ "delete_rule": "Delete",
+ "delete_group": "Delete",
+ "conditions": {
+ "AND": "AND",
+ "OR": "OR"
+ },
+ "operators": {
+ "equal": "equal",
+ "not_equal": "not equal",
+ "in": "in",
+ "not_in": "not in",
+ "less": "less",
+ "less_or_equal": "less or equal",
+ "greater": "greater",
+ "greater_or_equal": "greater or equal",
+ "between": "between",
+ "not_between": "not between",
+ "begins_with": "begins with",
+ "not_begins_with": "doesn't begin with",
+ "contains": "contains",
+ "not_contains": "doesn't contain",
+ "ends_with": "ends with",
+ "not_ends_with": "doesn't end with",
+ "is_empty": "is empty",
+ "is_not_empty": "is not empty",
+ "is_null": "is null",
+ "is_not_null": "is not null"
+ },
+ "errors": {
+ "no_filter": "No filter selected",
+ "empty_group": "The group is empty",
+ "radio_empty": "No value selected",
+ "checkbox_empty": "No value selected",
+ "select_empty": "No value selected",
+ "string_empty": "Empty value",
+ "string_exceed_min_length": "Must contain at least {0} characters",
+ "string_exceed_max_length": "Must not contain more than {0} characters",
+ "string_invalid_format": "Invalid format ({0})",
+ "number_nan": "Not a number",
+ "number_not_integer": "Not an integer",
+ "number_not_double": "Not a real number",
+ "number_exceed_min": "Must be greater than {0}",
+ "number_exceed_max": "Must be lower than {0}",
+ "number_wrong_step": "Must be a multiple of {0}",
+ "number_between_invalid": "Invalid values, {0} is greater than {1}",
+ "datetime_empty": "Empty value",
+ "datetime_invalid": "Invalid date format ({0})",
+ "datetime_exceed_min": "Must be after {0}",
+ "datetime_exceed_max": "Must be before {0}",
+ "datetime_between_invalid": "Invalid values, {0} is greater than {1}",
+ "boolean_not_valid": "Not a boolean",
+ "operator_not_multiple": "Operator \"{1}\" cannot accept multiple values"
+ },
+ "invert": "Invert",
+ "NOT": "NOT"
+};
+
+QueryBuilder.defaults({ lang_code: 'en' });
+return QueryBuilder;
+
+}));
diff --git a/dist/js/query-builder.standalone.js b/dist/js/query-builder.standalone.js
new file mode 100644
index 00000000..f56d166b
--- /dev/null
+++ b/dist/js/query-builder.standalone.js
@@ -0,0 +1,6308 @@
+/*!
+ * jQuery.extendext 1.0.0
+ *
+ * Copyright 2014-2019 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
+ * Licensed under MIT (http://opensource.org/licenses/MIT)
+ *
+ * Based on jQuery.extend by jQuery Foundation, Inc. and other contributors
+ */
+
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ define('jquery-extendext', ['jquery'], factory);
+ }
+ else if (typeof module === 'object' && module.exports) {
+ module.exports = factory(require('jquery'));
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function ($) {
+ "use strict";
+
+ $.extendext = function () {
+ var options, name, src, copy, copyIsArray, clone,
+ target = arguments[0] || {},
+ i = 1,
+ length = arguments.length,
+ deep = false,
+ arrayMode = 'default';
+
+ // Handle a deep copy situation
+ if (typeof target === "boolean") {
+ deep = target;
+
+ // Skip the boolean and the target
+ target = arguments[i++] || {};
+ }
+
+ // Handle array mode parameter
+ if (typeof target === "string") {
+ arrayMode = target.toLowerCase();
+ if (arrayMode !== 'concat' && arrayMode !== 'replace' && arrayMode !== 'extend') {
+ arrayMode = 'default';
+ }
+
+ // Skip the string param
+ target = arguments[i++] || {};
+ }
+
+ // Handle case when target is a string or something (possible in deep copy)
+ if (typeof target !== "object" && !$.isFunction(target)) {
+ target = {};
+ }
+
+ // Extend jQuery itself if only one argument is passed
+ if (i === length) {
+ target = this;
+ i--;
+ }
+
+ for (; i < length; i++) {
+ // Only deal with non-null/undefined values
+ if ((options = arguments[i]) !== null) {
+ // Special operations for arrays
+ if ($.isArray(options) && arrayMode !== 'default') {
+ clone = target && $.isArray(target) ? target : [];
+
+ switch (arrayMode) {
+ case 'concat':
+ target = clone.concat($.extend(deep, [], options));
+ break;
+
+ case 'replace':
+ target = $.extend(deep, [], options);
+ break;
+
+ case 'extend':
+ options.forEach(function (e, i) {
+ if (typeof e === 'object') {
+ var type = $.isArray(e) ? [] : {};
+ clone[i] = $.extendext(deep, arrayMode, clone[i] || type, e);
+
+ } else if (clone.indexOf(e) === -1) {
+ clone.push(e);
+ }
+ });
+
+ target = clone;
+ break;
+ }
+
+ } else {
+ // Extend the base object
+ for (name in options) {
+ copy = options[name];
+
+ // Prevent never-ending loop
+ if (name === '__proto__' || target === copy) {
+ continue;
+ }
+
+ // Recurse if we're merging plain objects or arrays
+ if (deep && copy && ( $.isPlainObject(copy) ||
+ (copyIsArray = $.isArray(copy)) )) {
+ src = target[name];
+
+ // Ensure proper type for the source value
+ if ( copyIsArray && !Array.isArray( src ) ) {
+ clone = [];
+ } else if ( !copyIsArray && !$.isPlainObject( src ) ) {
+ clone = {};
+ } else {
+ clone = src;
+ }
+ copyIsArray = false;
+
+ // Never move original objects, clone them
+ target[name] = $.extendext(deep, arrayMode, clone, copy);
+
+ // Don't bring in undefined values
+ } else if (copy !== undefined) {
+ target[name] = copy;
+ }
+ }
+ }
+ }
+ }
+
+ // Return the modified object
+ return target;
+ };
+}));
+
+
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Copyright 2014-2024 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+(function(root, factory) {
+ if (typeof define == 'function' && define.amd) {
+ define('query-builder', ['jquery', 'jquery-extendext'], factory);
+ }
+ else if (typeof module === 'object' && module.exports) {
+ module.exports = factory(require('jquery'), require('jquery-extendext'));
+ }
+ else {
+ factory(root.jQuery);
+ }
+}(this, function($) {
+"use strict";
+
+/**
+ * @typedef {object} Filter
+ * @memberof QueryBuilder
+ * @description See {@link http://querybuilder.js.org/index.html#filters}
+ */
+
+/**
+ * @typedef {object} Operator
+ * @memberof QueryBuilder
+ * @description See {@link http://querybuilder.js.org/index.html#operators}
+ */
+
+/**
+ * @param {jQuery} $el
+ * @param {object} options - see {@link http://querybuilder.js.org/#options}
+ * @constructor
+ */
+var QueryBuilder = function($el, options) {
+ $el[0].queryBuilder = this;
+
+ /**
+ * Element container
+ * @member {jQuery}
+ * @readonly
+ */
+ this.$el = $el;
+
+ /**
+ * Configuration object
+ * @member {object}
+ * @readonly
+ */
+ this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options);
+
+ /**
+ * Internal model
+ * @member {Model}
+ * @readonly
+ */
+ this.model = new Model();
+
+ /**
+ * Internal status
+ * @member {object}
+ * @property {string} id - id of the container
+ * @property {boolean} generated_id - if the container id has been generated
+ * @property {int} group_id - current group id
+ * @property {int} rule_id - current rule id
+ * @property {boolean} has_optgroup - if filters have optgroups
+ * @property {boolean} has_operator_optgroup - if operators have optgroups
+ * @readonly
+ * @private
+ */
+ this.status = {
+ id: null,
+ generated_id: false,
+ group_id: 0,
+ rule_id: 0,
+ has_optgroup: false,
+ has_operator_optgroup: false
+ };
+
+ /**
+ * List of filters
+ * @member {QueryBuilder.Filter[]}
+ * @readonly
+ */
+ this.filters = this.settings.filters;
+
+ /**
+ * List of icons
+ * @member {object.}
+ * @readonly
+ */
+ this.icons = this.settings.icons;
+
+ /**
+ * List of operators
+ * @member {QueryBuilder.Operator[]}
+ * @readonly
+ */
+ this.operators = this.settings.operators;
+
+ /**
+ * List of templates
+ * @member {object.}
+ * @readonly
+ */
+ this.templates = this.settings.templates;
+
+ /**
+ * Plugins configuration
+ * @member {object.}
+ * @readonly
+ */
+ this.plugins = this.settings.plugins;
+
+ /**
+ * Translations object
+ * @member {object}
+ * @readonly
+ */
+ this.lang = null;
+
+ // translations : english << 'lang_code' << custom
+ if (QueryBuilder.regional['en'] === undefined) {
+ Utils.error('Config', '"i18n/en.js" not loaded.');
+ }
+ this.lang = $.extendext(true, 'replace', {}, QueryBuilder.regional['en'], QueryBuilder.regional[this.settings.lang_code], this.settings.lang);
+
+ // "allow_groups" can be boolean or int
+ if (this.settings.allow_groups === false) {
+ this.settings.allow_groups = 0;
+ }
+ else if (this.settings.allow_groups === true) {
+ this.settings.allow_groups = -1;
+ }
+
+ // init templates
+ Object.keys(this.templates).forEach(function(tpl) {
+ if (!this.templates[tpl]) {
+ this.templates[tpl] = QueryBuilder.templates[tpl];
+ }
+ if (typeof this.templates[tpl] !== 'function') {
+ throw new Error(`Template ${tpl} must be a function`);
+ }
+ }, this);
+
+ // ensure we have a container id
+ if (!this.$el.attr('id')) {
+ this.$el.attr('id', 'qb_' + Math.floor(Math.random() * 99999));
+ this.status.generated_id = true;
+ }
+ this.status.id = this.$el.attr('id');
+
+ // INIT
+ this.$el.addClass('query-builder');
+
+ this.filters = this.checkFilters(this.filters);
+ this.operators = this.checkOperators(this.operators);
+ this.bindEvents();
+ this.initPlugins();
+};
+
+$.extend(QueryBuilder.prototype, /** @lends QueryBuilder.prototype */ {
+ /**
+ * Triggers an event on the builder container
+ * @param {string} type
+ * @returns {$.Event}
+ */
+ trigger: function(type) {
+ var event = new $.Event(this._tojQueryEvent(type), {
+ builder: this
+ });
+
+ this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 1));
+
+ return event;
+ },
+
+ /**
+ * Triggers an event on the builder container and returns the modified value
+ * @param {string} type
+ * @param {*} value
+ * @returns {*}
+ */
+ change: function(type, value) {
+ var event = new $.Event(this._tojQueryEvent(type, true), {
+ builder: this,
+ value: value
+ });
+
+ this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 2));
+
+ return event.value;
+ },
+
+ /**
+ * Attaches an event listener on the builder container
+ * @param {string} type
+ * @param {function} cb
+ * @returns {QueryBuilder}
+ */
+ on: function(type, cb) {
+ this.$el.on(this._tojQueryEvent(type), cb);
+ return this;
+ },
+
+ /**
+ * Removes an event listener from the builder container
+ * @param {string} type
+ * @param {function} [cb]
+ * @returns {QueryBuilder}
+ */
+ off: function(type, cb) {
+ this.$el.off(this._tojQueryEvent(type), cb);
+ return this;
+ },
+
+ /**
+ * Attaches an event listener called once on the builder container
+ * @param {string} type
+ * @param {function} cb
+ * @returns {QueryBuilder}
+ */
+ once: function(type, cb) {
+ this.$el.one(this._tojQueryEvent(type), cb);
+ return this;
+ },
+
+ /**
+ * Appends `.queryBuilder` and optionally `.filter` to the events names
+ * @param {string} name
+ * @param {boolean} [filter=false]
+ * @returns {string}
+ * @private
+ */
+ _tojQueryEvent: function(name, filter) {
+ return name.split(' ').map(function(type) {
+ return type + '.queryBuilder' + (filter ? '.filter' : '');
+ }).join(' ');
+ }
+});
+
+
+/**
+ * Allowed types and their internal representation
+ * @type {object.}
+ * @readonly
+ * @private
+ */
+QueryBuilder.types = {
+ 'string': 'string',
+ 'integer': 'number',
+ 'double': 'number',
+ 'date': 'datetime',
+ 'time': 'datetime',
+ 'datetime': 'datetime',
+ 'boolean': 'boolean'
+};
+
+/**
+ * Allowed inputs
+ * @type {string[]}
+ * @readonly
+ * @private
+ */
+QueryBuilder.inputs = [
+ 'text',
+ 'number',
+ 'textarea',
+ 'radio',
+ 'checkbox',
+ 'select'
+];
+
+/**
+ * Runtime modifiable options with `setOptions` method
+ * @type {string[]}
+ * @readonly
+ * @private
+ */
+QueryBuilder.modifiable_options = [
+ 'display_errors',
+ 'allow_groups',
+ 'allow_empty',
+ 'default_condition',
+ 'default_filter'
+];
+
+/**
+ * CSS selectors for common components
+ * @type {object.}
+ * @readonly
+ */
+QueryBuilder.selectors = {
+ group_container: '.rules-group-container',
+ rule_container: '.rule-container',
+ filter_container: '.rule-filter-container',
+ operator_container: '.rule-operator-container',
+ value_container: '.rule-value-container',
+ error_container: '.error-container',
+ condition_container: '.rules-group-header .group-conditions',
+
+ rule_header: '.rule-header',
+ group_header: '.rules-group-header',
+ group_actions: '.group-actions',
+ rule_actions: '.rule-actions',
+
+ rules_list: '.rules-group-body>.rules-list',
+
+ group_condition: '.rules-group-header [name$=_cond]',
+ rule_filter: '.rule-filter-container [name$=_filter]',
+ rule_operator: '.rule-operator-container [name$=_operator]',
+ rule_value: '.rule-value-container [name*=_value_]',
+
+ add_rule: '[data-add=rule]',
+ delete_rule: '[data-delete=rule]',
+ add_group: '[data-add=group]',
+ delete_group: '[data-delete=group]'
+};
+
+/**
+ * Template strings (see template.js)
+ * @type {object.}
+ * @readonly
+ */
+QueryBuilder.templates = {};
+
+/**
+ * Localized strings (see i18n/)
+ * @type {object.}
+ * @readonly
+ */
+QueryBuilder.regional = {};
+
+/**
+ * Default operators
+ * @type {object.}
+ * @readonly
+ */
+QueryBuilder.OPERATORS = {
+ equal: { type: 'equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] },
+ not_equal: { type: 'not_equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] },
+ in: { type: 'in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime'] },
+ not_in: { type: 'not_in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime'] },
+ less: { type: 'less', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
+ less_or_equal: { type: 'less_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
+ greater: { type: 'greater', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
+ greater_or_equal: { type: 'greater_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
+ between: { type: 'between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] },
+ not_between: { type: 'not_between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] },
+ begins_with: { type: 'begins_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
+ not_begins_with: { type: 'not_begins_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
+ contains: { type: 'contains', nb_inputs: 1, multiple: false, apply_to: ['string'] },
+ not_contains: { type: 'not_contains', nb_inputs: 1, multiple: false, apply_to: ['string'] },
+ ends_with: { type: 'ends_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
+ not_ends_with: { type: 'not_ends_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
+ is_empty: { type: 'is_empty', nb_inputs: 0, multiple: false, apply_to: ['string'] },
+ is_not_empty: { type: 'is_not_empty', nb_inputs: 0, multiple: false, apply_to: ['string'] },
+ is_null: { type: 'is_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] },
+ is_not_null: { type: 'is_not_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] }
+};
+
+/**
+ * Default configuration
+ * @type {object}
+ * @readonly
+ */
+QueryBuilder.DEFAULTS = {
+ filters: [],
+ plugins: [],
+
+ sort_filters: false,
+ display_errors: true,
+ allow_groups: -1,
+ allow_empty: false,
+ conditions: ['AND', 'OR'],
+ default_condition: 'AND',
+ inputs_separator: ' , ',
+ select_placeholder: '------',
+ display_empty_filter: true,
+ default_filter: null,
+ optgroups: {},
+
+ default_rule_flags: {
+ filter_readonly: false,
+ operator_readonly: false,
+ value_readonly: false,
+ no_delete: false
+ },
+
+ default_group_flags: {
+ condition_readonly: false,
+ no_add_rule: false,
+ no_add_group: false,
+ no_delete: false
+ },
+
+ templates: {
+ group: null,
+ rule: null,
+ filterSelect: null,
+ operatorSelect: null,
+ ruleValueSelect: null
+ },
+
+ lang_code: 'en',
+ lang: {},
+
+ operators: [
+ 'equal',
+ 'not_equal',
+ 'in',
+ 'not_in',
+ 'less',
+ 'less_or_equal',
+ 'greater',
+ 'greater_or_equal',
+ 'between',
+ 'not_between',
+ 'begins_with',
+ 'not_begins_with',
+ 'contains',
+ 'not_contains',
+ 'ends_with',
+ 'not_ends_with',
+ 'is_empty',
+ 'is_not_empty',
+ 'is_null',
+ 'is_not_null'
+ ],
+
+ icons: {
+ add_group: 'bi-plus-circle-fill',
+ add_rule: 'bi-plus-lg',
+ remove_group: 'bi-x-lg',
+ remove_rule: 'bi-x-lg',
+ error: 'bi-exclamation-triangle'
+ }
+};
+
+
+/**
+ * @module plugins
+ */
+
+/**
+ * Definition of available plugins
+ * @type {object.}
+ */
+QueryBuilder.plugins = {};
+
+/**
+ * Gets or extends the default configuration
+ * @param {object} [options] - new configuration
+ * @returns {undefined|object} nothing or configuration object (copy)
+ */
+QueryBuilder.defaults = function(options) {
+ if (typeof options == 'object') {
+ $.extendext(true, 'replace', QueryBuilder.DEFAULTS, options);
+ }
+ else if (typeof options == 'string') {
+ if (typeof QueryBuilder.DEFAULTS[options] == 'object') {
+ return $.extend(true, {}, QueryBuilder.DEFAULTS[options]);
+ }
+ else {
+ return QueryBuilder.DEFAULTS[options];
+ }
+ }
+ else {
+ return $.extend(true, {}, QueryBuilder.DEFAULTS);
+ }
+};
+
+/**
+ * Registers a new plugin
+ * @param {string} name
+ * @param {function} fct - init function
+ * @param {object} [def] - default options
+ */
+QueryBuilder.define = function(name, fct, def) {
+ QueryBuilder.plugins[name] = {
+ fct: fct,
+ def: def || {}
+ };
+};
+
+/**
+ * Adds new methods to QueryBuilder prototype
+ * @param {object.} methods
+ */
+QueryBuilder.extend = function(methods) {
+ $.extend(QueryBuilder.prototype, methods);
+};
+
+/**
+ * Initializes plugins for an instance
+ * @throws ConfigError
+ * @private
+ */
+QueryBuilder.prototype.initPlugins = function() {
+ if (!this.plugins) {
+ return;
+ }
+
+ if ($.isArray(this.plugins)) {
+ var tmp = {};
+ this.plugins.forEach(function(plugin) {
+ tmp[plugin] = null;
+ });
+ this.plugins = tmp;
+ }
+
+ Object.keys(this.plugins).forEach(function(plugin) {
+ if (plugin in QueryBuilder.plugins) {
+ this.plugins[plugin] = $.extend(true, {},
+ QueryBuilder.plugins[plugin].def,
+ this.plugins[plugin] || {}
+ );
+
+ QueryBuilder.plugins[plugin].fct.call(this, this.plugins[plugin]);
+ }
+ else {
+ Utils.error('Config', 'Unable to find plugin "{0}"', plugin);
+ }
+ }, this);
+};
+
+/**
+ * Returns the config of a plugin, if the plugin is not loaded, returns the default config.
+ * @param {string} name
+ * @param {string} [property]
+ * @throws ConfigError
+ * @returns {*}
+ */
+QueryBuilder.prototype.getPluginOptions = function(name, property) {
+ var plugin;
+ if (this.plugins && this.plugins[name]) {
+ plugin = this.plugins[name];
+ }
+ else if (QueryBuilder.plugins[name]) {
+ plugin = QueryBuilder.plugins[name].def;
+ }
+
+ if (plugin) {
+ if (property) {
+ return plugin[property];
+ }
+ else {
+ return plugin;
+ }
+ }
+ else {
+ Utils.error('Config', 'Unable to find plugin "{0}"', name);
+ }
+};
+
+
+/**
+ * Final initialisation of the builder
+ * @param {object} [rules]
+ * @fires QueryBuilder.afterInit
+ * @private
+ */
+QueryBuilder.prototype.init = function(rules) {
+ /**
+ * When the initilization is done, just before creating the root group
+ * @event afterInit
+ * @memberof QueryBuilder
+ */
+ this.trigger('afterInit');
+
+ if (rules) {
+ this.setRules(rules);
+ delete this.settings.rules;
+ }
+ else {
+ this.setRoot(true);
+ }
+};
+
+/**
+ * Checks the configuration of each filter
+ * @param {QueryBuilder.Filter[]} filters
+ * @returns {QueryBuilder.Filter[]}
+ * @throws ConfigError
+ */
+QueryBuilder.prototype.checkFilters = function(filters) {
+ var definedFilters = [];
+
+ if (!filters || filters.length === 0) {
+ Utils.error('Config', 'Missing filters list');
+ }
+
+ filters.forEach(function(filter, i) {
+ if (!filter.id) {
+ Utils.error('Config', 'Missing filter {0} id', i);
+ }
+ if (definedFilters.indexOf(filter.id) != -1) {
+ Utils.error('Config', 'Filter "{0}" already defined', filter.id);
+ }
+ definedFilters.push(filter.id);
+
+ if (!filter.type) {
+ filter.type = 'string';
+ }
+ else if (!QueryBuilder.types[filter.type]) {
+ Utils.error('Config', 'Invalid type "{0}"', filter.type);
+ }
+
+ if (!filter.input) {
+ filter.input = QueryBuilder.types[filter.type] === 'number' ? 'number' : 'text';
+ }
+ else if (typeof filter.input != 'function' && QueryBuilder.inputs.indexOf(filter.input) == -1) {
+ Utils.error('Config', 'Invalid input "{0}"', filter.input);
+ }
+
+ if (filter.operators) {
+ filter.operators.forEach(function(operator) {
+ if (typeof operator != 'string') {
+ Utils.error('Config', 'Filter operators must be global operators types (string)');
+ }
+ });
+ }
+
+ if (!filter.field) {
+ filter.field = filter.id;
+ }
+ if (!filter.label) {
+ filter.label = filter.field;
+ }
+
+ if (!filter.optgroup) {
+ filter.optgroup = null;
+ }
+ else {
+ this.status.has_optgroup = true;
+
+ // register optgroup if needed
+ if (!this.settings.optgroups[filter.optgroup]) {
+ this.settings.optgroups[filter.optgroup] = filter.optgroup;
+ }
+ }
+
+ switch (filter.input) {
+ case 'radio':
+ case 'checkbox':
+ if (!filter.values || filter.values.length < 1) {
+ Utils.error('Config', 'Missing filter "{0}" values', filter.id);
+ }
+ break;
+
+ case 'select':
+ var cleanValues = [];
+ filter.has_optgroup = false;
+
+ Utils.iterateOptions(filter.values, function(value, label, optgroup) {
+ cleanValues.push({
+ value: value,
+ label: label,
+ optgroup: optgroup || null
+ });
+
+ if (optgroup) {
+ filter.has_optgroup = true;
+
+ // register optgroup if needed
+ if (!this.settings.optgroups[optgroup]) {
+ this.settings.optgroups[optgroup] = optgroup;
+ }
+ }
+ }.bind(this));
+
+ if (filter.has_optgroup) {
+ filter.values = Utils.groupSort(cleanValues, 'optgroup');
+ }
+ else {
+ filter.values = cleanValues;
+ }
+
+ if (filter.placeholder) {
+ if (filter.placeholder_value === undefined) {
+ filter.placeholder_value = -1;
+ }
+
+ filter.values.forEach(function(entry) {
+ if (entry.value == filter.placeholder_value) {
+ Utils.error('Config', 'Placeholder of filter "{0}" overlaps with one of its values', filter.id);
+ }
+ });
+ }
+ break;
+ }
+ }, this);
+
+ if (this.settings.sort_filters) {
+ if (typeof this.settings.sort_filters == 'function') {
+ filters.sort(this.settings.sort_filters);
+ }
+ else {
+ var self = this;
+ filters.sort(function(a, b) {
+ return self.translate(a.label).localeCompare(self.translate(b.label));
+ });
+ }
+ }
+
+ if (this.status.has_optgroup) {
+ filters = Utils.groupSort(filters, 'optgroup');
+ }
+
+ return filters;
+};
+
+/**
+ * Checks the configuration of each operator
+ * @param {QueryBuilder.Operator[]} operators
+ * @returns {QueryBuilder.Operator[]}
+ * @throws ConfigError
+ */
+QueryBuilder.prototype.checkOperators = function(operators) {
+ var definedOperators = [];
+
+ operators.forEach(function(operator, i) {
+ if (typeof operator == 'string') {
+ if (!QueryBuilder.OPERATORS[operator]) {
+ Utils.error('Config', 'Unknown operator "{0}"', operator);
+ }
+
+ operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator]);
+ }
+ else {
+ if (!operator.type) {
+ Utils.error('Config', 'Missing "type" for operator {0}', i);
+ }
+
+ if (QueryBuilder.OPERATORS[operator.type]) {
+ operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator.type], operator);
+ }
+
+ if (operator.nb_inputs === undefined || operator.apply_to === undefined) {
+ Utils.error('Config', 'Missing "nb_inputs" and/or "apply_to" for operator "{0}"', operator.type);
+ }
+ }
+
+ if (definedOperators.indexOf(operator.type) != -1) {
+ Utils.error('Config', 'Operator "{0}" already defined', operator.type);
+ }
+ definedOperators.push(operator.type);
+
+ if (!operator.optgroup) {
+ operator.optgroup = null;
+ }
+ else {
+ this.status.has_operator_optgroup = true;
+
+ // register optgroup if needed
+ if (!this.settings.optgroups[operator.optgroup]) {
+ this.settings.optgroups[operator.optgroup] = operator.optgroup;
+ }
+ }
+ }, this);
+
+ if (this.status.has_operator_optgroup) {
+ operators = Utils.groupSort(operators, 'optgroup');
+ }
+
+ return operators;
+};
+
+/**
+ * Adds all events listeners to the builder
+ * @private
+ */
+QueryBuilder.prototype.bindEvents = function() {
+ var self = this;
+ var Selectors = QueryBuilder.selectors;
+
+ // group condition change
+ this.$el.on('change.queryBuilder', Selectors.group_condition, function() {
+ if ($(this).is(':checked')) {
+ var $group = $(this).closest(Selectors.group_container);
+ self.getModel($group).condition = $(this).val();
+ }
+ });
+
+ // rule filter change
+ this.$el.on('change.queryBuilder', Selectors.rule_filter, function() {
+ var $rule = $(this).closest(Selectors.rule_container);
+ self.getModel($rule).filter = self.getFilterById($(this).val());
+ });
+
+ // rule operator change
+ this.$el.on('change.queryBuilder', Selectors.rule_operator, function() {
+ var $rule = $(this).closest(Selectors.rule_container);
+ self.getModel($rule).operator = self.getOperatorByType($(this).val());
+ });
+
+ // add rule button
+ this.$el.on('click.queryBuilder', Selectors.add_rule, function() {
+ var $group = $(this).closest(Selectors.group_container);
+ self.addRule(self.getModel($group));
+ });
+
+ // delete rule button
+ this.$el.on('click.queryBuilder', Selectors.delete_rule, function() {
+ var $rule = $(this).closest(Selectors.rule_container);
+ self.deleteRule(self.getModel($rule));
+ });
+
+ if (this.settings.allow_groups !== 0) {
+ // add group button
+ this.$el.on('click.queryBuilder', Selectors.add_group, function() {
+ var $group = $(this).closest(Selectors.group_container);
+ self.addGroup(self.getModel($group));
+ });
+
+ // delete group button
+ this.$el.on('click.queryBuilder', Selectors.delete_group, function() {
+ var $group = $(this).closest(Selectors.group_container);
+ self.deleteGroup(self.getModel($group));
+ });
+ }
+
+ // model events
+ this.model.on({
+ 'drop': function(e, node) {
+ node.$el.remove();
+ self.refreshGroupsConditions();
+ },
+ 'add': function(e, parent, node, index) {
+ if (index === 0) {
+ node.$el.prependTo(parent.$el.find('>' + QueryBuilder.selectors.rules_list));
+ }
+ else {
+ node.$el.insertAfter(parent.rules[index - 1].$el);
+ }
+ self.refreshGroupsConditions();
+ },
+ 'move': function(e, node, group, index) {
+ node.$el.detach();
+
+ if (index === 0) {
+ node.$el.prependTo(group.$el.find('>' + QueryBuilder.selectors.rules_list));
+ }
+ else {
+ node.$el.insertAfter(group.rules[index - 1].$el);
+ }
+ self.refreshGroupsConditions();
+ },
+ 'update': function(e, node, field, value, oldValue) {
+ if (node instanceof Rule) {
+ switch (field) {
+ case 'error':
+ self.updateError(node);
+ break;
+
+ case 'flags':
+ self.applyRuleFlags(node);
+ break;
+
+ case 'filter':
+ self.updateRuleFilter(node, oldValue);
+ break;
+
+ case 'operator':
+ self.updateRuleOperator(node, oldValue);
+ break;
+
+ case 'value':
+ self.updateRuleValue(node, oldValue);
+ break;
+ }
+ }
+ else {
+ switch (field) {
+ case 'error':
+ self.updateError(node);
+ break;
+
+ case 'flags':
+ self.applyGroupFlags(node);
+ break;
+
+ case 'condition':
+ self.updateGroupCondition(node, oldValue);
+ break;
+ }
+ }
+ }
+ });
+};
+
+/**
+ * Creates the root group
+ * @param {boolean} [addRule=true] - adds a default empty rule
+ * @param {object} [data] - group custom data
+ * @param {object} [flags] - flags to apply to the group
+ * @returns {Group} root group
+ * @fires QueryBuilder.afterAddGroup
+ */
+QueryBuilder.prototype.setRoot = function(addRule, data, flags) {
+ addRule = (addRule === undefined || addRule === true);
+
+ var group_id = this.nextGroupId();
+ var $group = $($.parseHTML(this.getGroupTemplate(group_id, 1)));
+
+ this.$el.append($group);
+ this.model.root = new Group(null, $group);
+ this.model.root.model = this.model;
+
+ this.model.root.data = data;
+ this.model.root.flags = $.extend({}, this.settings.default_group_flags, flags);
+ this.model.root.condition = this.settings.default_condition;
+
+ this.trigger('afterAddGroup', this.model.root);
+
+ if (addRule) {
+ this.addRule(this.model.root);
+ }
+
+ return this.model.root;
+};
+
+/**
+ * Adds a new group
+ * @param {Group} parent
+ * @param {boolean} [addRule=true] - adds a default empty rule
+ * @param {object} [data] - group custom data
+ * @param {object} [flags] - flags to apply to the group
+ * @returns {Group}
+ * @fires QueryBuilder.beforeAddGroup
+ * @fires QueryBuilder.afterAddGroup
+ */
+QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) {
+ addRule = (addRule === undefined || addRule === true);
+
+ var level = parent.level + 1;
+
+ /**
+ * Just before adding a group, can be prevented.
+ * @event beforeAddGroup
+ * @memberof QueryBuilder
+ * @param {Group} parent
+ * @param {boolean} addRule - if an empty rule will be added in the group
+ * @param {int} level - nesting level of the group, 1 is the root group
+ */
+ var e = this.trigger('beforeAddGroup', parent, addRule, level);
+ if (e.isDefaultPrevented()) {
+ return null;
+ }
+
+ var group_id = this.nextGroupId();
+ var $group = $(this.getGroupTemplate(group_id, level));
+ var model = parent.addGroup($group);
+
+ model.data = data;
+ model.flags = $.extend({}, this.settings.default_group_flags, flags);
+ model.condition = this.settings.default_condition;
+
+ /**
+ * Just after adding a group
+ * @event afterAddGroup
+ * @memberof QueryBuilder
+ * @param {Group} group
+ */
+ this.trigger('afterAddGroup', model);
+
+ /**
+ * After any change in the rules
+ * @event rulesChanged
+ * @memberof QueryBuilder
+ */
+ this.trigger('rulesChanged');
+
+ if (addRule) {
+ this.addRule(model);
+ }
+
+ return model;
+};
+
+/**
+ * Tries to delete a group. The group is not deleted if at least one rule is flagged `no_delete`.
+ * @param {Group} group
+ * @returns {boolean} if the group has been deleted
+ * @fires QueryBuilder.beforeDeleteGroup
+ * @fires QueryBuilder.afterDeleteGroup
+ */
+QueryBuilder.prototype.deleteGroup = function(group) {
+ if (group.isRoot()) {
+ return false;
+ }
+
+ /**
+ * Just before deleting a group, can be prevented
+ * @event beforeDeleteGroup
+ * @memberof QueryBuilder
+ * @param {Group} parent
+ */
+ var e = this.trigger('beforeDeleteGroup', group);
+ if (e.isDefaultPrevented()) {
+ return false;
+ }
+
+ var del = true;
+
+ group.each('reverse', function(rule) {
+ del &= this.deleteRule(rule);
+ }, function(group) {
+ del &= this.deleteGroup(group);
+ }, this);
+
+ if (del) {
+ group.drop();
+
+ /**
+ * Just after deleting a group
+ * @event afterDeleteGroup
+ * @memberof QueryBuilder
+ */
+ this.trigger('afterDeleteGroup');
+
+ this.trigger('rulesChanged');
+ }
+
+ return del;
+};
+
+/**
+ * Performs actions when a group's condition changes
+ * @param {Group} group
+ * @param {object} previousCondition
+ * @fires QueryBuilder.afterUpdateGroupCondition
+ * @private
+ */
+QueryBuilder.prototype.updateGroupCondition = function(group, previousCondition) {
+ group.$el.find('>' + QueryBuilder.selectors.group_condition).each(function() {
+ var $this = $(this);
+ $this.prop('checked', $this.val() === group.condition);
+ $this.parent().toggleClass('active', $this.val() === group.condition);
+ });
+
+ /**
+ * After the group condition has been modified
+ * @event afterUpdateGroupCondition
+ * @memberof QueryBuilder
+ * @param {Group} group
+ * @param {object} previousCondition
+ */
+ this.trigger('afterUpdateGroupCondition', group, previousCondition);
+
+ this.trigger('rulesChanged');
+};
+
+/**
+ * Updates the visibility of conditions based on number of rules inside each group
+ * @private
+ */
+QueryBuilder.prototype.refreshGroupsConditions = function() {
+ (function walk(group) {
+ if (!group.flags || (group.flags && !group.flags.condition_readonly)) {
+ group.$el.find('>' + QueryBuilder.selectors.group_condition).prop('disabled', group.rules.length <= 1)
+ .parent().toggleClass('disabled', group.rules.length <= 1);
+ }
+
+ group.each(null, function(group) {
+ walk(group);
+ }, this);
+ }(this.model.root));
+};
+
+/**
+ * Adds a new rule
+ * @param {Group} parent
+ * @param {object} [data] - rule custom data
+ * @param {object} [flags] - flags to apply to the rule
+ * @returns {Rule}
+ * @fires QueryBuilder.beforeAddRule
+ * @fires QueryBuilder.afterAddRule
+ * @fires QueryBuilder.changer:getDefaultFilter
+ */
+QueryBuilder.prototype.addRule = function(parent, data, flags) {
+ /**
+ * Just before adding a rule, can be prevented
+ * @event beforeAddRule
+ * @memberof QueryBuilder
+ * @param {Group} parent
+ */
+ var e = this.trigger('beforeAddRule', parent);
+ if (e.isDefaultPrevented()) {
+ return null;
+ }
+
+ var rule_id = this.nextRuleId();
+ var $rule = $($.parseHTML(this.getRuleTemplate(rule_id)));
+ var model = parent.addRule($rule);
+
+ model.data = data;
+ model.flags = $.extend({}, this.settings.default_rule_flags, flags);
+
+ /**
+ * Just after adding a rule
+ * @event afterAddRule
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ */
+ this.trigger('afterAddRule', model);
+
+ this.trigger('rulesChanged');
+
+ this.createRuleFilters(model);
+
+ if (this.settings.default_filter || !this.settings.display_empty_filter) {
+ /**
+ * Modifies the default filter for a rule
+ * @event changer:getDefaultFilter
+ * @memberof QueryBuilder
+ * @param {QueryBuilder.Filter} filter
+ * @param {Rule} rule
+ * @returns {QueryBuilder.Filter}
+ */
+ model.filter = this.change('getDefaultFilter',
+ this.getFilterById(this.settings.default_filter || this.filters[0].id),
+ model
+ );
+ }
+
+ return model;
+};
+
+/**
+ * Tries to delete a rule
+ * @param {Rule} rule
+ * @returns {boolean} if the rule has been deleted
+ * @fires QueryBuilder.beforeDeleteRule
+ * @fires QueryBuilder.afterDeleteRule
+ */
+QueryBuilder.prototype.deleteRule = function(rule) {
+ if (rule.flags.no_delete) {
+ return false;
+ }
+
+ /**
+ * Just before deleting a rule, can be prevented
+ * @event beforeDeleteRule
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ */
+ var e = this.trigger('beforeDeleteRule', rule);
+ if (e.isDefaultPrevented()) {
+ return false;
+ }
+
+ rule.drop();
+
+ /**
+ * Just after deleting a rule
+ * @event afterDeleteRule
+ * @memberof QueryBuilder
+ */
+ this.trigger('afterDeleteRule');
+
+ this.trigger('rulesChanged');
+
+ return true;
+};
+
+/**
+ * Creates the filters for a rule
+ * @param {Rule} rule
+ * @fires QueryBuilder.changer:getRuleFilters
+ * @fires QueryBuilder.afterCreateRuleFilters
+ * @private
+ */
+QueryBuilder.prototype.createRuleFilters = function(rule) {
+ /**
+ * Modifies the list a filters available for a rule
+ * @event changer:getRuleFilters
+ * @memberof QueryBuilder
+ * @param {QueryBuilder.Filter[]} filters
+ * @param {Rule} rule
+ * @returns {QueryBuilder.Filter[]}
+ */
+ var filters = this.change('getRuleFilters', this.filters, rule);
+ var $filterSelect = $($.parseHTML(this.getRuleFilterSelect(rule, filters)));
+
+ rule.$el.find(QueryBuilder.selectors.filter_container).html($filterSelect);
+
+ /**
+ * After creating the dropdown for filters
+ * @event afterCreateRuleFilters
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ */
+ this.trigger('afterCreateRuleFilters', rule);
+
+ this.applyRuleFlags(rule);
+};
+
+/**
+ * Creates the operators for a rule and init the rule operator
+ * @param {Rule} rule
+ * @fires QueryBuilder.afterCreateRuleOperators
+ * @private
+ */
+QueryBuilder.prototype.createRuleOperators = function(rule) {
+ var $operatorContainer = rule.$el.find(QueryBuilder.selectors.operator_container).empty();
+
+ if (!rule.filter) {
+ return;
+ }
+
+ var operators = this.getOperators(rule.filter);
+ var $operatorSelect = $($.parseHTML(this.getRuleOperatorSelect(rule, operators)));
+
+ $operatorContainer.html($operatorSelect);
+
+ // set the operator without triggering update event
+ if (rule.filter.default_operator) {
+ rule.__.operator = this.getOperatorByType(rule.filter.default_operator);
+ }
+ else {
+ rule.__.operator = operators[0];
+ }
+
+ rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
+
+ /**
+ * After creating the dropdown for operators
+ * @event afterCreateRuleOperators
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ * @param {QueryBuilder.Operator[]} operators - allowed operators for this rule
+ */
+ this.trigger('afterCreateRuleOperators', rule, operators);
+
+ this.applyRuleFlags(rule);
+};
+
+/**
+ * Creates the main input for a rule
+ * @param {Rule} rule
+ * @fires QueryBuilder.afterCreateRuleInput
+ * @private
+ */
+QueryBuilder.prototype.createRuleInput = function(rule) {
+ var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container).empty();
+
+ rule.__.value = undefined;
+
+ if (!rule.filter || !rule.operator || rule.operator.nb_inputs === 0) {
+ return;
+ }
+
+ var self = this;
+ var $inputs = $();
+ var filter = rule.filter;
+
+ for (var i = 0; i < rule.operator.nb_inputs; i++) {
+ var $ruleInput = $($.parseHTML($.trim(this.getRuleInput(rule, i))));
+ if (i > 0) $valueContainer.append(this.settings.inputs_separator);
+ $valueContainer.append($ruleInput);
+ $inputs = $inputs.add($ruleInput);
+ }
+
+ $valueContainer.css('display', '');
+
+ $inputs.on('change ' + (filter.input_event || ''), function() {
+ if (!rule._updating_input) {
+ rule._updating_value = true;
+ rule.value = self.getRuleInputValue(rule);
+ rule._updating_value = false;
+ }
+ });
+
+ if (filter.plugin) {
+ $inputs[filter.plugin](filter.plugin_config || {});
+ }
+
+ /**
+ * After creating the input for a rule and initializing optional plugin
+ * @event afterCreateRuleInput
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ */
+ this.trigger('afterCreateRuleInput', rule);
+
+ if (filter.default_value !== undefined) {
+ rule.value = filter.default_value;
+ }
+ else {
+ rule._updating_value = true;
+ rule.value = self.getRuleInputValue(rule);
+ rule._updating_value = false;
+ }
+
+ this.applyRuleFlags(rule);
+};
+
+/**
+ * Performs action when a rule's filter changes
+ * @param {Rule} rule
+ * @param {object} previousFilter
+ * @fires QueryBuilder.afterUpdateRuleFilter
+ * @private
+ */
+QueryBuilder.prototype.updateRuleFilter = function(rule, previousFilter) {
+ this.createRuleOperators(rule);
+ this.createRuleInput(rule);
+
+ rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
+
+ // clear rule data if the filter changed
+ if (previousFilter && rule.filter && previousFilter.id !== rule.filter.id) {
+ rule.data = undefined;
+ }
+
+ /**
+ * After the filter has been updated and the operators and input re-created
+ * @event afterUpdateRuleFilter
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ * @param {object} previousFilter
+ */
+ this.trigger('afterUpdateRuleFilter', rule, previousFilter);
+
+ this.trigger('rulesChanged');
+};
+
+/**
+ * Performs actions when a rule's operator changes
+ * @param {Rule} rule
+ * @param {object} previousOperator
+ * @fires QueryBuilder.afterUpdateRuleOperator
+ * @private
+ */
+QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) {
+ var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container);
+
+ if (!rule.operator || rule.operator.nb_inputs === 0) {
+ $valueContainer.hide();
+
+ rule.__.value = undefined;
+ }
+ else {
+ $valueContainer.css('display', '');
+
+ if ($valueContainer.is(':empty') || !previousOperator ||
+ rule.operator.nb_inputs !== previousOperator.nb_inputs ||
+ rule.operator.optgroup !== previousOperator.optgroup
+ ) {
+ this.createRuleInput(rule);
+ }
+ }
+
+ if (rule.operator) {
+ rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
+
+ // refresh value if the format changed for this operator
+ rule.__.value = this.getRuleInputValue(rule);
+ }
+
+ /**
+ * After the operator has been updated and the input optionally re-created
+ * @event afterUpdateRuleOperator
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ * @param {object} previousOperator
+ */
+ this.trigger('afterUpdateRuleOperator', rule, previousOperator);
+
+ this.trigger('rulesChanged');
+};
+
+/**
+ * Performs actions when rule's value changes
+ * @param {Rule} rule
+ * @param {object} previousValue
+ * @fires QueryBuilder.afterUpdateRuleValue
+ * @private
+ */
+QueryBuilder.prototype.updateRuleValue = function(rule, previousValue) {
+ if (!rule._updating_value) {
+ this.setRuleInputValue(rule, rule.value);
+ }
+
+ /**
+ * After the rule value has been modified
+ * @event afterUpdateRuleValue
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ * @param {*} previousValue
+ */
+ this.trigger('afterUpdateRuleValue', rule, previousValue);
+
+ this.trigger('rulesChanged');
+};
+
+/**
+ * Changes a rule's properties depending on its flags
+ * @param {Rule} rule
+ * @fires QueryBuilder.afterApplyRuleFlags
+ * @private
+ */
+QueryBuilder.prototype.applyRuleFlags = function(rule) {
+ var flags = rule.flags;
+ var Selectors = QueryBuilder.selectors;
+
+ rule.$el.find(Selectors.rule_filter).prop('disabled', flags.filter_readonly);
+ rule.$el.find(Selectors.rule_operator).prop('disabled', flags.operator_readonly);
+ rule.$el.find(Selectors.rule_value).prop('disabled', flags.value_readonly);
+
+ if (flags.no_delete) {
+ rule.$el.find(Selectors.delete_rule).remove();
+ }
+
+ /**
+ * After rule's flags has been applied
+ * @event afterApplyRuleFlags
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ */
+ this.trigger('afterApplyRuleFlags', rule);
+};
+
+/**
+ * Changes group's properties depending on its flags
+ * @param {Group} group
+ * @fires QueryBuilder.afterApplyGroupFlags
+ * @private
+ */
+QueryBuilder.prototype.applyGroupFlags = function(group) {
+ var flags = group.flags;
+ var Selectors = QueryBuilder.selectors;
+
+ group.$el.find('>' + Selectors.group_condition).prop('disabled', flags.condition_readonly)
+ .parent().toggleClass('readonly', flags.condition_readonly);
+
+ if (flags.no_add_rule) {
+ group.$el.find(Selectors.add_rule).remove();
+ }
+ if (flags.no_add_group) {
+ group.$el.find(Selectors.add_group).remove();
+ }
+ if (flags.no_delete) {
+ group.$el.find(Selectors.delete_group).remove();
+ }
+
+ /**
+ * After group's flags has been applied
+ * @event afterApplyGroupFlags
+ * @memberof QueryBuilder
+ * @param {Group} group
+ */
+ this.trigger('afterApplyGroupFlags', group);
+};
+
+/**
+ * Clears all errors markers
+ * @param {Node} [node] default is root Group
+ */
+QueryBuilder.prototype.clearErrors = function(node) {
+ node = node || this.model.root;
+
+ if (!node) {
+ return;
+ }
+
+ node.error = null;
+
+ if (node instanceof Group) {
+ node.each(function(rule) {
+ rule.error = null;
+ }, function(group) {
+ this.clearErrors(group);
+ }, this);
+ }
+};
+
+/**
+ * Adds/Removes error on a Rule or Group
+ * @param {Node} node
+ * @fires QueryBuilder.changer:displayError
+ * @private
+ */
+QueryBuilder.prototype.updateError = function(node) {
+ if (this.settings.display_errors) {
+ if (node.error === null) {
+ node.$el.removeClass('has-error');
+ }
+ else {
+ var errorMessage = this.translate('errors', node.error[0]);
+ errorMessage = Utils.fmt(errorMessage, node.error.slice(1));
+
+ /**
+ * Modifies an error message before display
+ * @event changer:displayError
+ * @memberof QueryBuilder
+ * @param {string} errorMessage - the error message (translated and formatted)
+ * @param {array} error - the raw error array (error code and optional arguments)
+ * @param {Node} node
+ * @returns {string}
+ */
+ errorMessage = this.change('displayError', errorMessage, node.error, node);
+
+ node.$el.addClass('has-error')
+ .find(QueryBuilder.selectors.error_container).eq(0)
+ .attr('title', errorMessage);
+ }
+ }
+};
+
+/**
+ * Triggers a validation error event
+ * @param {Node} node
+ * @param {string|array} error
+ * @param {*} value
+ * @fires QueryBuilder.validationError
+ * @private
+ */
+QueryBuilder.prototype.triggerValidationError = function(node, error, value) {
+ if (!$.isArray(error)) {
+ error = [error];
+ }
+
+ /**
+ * Fired when a validation error occurred, can be prevented
+ * @event validationError
+ * @memberof QueryBuilder
+ * @param {Node} node
+ * @param {string} error
+ * @param {*} value
+ */
+ var e = this.trigger('validationError', node, error, value);
+ if (!e.isDefaultPrevented()) {
+ node.error = error;
+ }
+};
+
+
+/**
+ * Destroys the builder
+ * @fires QueryBuilder.beforeDestroy
+ */
+QueryBuilder.prototype.destroy = function() {
+ /**
+ * Before the {@link QueryBuilder#destroy} method
+ * @event beforeDestroy
+ * @memberof QueryBuilder
+ */
+ this.trigger('beforeDestroy');
+
+ if (this.status.generated_id) {
+ this.$el.removeAttr('id');
+ }
+
+ this.clear();
+ this.model = null;
+
+ this.$el
+ .off('.queryBuilder')
+ .removeClass('query-builder')
+ .removeData('queryBuilder');
+
+ delete this.$el[0].queryBuilder;
+};
+
+/**
+ * Clear all rules and resets the root group
+ * @fires QueryBuilder.beforeReset
+ * @fires QueryBuilder.afterReset
+ */
+QueryBuilder.prototype.reset = function() {
+ /**
+ * Before the {@link QueryBuilder#reset} method, can be prevented
+ * @event beforeReset
+ * @memberof QueryBuilder
+ */
+ var e = this.trigger('beforeReset');
+ if (e.isDefaultPrevented()) {
+ return;
+ }
+
+ this.status.group_id = 1;
+ this.status.rule_id = 0;
+
+ this.model.root.empty();
+
+ this.model.root.data = undefined;
+ this.model.root.flags = $.extend({}, this.settings.default_group_flags);
+ this.model.root.condition = this.settings.default_condition;
+
+ this.addRule(this.model.root);
+
+ /**
+ * After the {@link QueryBuilder#reset} method
+ * @event afterReset
+ * @memberof QueryBuilder
+ */
+ this.trigger('afterReset');
+
+ this.trigger('rulesChanged');
+};
+
+/**
+ * Clears all rules and removes the root group
+ * @fires QueryBuilder.beforeClear
+ * @fires QueryBuilder.afterClear
+ */
+QueryBuilder.prototype.clear = function() {
+ /**
+ * Before the {@link QueryBuilder#clear} method, can be prevented
+ * @event beforeClear
+ * @memberof QueryBuilder
+ */
+ var e = this.trigger('beforeClear');
+ if (e.isDefaultPrevented()) {
+ return;
+ }
+
+ this.status.group_id = 0;
+ this.status.rule_id = 0;
+
+ if (this.model.root) {
+ this.model.root.drop();
+ this.model.root = null;
+ }
+
+ /**
+ * After the {@link QueryBuilder#clear} method
+ * @event afterClear
+ * @memberof QueryBuilder
+ */
+ this.trigger('afterClear');
+
+ this.trigger('rulesChanged');
+};
+
+/**
+ * Modifies the builder configuration.
+ * Only options defined in QueryBuilder.modifiable_options are modifiable
+ * @param {object} options
+ */
+QueryBuilder.prototype.setOptions = function(options) {
+ $.each(options, function(opt, value) {
+ if (QueryBuilder.modifiable_options.indexOf(opt) !== -1) {
+ this.settings[opt] = value;
+ }
+ }.bind(this));
+};
+
+/**
+ * Returns the model associated to a DOM object, or the root model
+ * @param {jQuery} [target]
+ * @returns {Node}
+ */
+QueryBuilder.prototype.getModel = function(target) {
+ if (!target) {
+ return this.model.root;
+ }
+ else if (target instanceof Node) {
+ return target;
+ }
+ else {
+ return $(target).data('queryBuilderModel');
+ }
+};
+
+/**
+ * Validates the whole builder
+ * @param {object} [options]
+ * @param {boolean} [options.skip_empty=false] - skips validating rules that have no filter selected
+ * @returns {boolean}
+ * @fires QueryBuilder.changer:validate
+ */
+QueryBuilder.prototype.validate = function(options) {
+ options = $.extend({
+ skip_empty: false
+ }, options);
+
+ this.clearErrors();
+
+ var self = this;
+
+ var valid = (function parse(group) {
+ var done = 0;
+ var errors = 0;
+
+ group.each(function(rule) {
+ if (!rule.filter && options.skip_empty) {
+ return;
+ }
+
+ if (!rule.filter) {
+ self.triggerValidationError(rule, 'no_filter', null);
+ errors++;
+ return;
+ }
+
+ if (!rule.operator) {
+ self.triggerValidationError(rule, 'no_operator', null);
+ errors++;
+ return;
+ }
+
+ if (rule.operator.nb_inputs !== 0) {
+ var valid = self.validateValue(rule, rule.value);
+
+ if (valid !== true) {
+ self.triggerValidationError(rule, valid, rule.value);
+ errors++;
+ return;
+ }
+ }
+
+ done++;
+
+ }, function(group) {
+ var res = parse(group);
+ if (res === true) {
+ done++;
+ }
+ else if (res === false) {
+ errors++;
+ }
+ });
+
+ if (errors > 0) {
+ return false;
+ }
+ else if (done === 0 && !group.isRoot() && options.skip_empty) {
+ return null;
+ }
+ else if (done === 0 && (!self.settings.allow_empty || !group.isRoot())) {
+ self.triggerValidationError(group, 'empty_group', null);
+ return false;
+ }
+
+ return true;
+
+ }(this.model.root));
+
+ /**
+ * Modifies the result of the {@link QueryBuilder#validate} method
+ * @event changer:validate
+ * @memberof QueryBuilder
+ * @param {boolean} valid
+ * @returns {boolean}
+ */
+ return this.change('validate', valid);
+};
+
+/**
+ * Gets an object representing current rules
+ * @param {object} [options]
+ * @param {boolean|string} [options.get_flags=false] - export flags, true: only changes from default flags or 'all'
+ * @param {boolean} [options.allow_invalid=false] - returns rules even if they are invalid
+ * @param {boolean} [options.skip_empty=false] - remove rules that have no filter selected
+ * @returns {object}
+ * @fires QueryBuilder.changer:ruleToJson
+ * @fires QueryBuilder.changer:groupToJson
+ * @fires QueryBuilder.changer:getRules
+ */
+QueryBuilder.prototype.getRules = function(options) {
+ options = $.extend({
+ get_flags: false,
+ allow_invalid: false,
+ skip_empty: false
+ }, options);
+
+ var valid = this.validate(options);
+ if (!valid && !options.allow_invalid) {
+ return null;
+ }
+
+ var self = this;
+
+ var out = (function parse(group) {
+ var groupData = {
+ condition: group.condition,
+ rules: []
+ };
+
+ if (group.data) {
+ groupData.data = $.extendext(true, 'replace', {}, group.data);
+ }
+
+ if (options.get_flags) {
+ var flags = self.getGroupFlags(group.flags, options.get_flags === 'all');
+ if (!$.isEmptyObject(flags)) {
+ groupData.flags = flags;
+ }
+ }
+
+ group.each(function(rule) {
+ if (!rule.filter && options.skip_empty) {
+ return;
+ }
+
+ var value = null;
+ if (!rule.operator || rule.operator.nb_inputs !== 0) {
+ value = rule.value;
+ }
+
+ var ruleData = {
+ id: rule.filter ? rule.filter.id : null,
+ field: rule.filter ? rule.filter.field : null,
+ type: rule.filter ? rule.filter.type : null,
+ input: rule.filter ? rule.filter.input : null,
+ operator: rule.operator ? rule.operator.type : null,
+ value: value
+ };
+
+ if (rule.filter && rule.filter.data || rule.data) {
+ ruleData.data = $.extendext(true, 'replace', {}, rule.filter ? rule.filter.data : {}, rule.data);
+ }
+
+ if (options.get_flags) {
+ var flags = self.getRuleFlags(rule.flags, options.get_flags === 'all');
+ if (!$.isEmptyObject(flags)) {
+ ruleData.flags = flags;
+ }
+ }
+
+ /**
+ * Modifies the JSON generated from a Rule object
+ * @event changer:ruleToJson
+ * @memberof QueryBuilder
+ * @param {object} json
+ * @param {Rule} rule
+ * @returns {object}
+ */
+ groupData.rules.push(self.change('ruleToJson', ruleData, rule));
+
+ }, function(model) {
+ var data = parse(model);
+ if (data.rules.length !== 0 || !options.skip_empty) {
+ groupData.rules.push(data);
+ }
+ }, this);
+
+ /**
+ * Modifies the JSON generated from a Group object
+ * @event changer:groupToJson
+ * @memberof QueryBuilder
+ * @param {object} json
+ * @param {Group} group
+ * @returns {object}
+ */
+ return self.change('groupToJson', groupData, group);
+
+ }(this.model.root));
+
+ out.valid = valid;
+
+ /**
+ * Modifies the result of the {@link QueryBuilder#getRules} method
+ * @event changer:getRules
+ * @memberof QueryBuilder
+ * @param {object} json
+ * @returns {object}
+ */
+ return this.change('getRules', out);
+};
+
+/**
+ * Sets rules from object
+ * @param {object} data
+ * @param {object} [options]
+ * @param {boolean} [options.allow_invalid=false] - silent-fail if the data are invalid
+ * @throws RulesError, UndefinedConditionError
+ * @fires QueryBuilder.changer:setRules
+ * @fires QueryBuilder.changer:jsonToRule
+ * @fires QueryBuilder.changer:jsonToGroup
+ * @fires QueryBuilder.afterSetRules
+ */
+QueryBuilder.prototype.setRules = function(data, options) {
+ options = $.extend({
+ allow_invalid: false
+ }, options);
+
+ if ($.isArray(data)) {
+ data = {
+ condition: this.settings.default_condition,
+ rules: data
+ };
+ }
+
+ if (!data || !data.rules || (data.rules.length === 0 && !this.settings.allow_empty)) {
+ Utils.error('RulesParse', 'Incorrect data object passed');
+ }
+
+ this.clear();
+ this.setRoot(false, data.data, this.parseGroupFlags(data));
+
+ /**
+ * Modifies data before the {@link QueryBuilder#setRules} method
+ * @event changer:setRules
+ * @memberof QueryBuilder
+ * @param {object} json
+ * @param {object} options
+ * @returns {object}
+ */
+ data = this.change('setRules', data, options);
+
+ var self = this;
+
+ (function add(data, group) {
+ if (group === null) {
+ return;
+ }
+
+ if (data.condition === undefined) {
+ data.condition = self.settings.default_condition;
+ }
+ else if (self.settings.conditions.indexOf(data.condition) == -1) {
+ Utils.error(!options.allow_invalid, 'UndefinedCondition', 'Invalid condition "{0}"', data.condition);
+ data.condition = self.settings.default_condition;
+ }
+
+ group.condition = data.condition;
+
+ data.rules.forEach(function(item) {
+ var model;
+
+ if (item.rules !== undefined) {
+ if (self.settings.allow_groups !== -1 && self.settings.allow_groups < group.level) {
+ Utils.error(!options.allow_invalid, 'RulesParse', 'No more than {0} groups are allowed', self.settings.allow_groups);
+ self.reset();
+ }
+ else {
+ model = self.addGroup(group, false, item.data, self.parseGroupFlags(item));
+ if (model === null) {
+ return;
+ }
+
+ add(item, model);
+ }
+ }
+ else {
+ if (!item.empty) {
+ if (item.id === undefined) {
+ Utils.error(!options.allow_invalid, 'RulesParse', 'Missing rule field id');
+ item.empty = true;
+ }
+ if (item.operator === undefined) {
+ item.operator = 'equal';
+ }
+ }
+
+ model = self.addRule(group, item.data, self.parseRuleFlags(item));
+ if (model === null) {
+ return;
+ }
+
+ if (!item.empty) {
+ model.filter = self.getFilterById(item.id, !options.allow_invalid);
+ }
+
+ if (model.filter) {
+ model.operator = self.getOperatorByType(item.operator, !options.allow_invalid);
+
+ if (!model.operator) {
+ model.operator = self.getOperators(model.filter)[0];
+ }
+ }
+
+ if (model.operator && model.operator.nb_inputs !== 0) {
+ if (item.value !== undefined) {
+ model.value = item.value;
+ }
+ else if (model.filter.default_value !== undefined) {
+ model.value = model.filter.default_value;
+ }
+ }
+
+ /**
+ * Modifies the Rule object generated from the JSON
+ * @event changer:jsonToRule
+ * @memberof QueryBuilder
+ * @param {Rule} rule
+ * @param {object} json
+ * @returns {Rule} the same rule
+ */
+ if (self.change('jsonToRule', model, item) != model) {
+ Utils.error('RulesParse', 'Plugin tried to change rule reference');
+ }
+ }
+ });
+
+ /**
+ * Modifies the Group object generated from the JSON
+ * @event changer:jsonToGroup
+ * @memberof QueryBuilder
+ * @param {Group} group
+ * @param {object} json
+ * @returns {Group} the same group
+ */
+ if (self.change('jsonToGroup', group, data) != group) {
+ Utils.error('RulesParse', 'Plugin tried to change group reference');
+ }
+
+ }(data, this.model.root));
+
+ /**
+ * After the {@link QueryBuilder#setRules} method
+ * @event afterSetRules
+ * @memberof QueryBuilder
+ */
+ this.trigger('afterSetRules');
+};
+
+
+/**
+ * Performs value validation
+ * @param {Rule} rule
+ * @param {string|string[]} value
+ * @returns {array|boolean} true or error array
+ * @fires QueryBuilder.changer:validateValue
+ */
+QueryBuilder.prototype.validateValue = function(rule, value) {
+ var validation = rule.filter.validation || {};
+ var result = true;
+
+ if (validation.callback) {
+ result = validation.callback.call(this, value, rule);
+ }
+ else {
+ result = this._validateValue(rule, value);
+ }
+
+ /**
+ * Modifies the result of the rule validation method
+ * @event changer:validateValue
+ * @memberof QueryBuilder
+ * @param {array|boolean} result - true or an error array
+ * @param {*} value
+ * @param {Rule} rule
+ * @returns {array|boolean}
+ */
+ return this.change('validateValue', result, value, rule);
+};
+
+/**
+ * Default validation function
+ * @param {Rule} rule
+ * @param {string|string[]} value
+ * @returns {array|boolean} true or error array
+ * @throws ConfigError
+ * @private
+ */
+QueryBuilder.prototype._validateValue = function(rule, value) {
+ var filter = rule.filter;
+ var operator = rule.operator;
+ var validation = filter.validation || {};
+ var result = true;
+ var tmp, tempValue;
+
+ if (rule.operator.nb_inputs === 1) {
+ value = [value];
+ }
+
+ for (var i = 0; i < operator.nb_inputs; i++) {
+ if (!operator.multiple && $.isArray(value[i]) && value[i].length > 1) {
+ result = ['operator_not_multiple', operator.type, this.translate('operators', operator.type)];
+ break;
+ }
+
+ switch (filter.input) {
+ case 'radio':
+ if (value[i] === undefined || value[i].length === 0) {
+ if (!validation.allow_empty_value) {
+ result = ['radio_empty'];
+ }
+ break;
+ }
+ break;
+
+ case 'checkbox':
+ if (value[i] === undefined || value[i].length === 0) {
+ if (!validation.allow_empty_value) {
+ result = ['checkbox_empty'];
+ }
+ break;
+ }
+ break;
+
+ case 'select':
+ if (value[i] === undefined || value[i].length === 0 || (filter.placeholder && value[i] == filter.placeholder_value)) {
+ if (!validation.allow_empty_value) {
+ result = ['select_empty'];
+ }
+ break;
+ }
+ break;
+
+ default:
+ tempValue = $.isArray(value[i]) ? value[i] : [value[i]];
+
+ for (var j = 0; j < tempValue.length; j++) {
+ switch (QueryBuilder.types[filter.type]) {
+ case 'string':
+ if (tempValue[j] === undefined || tempValue[j].length === 0) {
+ if (!validation.allow_empty_value) {
+ result = ['string_empty'];
+ }
+ break;
+ }
+ if (validation.min !== undefined) {
+ if (tempValue[j].length < parseInt(validation.min)) {
+ result = [this.getValidationMessage(validation, 'min', 'string_exceed_min_length'), validation.min];
+ break;
+ }
+ }
+ if (validation.max !== undefined) {
+ if (tempValue[j].length > parseInt(validation.max)) {
+ result = [this.getValidationMessage(validation, 'max', 'string_exceed_max_length'), validation.max];
+ break;
+ }
+ }
+ if (validation.format) {
+ if (typeof validation.format == 'string') {
+ validation.format = new RegExp(validation.format);
+ }
+ if (!validation.format.test(tempValue[j])) {
+ result = [this.getValidationMessage(validation, 'format', 'string_invalid_format'), validation.format];
+ break;
+ }
+ }
+ break;
+
+ case 'number':
+ if (tempValue[j] === undefined || tempValue[j].length === 0) {
+ if (!validation.allow_empty_value) {
+ result = ['number_nan'];
+ }
+ break;
+ }
+ if (isNaN(tempValue[j])) {
+ result = ['number_nan'];
+ break;
+ }
+ if (filter.type == 'integer') {
+ if (parseInt(tempValue[j]) != tempValue[j]) {
+ result = ['number_not_integer'];
+ break;
+ }
+ }
+ else {
+ if (parseFloat(tempValue[j]) != tempValue[j]) {
+ result = ['number_not_double'];
+ break;
+ }
+ }
+ if (validation.min !== undefined) {
+ if (tempValue[j] < parseFloat(validation.min)) {
+ result = [this.getValidationMessage(validation, 'min', 'number_exceed_min'), validation.min];
+ break;
+ }
+ }
+ if (validation.max !== undefined) {
+ if (tempValue[j] > parseFloat(validation.max)) {
+ result = [this.getValidationMessage(validation, 'max', 'number_exceed_max'), validation.max];
+ break;
+ }
+ }
+ if (validation.step !== undefined && validation.step !== 'any') {
+ var v = (tempValue[j] / validation.step).toPrecision(14);
+ if (parseInt(v) != v) {
+ result = [this.getValidationMessage(validation, 'step', 'number_wrong_step'), validation.step];
+ break;
+ }
+ }
+ break;
+
+ case 'datetime':
+ if (tempValue[j] === undefined || tempValue[j].length === 0) {
+ if (!validation.allow_empty_value) {
+ result = ['datetime_empty'];
+ }
+ break;
+ }
+
+ // we need MomentJS
+ if (validation.format) {
+ if (!('moment' in window)) {
+ Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
+ }
+
+ var datetime = moment(tempValue[j], validation.format);
+ if (!datetime.isValid()) {
+ result = [this.getValidationMessage(validation, 'format', 'datetime_invalid'), validation.format];
+ break;
+ }
+ else {
+ if (validation.min) {
+ if (datetime < moment(validation.min, validation.format)) {
+ result = [this.getValidationMessage(validation, 'min', 'datetime_exceed_min'), validation.min];
+ break;
+ }
+ }
+ if (validation.max) {
+ if (datetime > moment(validation.max, validation.format)) {
+ result = [this.getValidationMessage(validation, 'max', 'datetime_exceed_max'), validation.max];
+ break;
+ }
+ }
+ }
+ }
+ break;
+
+ case 'boolean':
+ if (tempValue[j] === undefined || tempValue[j].length === 0) {
+ if (!validation.allow_empty_value) {
+ result = ['boolean_not_valid'];
+ }
+ break;
+ }
+ tmp = ('' + tempValue[j]).trim().toLowerCase();
+ if (tmp !== 'true' && tmp !== 'false' && tmp !== '1' && tmp !== '0' && tempValue[j] !== 1 && tempValue[j] !== 0) {
+ result = ['boolean_not_valid'];
+ break;
+ }
+ }
+
+ if (result !== true) {
+ break;
+ }
+ }
+ }
+
+ if (result !== true) {
+ break;
+ }
+ }
+
+ if ((rule.operator.type === 'between' || rule.operator.type === 'not_between') && value.length === 2) {
+ switch (QueryBuilder.types[filter.type]) {
+ case 'number':
+ if (value[0] > value[1]) {
+ result = ['number_between_invalid', value[0], value[1]];
+ }
+ break;
+
+ case 'datetime':
+ // we need MomentJS
+ if (validation.format) {
+ if (!('moment' in window)) {
+ Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
+ }
+
+ if (moment(value[0], validation.format).isAfter(moment(value[1], validation.format))) {
+ result = ['datetime_between_invalid', value[0], value[1]];
+ }
+ }
+ break;
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Returns an incremented group ID
+ * @returns {string}
+ * @private
+ */
+QueryBuilder.prototype.nextGroupId = function() {
+ return this.status.id + '_group_' + (this.status.group_id++);
+};
+
+/**
+ * Returns an incremented rule ID
+ * @returns {string}
+ * @private
+ */
+QueryBuilder.prototype.nextRuleId = function() {
+ return this.status.id + '_rule_' + (this.status.rule_id++);
+};
+
+/**
+ * Returns the operators for a filter
+ * @param {string|object} filter - filter id or filter object
+ * @returns {object[]}
+ * @fires QueryBuilder.changer:getOperators
+ */
+QueryBuilder.prototype.getOperators = function(filter) {
+ if (typeof filter == 'string') {
+ filter = this.getFilterById(filter);
+ }
+
+ var result = [];
+
+ for (var i = 0, l = this.operators.length; i < l; i++) {
+ // filter operators check
+ if (filter.operators) {
+ if (filter.operators.indexOf(this.operators[i].type) == -1) {
+ continue;
+ }
+ }
+ // type check
+ else if (this.operators[i].apply_to.indexOf(QueryBuilder.types[filter.type]) == -1) {
+ continue;
+ }
+
+ result.push(this.operators[i]);
+ }
+
+ // keep sort order defined for the filter
+ if (filter.operators) {
+ result.sort(function(a, b) {
+ return filter.operators.indexOf(a.type) - filter.operators.indexOf(b.type);
+ });
+ }
+
+ /**
+ * Modifies the operators available for a filter
+ * @event changer:getOperators
+ * @memberof QueryBuilder
+ * @param {QueryBuilder.Operator[]} operators
+ * @param {QueryBuilder.Filter} filter
+ * @returns {QueryBuilder.Operator[]}
+ */
+ return this.change('getOperators', result, filter);
+};
+
+/**
+ * Returns a particular filter by its id
+ * @param {string} id
+ * @param {boolean} [doThrow=true]
+ * @returns {object|null}
+ * @throws UndefinedFilterError
+ */
+QueryBuilder.prototype.getFilterById = function(id, doThrow) {
+ if (id == '-1') {
+ return null;
+ }
+
+ for (var i = 0, l = this.filters.length; i < l; i++) {
+ if (this.filters[i].id == id) {
+ return this.filters[i];
+ }
+ }
+
+ Utils.error(doThrow !== false, 'UndefinedFilter', 'Undefined filter "{0}"', id);
+
+ return null;
+};
+
+/**
+ * Returns a particular operator by its type
+ * @param {string} type
+ * @param {boolean} [doThrow=true]
+ * @returns {object|null}
+ * @throws UndefinedOperatorError
+ */
+QueryBuilder.prototype.getOperatorByType = function(type, doThrow) {
+ if (type == '-1') {
+ return null;
+ }
+
+ for (var i = 0, l = this.operators.length; i < l; i++) {
+ if (this.operators[i].type == type) {
+ return this.operators[i];
+ }
+ }
+
+ Utils.error(doThrow !== false, 'UndefinedOperator', 'Undefined operator "{0}"', type);
+
+ return null;
+};
+
+/**
+ * Returns rule's current input value
+ * @param {Rule} rule
+ * @returns {*}
+ * @fires QueryBuilder.changer:getRuleValue
+ * @private
+ */
+QueryBuilder.prototype.getRuleInputValue = function(rule) {
+ var filter = rule.filter;
+ var operator = rule.operator;
+ var value = [];
+
+ if (filter.valueGetter) {
+ value = filter.valueGetter.call(this, rule);
+ }
+ else {
+ var $value = rule.$el.find(QueryBuilder.selectors.value_container);
+
+ for (var i = 0; i < operator.nb_inputs; i++) {
+ var name = Utils.escapeElementId(rule.id + '_value_' + i);
+ var tmp;
+
+ switch (filter.input) {
+ case 'radio':
+ value.push($value.find('[name=' + name + ']:checked').val());
+ break;
+
+ case 'checkbox':
+ tmp = [];
+ $value.find('[name=' + name + ']:checked').each(function() {
+ tmp.push($(this).val());
+ });
+ value.push(tmp);
+ break;
+
+ case 'select':
+ if (filter.multiple) {
+ tmp = [];
+ $value.find('[name=' + name + '] option:selected').each(function() {
+ tmp.push($(this).val());
+ });
+ value.push(tmp);
+ }
+ else {
+ value.push($value.find('[name=' + name + '] option:selected').val());
+ }
+ break;
+
+ default:
+ value.push($value.find('[name=' + name + ']').val());
+ }
+ }
+
+ value = value.map(function(val) {
+ if (operator.multiple && filter.value_separator && typeof val == 'string') {
+ val = val.split(filter.value_separator);
+ }
+
+ if ($.isArray(val)) {
+ return val.map(function(subval) {
+ return Utils.changeType(subval, filter.type);
+ });
+ }
+ else {
+ return Utils.changeType(val, filter.type);
+ }
+ });
+
+ if (operator.nb_inputs === 1) {
+ value = value[0];
+ }
+
+ // @deprecated
+ if (filter.valueParser) {
+ value = filter.valueParser.call(this, rule, value);
+ }
+ }
+
+ /**
+ * Modifies the rule's value grabbed from the DOM
+ * @event changer:getRuleValue
+ * @memberof QueryBuilder
+ * @param {*} value
+ * @param {Rule} rule
+ * @returns {*}
+ */
+ return this.change('getRuleValue', value, rule);
+};
+
+/**
+ * Sets the value of a rule's input
+ * @param {Rule} rule
+ * @param {*} value
+ * @private
+ */
+QueryBuilder.prototype.setRuleInputValue = function(rule, value) {
+ var filter = rule.filter;
+ var operator = rule.operator;
+
+ if (!filter || !operator) {
+ return;
+ }
+
+ rule._updating_input = true;
+
+ if (filter.valueSetter) {
+ filter.valueSetter.call(this, rule, value);
+ }
+ else {
+ var $value = rule.$el.find(QueryBuilder.selectors.value_container);
+
+ if (operator.nb_inputs == 1) {
+ value = [value];
+ }
+
+ for (var i = 0; i < operator.nb_inputs; i++) {
+ var name = Utils.escapeElementId(rule.id + '_value_' + i);
+
+ switch (filter.input) {
+ case 'radio':
+ $value.find('[name=' + name + '][value="' + value[i] + '"]').prop('checked', true).trigger('change');
+ break;
+
+ case 'checkbox':
+ if (!$.isArray(value[i])) {
+ value[i] = [value[i]];
+ }
+ value[i].forEach(function(value) {
+ $value.find('[name=' + name + '][value="' + value + '"]').prop('checked', true).trigger('change');
+ });
+ break;
+
+ default:
+ if (operator.multiple && filter.value_separator && $.isArray(value[i])) {
+ value[i] = value[i].join(filter.value_separator);
+ }
+ $value.find('[name=' + name + ']').val(value[i]).trigger('change');
+ break;
+ }
+ }
+ }
+
+ rule._updating_input = false;
+};
+
+/**
+ * Parses rule flags
+ * @param {object} rule
+ * @returns {object}
+ * @fires QueryBuilder.changer:parseRuleFlags
+ * @private
+ */
+QueryBuilder.prototype.parseRuleFlags = function(rule) {
+ var flags = $.extend({}, this.settings.default_rule_flags);
+
+ if (rule.readonly) {
+ $.extend(flags, {
+ filter_readonly: true,
+ operator_readonly: true,
+ value_readonly: true,
+ no_delete: true
+ });
+ }
+
+ if (rule.flags) {
+ $.extend(flags, rule.flags);
+ }
+
+ /**
+ * Modifies the consolidated rule's flags
+ * @event changer:parseRuleFlags
+ * @memberof QueryBuilder
+ * @param {object} flags
+ * @param {object} rule - not a Rule object
+ * @returns {object}
+ */
+ return this.change('parseRuleFlags', flags, rule);
+};
+
+/**
+ * Gets a copy of flags of a rule
+ * @param {object} flags
+ * @param {boolean} [all=false] - return all flags or only changes from default flags
+ * @returns {object}
+ * @private
+ */
+QueryBuilder.prototype.getRuleFlags = function(flags, all) {
+ if (all) {
+ return $.extend({}, flags);
+ }
+ else {
+ var ret = {};
+ $.each(this.settings.default_rule_flags, function(key, value) {
+ if (flags[key] !== value) {
+ ret[key] = flags[key];
+ }
+ });
+ return ret;
+ }
+};
+
+/**
+ * Parses group flags
+ * @param {object} group
+ * @returns {object}
+ * @fires QueryBuilder.changer:parseGroupFlags
+ * @private
+ */
+QueryBuilder.prototype.parseGroupFlags = function(group) {
+ var flags = $.extend({}, this.settings.default_group_flags);
+
+ if (group.readonly) {
+ $.extend(flags, {
+ condition_readonly: true,
+ no_add_rule: true,
+ no_add_group: true,
+ no_delete: true
+ });
+ }
+
+ if (group.flags) {
+ $.extend(flags, group.flags);
+ }
+
+ /**
+ * Modifies the consolidated group's flags
+ * @event changer:parseGroupFlags
+ * @memberof QueryBuilder
+ * @param {object} flags
+ * @param {object} group - not a Group object
+ * @returns {object}
+ */
+ return this.change('parseGroupFlags', flags, group);
+};
+
+/**
+ * Gets a copy of flags of a group
+ * @param {object} flags
+ * @param {boolean} [all=false] - return all flags or only changes from default flags
+ * @returns {object}
+ * @private
+ */
+QueryBuilder.prototype.getGroupFlags = function(flags, all) {
+ if (all) {
+ return $.extend({}, flags);
+ }
+ else {
+ var ret = {};
+ $.each(this.settings.default_group_flags, function(key, value) {
+ if (flags[key] !== value) {
+ ret[key] = flags[key];
+ }
+ });
+ return ret;
+ }
+};
+
+/**
+ * Translate a label either by looking in the `lang` object or in itself if it's an object where keys are language codes
+ * @param {string} [category]
+ * @param {string|object} key
+ * @returns {string}
+ * @fires QueryBuilder.changer:translate
+ */
+QueryBuilder.prototype.translate = function(category, key) {
+ if (!key) {
+ key = category;
+ category = undefined;
+ }
+
+ var translation;
+ if (typeof key === 'object') {
+ translation = key[this.settings.lang_code] || key['en'];
+ }
+ else {
+ translation = (category ? this.lang[category] : this.lang)[key] || key;
+ }
+
+ /**
+ * Modifies the translated label
+ * @event changer:translate
+ * @memberof QueryBuilder
+ * @param {string} translation
+ * @param {string|object} key
+ * @param {string} [category]
+ * @returns {string}
+ */
+ return this.change('translate', translation, key, category);
+};
+
+/**
+ * Returns a validation message
+ * @param {object} validation
+ * @param {string} type
+ * @param {string} def
+ * @returns {string}
+ * @private
+ */
+QueryBuilder.prototype.getValidationMessage = function(validation, type, def) {
+ return validation.messages && validation.messages[type] || def;
+};
+
+
+QueryBuilder.templates.group = ({ group_id, level, conditions, icons, settings, translate, builder }) => {
+ return `
+`;
+};
+
+QueryBuilder.templates.rule = ({ rule_id, icons, settings, translate, builder }) => {
+ return `
+
+
+ ${settings.display_errors ? `
+
+ ` : ''}
+
+
+
+
`;
+};
+
+QueryBuilder.templates.filterSelect = ({ rule, filters, icons, settings, translate, builder }) => {
+ let optgroup = null;
+ return `
+`;
+};
+
+QueryBuilder.templates.operatorSelect = ({ rule, operators, icons, settings, translate, builder }) => {
+ let optgroup = null;
+ return `
+${operators.length === 1 ? `
+
+${translate("operators", operators[0].type)}
+
+` : ''}
+`;
+};
+
+QueryBuilder.templates.ruleValueSelect = ({ name, rule, icons, settings, translate, builder }) => {
+ let optgroup = null;
+ return `
+`;
+};
+
+/**
+ * Returns group's HTML
+ * @param {string} group_id
+ * @param {int} level
+ * @returns {string}
+ * @fires QueryBuilder.changer:getGroupTemplate
+ * @private
+ */
+QueryBuilder.prototype.getGroupTemplate = function (group_id, level) {
+ var h = this.templates.group({
+ builder: this,
+ group_id: group_id,
+ level: level,
+ conditions: this.settings.conditions,
+ icons: this.icons,
+ settings: this.settings,
+ translate: this.translate.bind(this)
+ }).trim();
+
+ /**
+ * Modifies the raw HTML of a group
+ * @event changer:getGroupTemplate
+ * @memberof QueryBuilder
+ * @param {string} html
+ * @param {int} level
+ * @returns {string}
+ */
+ return this.change('getGroupTemplate', h, level);
+};
+
+/**
+ * Returns rule's HTML
+ * @param {string} rule_id
+ * @returns {string}
+ * @fires QueryBuilder.changer:getRuleTemplate
+ * @private
+ */
+QueryBuilder.prototype.getRuleTemplate = function (rule_id) {
+ var h = this.templates.rule({
+ builder: this,
+ rule_id: rule_id,
+ icons: this.icons,
+ settings: this.settings,
+ translate: this.translate.bind(this)
+ }).trim();
+
+ /**
+ * Modifies the raw HTML of a rule
+ * @event changer:getRuleTemplate
+ * @memberof QueryBuilder
+ * @param {string} html
+ * @returns {string}
+ */
+ return this.change('getRuleTemplate', h);
+};
+
+/**
+ * Returns rule's filter HTML
+ * @param {Rule} rule
+ * @param {object[]} filters
+ * @returns {string}
+ * @fires QueryBuilder.changer:getRuleFilterTemplate
+ * @private
+ */
+QueryBuilder.prototype.getRuleFilterSelect = function (rule, filters) {
+ var h = this.templates.filterSelect({
+ builder: this,
+ rule: rule,
+ filters: filters,
+ icons: this.icons,
+ settings: this.settings,
+ translate: this.translate.bind(this)
+ }).trim();
+
+ /**
+ * Modifies the raw HTML of the rule's filter dropdown
+ * @event changer:getRuleFilterSelect
+ * @memberof QueryBuilder
+ * @param {string} html
+ * @param {Rule} rule
+ * @param {QueryBuilder.Filter[]} filters
+ * @returns {string}
+ */
+ return this.change('getRuleFilterSelect', h, rule, filters);
+};
+
+/**
+ * Returns rule's operator HTML
+ * @param {Rule} rule
+ * @param {object[]} operators
+ * @returns {string}
+ * @fires QueryBuilder.changer:getRuleOperatorTemplate
+ * @private
+ */
+QueryBuilder.prototype.getRuleOperatorSelect = function (rule, operators) {
+ var h = this.templates.operatorSelect({
+ builder: this,
+ rule: rule,
+ operators: operators,
+ icons: this.icons,
+ settings: this.settings,
+ translate: this.translate.bind(this)
+ }).trim();
+
+ /**
+ * Modifies the raw HTML of the rule's operator dropdown
+ * @event changer:getRuleOperatorSelect
+ * @memberof QueryBuilder
+ * @param {string} html
+ * @param {Rule} rule
+ * @param {QueryBuilder.Operator[]} operators
+ * @returns {string}
+ */
+ return this.change('getRuleOperatorSelect', h, rule, operators);
+};
+
+/**
+ * Returns the rule's value select HTML
+ * @param {string} name
+ * @param {Rule} rule
+ * @returns {string}
+ * @fires QueryBuilder.changer:getRuleValueSelect
+ * @private
+ */
+QueryBuilder.prototype.getRuleValueSelect = function (name, rule) {
+ var h = this.templates.ruleValueSelect({
+ builder: this,
+ name: name,
+ rule: rule,
+ icons: this.icons,
+ settings: this.settings,
+ translate: this.translate.bind(this)
+ }).trim();
+
+ /**
+ * Modifies the raw HTML of the rule's value dropdown (in case of a "select filter)
+ * @event changer:getRuleValueSelect
+ * @memberof QueryBuilder
+ * @param {string} html
+ * @param [string} name
+ * @param {Rule} rule
+ * @returns {string}
+ */
+ return this.change('getRuleValueSelect', h, name, rule);
+};
+
+/**
+ * Returns the rule's value HTML
+ * @param {Rule} rule
+ * @param {int} value_id
+ * @returns {string}
+ * @fires QueryBuilder.changer:getRuleInput
+ * @private
+ */
+QueryBuilder.prototype.getRuleInput = function (rule, value_id) {
+ var filter = rule.filter;
+ var validation = rule.filter.validation || {};
+ var name = rule.id + '_value_' + value_id;
+ var c = filter.vertical ? ' class=block' : '';
+ var h = '';
+ var placeholder = Array.isArray(filter.placeholder) ? filter.placeholder[value_id] : filter.placeholder;
+
+ if (typeof filter.input == 'function') {
+ h = filter.input.call(this, rule, name);
+ }
+ else {
+ switch (filter.input) {
+ case 'radio':
+ case 'checkbox':
+ Utils.iterateOptions(filter.values, function (key, val) {
+ h += ' ';
+ });
+ break;
+
+ case 'select':
+ h = this.getRuleValueSelect(name, rule);
+ break;
+
+ case 'textarea':
+ h += '';
+ break;
+
+ case 'number':
+ h += '" ; + break + + default + h +='" ; + } + } + + + * Modifies the raw HTML of the rule s input + * @event changer:getRuleInput + * @memberof QueryBuilder + * @param {string html + * @param {Rule rule + * @param {string name - the name that the input must have + * @returns {string + * + return this.change getRuleInput , h rule name + + + + + * @namespace + * +var Utils="{};" + + + * @member {object + * @memberof QueryBuilder + * @see Utils + * +QueryBuilder.utils="Utils;" + + + * @callback Utils#OptionsIteratee + * @param {string key + * @param {string value + * @param {string [optgroup + * + + + * Iterates over radio/checkbox/selection options it accept four formats + * + * @example + * array of values + * options="['one'," two , three ] + * @example + * simple key-value map + * options="{1:" one , 2 two , 3 three } + * @example + * array of 1-element maps + * options="[{1:" one } {2 two } {3 three } + * @example + * array of elements + * options="[{value:" 1 label one , optgroup group } {value 2 label two } + * + * @param {object|array options + * @param {Utils#OptionsIteratee tpl + * +Utils.iterateOptions="function(options," tpl { + if (options { + if ($.isArray(options { + options.forEach(function(entry { + if ($.isPlainObject(entry { + array of elements + if ( value in entry { + tpl(entry.value entry.label | entry.value entry.optgroup + } + array of one-element maps + else { + $.each(entry function(key val { + tpl(key val + return false break after first entry + } + } + } + array of values + else { + tpl(entry entry + } + } + } + unordered map + else { + $.each(options function(key val { + tpl(key val + } + } + } + + + + * Replaces {0 {1 . in a string + * @param {string str + * @param { args + * @returns {string + * +Utils.fmt="function(str," args { + if (!Array.isArray(args { + args="Array.prototype.slice.call(arguments," 1 + } + + return str.replace(/{([0-9]+)}/g function(m i { + return args[parseInt(i + } + + + + * Throws an Error object with custom name or logs an error + * @param {boolean [doThrow="true]" + * @param {string type + * @param {string message + * @param { args + * +Utils.error="function()" { + var i="0;" + var doThrow="typeof" arguments[i="==" boolean ? arguments[i : true + var type="arguments[i++];" + var message="arguments[i++];" + var args="Array.isArray(arguments[i])" ? arguments[i : Array.prototype.slice.call(arguments i + + if (doThrow { + var err="new" Error(Utils.fmt(message args + err.name="type" + Error ; + err.args="args;" + throw err + } + else { + console.error(type + Error + Utils.fmt(message args + } + + + + * Changes the type of a value to int float or bool + * @param { value + * @param {string type - integer , double , boolean or anything else (passthrough + * @returns { + * +Utils.changeType="function(value," type { + if (value="==" | value="==" undefined { + return undefined + } + + switch (type { + @formatter:off + case integer : + if (typeof value="==" string & !/^-?\d+$/.test(value { + return value + } + return parseInt(value + case double : + if (typeof value="==" string & !/^-?\d+\.?\d*$/.test(value { + return value + } + return parseFloat(value + case boolean : + if (typeof value="==" string & !/^(0|1|true|false){1}$/i.test(value { + return value + } + return value="==" true | value="==" 1 | value.toLowerCase="==" true | value="==" 1 ; + default return value + @formatter:on + } + + + + * Escapes a string like PHP s mysql_real_escape_string does + * @param {string value + * @param {string [additionalEscape additionnal chars to escape + * @returns {string + * +Utils.escapeString="function(value," additionalEscape { + if (typeof value !="string" ) { + return value + } + + var escaped="value" + .replace(/[\0\n\r\b \ ]/g function(s { + switch (s { + @formatter:off + case \0 : return \\0 ; + case \n : return \\n ; + case \r : return \\r ; + case \b : return \\b ; + case \ : return \ \ ; + default return \ + s + @formatter:off + } + } + uglify compliant + .replace(/\t/g \\t ) + .replace(/\x1a/g \\Z ) + + if (additionalEscape { + escaped="escaped" + .replace(new RegExp [ + additionalEscape + ] , g ) function(s { + return \ + s + } + } + + return escaped + + + + * Escapes a string for use in regex + * @param {string str + * @returns {string + * +Utils.escapeRegExp="function(str)" { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g \ ) + + + + * Escapes a string for use in HTML element id + * @param {string str + * @returns {string + * +Utils.escapeElementId="function(str)" { + Regex based on that suggested by + https://learn.jquery.com/using-jquery-core/faq/how-do-i-select-an-element-by-an-id-that-has-characters-used-in-css-notation + - escapes : . [ ] , + - avoids escaping already escaped values + return (str ? str.replace(/(\\)?([:.\[\],])/g + function $0 $1 $2 ) { return $1 ? $0 : \ + $2 } : str + + + + * Sorts objects by grouping them by `key preserving initial order when possible + * @param {object items + * @param {string key + * @returns {object + * +Utils.groupSort="function(items," key { + var optgroups="[];" + var newItems="[];" + + items.forEach(function(item { + var idx + + if (item[key { + idx="optgroups.lastIndexOf(item[key]);" + + if (idx="=" -1 { + idx="optgroups.length;" + } + else { + idx + } + } + else { + idx="optgroups.length;" + } + + optgroups.splice(idx 0 item[key + newItems.splice(idx 0 item + } + + return newItems + + + + * Defines properties on an Node prototype with getter and setter />
+ * Update events are emitted in the setter through root Model (if any).
+ * The object must have a `__` object, non enumerable property to store values.
+ * @param {function} obj
+ * @param {string[]} fields
+ */
+Utils.defineModelProperties = function(obj, fields) {
+ fields.forEach(function(field) {
+ Object.defineProperty(obj.prototype, field, {
+ enumerable: true,
+ get: function() {
+ return this.__[field];
+ },
+ set: function(value) {
+ var previousValue = (this.__[field] !== null && typeof this.__[field] == 'object') ?
+ $.extend({}, this.__[field]) :
+ this.__[field];
+
+ this.__[field] = value;
+
+ if (this.model !== null) {
+ /**
+ * After a value of the model changed
+ * @event model:update
+ * @memberof Model
+ * @param {Node} node
+ * @param {string} field
+ * @param {*} value
+ * @param {*} previousValue
+ */
+ this.model.trigger('update', this, field, value, previousValue);
+ }
+ }
+ });
+ });
+};
+
+
+/**
+ * Main object storing data model and emitting model events
+ * @constructor
+ */
+function Model() {
+ /**
+ * @member {Group}
+ * @readonly
+ */
+ this.root = null;
+
+ /**
+ * Base for event emitting
+ * @member {jQuery}
+ * @readonly
+ * @private
+ */
+ this.$ = $(this);
+}
+
+$.extend(Model.prototype, /** @lends Model.prototype */ {
+ /**
+ * Triggers an event on the model
+ * @param {string} type
+ * @returns {$.Event}
+ */
+ trigger: function(type) {
+ var event = new $.Event(type);
+ this.$.triggerHandler(event, Array.prototype.slice.call(arguments, 1));
+ return event;
+ },
+
+ /**
+ * Attaches an event listener on the model
+ * @param {string} type
+ * @param {function} cb
+ * @returns {Model}
+ */
+ on: function() {
+ this.$.on.apply(this.$, Array.prototype.slice.call(arguments));
+ return this;
+ },
+
+ /**
+ * Removes an event listener from the model
+ * @param {string} type
+ * @param {function} [cb]
+ * @returns {Model}
+ */
+ off: function() {
+ this.$.off.apply(this.$, Array.prototype.slice.call(arguments));
+ return this;
+ },
+
+ /**
+ * Attaches an event listener called once on the model
+ * @param {string} type
+ * @param {function} cb
+ * @returns {Model}
+ */
+ once: function() {
+ this.$.one.apply(this.$, Array.prototype.slice.call(arguments));
+ return this;
+ }
+});
+
+
+/**
+ * Root abstract object
+ * @constructor
+ * @param {Node} [parent]
+ * @param {jQuery} $el
+ */
+var Node = function(parent, $el) {
+ if (!(this instanceof Node)) {
+ return new Node(parent, $el);
+ }
+
+ Object.defineProperty(this, '__', { value: {} });
+
+ $el.data('queryBuilderModel', this);
+
+ /**
+ * @name level
+ * @member {int}
+ * @memberof Node
+ * @instance
+ * @readonly
+ */
+ this.__.level = 1;
+
+ /**
+ * @name error
+ * @member {string}
+ * @memberof Node
+ * @instance
+ */
+ this.__.error = null;
+
+ /**
+ * @name flags
+ * @member {object}
+ * @memberof Node
+ * @instance
+ * @readonly
+ */
+ this.__.flags = {};
+
+ /**
+ * @name data
+ * @member {object}
+ * @memberof Node
+ * @instance
+ */
+ this.__.data = undefined;
+
+ /**
+ * @member {jQuery}
+ * @readonly
+ */
+ this.$el = $el;
+
+ /**
+ * @member {string}
+ * @readonly
+ */
+ this.id = $el[0].id;
+
+ /**
+ * @member {Model}
+ * @readonly
+ */
+ this.model = null;
+
+ /**
+ * @member {Group}
+ * @readonly
+ */
+ this.parent = parent;
+};
+
+Utils.defineModelProperties(Node, ['level', 'error', 'data', 'flags']);
+
+Object.defineProperty(Node.prototype, 'parent', {
+ enumerable: true,
+ get: function() {
+ return this.__.parent;
+ },
+ set: function(value) {
+ this.__.parent = value;
+ this.level = value === null ? 1 : value.level + 1;
+ this.model = value === null ? null : value.model;
+ }
+});
+
+/**
+ * Checks if this Node is the root
+ * @returns {boolean}
+ */
+Node.prototype.isRoot = function() {
+ return (this.level === 1);
+};
+
+/**
+ * Returns the node position inside its parent
+ * @returns {int}
+ */
+Node.prototype.getPos = function() {
+ if (this.isRoot()) {
+ return -1;
+ }
+ else {
+ return this.parent.getNodePos(this);
+ }
+};
+
+/**
+ * Deletes self
+ * @fires Model.model:drop
+ */
+Node.prototype.drop = function() {
+ var model = this.model;
+
+ if (!!this.parent) {
+ this.parent.removeNode(this);
+ }
+
+ this.$el.removeData('queryBuilderModel');
+
+ if (model !== null) {
+ /**
+ * After a node of the model has been removed
+ * @event model:drop
+ * @memberof Model
+ * @param {Node} node
+ */
+ model.trigger('drop', this);
+ }
+};
+
+/**
+ * Moves itself after another Node
+ * @param {Node} target
+ * @fires Model.model:move
+ */
+Node.prototype.moveAfter = function(target) {
+ if (!this.isRoot()) {
+ this.move(target.parent, target.getPos() + 1);
+ }
+};
+
+/**
+ * Moves itself at the beginning of parent or another Group
+ * @param {Group} [target]
+ * @fires Model.model:move
+ */
+Node.prototype.moveAtBegin = function(target) {
+ if (!this.isRoot()) {
+ if (target === undefined) {
+ target = this.parent;
+ }
+
+ this.move(target, 0);
+ }
+};
+
+/**
+ * Moves itself at the end of parent or another Group
+ * @param {Group} [target]
+ * @fires Model.model:move
+ */
+Node.prototype.moveAtEnd = function(target) {
+ if (!this.isRoot()) {
+ if (target === undefined) {
+ target = this.parent;
+ }
+
+ this.move(target, target.length() === 0 ? 0 : target.length() - 1);
+ }
+};
+
+/**
+ * Moves itself at specific position of Group
+ * @param {Group} target
+ * @param {int} index
+ * @fires Model.model:move
+ */
+Node.prototype.move = function(target, index) {
+ if (!this.isRoot()) {
+ if (typeof target === 'number') {
+ index = target;
+ target = this.parent;
+ }
+
+ this.parent.removeNode(this);
+ target.insertNode(this, index, false);
+
+ if (this.model !== null) {
+ /**
+ * After a node of the model has been moved
+ * @event model:move
+ * @memberof Model
+ * @param {Node} node
+ * @param {Node} target
+ * @param {int} index
+ */
+ this.model.trigger('move', this, target, index);
+ }
+ }
+};
+
+
+/**
+ * Group object
+ * @constructor
+ * @extends Node
+ * @param {Group} [parent]
+ * @param {jQuery} $el
+ */
+var Group = function(parent, $el) {
+ if (!(this instanceof Group)) {
+ return new Group(parent, $el);
+ }
+
+ Node.call(this, parent, $el);
+
+ /**
+ * @member {object[]}
+ * @readonly
+ */
+ this.rules = [];
+
+ /**
+ * @name condition
+ * @member {string}
+ * @memberof Group
+ * @instance
+ */
+ this.__.condition = null;
+};
+
+Group.prototype = Object.create(Node.prototype);
+Group.prototype.constructor = Group;
+
+Utils.defineModelProperties(Group, ['condition']);
+
+/**
+ * Removes group's content
+ */
+Group.prototype.empty = function() {
+ this.each('reverse', function(rule) {
+ rule.drop();
+ }, function(group) {
+ group.drop();
+ });
+};
+
+/**
+ * Deletes self
+ */
+Group.prototype.drop = function() {
+ this.empty();
+ Node.prototype.drop.call(this);
+};
+
+/**
+ * Returns the number of children
+ * @returns {int}
+ */
+Group.prototype.length = function() {
+ return this.rules.length;
+};
+
+/**
+ * Adds a Node at specified index
+ * @param {Node} node
+ * @param {int} [index=end]
+ * @param {boolean} [trigger=false] - fire 'add' event
+ * @returns {Node} the inserted node
+ * @fires Model.model:add
+ */
+Group.prototype.insertNode = function(node, index, trigger) {
+ if (index === undefined) {
+ index = this.length();
+ }
+
+ this.rules.splice(index, 0, node);
+ node.parent = this;
+
+ if (trigger && this.model !== null) {
+ /**
+ * After a node of the model has been added
+ * @event model:add
+ * @memberof Model
+ * @param {Node} parent
+ * @param {Node} node
+ * @param {int} index
+ */
+ this.model.trigger('add', this, node, index);
+ }
+
+ return node;
+};
+
+/**
+ * Adds a new Group at specified index
+ * @param {jQuery} $el
+ * @param {int} [index=end]
+ * @returns {Group}
+ * @fires Model.model:add
+ */
+Group.prototype.addGroup = function($el, index) {
+ return this.insertNode(new Group(this, $el), index, true);
+};
+
+/**
+ * Adds a new Rule at specified index
+ * @param {jQuery} $el
+ * @param {int} [index=end]
+ * @returns {Rule}
+ * @fires Model.model:add
+ */
+Group.prototype.addRule = function($el, index) {
+ return this.insertNode(new Rule(this, $el), index, true);
+};
+
+/**
+ * Deletes a specific Node
+ * @param {Node} node
+ */
+Group.prototype.removeNode = function(node) {
+ var index = this.getNodePos(node);
+ if (index !== -1) {
+ node.parent = null;
+ this.rules.splice(index, 1);
+ }
+};
+
+/**
+ * Returns the position of a child Node
+ * @param {Node} node
+ * @returns {int}
+ */
+Group.prototype.getNodePos = function(node) {
+ return this.rules.indexOf(node);
+};
+
+/**
+ * @callback Model#GroupIteratee
+ * @param {Node} node
+ * @returns {boolean} stop the iteration
+ */
+
+/**
+ * Iterate over all Nodes
+ * @param {boolean} [reverse=false] - iterate in reverse order, required if you delete nodes
+ * @param {Model#GroupIteratee} cbRule - callback for Rules (can be `null` but not omitted)
+ * @param {Model#GroupIteratee} [cbGroup] - callback for Groups
+ * @param {object} [context] - context for callbacks
+ * @returns {boolean} if the iteration has been stopped by a callback
+ */
+Group.prototype.each = function(reverse, cbRule, cbGroup, context) {
+ if (typeof reverse !== 'boolean' && typeof reverse !== 'string') {
+ context = cbGroup;
+ cbGroup = cbRule;
+ cbRule = reverse;
+ reverse = false;
+ }
+ context = context === undefined ? null : context;
+
+ var i = reverse ? this.rules.length - 1 : 0;
+ var l = reverse ? 0 : this.rules.length - 1;
+ var c = reverse ? -1 : 1;
+ var next = function() {
+ return reverse ? i >= l : i <= l;
+ };
+ var stop = false;
+
+ for (; next(); i += c) {
+ if (this.rules[i] instanceof Group) {
+ if (!!cbGroup) {
+ stop = cbGroup.call(context, this.rules[i]) === false;
+ }
+ }
+ else if (!!cbRule) {
+ stop = cbRule.call(context, this.rules[i]) === false;
+ }
+
+ if (stop) {
+ break;
+ }
+ }
+
+ return !stop;
+};
+
+/**
+ * Checks if the group contains a particular Node
+ * @param {Node} node
+ * @param {boolean} [recursive=false]
+ * @returns {boolean}
+ */
+Group.prototype.contains = function(node, recursive) {
+ if (this.getNodePos(node) !== -1) {
+ return true;
+ }
+ else if (!recursive) {
+ return false;
+ }
+ else {
+ // the loop will return with false as soon as the Node is found
+ return !this.each(function() {
+ return true;
+ }, function(group) {
+ return !group.contains(node, true);
+ });
+ }
+};
+
+
+/**
+ * Rule object
+ * @constructor
+ * @extends Node
+ * @param {Group} parent
+ * @param {jQuery} $el
+ */
+var Rule = function(parent, $el) {
+ if (!(this instanceof Rule)) {
+ return new Rule(parent, $el);
+ }
+
+ Node.call(this, parent, $el);
+
+ this._updating_value = false;
+ this._updating_input = false;
+
+ /**
+ * @name filter
+ * @member {QueryBuilder.Filter}
+ * @memberof Rule
+ * @instance
+ */
+ this.__.filter = null;
+
+ /**
+ * @name operator
+ * @member {QueryBuilder.Operator}
+ * @memberof Rule
+ * @instance
+ */
+ this.__.operator = null;
+
+ /**
+ * @name value
+ * @member {*}
+ * @memberof Rule
+ * @instance
+ */
+ this.__.value = undefined;
+};
+
+Rule.prototype = Object.create(Node.prototype);
+Rule.prototype.constructor = Rule;
+
+Utils.defineModelProperties(Rule, ['filter', 'operator', 'value']);
+
+/**
+ * Checks if this Node is the root
+ * @returns {boolean} always false
+ */
+Rule.prototype.isRoot = function() {
+ return false;
+};
+
+
+/**
+ * @member {function}
+ * @memberof QueryBuilder
+ * @see Group
+ */
+QueryBuilder.Group = Group;
+
+/**
+ * @member {function}
+ * @memberof QueryBuilder
+ * @see Rule
+ */
+QueryBuilder.Rule = Rule;
+
+
+/**
+ * The {@link http://learn.jquery.com/plugins/|jQuery Plugins} namespace
+ * @external "jQuery.fn"
+ */
+
+/**
+ * Instanciates or accesses the {@link QueryBuilder} on an element
+ * @function
+ * @memberof external:"jQuery.fn"
+ * @param {*} option - initial configuration or method name
+ * @param {...*} args - method arguments
+ *
+ * @example
+ * $('#builder').queryBuilder({ /** configuration object *\/ });
+ * @example
+ * $('#builder').queryBuilder('methodName', methodParam1, methodParam2);
+ */
+$.fn.queryBuilder = function(option) {
+ if (this.length === 0) {
+ Utils.error('Config', 'No target defined');
+ }
+ if (this.length > 1) {
+ Utils.error('Config', 'Unable to initialize on multiple target');
+ }
+
+ var data = this.data('queryBuilder');
+ var options = (typeof option == 'object' && option) || {};
+
+ if (!data && option == 'destroy') {
+ return this;
+ }
+ if (!data) {
+ var builder = new QueryBuilder(this, options);
+ this.data('queryBuilder', builder);
+ builder.init(options.rules);
+ }
+ if (typeof option == 'string') {
+ return data[option].apply(data, Array.prototype.slice.call(arguments, 1));
+ }
+
+ return this;
+};
+
+/**
+ * @function
+ * @memberof external:"jQuery.fn"
+ * @see QueryBuilder
+ */
+$.fn.queryBuilder.constructor = QueryBuilder;
+
+/**
+ * @function
+ * @memberof external:"jQuery.fn"
+ * @see QueryBuilder.defaults
+ */
+$.fn.queryBuilder.defaults = QueryBuilder.defaults;
+
+/**
+ * @function
+ * @memberof external:"jQuery.fn"
+ * @see QueryBuilder.defaults
+ */
+$.fn.queryBuilder.extend = QueryBuilder.extend;
+
+/**
+ * @function
+ * @memberof external:"jQuery.fn"
+ * @see QueryBuilder.define
+ */
+$.fn.queryBuilder.define = QueryBuilder.define;
+
+/**
+ * @function
+ * @memberof external:"jQuery.fn"
+ * @see QueryBuilder.regional
+ */
+$.fn.queryBuilder.regional = QueryBuilder.regional;
+
+
+/**
+ * @class BtCheckbox
+ * @memberof module:plugins
+ * @description Applies Awesome Bootstrap Checkbox for checkbox and radio inputs.
+ * @param {object} [options]
+ * @param {string} [options.font='bootstrap-icons']
+ * @param {string} [options.color='default']
+ */
+QueryBuilder.define('bt-checkbox', function(options) {
+ if (options.font === 'bootstrap-icons') {
+ this.$el.addClass('bt-checkbox-bootstrap-icons');
+ }
+
+ this.on('getRuleInput.filter', function(h, rule, name) {
+ var filter = rule.filter;
+
+ if ((filter.input === 'radio' || filter.input === 'checkbox') && !filter.plugin) {
+ h.value = '';
+
+ if (!filter.colors) {
+ filter.colors = {};
+ }
+ if (filter.color) {
+ filter.colors._def_ = filter.color;
+ }
+
+ var style = filter.vertical ? ' style="display:block"' : '';
+ var i = 0;
+
+ Utils.iterateOptions(filter.values, function(key, val) {
+ var color = filter.colors[key] || filter.colors._def_ || options.color;
+ var id = name + '_' + (i++);
+
+ h.value += `
`;
+ });
+ }
+ });
+}, {
+ font: 'bootstrap-icons',
+ color: 'default'
+});
+
+
+/**
+ * @class BtTooltipErrors
+ * @memberof module:plugins
+ * @description Applies Bootstrap Tooltips on validation error messages.
+ * @param {object} [options]
+ * @param {string} [options.placement='right']
+ * @throws MissingLibraryError
+ */
+QueryBuilder.define('bt-tooltip-errors', function(options) {
+ if (! typeof bootstrap.Tooltip === "function") {
+ alert(typeof bootstrap.Tooltip );
+ Utils.error('MissingLibrary', 'Bootstrap Popper is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com');
+ }
+
+ var self = this;
+
+ // add BT Tooltip data
+ this.on('getRuleTemplate.filter getGroupTemplate.filter', function(h) {
+ var $h = $($.parseHTML(h.value));
+ $h.find(QueryBuilder.selectors.error_container).attr('data-bs-toggle', 'tooltip');
+ h.value = $h.prop('outerHTML');
+ });
+
+ // init/refresh tooltip when title changes
+ this.model.on('update', function(e, node, field) {
+ if (field == 'error' && self.settings.display_errors) {
+ node.$el.find(QueryBuilder.selectors.error_container).eq(0)
+ .attr('data-bs-original-title',options).attr('data-bs-title',options).tooltip();
+ }
+ });
+}, {
+ placement: 'right'
+});
+
+
+/**
+ * @class ChangeFilters
+ * @memberof module:plugins
+ * @description Allows to change available filters after plugin initialization.
+ */
+
+QueryBuilder.extend(/** @lends module:plugins.ChangeFilters.prototype */ {
+ /**
+ * Change the filters of the builder
+ * @param {boolean} [deleteOrphans=false] - delete rules using old filters
+ * @param {QueryBuilder[]} filters
+ * @fires module:plugins.ChangeFilters.changer:setFilters
+ * @fires module:plugins.ChangeFilters.afterSetFilters
+ * @throws ChangeFilterError
+ */
+ setFilters: function(deleteOrphans, filters) {
+ var self = this;
+
+ if (filters === undefined) {
+ filters = deleteOrphans;
+ deleteOrphans = false;
+ }
+
+ filters = this.checkFilters(filters);
+
+ /**
+ * Modifies the filters before {@link module:plugins.ChangeFilters.setFilters} method
+ * @event changer:setFilters
+ * @memberof module:plugins.ChangeFilters
+ * @param {QueryBuilder.Filter[]} filters
+ * @returns {QueryBuilder.Filter[]}
+ */
+ filters = this.change('setFilters', filters);
+
+ var filtersIds = filters.map(function(filter) {
+ return filter.id;
+ });
+
+ // check for orphans
+ if (!deleteOrphans) {
+ (function checkOrphans(node) {
+ node.each(
+ function(rule) {
+ if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
+ Utils.error('ChangeFilter', 'A rule is using filter "{0}"', rule.filter.id);
+ }
+ },
+ checkOrphans
+ );
+ }(this.model.root));
+ }
+
+ // replace filters
+ this.filters = filters;
+
+ // apply on existing DOM
+ (function updateBuilder(node) {
+ node.each(true,
+ function(rule) {
+ if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
+ rule.drop();
+
+ self.trigger('rulesChanged');
+ }
+ else {
+ self.createRuleFilters(rule);
+
+ rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
+ self.trigger('afterUpdateRuleFilter', rule);
+ }
+ },
+ updateBuilder
+ );
+ }(this.model.root));
+
+ // update plugins
+ if (this.settings.plugins) {
+ if (this.settings.plugins['unique-filter']) {
+ this.updateDisabledFilters();
+ }
+ if (this.settings.plugins['bt-selectpicker']) {
+ this.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render');
+ }
+ }
+
+ // reset the default_filter if does not exist anymore
+ if (this.settings.default_filter) {
+ try {
+ this.getFilterById(this.settings.default_filter);
+ }
+ catch (e) {
+ this.settings.default_filter = null;
+ }
+ }
+
+ /**
+ * After {@link module:plugins.ChangeFilters.setFilters} method
+ * @event afterSetFilters
+ * @memberof module:plugins.ChangeFilters
+ * @param {QueryBuilder.Filter[]} filters
+ */
+ this.trigger('afterSetFilters', filters);
+ },
+
+ /**
+ * Adds a new filter to the builder
+ * @param {QueryBuilder.Filter|Filter[]} newFilters
+ * @param {int|string} [position=#end] - index or '#start' or '#end'
+ * @fires module:plugins.ChangeFilters.changer:setFilters
+ * @fires module:plugins.ChangeFilters.afterSetFilters
+ * @throws ChangeFilterError
+ */
+ addFilter: function(newFilters, position) {
+ if (position === undefined || position == '#end') {
+ position = this.filters.length;
+ }
+ else if (position == '#start') {
+ position = 0;
+ }
+
+ if (!$.isArray(newFilters)) {
+ newFilters = [newFilters];
+ }
+
+ var filters = $.extend(true, [], this.filters);
+
+ // numeric position
+ if (parseInt(position) == position) {
+ Array.prototype.splice.apply(filters, [position, 0].concat(newFilters));
+ }
+ else {
+ // after filter by its id
+ if (this.filters.some(function(filter, index) {
+ if (filter.id == position) {
+ position = index + 1;
+ return true;
+ }
+ })
+ ) {
+ Array.prototype.splice.apply(filters, [position, 0].concat(newFilters));
+ }
+ // defaults to end of list
+ else {
+ Array.prototype.push.apply(filters, newFilters);
+ }
+ }
+
+ this.setFilters(filters);
+ },
+
+ /**
+ * Removes a filter from the builder
+ * @param {string|string[]} filterIds
+ * @param {boolean} [deleteOrphans=false] delete rules using old filters
+ * @fires module:plugins.ChangeFilters.changer:setFilters
+ * @fires module:plugins.ChangeFilters.afterSetFilters
+ * @throws ChangeFilterError
+ */
+ removeFilter: function(filterIds, deleteOrphans) {
+ var filters = $.extend(true, [], this.filters);
+ if (typeof filterIds === 'string') {
+ filterIds = [filterIds];
+ }
+
+ filters = filters.filter(function(filter) {
+ return filterIds.indexOf(filter.id) === -1;
+ });
+
+ this.setFilters(deleteOrphans, filters);
+ }
+});
+
+
+/**
+ * @class ChosenSelectpicker
+ * @memberof module:plugins
+ * @descriptioon Applies chosen-js Select on filters and operators combo-boxes.
+ * @param {object} [options] Supports all the options for chosen
+ * @throws MissingLibraryError
+ */
+QueryBuilder.define('chosen-selectpicker', function(options) {
+
+ if (!$.fn.chosen) {
+ Utils.error('MissingLibrary', 'chosen is required to use "chosen-selectpicker" plugin. Get it here: https://github.com/harvesthq/chosen');
+ }
+
+ if (this.settings.plugins['bt-selectpicker']) {
+ Utils.error('Conflict', 'bt-selectpicker is already selected as the dropdown plugin. Please remove chosen-selectpicker from the plugin list');
+ }
+
+ var Selectors = QueryBuilder.selectors;
+
+ // init selectpicker
+ this.on('afterCreateRuleFilters', function(e, rule) {
+ rule.$el.find(Selectors.rule_filter).removeClass('form-control').chosen(options);
+ });
+
+ this.on('afterCreateRuleOperators', function(e, rule) {
+ if (e.builder.getOperators(rule.filter).length > 1) {
+ rule.$el.find(Selectors.rule_operator).removeClass('form-control').chosen(options);
+ }
+ });
+
+ // update selectpicker on change
+ this.on('afterUpdateRuleFilter', function(e, rule) {
+ rule.$el.find(Selectors.rule_filter).trigger('chosen:updated');
+ });
+
+ this.on('afterUpdateRuleOperator', function(e, rule) {
+ rule.$el.find(Selectors.rule_operator).trigger('chosen:updated');
+ });
+
+ this.on('beforeDeleteRule', function(e, rule) {
+ rule.$el.find(Selectors.rule_filter).chosen('destroy');
+ rule.$el.find(Selectors.rule_operator).chosen('destroy');
+ });
+});
+
+
+/**
+ * @class FilterDescription
+ * @memberof module:plugins
+ * @description Provides three ways to display a description about a filter: inline, Bootsrap Popover or Bootbox.
+ * @param {object} [options]
+ * @param {string} [options.icon='bi-info-circle-fill']
+ * @param {string} [options.mode='popover'] - inline, popover or bootbox
+ * @throws ConfigError
+ */
+QueryBuilder.define('filter-description', function(options) {
+ // INLINE
+ if (options.mode === 'inline') {
+ this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
+ var $p = rule.$el.find('p.filter-description');
+ var description = e.builder.getFilterDescription(rule.filter, rule);
+
+ if (!description) {
+ $p.hide();
+ }
+ else {
+ if ($p.length === 0) {
+ $p = $($.parseHTML(''));
+ $p.appendTo(rule.$el);
+ }
+ else {
+ $p.css('display', '');
+ }
+
+ $p.html(' ' + description);
+ }
+ });
+ }
+ // POPOVER
+ else if (options.mode === 'popover') {
+ if (!$.fn.popover || !$.fn.popover.Constructor || !$.fn.popover.Constructor.prototype.fixTitle) {
+ Utils.error('MissingLibrary', 'Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com');
+ }
+
+ this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
+ var $b = rule.$el.find('button.filter-description');
+ var description = e.builder.getFilterDescription(rule.filter, rule);
+
+ if (!description) {
+ $b.hide();
+
+ if ($b.data('bs-popover')) {
+ $b.popover('hide');
+ }
+ }
+ else {
+ if ($b.length === 0) {
+ $b = $($.parseHTML(''));
+ $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions));
+ const popover = new bootstrap.Popover($b.get(0), {
+ placement: 'left',
+ container: 'body',
+ html: true
+ })
+ $b.on('mouseout', function() {
+ popover('hide');
+ });
+ }
+ else {
+ $b.css('display', '');
+ }
+
+ $b.data('bs-popover').options.content = description;
+
+ if ($b.attr('aria-describedby')) {
+ $b.popover('show');
+ }
+ }
+ });
+ }
+ // BOOTBOX
+ else if (options.mode === 'bootbox') {
+ if (!('bootbox' in window)) {
+ Utils.error('MissingLibrary', 'Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com');
+ }
+
+ this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
+ var $b = rule.$el.find('button.filter-description');
+ var description = e.builder.getFilterDescription(rule.filter, rule);
+
+ if (!description) {
+ $b.hide();
+ }
+ else {
+ if ($b.length === 0) {
+ $b = $($.parseHTML(''));
+ $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions));
+
+ $b.on('click', function() {
+ bootbox.alert($b.data('description'));
+ });
+ }
+ else {
+ $b.css('display', '');
+ }
+
+ $b.data('description', description);
+ }
+ });
+ }
+}, {
+ icon: 'bi-info-circle-fill',
+ mode: 'popover'
+});
+
+QueryBuilder.extend(/** @lends module:plugins.FilterDescription.prototype */ {
+ /**
+ * Returns the description of a filter for a particular rule (if present)
+ * @param {object} filter
+ * @param {Rule} [rule]
+ * @returns {string}
+ * @private
+ */
+ getFilterDescription: function(filter, rule) {
+ if (!filter) {
+ return undefined;
+ }
+ else if (typeof filter.description == 'function') {
+ return filter.description.call(this, rule);
+ }
+ else {
+ return filter.description;
+ }
+ }
+});
+
+
+/**
+ * @class Invert
+ * @memberof module:plugins
+ * @description Allows to invert a rule operator, a group condition or the entire builder.
+ * @param {object} [options]
+ * @param {string} [options.icon='bi-shuffle']
+ * @param {boolean} [options.recursive=true]
+ * @param {boolean} [options.invert_rules=true]
+ * @param {boolean} [options.display_rules_button=false]
+ * @param {boolean} [options.silent_fail=false]
+ */
+QueryBuilder.define('invert', function(options) {
+ var self = this;
+ var Selectors = QueryBuilder.selectors;
+
+ // Bind events
+ this.on('afterInit', function() {
+ self.$el.on('click.queryBuilder', '[data-invert=group]', function() {
+ var $group = $(this).closest(Selectors.group_container);
+ self.invert(self.getModel($group), options);
+ });
+
+ if (options.display_rules_button && options.invert_rules) {
+ self.$el.on('click.queryBuilder', '[data-invert=rule]', function() {
+ var $rule = $(this).closest(Selectors.rule_container);
+ self.invert(self.getModel($rule), options);
+ });
+ }
+ });
+
+ // Modify templates
+ if (!options.disable_template) {
+ this.on('getGroupTemplate.filter', function(h) {
+ var $h = $($.parseHTML(h.value));
+ $h.find(Selectors.condition_container).after(
+ ''
+ );
+ h.value = $h.prop('outerHTML');
+ });
+
+ if (options.display_rules_button && options.invert_rules) {
+ this.on('getRuleTemplate.filter', function(h) {
+ var $h = $($.parseHTML(h.value));
+ $h.find(Selectors.rule_actions).prepend(
+ ''
+ );
+ h.value = $h.prop('outerHTML');
+ });
+ }
+ }
+}, {
+ icon: 'bi-shuffle',
+ recursive: true,
+ invert_rules: true,
+ display_rules_button: false,
+ silent_fail: false,
+ disable_template: false
+});
+
+QueryBuilder.defaults({
+ operatorOpposites: {
+ 'equal': 'not_equal',
+ 'not_equal': 'equal',
+ 'in': 'not_in',
+ 'not_in': 'in',
+ 'less': 'greater_or_equal',
+ 'less_or_equal': 'greater',
+ 'greater': 'less_or_equal',
+ 'greater_or_equal': 'less',
+ 'between': 'not_between',
+ 'not_between': 'between',
+ 'begins_with': 'not_begins_with',
+ 'not_begins_with': 'begins_with',
+ 'contains': 'not_contains',
+ 'not_contains': 'contains',
+ 'ends_with': 'not_ends_with',
+ 'not_ends_with': 'ends_with',
+ 'is_empty': 'is_not_empty',
+ 'is_not_empty': 'is_empty',
+ 'is_null': 'is_not_null',
+ 'is_not_null': 'is_null'
+ },
+
+ conditionOpposites: {
+ 'AND': 'OR',
+ 'OR': 'AND'
+ }
+});
+
+QueryBuilder.extend(/** @lends module:plugins.Invert.prototype */ {
+ /**
+ * Invert a Group, a Rule or the whole builder
+ * @param {Node} [node]
+ * @param {object} [options] {@link module:plugins.Invert}
+ * @fires module:plugins.Invert.afterInvert
+ * @throws InvertConditionError, InvertOperatorError
+ */
+ invert: function(node, options) {
+ if (!(node instanceof Node)) {
+ if (!this.model.root) return;
+ options = node;
+ node = this.model.root;
+ }
+
+ if (typeof options != 'object') options = {};
+ if (options.recursive === undefined) options.recursive = true;
+ if (options.invert_rules === undefined) options.invert_rules = true;
+ if (options.silent_fail === undefined) options.silent_fail = false;
+ if (options.trigger === undefined) options.trigger = true;
+
+ if (node instanceof Group) {
+ // invert group condition
+ if (this.settings.conditionOpposites[node.condition]) {
+ node.condition = this.settings.conditionOpposites[node.condition];
+ }
+ else if (!options.silent_fail) {
+ Utils.error('InvertCondition', 'Unknown inverse of condition "{0}"', node.condition);
+ }
+
+ // recursive call
+ if (options.recursive) {
+ var tempOpts = $.extend({}, options, { trigger: false });
+ node.each(function(rule) {
+ if (options.invert_rules) {
+ this.invert(rule, tempOpts);
+ }
+ }, function(group) {
+ this.invert(group, tempOpts);
+ }, this);
+ }
+ }
+ else if (node instanceof Rule) {
+ if (node.operator && !node.filter.no_invert) {
+ // invert rule operator
+ if (this.settings.operatorOpposites[node.operator.type]) {
+ var invert = this.settings.operatorOpposites[node.operator.type];
+ // check if the invert is "authorized"
+ if (!node.filter.operators || node.filter.operators.indexOf(invert) != -1) {
+ node.operator = this.getOperatorByType(invert);
+ }
+ }
+ else if (!options.silent_fail) {
+ Utils.error('InvertOperator', 'Unknown inverse of operator "{0}"', node.operator.type);
+ }
+ }
+ }
+
+ if (options.trigger) {
+ /**
+ * After {@link module:plugins.Invert.invert} method
+ * @event afterInvert
+ * @memberof module:plugins.Invert
+ * @param {Node} node - the main group or rule that has been modified
+ * @param {object} options
+ */
+ this.trigger('afterInvert', node, options);
+
+ this.trigger('rulesChanged');
+ }
+ }
+});
+
+
+/**
+ * @class MongoDbSupport
+ * @memberof module:plugins
+ * @description Allows to export rules as a MongoDB find object as well as populating the builder from a MongoDB object.
+ */
+
+QueryBuilder.defaults({
+ mongoOperators: {
+ // @formatter:off
+ equal: function(v) { return v[0]; },
+ not_equal: function(v) { return { '$ne': v[0] }; },
+ in: function(v) { return { '$in': v }; },
+ not_in: function(v) { return { '$nin': v }; },
+ less: function(v) { return { '$lt': v[0] }; },
+ less_or_equal: function(v) { return { '$lte': v[0] }; },
+ greater: function(v) { return { '$gt': v[0] }; },
+ greater_or_equal: function(v) { return { '$gte': v[0] }; },
+ between: function(v) { return { '$gte': v[0], '$lte': v[1] }; },
+ not_between: function(v) { return { '$lt': v[0], '$gt': v[1] }; },
+ begins_with: function(v) { return { '$regex': '^' + Utils.escapeRegExp(v[0]) }; },
+ not_begins_with: function(v) { return { '$regex': '^(?!' + Utils.escapeRegExp(v[0]) + ')' }; },
+ contains: function(v) { return { '$regex': Utils.escapeRegExp(v[0]) }; },
+ not_contains: function(v) { return { '$regex': '^((?!' + Utils.escapeRegExp(v[0]) + ').)*$', '$options': 's' }; },
+ ends_with: function(v) { return { '$regex': Utils.escapeRegExp(v[0]) + '$' }; },
+ not_ends_with: function(v) { return { '$regex': '(? 0) {
+ parts.push(parse(rule));
+ }
+ else {
+ var mdb = self.settings.mongoOperators[rule.operator];
+ var ope = self.getOperatorByType(rule.operator);
+
+ if (mdb === undefined) {
+ Utils.error('UndefinedMongoOperator', 'Unknown MongoDB operation for operator "{0}"', rule.operator);
+ }
+
+ if (ope.nb_inputs !== 0) {
+ if (!(rule.value instanceof Array)) {
+ rule.value = [rule.value];
+ }
+ }
+
+ /**
+ * Modifies the MongoDB field used by a rule
+ * @event changer:getMongoDBField
+ * @memberof module:plugins.MongoDbSupport
+ * @param {string} field
+ * @param {Rule} rule
+ * @returns {string}
+ */
+ var field = self.change('getMongoDBField', rule.field, rule);
+
+ var ruleExpression = {};
+ ruleExpression[field] = mdb.call(self, rule.value);
+
+ /**
+ * Modifies the MongoDB expression generated for a rul
+ * @event changer:ruleToMongo
+ * @memberof module:plugins.MongoDbSupport
+ * @param {object} expression
+ * @param {Rule} rule
+ * @param {*} value
+ * @param {function} valueWrapper - function that takes the value and adds the operator
+ * @returns {object}
+ */
+ parts.push(self.change('ruleToMongo', ruleExpression, rule, rule.value, mdb));
+ }
+ });
+
+ var groupExpression = {};
+ groupExpression['$' + group.condition.toLowerCase()] = parts;
+
+ /**
+ * Modifies the MongoDB expression generated for a group
+ * @event changer:groupToMongo
+ * @memberof module:plugins.MongoDbSupport
+ * @param {object} expression
+ * @param {Group} group
+ * @returns {object}
+ */
+ return self.change('groupToMongo', groupExpression, group);
+ }(data));
+ },
+
+ /**
+ * Converts a MongoDB query to rules
+ * @param {object} query
+ * @returns {object}
+ * @fires module:plugins.MongoDbSupport.changer:parseMongoNode
+ * @fires module:plugins.MongoDbSupport.changer:getMongoDBFieldID
+ * @fires module:plugins.MongoDbSupport.changer:mongoToRule
+ * @fires module:plugins.MongoDbSupport.changer:mongoToGroup
+ * @throws MongoParseError, UndefinedMongoConditionError, UndefinedMongoOperatorError
+ */
+ getRulesFromMongo: function(query) {
+ if (query === undefined || query === null) {
+ return null;
+ }
+
+ var self = this;
+
+ /**
+ * Custom parsing of a MongoDB expression, you can return a sub-part of the expression, or a well formed group or rule JSON
+ * @event changer:parseMongoNode
+ * @memberof module:plugins.MongoDbSupport
+ * @param {object} expression
+ * @returns {object} expression, rule or group
+ */
+ query = self.change('parseMongoNode', query);
+
+ // a plugin returned a group
+ if ('rules' in query && 'condition' in query) {
+ return query;
+ }
+
+ // a plugin returned a rule
+ if ('id' in query && 'operator' in query && 'value' in query) {
+ return {
+ condition: this.settings.default_condition,
+ rules: [query]
+ };
+ }
+
+ var key = self.getMongoCondition(query);
+ if (!key) {
+ Utils.error('MongoParse', 'Invalid MongoDB query format');
+ }
+
+ return (function parse(data, topKey) {
+ var rules = data[topKey];
+ var parts = [];
+
+ rules.forEach(function(data) {
+ // allow plugins to manually parse or handle special cases
+ data = self.change('parseMongoNode', data);
+
+ // a plugin returned a group
+ if ('rules' in data && 'condition' in data) {
+ parts.push(data);
+ return;
+ }
+
+ // a plugin returned a rule
+ if ('id' in data && 'operator' in data && 'value' in data) {
+ parts.push(data);
+ return;
+ }
+
+ var key = self.getMongoCondition(data);
+ if (key) {
+ parts.push(parse(data, key));
+ }
+ else {
+ var field = Object.keys(data)[0];
+ var value = data[field];
+
+ var operator = self.getMongoOperator(value);
+ if (operator === undefined) {
+ Utils.error('MongoParse', 'Invalid MongoDB query format');
+ }
+
+ var mdbrl = self.settings.mongoRuleOperators[operator];
+ if (mdbrl === undefined) {
+ Utils.error('UndefinedMongoOperator', 'JSON Rule operation unknown for operator "{0}"', operator);
+ }
+
+ var opVal = mdbrl.call(self, value);
+
+ var id = self.getMongoDBFieldID(field, value);
+
+ /**
+ * Modifies the rule generated from the MongoDB expression
+ * @event changer:mongoToRule
+ * @memberof module:plugins.MongoDbSupport
+ * @param {object} rule
+ * @param {object} expression
+ * @returns {object}
+ */
+ var rule = self.change('mongoToRule', {
+ id: id,
+ field: field,
+ operator: opVal.op,
+ value: opVal.val
+ }, data);
+
+ parts.push(rule);
+ }
+ });
+
+ /**
+ * Modifies the group generated from the MongoDB expression
+ * @event changer:mongoToGroup
+ * @memberof module:plugins.MongoDbSupport
+ * @param {object} group
+ * @param {object} expression
+ * @returns {object}
+ */
+ return self.change('mongoToGroup', {
+ condition: topKey.replace('$', '').toUpperCase(),
+ rules: parts
+ }, data);
+ }(query, key));
+ },
+
+ /**
+ * Sets rules a from MongoDB query
+ * @see module:plugins.MongoDbSupport.getRulesFromMongo
+ */
+ setRulesFromMongo: function(query) {
+ this.setRules(this.getRulesFromMongo(query));
+ },
+
+ /**
+ * Returns a filter identifier from the MongoDB field.
+ * Automatically use the only one filter with a matching field, fires a changer otherwise.
+ * @param {string} field
+ * @param {*} value
+ * @fires module:plugins.MongoDbSupport:changer:getMongoDBFieldID
+ * @returns {string}
+ * @private
+ */
+ getMongoDBFieldID: function(field, value) {
+ var matchingFilters = this.filters.filter(function(filter) {
+ return filter.field === field;
+ });
+
+ var id;
+ if (matchingFilters.length === 1) {
+ id = matchingFilters[0].id;
+ }
+ else {
+ /**
+ * Returns a filter identifier from the MongoDB field
+ * @event changer:getMongoDBFieldID
+ * @memberof module:plugins.MongoDbSupport
+ * @param {string} field
+ * @param {*} value
+ * @returns {string}
+ */
+ id = this.change('getMongoDBFieldID', field, value);
+ }
+
+ return id;
+ },
+
+ /**
+ * Finds which operator is used in a MongoDB sub-object
+ * @param {*} data
+ * @returns {string|undefined}
+ * @private
+ */
+ getMongoOperator: function(data) {
+ if (data !== null && typeof data === 'object') {
+ if (data.$gte !== undefined && data.$lte !== undefined) {
+ return 'between';
+ }
+ if (data.$lt !== undefined && data.$gt !== undefined) {
+ return 'not_between';
+ }
+
+ var knownKeys = Object.keys(data).filter(function(key) {
+ return !!this.settings.mongoRuleOperators[key];
+ }.bind(this));
+
+ if (knownKeys.length === 1) {
+ return knownKeys[0];
+ }
+ }
+ else {
+ return '$eq';
+ }
+ },
+
+
+ /**
+ * Returns the key corresponding to "$or" or "$and"
+ * @param {object} data
+ * @returns {string|undefined}
+ * @private
+ */
+ getMongoCondition: function(data) {
+ var keys = Object.keys(data);
+
+ for (var i = 0, l = keys.length; i < l; i++) {
+ if (keys[i].toLowerCase() === '$or' || keys[i].toLowerCase() === '$and') {
+ return keys[i];
+ }
+ }
+ }
+});
+
+
+/**
+ * @class NotGroup
+ * @memberof module:plugins
+ * @description Adds a "Not" checkbox in front of group conditions.
+ * @param {object} [options]
+ * @param {string} [options.icon_checked='bi-check2-square']
+ * @param {string} [options.icon_unchecked='bi-square']
+ */
+QueryBuilder.define('not-group', function(options) {
+ var self = this;
+
+ // Bind events
+ this.on('afterInit', function() {
+ self.$el.on('click.queryBuilder', '[data-not=group]', function() {
+ var $group = $(this).closest(QueryBuilder.selectors.group_container);
+ var group = self.getModel($group);
+ group.not = !group.not;
+ });
+
+ self.model.on('update', function(e, node, field) {
+ if (node instanceof Group && field === 'not') {
+ self.updateGroupNot(node);
+ }
+ });
+ });
+
+ // Init "not" property
+ this.on('afterAddGroup', function(e, group) {
+ group.__.not = false;
+ });
+
+ // Modify templates
+ if (!options.disable_template) {
+ this.on('getGroupTemplate.filter', function(h) {
+ var $h = $($.parseHTML(h.value));
+ $h.find(QueryBuilder.selectors.condition_container).prepend(
+ ''
+ );
+ h.value = $h.prop('outerHTML');
+ });
+ }
+
+ // Export "not" to JSON
+ this.on('groupToJson.filter', function(e, group) {
+ e.value.not = group.not;
+ });
+
+ // Read "not" from JSON
+ this.on('jsonToGroup.filter', function(e, json) {
+ e.value.not = !!json.not;
+ });
+
+ // Export "not" to SQL
+ this.on('groupToSQL.filter', function(e, group) {
+ if (group.not) {
+ e.value = 'NOT ( ' + e.value + ' )';
+ }
+ });
+
+ // Parse "NOT" function from sqlparser
+ this.on('parseSQLNode.filter', function(e) {
+ if (e.value.name && e.value.name.toUpperCase() == 'NOT') {
+ e.value = e.value.arguments.value[0];
+
+ // if the there is no sub-group, create one
+ if (['AND', 'OR'].indexOf(e.value.operation.toUpperCase()) === -1) {
+ e.value = new SQLParser.nodes.Op(
+ self.settings.default_condition,
+ e.value,
+ null
+ );
+ }
+
+ e.value.not = true;
+ }
+ });
+
+ // Request to create sub-group if the "not" flag is set
+ this.on('sqlGroupsDistinct.filter', function(e, group, data, i) {
+ if (data.not && i > 0) {
+ e.value = true;
+ }
+ });
+
+ // Read "not" from parsed SQL
+ this.on('sqlToGroup.filter', function(e, data) {
+ e.value.not = !!data.not;
+ });
+
+ // Export "not" to Mongo
+ this.on('groupToMongo.filter', function(e, group) {
+ var key = '$' + group.condition.toLowerCase();
+ if (group.not && e.value[key]) {
+ e.value = { '$nor': [e.value] };
+ }
+ });
+
+ // Parse "$nor" operator from Mongo
+ this.on('parseMongoNode.filter', function(e) {
+ var keys = Object.keys(e.value);
+
+ if (keys[0] == '$nor') {
+ e.value = e.value[keys[0]][0];
+ e.value.not = true;
+ }
+ });
+
+ // Read "not" from parsed Mongo
+ this.on('mongoToGroup.filter', function(e, data) {
+ e.value.not = !!data.not;
+ });
+}, {
+ icon_unchecked: 'bi-square',
+ icon_checked: 'bi-check2-square',
+ disable_template: false
+});
+
+/**
+ * From {@link module:plugins.NotGroup}
+ * @name not
+ * @member {boolean}
+ * @memberof Group
+ * @instance
+ */
+Utils.defineModelProperties(Group, ['not']);
+
+QueryBuilder.selectors.group_not = QueryBuilder.selectors.group_header + ' [data-not=group]';
+
+QueryBuilder.extend(/** @lends module:plugins.NotGroup.prototype */ {
+ /**
+ * Performs actions when a group's not changes
+ * @param {Group} group
+ * @fires module:plugins.NotGroup.afterUpdateGroupNot
+ * @private
+ */
+ updateGroupNot: function(group) {
+ var options = this.plugins['not-group'];
+ group.$el.find('>' + QueryBuilder.selectors.group_not)
+ .toggleClass('active', group.not)
+ .find('i').attr('class', group.not ? options.icon_checked : options.icon_unchecked);
+
+ /**
+ * After the group's not flag has been modified
+ * @event afterUpdateGroupNot
+ * @memberof module:plugins.NotGroup
+ * @param {Group} group
+ */
+ this.trigger('afterUpdateGroupNot', group);
+
+ this.trigger('rulesChanged');
+ }
+});
+
+
+/**
+ * @class Sortable
+ * @memberof module:plugins
+ * @description Enables drag & drop sort of rules.
+ * @param {object} [options]
+ * @param {boolean} [options.inherit_no_drop=true]
+ * @param {boolean} [options.inherit_no_sortable=true]
+ * @param {string} [options.icon='bi-sort-down']
+ * @throws MissingLibraryError, ConfigError
+ */
+QueryBuilder.define('sortable', function(options) {
+ if (!('interact' in window)) {
+ Utils.error('MissingLibrary', 'interact.js is required to use "sortable" plugin. Get it here: http://interactjs.io');
+ }
+
+ if (options.default_no_sortable !== undefined) {
+ Utils.error(false, 'Config', 'Sortable plugin : "default_no_sortable" options is deprecated, use standard "default_rule_flags" and "default_group_flags" instead');
+ this.settings.default_rule_flags.no_sortable = this.settings.default_group_flags.no_sortable = options.default_no_sortable;
+ }
+
+ // recompute drop-zones during drag (when a rule is hidden)
+ interact.dynamicDrop(true);
+
+ // set move threshold to 10px
+ interact.pointerMoveTolerance(10);
+
+ var placeholder;
+ var ghost;
+ var src;
+ var moved;
+
+ // Init drag and drop
+ this.on('afterAddRule afterAddGroup', function(e, node) {
+ if (node == placeholder) {
+ return;
+ }
+
+ var self = e.builder;
+
+ // Inherit flags
+ if (options.inherit_no_sortable && node.parent && node.parent.flags.no_sortable) {
+ node.flags.no_sortable = true;
+ }
+ if (options.inherit_no_drop && node.parent && node.parent.flags.no_drop) {
+ node.flags.no_drop = true;
+ }
+
+ // Configure drag
+ if (!node.flags.no_sortable) {
+ interact(node.$el[0])
+ .draggable({
+ allowFrom: QueryBuilder.selectors.drag_handle,
+ onstart: function(event) {
+ moved = false;
+
+ // get model of dragged element
+ src = self.getModel(event.target);
+
+ // create ghost
+ ghost = src.$el.clone()
+ .appendTo(src.$el.parent())
+ .width(src.$el.outerWidth())
+ .addClass('dragging');
+
+ // create drop placeholder
+ var ph = $($.parseHTML('
'))
+ .height(src.$el.outerHeight());
+
+ placeholder = src.parent.addRule(ph, src.getPos());
+
+ // hide dragged element
+ src.$el.hide();
+ },
+ onmove: function(event) {
+ // make the ghost follow the cursor
+ ghost[0].style.top = event.clientY - 15 + 'px';
+ ghost[0].style.left = event.clientX - 15 + 'px';
+ },
+ onend: function(event) {
+ // starting from Interact 1.3.3, onend is called before ondrop
+ if (event.dropzone) {
+ moveSortableToTarget(src, $(event.relatedTarget), self);
+ moved = true;
+ }
+
+ // remove ghost
+ ghost.remove();
+ ghost = undefined;
+
+ // remove placeholder
+ placeholder.drop();
+ placeholder = undefined;
+
+ // show element
+ src.$el.css('display', '');
+
+ /**
+ * After a node has been moved with {@link module:plugins.Sortable}
+ * @event afterMove
+ * @memberof module:plugins.Sortable
+ * @param {Node} node
+ */
+ self.trigger('afterMove', src);
+
+ self.trigger('rulesChanged');
+ }
+ });
+ }
+
+ if (!node.flags.no_drop) {
+ // Configure drop on groups and rules
+ interact(node.$el[0])
+ .dropzone({
+ accept: QueryBuilder.selectors.rule_and_group_containers,
+ ondragenter: function(event) {
+ moveSortableToTarget(placeholder, $(event.target), self);
+ },
+ ondrop: function(event) {
+ if (!moved) {
+ moveSortableToTarget(src, $(event.target), self);
+ }
+ }
+ });
+
+ // Configure drop on group headers
+ if (node instanceof Group) {
+ interact(node.$el.find(QueryBuilder.selectors.group_header)[0])
+ .dropzone({
+ accept: QueryBuilder.selectors.rule_and_group_containers,
+ ondragenter: function(event) {
+ moveSortableToTarget(placeholder, $(event.target), self);
+ },
+ ondrop: function(event) {
+ if (!moved) {
+ moveSortableToTarget(src, $(event.target), self);
+ }
+ }
+ });
+ }
+ }
+ });
+
+ // Detach interactables
+ this.on('beforeDeleteRule beforeDeleteGroup', function(e, node) {
+ if (!e.isDefaultPrevented()) {
+ interact(node.$el[0]).unset();
+
+ if (node instanceof Group) {
+ interact(node.$el.find(QueryBuilder.selectors.group_header)[0]).unset();
+ }
+ }
+ });
+
+ // Remove drag handle from non-sortable items
+ this.on('afterApplyRuleFlags afterApplyGroupFlags', function(e, node) {
+ if (node.flags.no_sortable) {
+ node.$el.find('.drag-handle').remove();
+ }
+ });
+
+ // Modify templates
+ if (!options.disable_template) {
+ this.on('getGroupTemplate.filter', function(h, level) {
+ if (level > 1) {
+ var $h = $($.parseHTML(h.value));
+ $h.find(QueryBuilder.selectors.condition_container).after('
');
+ h.value = $h.prop('outerHTML');
+ }
+ });
+
+ this.on('getRuleTemplate.filter', function(h) {
+ var $h = $($.parseHTML(h.value));
+ $h.find(QueryBuilder.selectors.rule_header).after('
');
+ h.value = $h.prop('outerHTML');
+ });
+ }
+}, {
+ inherit_no_sortable: true,
+ inherit_no_drop: true,
+ icon: 'bi-sort-down',
+ disable_template: false
+});
+
+QueryBuilder.selectors.rule_and_group_containers = QueryBuilder.selectors.rule_container + ', ' + QueryBuilder.selectors.group_container;
+QueryBuilder.selectors.drag_handle = '.drag-handle';
+
+QueryBuilder.defaults({
+ default_rule_flags: {
+ no_sortable: false,
+ no_drop: false
+ },
+ default_group_flags: {
+ no_sortable: false,
+ no_drop: false
+ }
+});
+
+/**
+ * Moves an element (placeholder or actual object) depending on active target
+ * @memberof module:plugins.Sortable
+ * @param {Node} node
+ * @param {jQuery} target
+ * @param {QueryBuilder} [builder]
+ * @private
+ */
+function moveSortableToTarget(node, target, builder) {
+ var parent, method;
+ var Selectors = QueryBuilder.selectors;
+
+ // on rule
+ parent = target.closest(Selectors.rule_container);
+ if (parent.length) {
+ method = 'moveAfter';
+ }
+
+ // on group header
+ if (!method) {
+ parent = target.closest(Selectors.group_header);
+ if (parent.length) {
+ parent = target.closest(Selectors.group_container);
+ method = 'moveAtBegin';
+ }
+ }
+
+ // on group
+ if (!method) {
+ parent = target.closest(Selectors.group_container);
+ if (parent.length) {
+ method = 'moveAtEnd';
+ }
+ }
+
+ if (method) {
+ node[method](builder.getModel(parent));
+
+ // refresh radio value
+ if (builder && node instanceof Rule) {
+ builder.setRuleInputValue(node, node.value);
+ }
+ }
+}
+
+
+/**
+ * @class SqlSupport
+ * @memberof module:plugins
+ * @description Allows to export rules as a SQL WHERE statement as well as populating the builder from an SQL query.
+ * @param {object} [options]
+ * @param {boolean} [options.boolean_as_integer=true] - `true` to convert boolean values to integer in the SQL output
+ */
+QueryBuilder.define('sql-support', function(options) {
+
+}, {
+ boolean_as_integer: true
+});
+
+QueryBuilder.defaults({
+ // operators for internal -> SQL conversion
+ sqlOperators: {
+ equal: { op: '= ?' },
+ not_equal: { op: '!= ?' },
+ in: { op: 'IN(?)', sep: ', ' },
+ not_in: { op: 'NOT IN(?)', sep: ', ' },
+ less: { op: '< ?' },
+ less_or_equal: { op: '<= ?' },
+ greater: { op: '> ?' },
+ greater_or_equal: { op: '>= ?' },
+ between: { op: 'BETWEEN ?', sep: ' AND ' },
+ not_between: { op: 'NOT BETWEEN ?', sep: ' AND ' },
+ begins_with: { op: 'LIKE ?', mod: '{0}%', escape: '%_' },
+ not_begins_with: { op: 'NOT LIKE ?', mod: '{0}%', escape: '%_' },
+ contains: { op: 'LIKE ?', mod: '%{0}%', escape: '%_' },
+ not_contains: { op: 'NOT LIKE ?', mod: '%{0}%', escape: '%_' },
+ ends_with: { op: 'LIKE ?', mod: '%{0}', escape: '%_' },
+ not_ends_with: { op: 'NOT LIKE ?', mod: '%{0}', escape: '%_' },
+ is_empty: { op: '= \'\'' },
+ is_not_empty: { op: '!= \'\'' },
+ is_null: { op: 'IS NULL' },
+ is_not_null: { op: 'IS NOT NULL' }
+ },
+
+ // operators for SQL -> internal conversion
+ sqlRuleOperator: {
+ '=': function(v) {
+ return {
+ val: v,
+ op: v === '' ? 'is_empty' : 'equal'
+ };
+ },
+ '!=': function(v) {
+ return {
+ val: v,
+ op: v === '' ? 'is_not_empty' : 'not_equal'
+ };
+ },
+ 'LIKE': function(v) {
+ if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
+ return {
+ val: v.slice(1, -1),
+ op: 'contains'
+ };
+ }
+ else if (v.slice(0, 1) == '%') {
+ return {
+ val: v.slice(1),
+ op: 'ends_with'
+ };
+ }
+ else if (v.slice(-1) == '%') {
+ return {
+ val: v.slice(0, -1),
+ op: 'begins_with'
+ };
+ }
+ else {
+ Utils.error('SQLParse', 'Invalid value for LIKE operator "{0}"', v);
+ }
+ },
+ 'NOT LIKE': function(v) {
+ if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
+ return {
+ val: v.slice(1, -1),
+ op: 'not_contains'
+ };
+ }
+ else if (v.slice(0, 1) == '%') {
+ return {
+ val: v.slice(1),
+ op: 'not_ends_with'
+ };
+ }
+ else if (v.slice(-1) == '%') {
+ return {
+ val: v.slice(0, -1),
+ op: 'not_begins_with'
+ };
+ }
+ else {
+ Utils.error('SQLParse', 'Invalid value for NOT LIKE operator "{0}"', v);
+ }
+ },
+ 'IN': function(v) {
+ return { val: v, op: 'in' };
+ },
+ 'NOT IN': function(v) {
+ return { val: v, op: 'not_in' };
+ },
+ '<': function(v) {
+ return { val: v, op: 'less' };
+ },
+ '<=': function(v) {
+ return { val: v, op: 'less_or_equal' };
+ },
+ '>': function(v) {
+ return { val: v, op: 'greater' };
+ },
+ '>=': function(v) {
+ return { val: v, op: 'greater_or_equal' };
+ },
+ 'BETWEEN': function(v) {
+ return { val: v, op: 'between' };
+ },
+ 'NOT BETWEEN': function(v) {
+ return { val: v, op: 'not_between' };
+ },
+ 'IS': function(v) {
+ if (v !== null) {
+ Utils.error('SQLParse', 'Invalid value for IS operator');
+ }
+ return { val: null, op: 'is_null' };
+ },
+ 'IS NOT': function(v) {
+ if (v !== null) {
+ Utils.error('SQLParse', 'Invalid value for IS operator');
+ }
+ return { val: null, op: 'is_not_null' };
+ }
+ },
+
+ // statements for internal -> SQL conversion
+ sqlStatements: {
+ 'question_mark': function() {
+ var params = [];
+ return {
+ add: function(rule, value) {
+ params.push(value);
+ return '?';
+ },
+ run: function() {
+ return params;
+ }
+ };
+ },
+
+ 'numbered': function(char) {
+ if (!char || char.length > 1) char = '$';
+ var index = 0;
+ var params = [];
+ return {
+ add: function(rule, value) {
+ params.push(value);
+ index++;
+ return char + index;
+ },
+ run: function() {
+ return params;
+ }
+ };
+ },
+
+ 'named': function(char) {
+ if (!char || char.length > 1) char = ':';
+ var indexes = {};
+ var params = {};
+ return {
+ add: function(rule, value) {
+ if (!indexes[rule.field]) indexes[rule.field] = 1;
+ var key = rule.field + '_' + (indexes[rule.field]++);
+ params[key] = value;
+ return char + key;
+ },
+ run: function() {
+ return params;
+ }
+ };
+ }
+ },
+
+ // statements for SQL -> internal conversion
+ sqlRuleStatement: {
+ 'question_mark': function(values) {
+ var index = 0;
+ return {
+ parse: function(v) {
+ return v == '?' ? values[index++] : v;
+ },
+ esc: function(sql) {
+ return sql.replace(/\?/g, '\'?\'');
+ }
+ };
+ },
+
+ 'numbered': function(values, char) {
+ if (!char || char.length > 1) char = '$';
+ var regex1 = new RegExp('^\\' + char + '[0-9]+$');
+ var regex2 = new RegExp('\\' + char + '([0-9]+)', 'g');
+ return {
+ parse: function(v) {
+ return regex1.test(v) ? values[v.slice(1) - 1] : v;
+ },
+ esc: function(sql) {
+ return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\'');
+ }
+ };
+ },
+
+ 'named': function(values, char) {
+ if (!char || char.length > 1) char = ':';
+ var regex1 = new RegExp('^\\' + char);
+ var regex2 = new RegExp('\\' + char + '(' + Object.keys(values).join('|') + ')\\b', 'g');
+ return {
+ parse: function(v) {
+ return regex1.test(v) ? values[v.slice(1)] : v;
+ },
+ esc: function(sql) {
+ return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\'');
+ }
+ };
+ }
+ }
+});
+
+/**
+ * @typedef {object} SqlQuery
+ * @memberof module:plugins.SqlSupport
+ * @property {string} sql
+ * @property {object} params
+ */
+
+QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ {
+ /**
+ * Returns rules as a SQL query
+ * @param {boolean|string} [stmt] - use prepared statements: false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)'
+ * @param {boolean} [nl=false] output with new lines
+ * @param {object} [data] - current rules by default
+ * @returns {module:plugins.SqlSupport.SqlQuery}
+ * @fires module:plugins.SqlSupport.changer:getSQLField
+ * @fires module:plugins.SqlSupport.changer:ruleToSQL
+ * @fires module:plugins.SqlSupport.changer:groupToSQL
+ * @throws UndefinedSQLConditionError, UndefinedSQLOperatorError
+ */
+ getSQL: function(stmt, nl, data) {
+ data = (data === undefined) ? this.getRules() : data;
+
+ if (!data) {
+ return null;
+ }
+
+ nl = !!nl ? '\n' : ' ';
+ var boolean_as_integer = this.getPluginOptions('sql-support', 'boolean_as_integer');
+
+ if (stmt === true) {
+ stmt = 'question_mark';
+ }
+ if (typeof stmt == 'string') {
+ var config = getStmtConfig(stmt);
+ stmt = this.settings.sqlStatements[config[1]](config[2]);
+ }
+
+ var self = this;
+
+ var sql = (function parse(group) {
+ if (!group.condition) {
+ group.condition = self.settings.default_condition;
+ }
+ if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) {
+ Utils.error('UndefinedSQLCondition', 'Unable to build SQL query with condition "{0}"', group.condition);
+ }
+
+ if (!group.rules) {
+ return '';
+ }
+
+ var parts = [];
+
+ group.rules.forEach(function(rule) {
+ if (rule.rules && rule.rules.length > 0) {
+ parts.push('(' + nl + parse(rule) + nl + ')' + nl);
+ }
+ else {
+ var sql = self.settings.sqlOperators[rule.operator];
+ var ope = self.getOperatorByType(rule.operator);
+ var value = '';
+
+ if (sql === undefined) {
+ Utils.error('UndefinedSQLOperator', 'Unknown SQL operation for operator "{0}"', rule.operator);
+ }
+
+ if (ope.nb_inputs !== 0) {
+ if (!(rule.value instanceof Array)) {
+ rule.value = [rule.value];
+ }
+
+ rule.value.forEach(function(v, i) {
+ if (i > 0) {
+ value += sql.sep;
+ }
+
+ if (rule.type == 'boolean' && boolean_as_integer) {
+ v = v ? 1 : 0;
+ }
+ else if (!stmt && rule.type !== 'integer' && rule.type !== 'double' && rule.type !== 'boolean') {
+ v = Utils.escapeString(v, sql.escape);
+ }
+
+ if (sql.mod) {
+ v = Utils.fmt(sql.mod, v);
+ }
+
+ if (stmt) {
+ value += stmt.add(rule, v);
+ }
+ else {
+ if (typeof v == 'string') {
+ v = '\'' + v + '\'';
+ }
+
+ value += v;
+ }
+ });
+ }
+
+ var sqlFn = function(v) {
+ return sql.op.replace('?', function() {
+ return v;
+ });
+ };
+
+ /**
+ * Modifies the SQL field used by a rule
+ * @event changer:getSQLField
+ * @memberof module:plugins.SqlSupport
+ * @param {string} field
+ * @param {Rule} rule
+ * @returns {string}
+ */
+ var field = self.change('getSQLField', rule.field, rule);
+
+ var ruleExpression = field + ' ' + sqlFn(value);
+
+ /**
+ * Modifies the SQL generated for a rule
+ * @event changer:ruleToSQL
+ * @memberof module:plugins.SqlSupport
+ * @param {string} expression
+ * @param {Rule} rule
+ * @param {*} value
+ * @param {function} valueWrapper - function that takes the value and adds the operator
+ * @returns {string}
+ */
+ parts.push(self.change('ruleToSQL', ruleExpression, rule, value, sqlFn));
+ }
+ });
+
+ var groupExpression = parts.join(' ' + group.condition + nl);
+
+ /**
+ * Modifies the SQL generated for a group
+ * @event changer:groupToSQL
+ * @memberof module:plugins.SqlSupport
+ * @param {string} expression
+ * @param {Group} group
+ * @returns {string}
+ */
+ return self.change('groupToSQL', groupExpression, group);
+ }(data));
+
+ if (stmt) {
+ return {
+ sql: sql,
+ params: stmt.run()
+ };
+ }
+ else {
+ return {
+ sql: sql
+ };
+ }
+ },
+
+ /**
+ * Convert a SQL query to rules
+ * @param {string|module:plugins.SqlSupport.SqlQuery} query
+ * @param {boolean|string} stmt
+ * @returns {object}
+ * @fires module:plugins.SqlSupport.changer:parseSQLNode
+ * @fires module:plugins.SqlSupport.changer:getSQLFieldID
+ * @fires module:plugins.SqlSupport.changer:sqlToRule
+ * @fires module:plugins.SqlSupport.changer:sqlToGroup
+ * @throws MissingLibraryError, SQLParseError, UndefinedSQLOperatorError
+ */
+ getRulesFromSQL: function(query, stmt) {
+ if (!('SQLParser' in window)) {
+ Utils.error('MissingLibrary', 'SQLParser is required to parse SQL queries. Get it here https://github.com/mistic100/sql-parser');
+ }
+
+ var self = this;
+
+ if (typeof query == 'string') {
+ query = { sql: query };
+ }
+
+ if (stmt === true) stmt = 'question_mark';
+ if (typeof stmt == 'string') {
+ var config = getStmtConfig(stmt);
+ stmt = this.settings.sqlRuleStatement[config[1]](query.params, config[2]);
+ }
+
+ if (stmt) {
+ query.sql = stmt.esc(query.sql);
+ }
+
+ if (query.sql.toUpperCase().indexOf('SELECT') !== 0) {
+ query.sql = 'SELECT * FROM table WHERE ' + query.sql;
+ }
+
+ var parsed = SQLParser.parse(query.sql);
+
+ if (!parsed.where) {
+ Utils.error('SQLParse', 'No WHERE clause found');
+ }
+
+ /**
+ * Custom parsing of an AST node generated by SQLParser, you can return a sub-part of the tree, or a well formed group or rule JSON
+ * @event changer:parseSQLNode
+ * @memberof module:plugins.SqlSupport
+ * @param {object} AST node
+ * @returns {object} tree, rule or group
+ */
+ var data = self.change('parseSQLNode', parsed.where.conditions);
+
+ // a plugin returned a group
+ if ('rules' in data && 'condition' in data) {
+ return data;
+ }
+
+ // a plugin returned a rule
+ if ('id' in data && 'operator' in data && 'value' in data) {
+ return {
+ condition: this.settings.default_condition,
+ rules: [data]
+ };
+ }
+
+ // create root group
+ var out = self.change('sqlToGroup', {
+ condition: this.settings.default_condition,
+ rules: []
+ }, data);
+
+ // keep track of current group
+ var curr = out;
+
+ (function flatten(data, i) {
+ if (data === null) {
+ return;
+ }
+
+ // allow plugins to manually parse or handle special cases
+ data = self.change('parseSQLNode', data);
+
+ // a plugin returned a group
+ if ('rules' in data && 'condition' in data) {
+ curr.rules.push(data);
+ return;
+ }
+
+ // a plugin returned a rule
+ if ('id' in data && 'operator' in data && 'value' in data) {
+ curr.rules.push(data);
+ return;
+ }
+
+ // data must be a SQL parser node
+ if (!('left' in data) || !('right' in data) || !('operation' in data)) {
+ Utils.error('SQLParse', 'Unable to parse WHERE clause');
+ }
+
+ // it's a node
+ if (['AND', 'OR'].indexOf(data.operation.toUpperCase()) !== -1) {
+ // create a sub-group if the condition is not the same and it's not the first level
+
+ /**
+ * Given an existing group and an AST node, determines if a sub-group must be created
+ * @event changer:sqlGroupsDistinct
+ * @memberof module:plugins.SqlSupport
+ * @param {boolean} create - true by default if the group condition is different
+ * @param {object} group
+ * @param {object} AST
+ * @param {int} current group level
+ * @returns {boolean}
+ */
+ var createGroup = self.change('sqlGroupsDistinct', i > 0 && curr.condition != data.operation.toUpperCase(), curr, data, i);
+
+ if (createGroup) {
+ /**
+ * Modifies the group generated from the SQL expression (this is called before the group is filled with rules)
+ * @event changer:sqlToGroup
+ * @memberof module:plugins.SqlSupport
+ * @param {object} group
+ * @param {object} AST
+ * @returns {object}
+ */
+ var group = self.change('sqlToGroup', {
+ condition: self.settings.default_condition,
+ rules: []
+ }, data);
+
+ curr.rules.push(group);
+ curr = group;
+ }
+
+ curr.condition = data.operation.toUpperCase();
+ i++;
+
+ // some magic !
+ var next = curr;
+ flatten(data.left, i);
+
+ curr = next;
+ flatten(data.right, i);
+ }
+ // it's a leaf
+ else {
+ if ($.isPlainObject(data.right.value)) {
+ Utils.error('SQLParse', 'Value format not supported for {0}.', data.left.value);
+ }
+
+ // convert array
+ var value;
+ if ($.isArray(data.right.value)) {
+ value = data.right.value.map(function(v) {
+ return v.value;
+ });
+ }
+ else {
+ value = data.right.value;
+ }
+
+ // get actual values
+ if (stmt) {
+ if ($.isArray(value)) {
+ value = value.map(stmt.parse);
+ }
+ else {
+ value = stmt.parse(value);
+ }
+ }
+
+ // convert operator
+ var operator = data.operation.toUpperCase();
+ if (operator == '<>') {
+ operator = '!=';
+ }
+
+ var sqlrl = self.settings.sqlRuleOperator[operator];
+ if (sqlrl === undefined) {
+ Utils.error('UndefinedSQLOperator', 'Invalid SQL operation "{0}".', data.operation);
+ }
+
+ var opVal = sqlrl.call(this, value, data.operation);
+
+ // find field name
+ var field;
+ if ('values' in data.left) {
+ field = data.left.values.join('.');
+ }
+ else if ('value' in data.left) {
+ field = data.left.value;
+ }
+ else {
+ Utils.error('SQLParse', 'Cannot find field name in {0}', JSON.stringify(data.left));
+ }
+
+ // unescape chars declared by the operator
+ var finalValue = opVal.val;
+ var sql = self.settings.sqlOperators[opVal.op];
+ if (!stmt && sql && sql.escape) {
+ var searchChars = sql.escape.split('').map(function(c) {
+ return '\\\\' + c;
+ }).join('|');
+ finalValue = finalValue
+ .replace(new RegExp('(' + searchChars + ')', 'g'), function(s) {
+ return s[1];
+ });
+ }
+
+ var id = self.getSQLFieldID(field, value);
+
+ /**
+ * Modifies the rule generated from the SQL expression
+ * @event changer:sqlToRule
+ * @memberof module:plugins.SqlSupport
+ * @param {object} rule
+ * @param {object} AST
+ * @returns {object}
+ */
+ var rule = self.change('sqlToRule', {
+ id: id,
+ field: field,
+ operator: opVal.op,
+ value: finalValue
+ }, data);
+
+ curr.rules.push(rule);
+ }
+ }(data, 0));
+
+ return out;
+ },
+
+ /**
+ * Sets the builder's rules from a SQL query
+ * @see module:plugins.SqlSupport.getRulesFromSQL
+ */
+ setRulesFromSQL: function(query, stmt) {
+ this.setRules(this.getRulesFromSQL(query, stmt));
+ },
+
+ /**
+ * Returns a filter identifier from the SQL field.
+ * Automatically use the only one filter with a matching field, fires a changer otherwise.
+ * @param {string} field
+ * @param {*} value
+ * @fires module:plugins.SqlSupport:changer:getSQLFieldID
+ * @returns {string}
+ * @private
+ */
+ getSQLFieldID: function(field, value) {
+ var matchingFilters = this.filters.filter(function(filter) {
+ return filter.field.toLowerCase() === field.toLowerCase();
+ });
+
+ var id;
+ if (matchingFilters.length === 1) {
+ id = matchingFilters[0].id;
+ }
+ else {
+ /**
+ * Returns a filter identifier from the SQL field
+ * @event changer:getSQLFieldID
+ * @memberof module:plugins.SqlSupport
+ * @param {string} field
+ * @param {*} value
+ * @returns {string}
+ */
+ id = this.change('getSQLFieldID', field, value);
+ }
+
+ return id;
+ }
+});
+
+/**
+ * Parses the statement configuration
+ * @memberof module:plugins.SqlSupport
+ * @param {string} stmt
+ * @returns {Array} null, mode, option
+ * @private
+ */
+function getStmtConfig(stmt) {
+ var config = stmt.match(/(question_mark|numbered|named)(?:\((.)\))?/);
+ if (!config) config = [null, 'question_mark', undefined];
+ return config;
+}
+
+
+/**
+ * @class UniqueFilter
+ * @memberof module:plugins
+ * @description Allows to define some filters as "unique": ie which can be used for only one rule, globally or in the same group.
+ */
+QueryBuilder.define('unique-filter', function() {
+ this.status.used_filters = {};
+
+ this.on('afterUpdateRuleFilter', this.updateDisabledFilters);
+ this.on('afterDeleteRule', this.updateDisabledFilters);
+ this.on('afterCreateRuleFilters', this.applyDisabledFilters);
+ this.on('afterReset', this.clearDisabledFilters);
+ this.on('afterClear', this.clearDisabledFilters);
+
+ // Ensure that the default filter is not already used if unique
+ this.on('getDefaultFilter.filter', function(e, model) {
+ var self = e.builder;
+
+ self.updateDisabledFilters();
+
+ if (e.value.id in self.status.used_filters) {
+ var found = self.filters.some(function(filter) {
+ if (!(filter.id in self.status.used_filters) || self.status.used_filters[filter.id].length > 0 && self.status.used_filters[filter.id].indexOf(model.parent) === -1) {
+ e.value = filter;
+ return true;
+ }
+ });
+
+ if (!found) {
+ Utils.error(false, 'UniqueFilter', 'No more non-unique filters available');
+ e.value = undefined;
+ }
+ }
+ });
+});
+
+QueryBuilder.extend(/** @lends module:plugins.UniqueFilter.prototype */ {
+ /**
+ * Updates the list of used filters
+ * @param {$.Event} [e]
+ * @private
+ */
+ updateDisabledFilters: function(e) {
+ var self = e ? e.builder : this;
+
+ self.status.used_filters = {};
+
+ if (!self.model) {
+ return;
+ }
+
+ // get used filters
+ (function walk(group) {
+ group.each(function(rule) {
+ if (rule.filter && rule.filter.unique) {
+ if (!self.status.used_filters[rule.filter.id]) {
+ self.status.used_filters[rule.filter.id] = [];
+ }
+ if (rule.filter.unique == 'group') {
+ self.status.used_filters[rule.filter.id].push(rule.parent);
+ }
+ }
+ }, function(group) {
+ walk(group);
+ });
+ }(self.model.root));
+
+ self.applyDisabledFilters(e);
+ },
+
+ /**
+ * Clear the list of used filters
+ * @param {$.Event} [e]
+ * @private
+ */
+ clearDisabledFilters: function(e) {
+ var self = e ? e.builder : this;
+
+ self.status.used_filters = {};
+
+ self.applyDisabledFilters(e);
+ },
+
+ /**
+ * Disabled filters depending on the list of used ones
+ * @param {$.Event} [e]
+ * @private
+ */
+ applyDisabledFilters: function(e) {
+ var self = e ? e.builder : this;
+
+ // re-enable everything
+ self.$el.find(QueryBuilder.selectors.filter_container + ' option').prop('disabled', false);
+
+ // disable some
+ $.each(self.status.used_filters, function(filterId, groups) {
+ if (groups.length === 0) {
+ self.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true);
+ }
+ else {
+ groups.forEach(function(group) {
+ group.each(function(rule) {
+ rule.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true);
+ });
+ });
+ }
+ });
+
+ // update Selectpicker
+ if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) {
+ self.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render');
+ }
+ }
+});
+
+
+/*!
+ * jQuery QueryBuilder 3.0.0
+ * Locale: English (en)
+ * Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+QueryBuilder.regional['en'] = {
+ "__locale": "English (en)",
+ "__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr",
+ "add_rule": "Add rule",
+ "add_group": "Add group",
+ "delete_rule": "Delete",
+ "delete_group": "Delete",
+ "conditions": {
+ "AND": "AND",
+ "OR": "OR"
+ },
+ "operators": {
+ "equal": "equal",
+ "not_equal": "not equal",
+ "in": "in",
+ "not_in": "not in",
+ "less": "less",
+ "less_or_equal": "less or equal",
+ "greater": "greater",
+ "greater_or_equal": "greater or equal",
+ "between": "between",
+ "not_between": "not between",
+ "begins_with": "begins with",
+ "not_begins_with": "doesn't begin with",
+ "contains": "contains",
+ "not_contains": "doesn't contain",
+ "ends_with": "ends with",
+ "not_ends_with": "doesn't end with",
+ "is_empty": "is empty",
+ "is_not_empty": "is not empty",
+ "is_null": "is null",
+ "is_not_null": "is not null"
+ },
+ "errors": {
+ "no_filter": "No filter selected",
+ "empty_group": "The group is empty",
+ "radio_empty": "No value selected",
+ "checkbox_empty": "No value selected",
+ "select_empty": "No value selected",
+ "string_empty": "Empty value",
+ "string_exceed_min_length": "Must contain at least {0} characters",
+ "string_exceed_max_length": "Must not contain more than {0} characters",
+ "string_invalid_format": "Invalid format ({0})",
+ "number_nan": "Not a number",
+ "number_not_integer": "Not an integer",
+ "number_not_double": "Not a real number",
+ "number_exceed_min": "Must be greater than {0}",
+ "number_exceed_max": "Must be lower than {0}",
+ "number_wrong_step": "Must be a multiple of {0}",
+ "number_between_invalid": "Invalid values, {0} is greater than {1}",
+ "datetime_empty": "Empty value",
+ "datetime_invalid": "Invalid date format ({0})",
+ "datetime_exceed_min": "Must be after {0}",
+ "datetime_exceed_max": "Must be before {0}",
+ "datetime_between_invalid": "Invalid values, {0} is greater than {1}",
+ "boolean_not_valid": "Not a boolean",
+ "operator_not_multiple": "Operator \"{1}\" cannot accept multiple values"
+ },
+ "invert": "Invert",
+ "NOT": "NOT"
+};
+
+QueryBuilder.defaults({ lang_code: 'en' });
+return QueryBuilder;
+
+}));
diff --git a/dist/query-builder.css b/dist/query-builder.css
deleted file mode 100644
index 9722cee5..00000000
--- a/dist/query-builder.css
+++ /dev/null
@@ -1,113 +0,0 @@
-/*!
- * jQuery QueryBuilder 1.3.0-SNAPSHOT
- * Copyright 2014 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
- * Licensed under MIT (http://opensource.org/licenses/MIT)
- */
-.rule-container,
-.rules-group-container,
-.rule-placeholder {
- margin:4px 0;
- border-radius:5px;
- padding:5px;
- border:1px solid #eee;
- background:#fff;
- background:rgba(255, 255, 255, 0.9);
-}
-
-/* GROUPS */
-.rules-group-container {
- padding:10px 10px 5px 10px;
- border:1px solid #DCC896;
- background:#FCF9ED;
- background:rgba(250, 240, 210, 0.5);
-}
- .rules-group-header {
- margin-bottom:10px;
- }
- .rules-group-header input[name$=_cond] {
- display:none;
- }
-
-/* RULES */
-.rules-list {
- list-style:none;
- padding:0 0 0 20px;
- margin:0;
-}
- .rule-container {}
- .rule-container.has-error {
- background:#fdd;
- border-color:#f99;
- }
- .rule-container>div:not(.rule-header) {
- display:inline-block;
- margin:0 5px 0 0;
- vertical-align:top;
- }
- .rule-value-container:not(:empty) {
- border-left:1px solid #ddd;
- padding-left:5px;
- }
- .rule-value-container label {
- margin-bottom:0;
- }
- .rule-value-container label.block {
- display:block;
- }
- .rule-container select,
- .rule-container input[type=text],
- .rule-container input[type=number] {
- padding:1px;
- }
-
-/* TICKS */
-.rules-list>* {
- position:relative;
-}
- .rules-list>*:before,
- .rules-list>*:after {
- content:'';
- position:absolute;
- left:-15px;
- width:15px;
- height:calc(50% + 4px);
- border-color:#ccc;
- border-style:solid;
- }
-
- .rules-list>*:before {
- top:-2px;
- border-width:0 0 2px 2px;
- }
- .rules-list>*:after {
- top:50%;
- border-width:0 0 0 2px;
- }
-
- .rules-list>*:first-child:before {
- top:-12px;
- height:calc(50% + 14px);
- }
- .rules-list>*:last-child:before {
- border-radius:0 0 0 4px;
- }
- .rules-list>*:last-child:after {
- display:none;
- }
-
-/* DRAG & DROP */
-.rule-container .drag-handle,
-.rules-group-container .drag-handle {
- cursor:move;
- display:inline-block;
-}
-
-.rule-container .dragged,
-.rules-group-container .dragged {
- opacity:0.5;
-}
-
-.rule-placeholder {
- border:1px dashed #bbb;
- opacity:0.7;
-}
diff --git a/dist/query-builder.js b/dist/query-builder.js
deleted file mode 100644
index c1e4e870..00000000
--- a/dist/query-builder.js
+++ /dev/null
@@ -1,1538 +0,0 @@
-/*!
- * jQuery QueryBuilder 1.3.0-SNAPSHOT
- * Copyright 2014 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
- * Licensed under MIT (http://opensource.org/licenses/MIT)
- */
-/*jshint multistr:true */
-
-(function($){
- "use strict";
-
- // GLOBAL STATIC VARIABLES
- // ===============================
- var types = [
- 'string',
- 'integer',
- 'double',
- 'date',
- 'time',
- 'datetime'
- ],
- inputs = [
- 'text',
- 'radio',
- 'checkbox',
- 'select'
- ];
-
-
- // CLASS DEFINITION
- // ===============================
- var QueryBuilder = function($el, options) {
- // variables
- this.$el = $el;
-
- this.settings = merge(QueryBuilder.DEFAULTS, options);
- this.status = { group_id: 0, rule_id: 0, generatedId: false };
-
- this.filters = this.settings.filters;
- this.lang = this.settings.lang;
- this.operators = this.settings.operators;
- this.template = this.settings.template;
-
- if (this.template.group === null) {
- this.template.group = this.getGroupTemplate;
- }
- if (this.template.rule === null) {
- this.template.rule = this.getRuleTemplate;
- }
-
- // ensure we have a container id
- if (!this.$el.attr('id')) {
- this.$el.attr('id', 'qb_'+Math.floor(Math.random()*99999));
- this.status.generatedId = true;
- }
- this.$el_id = this.$el.attr('id');
-
- // check filters
- if (!this.filters || this.filters.length < 1) {
- $.error('Missing filters list');
- }
- this.checkFilters();
-
- // init
- this.init(options);
- };
-
-
- // DEFAULT CONFIG
- // ===============================
- QueryBuilder.DEFAULTS = {
- onValidationError: null,
- onAfterAddGroup: null,
- onAfterAddRule: null,
-
- allow_groups: true,
- sortable: false,
- filters: [],
- conditions: ['AND', 'OR'],
- default_condition: 'AND',
- readonly_behavior: {
- delete_group: false,
- sortable: true
- },
-
- template: {
- group: null,
- rule: null
- },
-
- lang: {
- add_rule: 'Add rule',
- add_group: 'Add group',
- delete_rule: 'Delete',
- delete_group: 'Delete',
-
- condition_and: 'AND',
- condition_or: 'OR',
-
- filter_select_placeholder: '------',
-
- operator_equal: 'equal',
- operator_not_equal: 'not equal',
- operator_in: 'in',
- operator_not_in: 'not in',
- operator_less: 'less',
- operator_less_or_equal: 'less or equal',
- operator_greater: 'greater',
- operator_greater_or_equal: 'greater or equal',
- operator_begins_with: 'begins with',
- operator_not_begins_with: 'doesn\'t begin with',
- operator_contains: 'contains',
- operator_not_contains: 'doesn\'t contain',
- operator_ends_with: 'ends with',
- operator_not_ends_with: 'doesn\'t end with',
- operator_is_empty: 'is empty',
- operator_is_not_empty: 'is not empty',
- operator_is_null: 'is null',
- operator_is_not_null: 'is not null'
- },
-
- operators: [
- {type: 'equal', accept_values: true, apply_to: ['string', 'number', 'datetime']},
- {type: 'not_equal', accept_values: true, apply_to: ['string', 'number', 'datetime']},
- {type: 'in', accept_values: true, apply_to: ['string', 'number', 'datetime']},
- {type: 'not_in', accept_values: true, apply_to: ['string', 'number', 'datetime']},
- {type: 'less', accept_values: true, apply_to: ['number', 'datetime']},
- {type: 'less_or_equal', accept_values: true, apply_to: ['number', 'datetime']},
- {type: 'greater', accept_values: true, apply_to: ['number', 'datetime']},
- {type: 'greater_or_equal', accept_values: true, apply_to: ['number', 'datetime']},
- {type: 'begins_with', accept_values: true, apply_to: ['string']},
- {type: 'not_begins_with', accept_values: true, apply_to: ['string']},
- {type: 'contains', accept_values: true, apply_to: ['string']},
- {type: 'not_contains', accept_values: true, apply_to: ['string']},
- {type: 'ends_with', accept_values: true, apply_to: ['string']},
- {type: 'not_ends_with', accept_values: true, apply_to: ['string']},
- {type: 'is_empty', accept_values: false, apply_to: ['string']},
- {type: 'is_not_empty', accept_values: false, apply_to: ['string']},
- {type: 'is_null', accept_values: false, apply_to: ['string', 'number', 'datetime']},
- {type: 'is_not_null', accept_values: false, apply_to: ['string', 'number', 'datetime']}
- ]
- };
-
-
- // PUBLIC METHODS
- // ===============================
- /**
- * Init event handlers and default display
- */
- QueryBuilder.prototype.init = function(options) {
- var that = this;
-
- // EVENTS
- // group condition change
- this.$el.on('change.queryBuilder', '.rules-group-header input[name$=_cond]', function() {
- var $this = $(this);
-
- if ($this.is(':checked')) {
- $this.parent().addClass('active');
- $this.parent().siblings().removeClass('active');
- }
- });
-
- // rule filter change
- this.$el.on('change.queryBuilder', '.rule-filter-container select[name$=_filter]', function() {
- var $this = $(this),
- $rule = $this.closest('.rule-container');
-
- that.createRuleOperators($rule, $this.val());
- that.createRuleInput($rule, $this.val());
- });
-
- // rule operator change
- this.$el.on('change.queryBuilder', '.rule-operator-container select[name$=_operator]', function() {
- var $this = $(this),
- $rule = $this.closest('.rule-container');
-
- that.updateRuleOperator($rule, $this.val());
- });
-
- // add rule button
- this.$el.on('click.queryBuilder', '[data-add=rule]', function() {
- var $this = $(this),
- $ul = $this.closest('.rules-group-container').find('>.rules-group-body>.rules-list');
-
- that.addRule($ul);
- });
-
- // add group button
- if (this.settings.allow_groups) {
- this.$el.on('click.queryBuilder', '[data-add=group]', function() {
- var $this = $(this),
- $ul = $this.closest('.rules-group-container').find('>.rules-group-body>.rules-list');
-
- that.addGroup($ul);
- });
- }
-
- // delete rule button
- this.$el.on('click.queryBuilder', '[data-delete=rule]', function() {
- var $this = $(this),
- $rule = $this.closest('.rule-container');
-
- $rule.remove();
- });
-
- // delete group button
- this.$el.on('click.queryBuilder', '[data-delete=group]', function() {
- var $this = $(this),
- $group = $this.closest('.rules-group-container');
-
- that.deleteGroup($group);
- });
-
- // INIT
- if (this.settings.sortable) {
- this.initSortable();
- }
-
- this.$el.addClass('query-builder');
-
- if (options.rules) {
- this.setRules(options.rules);
- }
- else {
- this.addGroup(this.$el);
- }
- };
-
- /**
- * Destroy the plugin
- */
- QueryBuilder.prototype.destroy = function() {
- if (this.status.generatedId) {
- this.$el.removeAttr('id');
- }
-
- this.$el.empty()
- .off('click.queryBuilder change.queryBuilder')
- .removeClass('query-builder')
- .removeData('queryBuilder');
- };
-
- /**
- * Reset the plugin
- */
- QueryBuilder.prototype.reset = function() {
- this.status.group_id = 1;
- this.status.rule_id = 0;
-
- this.addRule(this.$el.find('>.rules-group-container>.rules-group-body>.rules-list').empty());
- };
-
- /**
- * Clear the plugin
- */
- QueryBuilder.prototype.clear = function() {
- this.status.group_id = 0;
- this.status.rule_id = 0;
-
- this.$el.empty();
- };
-
- /**
- * Get an object representing current rules
- * @return {object}
- */
- QueryBuilder.prototype.getRules = function() {
- this.markRuleAsError(this.$el.find('.rule-container'), false);
-
- var $group = this.$el.find('>.rules-group-container'),
- that = this;
-
- return (function parse($group) {
- var out = {},
- $elements = $group.find('>.rules-group-body>.rules-list>*');
-
- out.condition = $group.find('>.rules-group-header input[name$=_cond]:checked').val();
- out.rules = [];
-
- for (var i=0, l=$elements.length; i.rules-group-body>.rules-list'),
- $buttons = $group.find('>.rules-group-header input[name$=_cond]');
-
- if (data.condition === undefined) {
- data.condition = that.settings.default_condition;
- }
-
- for (var i=0, l=that.settings.conditions.length; i0) {
- if (!that.settings.allow_groups) {
- $.error('Groups are disabled');
- }
- else {
- add(rule, $ul);
- }
- }
- else {
- if (rule.id === undefined) {
- $.error('Missing rule field id');
- }
- if (rule.value === undefined) {
- rule.value = '';
- }
- if (rule.operator === undefined) {
- rule.operator = 'equal';
- }
-
- var $rule = that.addRule($ul),
- filter = that.getFilterById(rule.id),
- operator = that.getOperatorByType(rule.operator),
- $value = $rule.find('.rule-value-container');
-
- $rule.find('.rule-filter-container select[name$=_filter]').val(rule.id).trigger('change');
- $rule.find('.rule-operator-container select[name$=_operator]').val(rule.operator).trigger('change');
-
- if (operator.accept_values) {
- switch (filter.input) {
- case 'radio':
- $value.find('input[name$=_value][value="'+ rule.value +'"]').prop('checked', true).trigger('change');
- break;
-
- case 'checkbox':
- if (!$.isArray(rule.value)) {
- rule.value = [rule.value];
- }
- $.each(rule.value, function(i, value) {
- $value.find('input[name$=_value][value="'+ value +'"]').prop('checked', true).trigger('change');
- });
- break;
-
- case 'select':
- $value.find('select[name$=_value]').val(rule.value).trigger('change');
- break;
-
- /* falls through */
- case 'text': default:
- $value.find('input[name$=_value]').val(rule.value).trigger('change');
- break;
- }
-
- if (rule.readonly) {
- $rule.find('input, select').prop('disabled', true);
- $rule.addClass('disabled').find('[data-delete=rule]').remove();
-
- if (that.settings.sortable && !that.settings.readonly_behavior.sortable) {
- $rule.find('.drag-handle').remove();
- }
- }
- }
-
- if (filter.onAfterSetValue) {
- filter.onAfterSetValue.call(that, $rule, rule.value, filter, operator);
- }
- }
- });
-
- }(data, $container));
- };
-
-
- // MAIN METHODS
- // ===============================
- /**
- * Checks the configuration of each filter
- */
- QueryBuilder.prototype.checkFilters = function() {
- var definedFilters = [],
- that = this;
-
- $.each(this.filters, function(i, filter) {
- if (!filter.id) {
- $.error('Missing filter id: '+ i);
- }
- if (definedFilters.indexOf(filter.id) != -1) {
- $.error('Filter already defined: '+ filter.id);
- }
- definedFilters.push(filter.id);
-
- if (!filter.type) {
- $.error('Missing filter type: '+ filter.id);
- }
- if (types.indexOf(filter.type) == -1) {
- $.error('Invalid type: '+ filter.type);
- }
-
- if (!filter.input) {
- filter.input = 'text';
- }
- else if (typeof filter.input != 'function' && inputs.indexOf(filter.input) == -1) {
- $.error('Invalid input: '+ filter.input);
- }
-
- if (!filter.field) {
- filter.field = filter.id;
- }
- if (!filter.label) {
- filter.label = filter.field;
- }
-
- switch (filter.type) {
- case 'string':
- filter.internalType = 'string';
- break;
- case 'integer': case 'double':
- filter.internalType = 'number';
- break;
- case 'date': case 'time': case 'datetime':
- filter.internalType = 'datetime';
- break;
- }
-
- switch (filter.input) {
- case 'radio': case 'checkbox':
- if (!filter.values || filter.values.length < 1) {
- $.error('Missing values for filter: '+ filter.id);
- }
- break;
- }
- });
- };
-
- /**
- * Add a new rules group
- * @param container {jQuery} (parent )
- * @param addRule {bool} (optional - add a default empty rule)
- * @return $group {jQuery}
- */
- QueryBuilder.prototype.addGroup = function(container, addRule) {
- var group_id = this.nextGroupId(),
- first = group_id == this.$el_id + '_group_0',
- $group = $(this.template.group.call(this, group_id, first));
-
- container.append($group);
-
- if (this.settings.onAfterAddGroup) {
- this.settings.onAfterAddGroup.call(this, $group);
- }
-
- if (addRule === undefined || addRule === true) {
- this.addRule($group.find('>.rules-group-body>.rules-list'));
- }
-
- return $group;
- };
-
- /**
- * Tries to delete a group after checks
- * @param $group {jQuery}
- */
- QueryBuilder.prototype.deleteGroup = function($group) {
- if ($group[0].id == this.$el_id + '_group_0') {
- return;
- }
-
- if (this.settings.readonly_behavior.delete_group) {
- $group.remove();
- }
-
- var that = this,
- keepGroup = false;
-
- $group.find('>.rules-group-body>.rules-list>*').each(function() {
- var $element = $(this);
-
- if ($element.hasClass('rule-container')) {
- if ($element.hasClass('disabled')) {
- keepGroup = true;
- }
- else {
- $element.remove();
- }
- }
- else {
- that.deleteGroup($element);
- }
- });
-
- if (!keepGroup) {
- $group.remove();
- }
- };
-
- /**
- * Add a new rule
- * @param container {jQuery} (parent )
- * @return $rule {jQuery}
- */
- QueryBuilder.prototype.addRule = function(container) {
- var rule_id = this.nextRuleId(),
- $rule = $(this.template.rule.call(this, rule_id)),
- $filterSelect = $(this.getRuleFilterSelect(rule_id));
-
- container.append($rule);
- $rule.find('.rule-filter-container').append($filterSelect);
-
- if (this.settings.onAfterAddRule) {
- this.settings.onAfterAddRule.call(this, $rule);
- }
-
- return $rule;
- };
-
- /**
- * Create operators