diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index fe882f7e..be3320fd 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -3,6 +3,8 @@
## 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.
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index a9007708..7a8c011b 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -3,4 +3,4 @@
- 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://www.jsdelivr.com/projects/jquery.query-builder) to be used on such platforms).
+- 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
index 25399e11..1131297f 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -2,6 +2,6 @@
- [ ] 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`
-- [ ] Unit tests are OK
+- [ ] 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 f4b28947..a50c5eea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,7 @@
-bower_components
node_modules
dist
+doc
.sass-cache
-.coverage-results
.idea
*.iml
+package-lock.json
diff --git a/.jscsrc b/.jscsrc
deleted file mode 100644
index ce1e5220..00000000
--- a/.jscsrc
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "preset": "idiomatic",
- "validateIndentation": 4,
- "requireCamelCaseOrUpperCaseIdentifiers": false,
- "disallowKeywordsOnNewLine": [],
- "requireKeywordsOnNewLine": [
- "else"
- ],
- "requirePaddingNewLinesAfterBlocks": false,
- "safeContextKeyword": "self",
- "disallowMultipleLineStrings": false,
- "requirePaddingNewLinesBeforeLineComments": false,
- "requireSpaceBeforeBinaryOperators": [
- "=", "+", "-", "*", "/", "%",
- "<<", ">>", ">>>", "&", "|", "^",
- "&&", "||",
- "===", "==", ">=", "<=", "<", ">", "!=", "!=="
- ],
- "requireDotNotation": false,
- "requireSpacesInsideBrackets": false,
- "requireSpacesInsideParentheses": false,
- "maximumLineLength": null,
- "maximumNumberOfLines": null,
- "validateQuoteMarks": {
- "mark": "'",
- "escape": true
- },
- "requireCurlyBraces": [
- "for",
- "while",
- "do",
- "try",
- "catch"
- ],
- "requireEarlyReturn": false,
- "validateCommentPosition": false
-}
\ No newline at end of file
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/.jshintrc b/.jshintrc
deleted file mode 100644
index 518111a8..00000000
--- a/.jshintrc
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "-W069": true, // accesses to "regional" in language files
- "multistr": true
-}
\ No newline at end of file
diff --git a/.scss-lint.yml b/.scss-lint.yml
deleted file mode 100644
index db8d4f79..00000000
--- a/.scss-lint.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-linters:
- PropertySortOrder:
- enabled: false
-
- SingleLinePerSelector:
- enabled: false
-
- SelectorDepth:
- max_depth: 4
-
- NestingDepth:
- max_depth: 4
-
- HexLength:
- enabled: false
-
- HexNotation:
- style: uppercase
-
- Shorthand:
- allowed_shorthands: [1, 2, 4]
-
- QualifyingElement:
- enabled: false
-
- ImportantRule:
- enabled: false
-
- VendorPrefix:
- exclude:
- - src/scss/_mixins.scss
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 59e0af84..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-language: node_js
-node_js:
- - "5"
-before_install:
- - gem install sass
- - gem install scss_lint -v 0.49.0
- - npm install -g grunt-cli
- - npm install -g bower
-before_script:
- - bower install
-after_success: grunt coveralls
diff --git a/Gruntfile.js b/Gruntfile.js
deleted file mode 100644
index 36ab0d62..00000000
--- a/Gruntfile.js
+++ /dev/null
@@ -1,625 +0,0 @@
-var deepmerge = require('deepmerge');
-
-module.exports = function(grunt) {
- require('time-grunt')(grunt);
- require('jit-grunt')(grunt, {
- scsslint: 'grunt-scss-lint'
- });
-
- grunt.util.linefeed = '\n';
-
- function removeJshint(src) {
- return src
- .replace(/\/\*jshint [a-z:]+ \*\/\r?\n\r?\n?/g, '')
- .replace(/\/\*jshint -[EWI]{1}[0-9]{3} \*\/\r?\n\r?\n?/g, '');
- }
-
- function process_lang(file, src, wrapper) {
- var lang = file.split(/[\/\.]/)[2];
- var content = JSON.parse(src);
- wrapper = wrapper || ['', ''];
-
- grunt.config.set('lang_locale', content.__locale || lang);
- grunt.config.set('lang_author', content.__author);
- var header = grunt.template.process('<%= langBanner %>');
-
- loaded_plugins.forEach(function(p) {
- var plugin_file = 'src/plugins/' + p + '/i18n/' + lang + '.json';
-
- if (grunt.file.exists(plugin_file)) {
- content = deepmerge(content, grunt.file.readJSON(plugin_file));
- }
- });
-
- return header
- + '\n\n'
- + wrapper[0]
- + 'QueryBuilder.regional[\'' + lang + '\'] = '
- + JSON.stringify(content, null, 2)
- + ';\n\n'
- + 'QueryBuilder.defaults({ lang_code: \'' + lang + '\' });'
- + wrapper[1];
- }
-
-
- var all_plugins = {},
- all_langs = {},
- loaded_plugins = [],
- loaded_langs = [],
- js_core_files = [
- 'src/main.js',
- 'src/defaults.js',
- 'src/core.js',
- 'src/public.js',
- 'src/data.js',
- 'src/template.js',
- 'src/model.js',
- 'src/utils.js',
- 'src/jquery.js'
- ],
- js_files_to_load = js_core_files.slice(),
- all_js_files = js_core_files.slice(),
- js_files_for_standalone = [
- 'bower_components/jquery-extendext/jQuery.extendext.js',
- 'bower_components/doT/doT.js',
- 'dist/js/query-builder.js'
- ];
-
-
- (function() {
- // list available plugins and languages
- grunt.file.expand('src/plugins/**/plugin.js')
- .forEach(function(f) {
- var n = f.split('/')[2];
- all_plugins[n] = f;
- });
-
- grunt.file.expand('src/i18n/*.json')
- .forEach(function(f) {
- var n = f.split(/[\/\.]/)[2];
- all_langs[n] = f;
- });
-
- // fill all js files
- for (var p in all_plugins) {
- all_js_files.push(all_plugins[p]);
- }
-
- // parse 'plugins' parameter
- var arg_plugins = grunt.option('plugins');
- if (typeof arg_plugins === 'string') {
- arg_plugins.replace(/ /g, '').split(',').forEach(function(p) {
- if (all_plugins[p]) {
- js_files_to_load.push(all_plugins[p]);
- loaded_plugins.push(p);
- }
- else {
- grunt.fail.warn('Plugin ' + p + ' unknown');
- }
- });
- }
- else if (arg_plugins === undefined) {
- for (var p in all_plugins) {
- js_files_to_load.push(all_plugins[p]);
- loaded_plugins.push(p);
- }
- }
-
- // default language
- js_files_to_load.push('.temp/i18n/en.js');
- loaded_langs.push('en');
-
- // parse 'lang' parameter
- var arg_langs = grunt.option('languages');
- if (typeof arg_langs === 'string') {
- arg_langs.replace(/ /g, '').split(',').forEach(function(l) {
- if (all_langs[l]) {
- if (l !== 'en') {
- js_files_to_load.push(all_langs[l].replace(/^src/, '.temp').replace(/json$/, 'js'));
- loaded_langs.push(l);
- }
- }
- else {
- grunt.fail.warn('Language ' + l + ' unknown');
- }
- });
- }
- }());
-
-
- grunt.initConfig({
- pkg: grunt.file.readJSON('package.json'),
-
- banner:
- '/*!\n' +
- ' * jQuery QueryBuilder <%= pkg.version %>\n' +
- ' * Copyright 2014-<%= grunt.template.today("yyyy") %> Damien "Mistic" Sorel (http://www.strangeplanet.fr)\n' +
- ' * Licensed under MIT (http://opensource.org/licenses/MIT)\n' +
- ' */',
-
- langBanner:
- '/*!\n' +
- ' * jQuery QueryBuilder <%= pkg.version %>\n' +
- ' * Locale: <%= lang_locale %>\n' +
- '<% if (lang_author) { %> * Author: <%= lang_author %>\n<% } %>' +
- ' * Licensed under MIT (http://opensource.org/licenses/MIT)\n' +
- ' */',
-
- // serve folder content
- connect: {
- dev: {
- options: {
- host: '0.0.0.0',
- port: 9000,
- livereload: true
- }
- }
- },
-
- // watchers
- watch: {
- options: {
- livereload: true
- },
- js: {
- files: ['src/*.js', 'src/plugins/**/plugin.js'],
- tasks: ['build_js']
- },
- css: {
- files: ['src/scss/*.scss', 'src/plugins/**/plugin.scss'],
- tasks: ['build_css']
- },
- lang: {
- files: ['src/i18n/*.json', 'src/plugins/**/i18n/*.json'],
- tasks: ['build_lang']
- },
- example: {
- files: ['examples/**'],
- tasks: []
- }
- },
-
- // open example
- open: {
- dev: {
- path: 'http://localhost:<%= connect.dev.options.port%>/examples/index.html'
- }
- },
-
- // copy SASS files
- copy: {
- sass_core: {
- files: [{
- expand: true,
- flatten: true,
- src: ['src/scss/*.scss'],
- dest: 'dist/scss'
- }]
- },
- sass_plugins: {
- files: loaded_plugins.map(function(name) {
- return {
- src: 'src/plugins/' + name + '/plugin.scss',
- dest: 'dist/scss/plugins/' + name + '.scss'
- };
- })
- }
- },
-
- concat: {
- // concat all JS
- js: {
- src: js_files_to_load,
- dest: 'dist/js/query-builder.js',
- options: {
- stripBanners: false,
- separator: '\n\n',
- process: function(src) {
- return removeJshint(src).replace(/\r\n/g, '\n');
- }
- }
- },
- // create standalone version
- js_standalone: {
- src: js_files_for_standalone,
- dest: 'dist/js/query-builder.standalone.js',
- options: {
- stripBanners: false,
- separator: '\n\n',
- process: function(src, file) {
- var name = file.match(/([^\/]+?).js$/)[1];
-
- return removeJshint(src)
- .replace(/\r\n/g, '\n')
- .replace(/define\((.*?)\);/, 'define(\'' + name + '\', $1);');
- }
- }
- },
- // compile language files with AMD wrapper
- lang: {
- files: Object.keys(all_langs).map(function(name) {
- return {
- src: 'src/i18n/' + name + '.json',
- dest: 'dist/i18n/query-builder.' + name + '.js'
- };
- }),
- options: {
- process: function(src, file) {
- var wrapper = grunt.file.read('src/i18n/.wrapper.js').replace(/\r\n/g, '\n').split(/@@js\n/);
- return process_lang(file, src, wrapper);
- }
- }
- },
- // compile language files without wrapper
- lang_temp: {
- files: Object.keys(all_langs).map(function(name) {
- return {
- src: 'src/i18n/' + name + '.json',
- dest: '.temp/i18n/' + name + '.js'
- };
- }),
- options: {
- process: function(src, file) {
- return process_lang(file, src);
- }
- }
- },
- // add banner to CSS files
- css: {
- options: {
- banner: '<%= banner %>\n\n',
- },
- files: [{
- expand: true,
- src: ['dist/css/*.css', 'dist/scss/*.scss'],
- dest: ''
- }]
- }
- },
-
- wrap: {
- // add AMD wrapper and banner
- js: {
- src: ['dist/js/query-builder.js'],
- dest: '',
- options: {
- separator: '',
- wrapper: function() {
- var wrapper = grunt.file.read('src/.wrapper.js').replace(/\r\n/g, '\n').split(/@@js\n/);
-
- if (loaded_plugins.length) {
- wrapper[0] = '// Plugins: ' + loaded_plugins.join(', ') + '\n' + wrapper[0];
- }
- if (loaded_langs.length) {
- wrapper[0] = '// Languages: ' + loaded_langs.join(', ') + '\n' + wrapper[0];
- }
- wrapper[0] = grunt.template.process('<%= banner %>\n\n') + wrapper[0];
-
- return wrapper;
- }
- }
- },
- // add plugins SASS imports
- sass: {
- src: ['dist/scss/default.scss'],
- dest: '',
- options: {
- separator: '',
- wrapper: function() {
- return ['', loaded_plugins.reduce(function(wrapper, name) {
- if (grunt.file.exists('dist/scss/plugins/' + name + '.scss')) {
- wrapper += '\n@import \'plugins/' + name + '\';';
- }
- return wrapper;
- }, '\n')];
- }
- }
- }
- },
-
- // parse scss
- sass: {
- options: {
- sourcemap: 'none',
- style: 'expanded'
- },
- dist: {
- files: [{
- expand: true,
- flatten: true,
- src: ['dist/scss/*.scss'],
- dest: 'dist/css',
- ext: '.css',
- rename: function(dest, src) {
- return dest + '/query-builder.' + src;
- }
- }]
- }
- },
-
- // compress js
- uglify: {
- options: {
- banner: '<%= banner %>\n\n',
- mangle: { except: ['$'] }
- },
- dist: {
- files: [{
- expand: true,
- flatten: true,
- src: ['dist/js/*.js', '!dist/js/*.min.js'],
- dest: 'dist/js',
- ext: '.min.js',
- extDot: 'last'
- }]
- }
- },
-
- // compress css
- cssmin: {
- dist: {
- files: [{
- expand: true,
- flatten: true,
- src: ['dist/css/*.css', '!dist/css/*.min.css'],
- dest: 'dist/css',
- ext: '.min.css',
- extDot: 'last'
- }]
- }
- },
-
- // clean build dir
- clean: {
- temp: ['.temp']
- },
-
- // jshint tests
- jshint: {
- lib: {
- options: {
- jshintrc: '.jshintrc'
- },
- src: js_files_to_load
- }
- },
-
- // jscs tests
- jscs: {
- lib: {
- options: {
- config: '.jscsrc'
- },
- src: js_files_to_load
- }
- },
-
- // scss tests
- scsslint: {
- lib: {
- options: {
- colorizeOutput: true,
- config: '.scss-lint.yml'
- },
- src: ['src/**/*.scss']
- }
- },
-
- // inject all source files and test modules in the test file
- 'string-replace': {
- test: {
- src: 'tests/index.html',
- dest: 'tests/index.html',
- options: {
- replacements: [{
- pattern: /()(?:[\s\S]*)()/m,
- replacement: function(match, m1, m2) {
- var scripts = '\n';
-
- js_core_files.forEach(function(file) {
- scripts += '\n';
- });
-
- scripts += '\n';
-
- for (var p in all_plugins) {
- scripts += '\n';
- }
-
- return m1 + scripts + m2;
- }
- }, {
- pattern: /()(?:[\s\S]*)()/m,
- replacement: function(match, m1, m2) {
- var scripts = '\n';
-
- grunt.file.expand('tests/*.module.js').forEach(function(file) {
- scripts += '\n';
- });
-
- return m1 + scripts + m2;
- }
- }]
- }
- }
- },
-
- // qunit test suite
- qunit: {
- all: {
- options: {
- urls: ['tests/index.html?coverage=true'],
- noGlobals: true
- }
- }
- },
-
- // save LCOV files
- qunit_blanket_lcov: {
- all: {
- files: [{
- expand: true,
- src: ['src/*.js', 'src/plugins/**/plugin.js']
- }],
- options: {
- dest: '.coverage-results/all.lcov'
- }
- }
- },
-
- // coveralls data
- coveralls: {
- options: {
- force: true
- },
- all: {
- src: '.coverage-results/all.lcov',
- }
- }
- });
-
-
- // list the triggers and changes
- grunt.registerTask('describe_triggers', 'List QueryBuilder triggers.', function() {
- var triggers = {};
- var total = 0;
-
- for (var f in all_js_files) {
- grunt.file.read(all_js_files[f]).split(/\r?\n/).forEach(function(line, i) {
- var matches = /(e = )?(?:this|that)\.(trigger|change)\('(\w+)'([^)]*)\);/.exec(line);
- if (matches !== null) {
- triggers[matches[3]] = {
- name: matches[3],
- type: matches[2],
- file: all_js_files[f],
- line: i,
- args: matches[4].slice(2),
- prevent: !!matches[1]
- };
-
- total++;
- }
- });
- }
-
- grunt.log.write('\n');
-
- for (var t in triggers) {
- grunt.log.write(t['cyan'] + ' ' + triggers[t].type['magenta']);
- if (triggers[t].prevent) grunt.log.write(' (*)'['yellow']);
- grunt.log.write('\n');
- grunt.log.writeln(' ' + (triggers[t].file + ':' + triggers[t].line)['red'] + ' ' + triggers[t].args);
- grunt.log.write('\n');
- }
-
- grunt.log.writeln((total + ' Triggers in QueryBuilder.')['cyan']['bold']);
- });
-
- // list all possible thrown errors
- grunt.registerTask('describe_errors', 'List QueryBuilder errors.', function() {
- var errors = {};
- var total = 0;
-
- for (var f in all_js_files) {
- grunt.file.read(all_js_files[f]).split(/\r?\n/).forEach(function(line, i) {
- var matches = /Utils\.error\('(\w+)', '([^)]+)'([^)]*)\);/.exec(line);
- if (matches !== null) {
- (errors[matches[1]] = errors[matches[1]] || []).push({
- type: matches[1],
- message: matches[2],
- file: all_js_files[f],
- line: i,
- args: matches[3].slice(2).split(', ')
- });
-
- total++;
- }
- });
- }
-
- grunt.log.write('\n');
-
- for (var e in errors) {
- grunt.log.writeln((e + 'Error')['cyan']);
- errors[e].forEach(function(error) {
- var message = error.message.replace(/{([0-9]+)}/g, function(m, i) {
- return error.args[parseInt(i)]['yellow'];
- });
- grunt.log.writeln(' ' + (error.file + ':' + error.line)['red']);
- grunt.log.writeln(' ' + message);
- });
- grunt.log.write('\n');
- }
-
- grunt.log.writeln((total + ' Errors in QueryBuilder.')['cyan']['bold']);
- });
-
- // display available modules
- grunt.registerTask('list_modules', 'List QueryBuilder plugins and languages.', function() {
- grunt.log.writeln('\nAvailable QueryBuilder plugins:\n');
-
- for (var p in all_plugins) {
- grunt.log.write(p['cyan']);
-
- if (grunt.file.exists(all_plugins[p].replace(/js$/, 'scss'))) {
- grunt.log.write(' + CSS');
- }
-
- grunt.log.write('\n');
- }
-
- grunt.log.writeln('\nAvailable QueryBuilder languages:\n');
-
- for (var l in all_langs) {
- if (l !== 'en') {
- grunt.log.writeln(l['cyan']);
- }
- }
- });
-
-
- grunt.registerTask('build_js', [
- 'concat:lang_temp',
- 'concat:js',
- 'wrap:js',
- 'concat:js_standalone',
- 'uglify',
- 'clean:temp'
- ]);
-
- grunt.registerTask('build_css', [
- 'copy:sass_core',
- 'copy:sass_plugins',
- 'wrap:sass',
- 'sass',
- 'cssmin',
- 'concat:css'
- ]);
-
- grunt.registerTask('build_lang', [
- 'concat:lang'
- ]);
-
- grunt.registerTask('default', [
- 'build_lang',
- 'build_js',
- 'build_css'
- ]);
-
- grunt.registerTask('test', [
- 'jshint',
- 'jscs',
- 'scsslint',
- 'default',
- 'string-replace:test',
- 'qunit_blanket_lcov',
- 'qunit'
- ]);
-
- grunt.registerTask('serve', [
- 'default',
- 'open',
- 'connect',
- 'watch'
- ]);
-};
diff --git a/LICENSE b/LICENSE
index 99070e3d..2558fa6a 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2014-2015 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 4aa83f81..f0fd8ab7 100644
--- a/README.md
+++ b/README.md
@@ -1,72 +1,61 @@
# jQuery QueryBuilder
-[](http://querybuilder.js.org)
-[](http://www.jsdelivr.com/projects/jquery.query-builder)
-[](https://travis-ci.org/mistic100/jQuery-QueryBuilder)
-[](https://coveralls.io/r/mistic100/jQuery-QueryBuilder)
-[](https://saythanks.io/to/mistic100)
+[](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.
-[](http://querybuilder.js.org)
+[](https://querybuilder.js.org)
+
+
## Documentation
-http://querybuilder.js.org
+[querybuilder.js.org](https://querybuilder.js.org)
-### Dependencies
- * jQuery >= 1.10
- * Bootstrap >= 3.1 (CSS only)
- * [jQuery.extendext](https://github.com/mistic100/jQuery.extendext)
- * [doT.js >= 1.0.3](http://olado.github.io/doT)
- * [MomentJS](http://momentjs.com) (optional, for Date/Time validation)
- * Other Bootstrap/jQuery plugins used by plugins
-($.extendext and doT.js are directly included in the [standalone](https://github.com/mistic100/jQuery-QueryBuilder/blob/master/dist/js/query-builder.standalone.js) file)
-### Browser support
- * Internet Explorer >= 9
- * All other recent browsers
+## Install
-### Build
+#### Manually
-#### Prerequisites
+[Download the latest release](https://github.com/mistic100/jQuery-QueryBuilder/releases)
- * NodeJS + NPM: `apt-get install nodejs-legacy npm`
- * Ruby Dev: `apt-get install ruby-dev`
- * Grunt CLI: `npm install -g grunt-cli`
- * Bower: `npm install -g bower`
- * SASS: `gem install sass`
+#### With npm
-#### Run
+```bash
+$ npm install jQuery-QueryBuilder
+```
-Install Node and Bower dependencies `npm install & bower install` then run `grunt` in the root directory to generate production files inside `dist`.
+#### Via CDN
-#### Options
+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
-You can choose which plugins to include with `--plugins` :
-```bash
-# include "sql-support" and "mongodb-support" plugins
-grunt --plugins=sql-support,mongodb-support
+($.extendext is directly included in the [standalone](https://github.com/mistic100/jQuery-QueryBuilder/blob/master/dist/js/query-builder.standalone.js) file)
-# disable all plugins
-grunt --plugins=false
-```
-All plugins are included by default.
-You can also include language files with `--languages` :
-```bash
-# include French & Italian translation
-grunt --languages=fr,it
-```
-#### Other commands
+## 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.
- * `grunt test` to run jshint/jscs/scsslint and the QUnit test suite.
- * `grunt list_modules` to get the list of available plugins and languages.
- * `grunt describe_triggers` to get the list of all triggers.
- * `grunt describe_errors` to get the list of all fatal errors.
- * `grunt watch` to automatically build the library when modifying the source files.
-### Inspiration
- * [Knockout Query Builder](http://kindohm.github.io/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 bf6b6519..00000000
--- a/bower.json
+++ /dev/null
@@ -1,55 +0,0 @@
-{
- "name": "jQuery-QueryBuilder",
- "authors": [
- {
- "name": "Damien \"Mistic\" Sorel",
- "email": "contact@git.strangeplanet.fr",
- "homepage": "http://www.strangeplanet.fr"
- }
- ],
- "description": "jQuery plugin for user friendly query/filter creator",
- "main": [
- "dist/js/query-builder.js",
- "dist/css/query-builder.default.css"
- ],
- "dependencies": {
- "jquery": ">=1.9.0",
- "bootstrap": ">=3.1.0",
- "moment": ">=2.6.0",
- "jquery-extendext": ">=0.1.2",
- "doT": ">=1.0.3"
- },
- "devDependencies": {
- "blanket": "^1.1.0",
- "qunit": "^1.23.0",
- "bootstrap-select": "^1.10.0",
- "bootbox": "^4.4.0",
- "awesome-bootstrap-checkbox": "^0.3.0",
- "sql-parser": "^1.1.0",
- "bind-polyfill": "~1.0.0",
- "interact": "^1.2.6"
- },
- "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",
- "src",
- "tests",
- "composer.json",
- "package.json",
- "Gruntfile.js",
- "CONTRIBUTING.md"
- ]
-}
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 09831111..00000000
--- a/composer.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "name": "mistic100/jquery-querybuilder",
- "version": "2.4.0",
- "authors": [{
- "name": "Damien \"Mistic\" Sorel",
- "email": "contact@git.strangeplanet.fr",
- "homepage": "http://www.strangeplanet.fr"
- }],
- "description": "jQuery plugin for user friendly query/filter creator",
- "require": {
- "components/jquery": ">=1.9.0",
- "moment/moment": ">=2.6.0",
- "twbs/bootstrap": ">=3.1.0"
- },
- "keywords": [
- "jquery",
- "query",
- "builder",
- "filter"
- ],
- "license": "MIT",
- "homepage": "https://github.com/mistic100/jQuery-QueryBuilder",
- "support": {
- "issues": "https://github.com/mistic100/jQuery-QueryBuilder/issues"
- }
-}
diff --git a/examples/bower.json b/examples/bower.json
deleted file mode 100644
index 85233d7e..00000000
--- a/examples/bower.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "name": "jQuery-QueryBuilder-example",
- "dependencies" : {
- "seiyria-bootstrap-slider": "latest",
- "bootswatch-dist": "#slate",
- "selectize": "latest"
- }
-}
diff --git a/examples/index.html b/examples/index.html
index 2f232ee6..180652b8 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -1,74 +1,70 @@
-
+
jQuery QueryBuilder Example
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
-
-
-
- You must execute bower install in the example directory to run this demo.
+
jQuery QueryBuilder
+ Example
+
-
-
+
+
-
@@ -76,7 +72,9 @@
jQuery QueryBuilder Example
-
+
@@ -100,491 +98,558 @@
Output
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ // remove filter 'coord'
+ $('#builder').queryBuilder('removeFilter',
+ ['coord', 'state', 'bson'],
+ true
+ );
-
diff --git a/package.json b/package.json
index c546cfad..4111f3a0 100644
--- a/package.json
+++ b/package.json
@@ -1,41 +1,40 @@
{
"name": "jQuery-QueryBuilder",
- "version": "2.4.0",
+ "version": "2.5.0",
"author": {
"name": "Damien \"Mistic\" Sorel",
"email": "contact@git.strangeplanet.fr",
- "url": "http://www.strangeplanet.fr"
+ "url": "https://www.strangeplanet.fr"
},
"description": "jQuery plugin for user friendly query/filter creator",
+ "main": "dist/js/query-builder.js",
+ "files": [
+ "dist/",
+ "src/"
+ ],
"dependencies": {
- "jquery": ">=1.9.0",
- "bootstrap": ">=3.1.0",
- "moment": ">=2.6.0",
- "jquery-extendext": ">=0.1.2",
- "dot": ">=1.0.3"
+ "bootstrap": "^5.3.0",
+ "@popperjs/core": "^2.11.8",
+ "bootstrap-icons": "^1.11.3",
+ "jquery": "^3.5.1",
+ "jquery-extendext": "^1.0.0",
+ "moment": "^2.29.1",
+ "sql-parser-mistic": "^1.2.3"
},
"devDependencies": {
- "deepmerge": "^0.2.0",
- "grunt": "^1.0.0",
- "grunt-contrib-clean": "^1.0.0",
- "grunt-contrib-concat": "^1.0.0",
- "grunt-contrib-connect": "^1.0.0",
- "grunt-contrib-copy": "^1.0.0",
- "grunt-contrib-cssmin": "^1.0.0",
- "grunt-contrib-jshint": "^1.0.0",
- "grunt-contrib-qunit": "^0.7.0",
- "grunt-contrib-sass": "^1.0.0",
- "grunt-contrib-uglify": "^1.0.0",
- "grunt-contrib-watch": "^1.0.0",
- "grunt-coveralls": "^1.0.0",
- "grunt-jscs": "^2.8.0",
- "grunt-open": "^0.2.3",
- "grunt-qunit-blanket-lcov": "^0.3.0",
- "grunt-scss-lint": "^0.3.8",
- "grunt-string-replace": "^1.2.0",
- "grunt-wrap": "^0.3.0",
- "jit-grunt": "^0.10.0",
- "time-grunt": "^1.3.0"
+ "alive-server": "^1.3.0",
+ "awesome-bootstrap-checkbox": "^0.3.7",
+ "bootbox": "^6.0.0",
+ "bootstrap-slider": "^10.0.0",
+ "chosenjs": "^1.4.3",
+ "concurrently": "^8.2.0",
+ "deepmerge": "^2.1.0",
+ "foodoc": "^0.0.9",
+ "glob": "^10.3.1",
+ "interactjs": "^1.3.3",
+ "nodemon": "^2.0.22",
+ "sass": "^1.63.6",
+ "@selectize/selectize": "^0.15.2"
},
"keywords": [
"jquery",
@@ -44,7 +43,7 @@
"filter"
],
"license": "MIT",
- "homepage": "https://github.com/mistic100/jQuery-QueryBuilder",
+ "homepage": "https://querybuilder.js.org",
"repository": {
"type": "git",
"url": "git://github.com/mistic100/jQuery-QueryBuilder.git"
@@ -53,6 +52,9 @@
"url": "https://github.com/mistic100/jQuery-QueryBuilder/issues"
},
"scripts": {
- "test": "grunt test"
+ "build": "node ./build/dist.mjs",
+ "watch:build": "nodemon --watch src -e js,scss,json ./build/dist.mjs --dev",
+ "watch:serve": "node ./build/liveserver.mjs",
+ "serve": "concurrently \"npm:watch:build\" \"npm:watch:serve\""
}
}
diff --git a/src/.wrapper.js b/src/.wrapper.js
index b4b9da8d..2b326090 100644
--- a/src/.wrapper.js
+++ b/src/.wrapper.js
@@ -1,13 +1,18 @@
(function(root, factory) {
if (typeof define == 'function' && define.amd) {
- define(['jquery', 'doT', 'jQuery.extendext'], factory);
+ define(['jquery', 'jquery-extendext'], factory);
+ }
+ else if (typeof module === 'object' && module.exports) {
+ module.exports = factory(require('jquery'), require('jquery-extendext'));
}
else {
- factory(root.jQuery, root.doT);
+ factory(root.jQuery);
}
-}(this, function($, doT) {
+}(this, function($) {
"use strict";
@@js
-}));
\ No newline at end of file
+return QueryBuilder;
+
+}));
diff --git a/src/core.js b/src/core.js
index d829bd67..bcb7c912 100644
--- a/src/core.js
+++ b/src/core.js
@@ -1,72 +1,19 @@
/**
- * Init the builder
+ * Final initialisation of the builder
+ * @param {object} [rules]
+ * @fires QueryBuilder.afterInit
+ * @private
*/
-QueryBuilder.prototype.init = function($el, options) {
- $el[0].queryBuilder = this;
- this.$el = $el;
-
- // PROPERTIES
- this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options);
- this.model = new Model();
- this.status = {
- group_id: 0,
- rule_id: 0,
- generated_id: false,
- has_optgroup: false,
- has_operator_oprgroup: false,
- id: null
- };
-
- // "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;
- }
-
- // SETTINGS SHORTCUTS
- this.filters = this.settings.filters;
- this.icons = this.settings.icons;
- this.operators = this.settings.operators;
- this.templates = this.settings.templates;
- this.plugins = this.settings.plugins;
-
- // 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);
-
- // init templates
- Object.keys(this.templates).forEach(function(tpl) {
- if (!this.templates[tpl]) {
- this.templates[tpl] = QueryBuilder.templates[tpl];
- }
- if (typeof this.templates[tpl] == 'string') {
- this.templates[tpl] = doT.template(this.templates[tpl]);
- }
- }, this);
-
- // ensure we have a container id
- if (!this.$el.attr('id')) {
- this.$el.attr('id', 'qb_' + Math.floor(Math.random() * 99999));
- this.status.generated_id = true;
- }
- this.status.id = this.$el.attr('id');
-
- // INIT
- this.$el.addClass('query-builder form-inline');
-
- this.filters = this.checkFilters(this.filters);
- this.operators = this.checkOperators(this.operators);
- this.bindEvents();
- this.initPlugins();
-
+QueryBuilder.prototype.init = function(rules) {
+ /**
+ * When the initilization is done, just before creating the root group
+ * @event afterInit
+ * @memberof QueryBuilder
+ */
this.trigger('afterInit');
- if (options.rules) {
- this.setRules(options.rules);
+ if (rules) {
+ this.setRules(rules);
delete this.settings.rules;
}
else {
@@ -76,6 +23,8 @@ QueryBuilder.prototype.init = function($el, options) {
/**
* Checks the configuration of each filter
+ * @param {QueryBuilder.Filter[]} filters
+ * @returns {QueryBuilder.Filter[]}
* @throws ConfigError
*/
QueryBuilder.prototype.checkFilters = function(filters) {
@@ -102,7 +51,7 @@ QueryBuilder.prototype.checkFilters = function(filters) {
}
if (!filter.input) {
- filter.input = 'text';
+ 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);
@@ -136,19 +85,48 @@ QueryBuilder.prototype.checkFilters = function(filters) {
}
switch (filter.input) {
- case 'radio': case 'checkbox':
+ 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;
}
- Utils.iterateOptions(filter.values, function(key) {
- if (key == filter.placeholder_value) {
+
+ 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);
}
});
@@ -164,7 +142,7 @@ QueryBuilder.prototype.checkFilters = function(filters) {
else {
var self = this;
filters.sort(function(a, b) {
- return self.translateLabel(a.label).localeCompare(self.translateLabel(b.label));
+ return self.translate(a.label).localeCompare(self.translate(b.label));
});
}
}
@@ -178,6 +156,8 @@ QueryBuilder.prototype.checkFilters = function(filters) {
/**
* Checks the configuration of each operator
+ * @param {QueryBuilder.Operator[]} operators
+ * @returns {QueryBuilder.Operator[]}
* @throws ConfigError
*/
QueryBuilder.prototype.checkOperators = function(operators) {
@@ -231,54 +211,56 @@ QueryBuilder.prototype.checkOperators = function(operators) {
};
/**
- * Add all events listeners
+ * 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);
- Model($group).condition = $(this).val();
+ 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);
- Model($rule).filter = self.getFilterById($(this).val());
+ 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);
- Model($rule).operator = self.getOperatorByType($(this).val());
+ 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(Model($group));
+ 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(Model($rule));
+ 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(Model($group));
+ 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(Model($group));
+ self.deleteGroup(self.getModel($group));
});
}
@@ -288,12 +270,12 @@ QueryBuilder.prototype.bindEvents = function() {
node.$el.remove();
self.refreshGroupsConditions();
},
- 'add': function(e, node, index) {
+ 'add': function(e, parent, node, index) {
if (index === 0) {
- node.$el.prependTo(node.parent.$el.find('>' + Selectors.rules_list));
+ node.$el.prependTo(parent.$el.find('>' + QueryBuilder.selectors.rules_list));
}
else {
- node.$el.insertAfter(node.parent.rules[index - 1].$el);
+ node.$el.insertAfter(parent.rules[index - 1].$el);
}
self.refreshGroupsConditions();
},
@@ -301,7 +283,7 @@ QueryBuilder.prototype.bindEvents = function() {
node.$el.detach();
if (index === 0) {
- node.$el.prependTo(group.$el.find('>' + Selectors.rules_list));
+ node.$el.prependTo(group.$el.find('>' + QueryBuilder.selectors.rules_list));
}
else {
node.$el.insertAfter(group.rules[index - 1].$el);
@@ -312,7 +294,7 @@ QueryBuilder.prototype.bindEvents = function() {
if (node instanceof Rule) {
switch (field) {
case 'error':
- self.displayError(node);
+ self.updateError(node);
break;
case 'flags':
@@ -328,14 +310,14 @@ QueryBuilder.prototype.bindEvents = function() {
break;
case 'value':
- self.updateRuleValue(node);
+ self.updateRuleValue(node, oldValue);
break;
}
}
else {
switch (field) {
case 'error':
- self.displayError(node);
+ self.updateError(node);
break;
case 'flags':
@@ -343,7 +325,7 @@ QueryBuilder.prototype.bindEvents = function() {
break;
case 'condition':
- self.updateGroupCondition(node);
+ self.updateGroupCondition(node, oldValue);
break;
}
}
@@ -352,29 +334,29 @@ QueryBuilder.prototype.bindEvents = function() {
};
/**
- * Create the root group
- * @param addRule {bool,optional} add a default empty rule
- * @param data {mixed,optional} group custom data
- * @param flags {object,optional} flags to apply to the group
- * @return group {Root}
+ * Creates the root group
+ * @param {boolean} [addRule=true] - adds a default empty rule
+ * @param {object} [data] - group custom data
+ * @param {object} [flags] - flags to apply to the group
+ * @returns {Group} root group
+ * @fires QueryBuilder.afterAddGroup
*/
QueryBuilder.prototype.setRoot = function(addRule, data, flags) {
addRule = (addRule === undefined || addRule === true);
var group_id = this.nextGroupId();
- var $group = $(this.getGroupTemplate(group_id, 1));
+ 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.flags = $.extend({}, this.settings.default_group_flags, flags);
+ this.model.root.condition = this.settings.default_condition;
this.trigger('afterAddGroup', this.model.root);
- this.model.root.condition = this.settings.default_condition;
-
if (addRule) {
this.addRule(this.model.root);
}
@@ -383,18 +365,28 @@ QueryBuilder.prototype.setRoot = function(addRule, data, flags) {
};
/**
- * Add a new group
- * @param parent {Group}
- * @param addRule {bool,optional} add a default empty rule
- * @param data {mixed,optional} group custom data
- * @param flags {object,optional} flags to apply to the group
- * @return group {Group}
+ * 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;
@@ -405,11 +397,23 @@ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) {
var model = parent.addGroup($group);
model.data = data;
- model.__.flags = $.extend({}, this.settings.default_group_flags, flags);
+ 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);
- model.condition = this.settings.default_condition;
+ /**
+ * After any change in the rules
+ * @event rulesChanged
+ * @memberof QueryBuilder
+ */
+ this.trigger('rulesChanged');
if (addRule) {
this.addRule(model);
@@ -419,15 +423,23 @@ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) {
};
/**
- * Tries to delete a group. The group is not deleted if at least one rule is no_delete.
- * @param group {Group}
- * @return {boolean} true if the group has been deleted
+ * 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;
@@ -436,77 +448,120 @@ QueryBuilder.prototype.deleteGroup = function(group) {
var del = true;
group.each('reverse', function(rule) {
- del&= this.deleteRule(rule);
+ del &= this.deleteRule(rule);
}, function(group) {
- del&= this.deleteGroup(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;
};
/**
- * Changes the condition of a group
- * @param group {Group}
+ * Performs actions when a group's condition changes
+ * @param {Group} group
+ * @param {object} previousCondition
+ * @fires QueryBuilder.afterUpdateGroupCondition
+ * @private
*/
-QueryBuilder.prototype.updateGroupCondition = function(group) {
- group.$el.find('>' + Selectors.group_condition).each(function() {
+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);
});
- this.trigger('afterUpdateGroupCondition', group);
+ /**
+ * 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');
};
/**
- * Update visibility of conditions based on number of rules inside each group
+ * 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('>' + Selectors.group_condition).prop('disabled', group.rules.length <= 1)
+ group.$el.find('>' + QueryBuilder.selectors.group_condition).prop('disabled', group.rules.length <= 1)
.parent().toggleClass('disabled', group.rules.length <= 1);
}
- group.each(function(rule) {}, function(group) {
+ group.each(null, function(group) {
walk(group);
}, this);
}(this.model.root));
};
/**
- * Add a new rule
- * @param parent {Group}
- * @param data {mixed,optional} rule custom data
- * @param flags {object,optional} flags to apply to the rule
- * @return rule {Rule}
+ * Adds a new rule
+ * @param {Group} parent
+ * @param {object} [data] - rule custom data
+ * @param {object} [flags] - flags to apply to the rule
+ * @returns {Rule}
+ * @fires QueryBuilder.beforeAddRule
+ * @fires QueryBuilder.afterAddRule
+ * @fires QueryBuilder.changer:getDefaultFilter
*/
QueryBuilder.prototype.addRule = function(parent, data, flags) {
+ /**
+ * Just before adding a rule, can be prevented
+ * @event beforeAddRule
+ * @memberof QueryBuilder
+ * @param {Group} parent
+ */
var e = this.trigger('beforeAddRule', parent);
if (e.isDefaultPrevented()) {
return null;
}
var rule_id = this.nextRuleId();
- var $rule = $(this.getRuleTemplate(rule_id));
+ var $rule = $($.parseHTML(this.getRuleTemplate(rule_id)));
var model = parent.addRule($rule);
- if (data !== undefined) {
- model.data = data;
- }
-
- model.__.flags = $.extend({}, this.settings.default_rule_flags, flags);
-
+ 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
@@ -517,15 +572,23 @@ QueryBuilder.prototype.addRule = function(parent, data, flags) {
};
/**
- * Delete a rule.
- * @param rule {Rule}
- * @return {boolean} true if the rule has been deleted
+ * 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;
@@ -533,52 +596,98 @@ QueryBuilder.prototype.deleteRule = function(rule) {
rule.drop();
+ /**
+ * Just after deleting a rule
+ * @event afterDeleteRule
+ * @memberof QueryBuilder
+ */
this.trigger('afterDeleteRule');
+ this.trigger('rulesChanged');
+
return true;
};
/**
- * Create the filters