diff --git a/.editorconfig b/.editorconfig index a082c141..7715ae28 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,5 +7,12 @@ 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 index 8dadc7c8..be3320fd 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,5 +1,10 @@ # 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. @@ -12,8 +17,5 @@ Any big feature must have it's own QUnit tests suite. Of course existing tests m I won't merge any branch not passing the TravisCI build, including JShint/JSCS/SCSSlint compliance. -## dist files -Keep it simple, don't commit any files in the `dist` directory, I build these files only before a release. - ## 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 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 ea7eaf38..1131297f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,7 @@ **Merge request checklist** - [ ] I read the [guidelines for contributing](https://github.com/mistic100/jQuery-QueryBuilder/blob/master/.github/CONTRIBUTING.md) -- [ ] I **didn't** commited files in the `dist` directory -- [ ] Unit tests are OK +- [ ] 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 d675f018..a952958a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ -bower_components node_modules +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 c05ec85b..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] - - 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 1e5ad881..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 - - 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 8f18252f..00000000 --- a/Gruntfile.js +++ /dev/null @@ -1,624 +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: { - 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 bd337f35..f0fd8ab7 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,61 @@ # jQuery QueryBuilder -[![Bower version](https://img.shields.io/bower/v/jQuery-QueryBuilder.svg?style=flat-square)](http://querybuilder.js.org) -[![CDN](https://img.shields.io/badge/cdn-jsdelivr-%23EB4C36.svg?style=flat-square)](http://www.jsdelivr.com/projects/jquery.query-builder) -[![Build Status](https://img.shields.io/travis/mistic100/jQuery-QueryBuilder.svg?style=flat-square)](https://travis-ci.org/mistic100/jQuery-QueryBuilder) -[![Coverage Status](https://img.shields.io/coveralls/mistic100/jQuery-QueryBuilder/master.svg?style=flat-square)](https://coveralls.io/r/mistic100/jQuery-QueryBuilder) +[![npm version](https://img.shields.io/npm/v/jQuery-QueryBuilder.svg?style=flat-square)](https://www.npmjs.com/package/jQuery-QueryBuilder) +[![jsDelivr CDN](https://data.jsdelivr.com/v1/package/npm/jQuery-QueryBuilder/badge)](https://www.jsdelivr.com/package/npm/jQuery-QueryBuilder) +[![Build Status](https://github.com/mistic100/jQuery-QueryBuilder/workflows/CI/badge.svg)](https://github.com/mistic100/jQuery-QueryBuilder/actions) +[![gitlocalized](https://gitlocalize.com/repo/5259/whole_project/badge.svg)](https://gitlocalize.com/repo/5259/whole_project?utm_source=badge) jQuery plugin offering an simple interface to create complex queries. -[![screenshot](https://raw.githubusercontent.com/mistic100/jQuery-QueryBuilder/master/examples/screenshot.png)](http://querybuilder.js.org) +[![screenshot](https://raw.githubusercontent.com/mistic100/jQuery-QueryBuilder/master/examples/screenshot.png)](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](http://olado.github.io/doT) (@master until the author decide to publish a new version) - * [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 104ba6c1..00000000 --- a/bower.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "name": "jQuery-QueryBuilder", - "version": "2.3.3", - "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": "master" - }, - "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.0.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", - "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 359b2cf1..00000000 --- a/composer.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "mistic100/jquery-querybuilder", - "version": "2.3.3", - "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/dist/css/query-builder.dark.css b/dist/css/query-builder.dark.css index 614972c2..90b5fbaa 100644 --- a/dist/css/query-builder.dark.css +++ b/dist/css/query-builder.dark.css @@ -1,10 +1,14 @@ /*! - * jQuery QueryBuilder 2.3.3 - * Copyright 2014-2016 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://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 .rules-group-container, .query-builder .rule-container, .query-builder .rule-placeholder { +/*! + * 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; @@ -13,9 +17,9 @@ background: rgba(40, 40, 40, 0.9); } -.query-builder .rule-container .rule-filter-container, +.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, .query-builder .error-container, .query-builder .drag-handle { +.query-builder .rule-container .rule-value-container { display: inline-block; margin: 0 5px 0 0; vertical-align: middle; @@ -32,7 +36,15 @@ } .query-builder .rules-group-header .group-conditions .btn.readonly:not(.active), .query-builder .rules-group-header .group-conditions input[name$=_cond] { - display: none; + 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; @@ -53,9 +65,6 @@ .query-builder .rule-value-container label.block { display: block; } -.query-builder .rule-value-container select, .query-builder .rule-value-container input[type=text], .query-builder .rule-value-container input[type=number] { - padding: 1px; -} .query-builder .error-container { display: none; cursor: help; @@ -69,7 +78,7 @@ display: inline-block !important; } .query-builder .rules-list > *::before, .query-builder .rules-list > *::after { - content: ''; + content: ""; position: absolute; left: -10px; width: 10px; @@ -96,6 +105,14 @@ 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; } @@ -107,7 +124,7 @@ color: #AAD1E4; border-radius: 5px; padding: 2.5px 5px; - font-size: .8em; + font-size: 0.8em; } .query-builder .rules-group-header [data-invert] { @@ -119,10 +136,15 @@ vertical-align: middle; margin-left: 5px; } -.query-builder .dragged { - opacity: .5; +.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: .7; -} + opacity: 0.7; +} \ No newline at end of file diff --git a/dist/css/query-builder.dark.min.css b/dist/css/query-builder.dark.min.css deleted file mode 100644 index f81dbb9d..00000000 --- a/dist/css/query-builder.dark.min.css +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * jQuery QueryBuilder 2.3.3 - * Copyright 2014-2016 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) - */ - -.query-builder .rule-container,.query-builder .rule-placeholder,.query-builder .rules-group-container{position:relative;margin:4px 0;border-radius:5px;padding:5px;border:1px solid #111;background:rgba(40,40,40,.9)}.query-builder .drag-handle,.query-builder .error-container,.query-builder .rule-container .rule-filter-container,.query-builder .rule-container .rule-operator-container,.query-builder .rule-container .rule-value-container{display:inline-block;margin:0 5px 0 0;vertical-align:middle}.query-builder .rules-group-container{padding:10px 10px 6px;border:1px solid #00164A;background:rgba(50,70,80,.5)}.query-builder .rules-group-header{margin-bottom:10px}.query-builder .rules-group-header .group-conditions .btn.readonly:not(.active),.query-builder .rules-group-header .group-conditions input[name$=_cond]{display:none}.query-builder .rules-group-header .group-conditions .btn.readonly{border-radius:3px}.query-builder .rules-list{list-style:none;padding:0 0 0 15px;margin:0}.query-builder .rule-value-container{border-left:1px solid #DDD;padding-left:5px}.query-builder .rule-value-container label{margin-bottom:0;font-weight:400}.query-builder .rule-value-container label.block{display:block}.query-builder .rule-value-container input[type=number],.query-builder .rule-value-container input[type=text],.query-builder .rule-value-container select{padding:1px}.query-builder .error-container{display:none;cursor:help;color:red}.query-builder .has-error{background-color:#322;border-color:#800}.query-builder .has-error .error-container{display:inline-block!important}.query-builder .rules-list>::after,.query-builder .rules-list>::before{content:'';position:absolute;left:-10px;width:10px;height:calc(50% + 4px);border-color:#222;border-style:solid}.query-builder .rules-list>::before{top:-4px;border-width:0 0 2px 2px}.query-builder .rules-list>::after{top:50%;border-width:0 0 0 2px}.query-builder .rules-list>:first-child::before{top:-12px;height:calc(50% + 14px)}.query-builder .rules-list>:last-child::before{border-radius:0 0 0 4px}.query-builder .rules-list>:last-child::after{display:none}.query-builder .error-container+.tooltip .tooltip-inner{color:#F22!important}.query-builder p.filter-description{margin:5px 0 0;background:rgba(0,170,255,.2);border:1px solid #346F7B;color:#AAD1E4;border-radius:5px;padding:2.5px 5px;font-size:.8em}.query-builder .rules-group-header [data-invert]{margin-left:5px}.query-builder .drag-handle{cursor:move;vertical-align:middle;margin-left:5px}.query-builder .dragged{opacity:.5}.query-builder .rule-placeholder{border:1px dashed #BBB;opacity:.7} \ No newline at end of file diff --git a/dist/css/query-builder.default.css b/dist/css/query-builder.default.css index 8391656b..b5edf68e 100644 --- a/dist/css/query-builder.default.css +++ b/dist/css/query-builder.default.css @@ -1,10 +1,9 @@ /*! - * jQuery QueryBuilder 2.3.3 - * Copyright 2014-2016 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://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 .rules-group-container, .query-builder .rule-container, .query-builder .rule-placeholder { +.query-builder .rule-placeholder, .query-builder .rule-container, .query-builder .rules-group-container { position: relative; margin: 4px 0; border-radius: 5px; @@ -13,9 +12,9 @@ background: rgba(255, 255, 255, 0.9); } -.query-builder .rule-container .rule-filter-container, +.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, .query-builder .error-container, .query-builder .drag-handle { +.query-builder .rule-container .rule-value-container { display: inline-block; margin: 0 5px 0 0; vertical-align: middle; @@ -32,7 +31,15 @@ } .query-builder .rules-group-header .group-conditions .btn.readonly:not(.active), .query-builder .rules-group-header .group-conditions input[name$=_cond] { - display: none; + 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; @@ -53,9 +60,6 @@ .query-builder .rule-value-container label.block { display: block; } -.query-builder .rule-value-container select, .query-builder .rule-value-container input[type=text], .query-builder .rule-value-container input[type=number] { - padding: 1px; -} .query-builder .error-container { display: none; cursor: help; @@ -69,7 +73,7 @@ display: inline-block !important; } .query-builder .rules-list > *::before, .query-builder .rules-list > *::after { - content: ''; + content: ""; position: absolute; left: -10px; width: 10px; @@ -96,6 +100,14 @@ 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; } @@ -107,7 +119,7 @@ color: #31708F; border-radius: 5px; padding: 2.5px 5px; - font-size: .8em; + font-size: 0.8em; } .query-builder .rules-group-header [data-invert] { @@ -119,10 +131,15 @@ vertical-align: middle; margin-left: 5px; } -.query-builder .dragged { - opacity: .5; +.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: .7; -} + opacity: 0.7; +} \ No newline at end of file diff --git a/dist/css/query-builder.default.min.css b/dist/css/query-builder.default.min.css deleted file mode 100644 index c5a5d29e..00000000 --- a/dist/css/query-builder.default.min.css +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * jQuery QueryBuilder 2.3.3 - * Copyright 2014-2016 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) - */ - -.query-builder .rule-container,.query-builder .rule-placeholder,.query-builder .rules-group-container{position:relative;margin:4px 0;border-radius:5px;padding:5px;border:1px solid #EEE;background:rgba(255,255,255,.9)}.query-builder .drag-handle,.query-builder .error-container,.query-builder .rule-container .rule-filter-container,.query-builder .rule-container .rule-operator-container,.query-builder .rule-container .rule-value-container{display:inline-block;margin:0 5px 0 0;vertical-align:middle}.query-builder .rules-group-container{padding:10px 10px 6px;border:1px solid #DCC896;background:rgba(250,240,210,.5)}.query-builder .rules-group-header{margin-bottom:10px}.query-builder .rules-group-header .group-conditions .btn.readonly:not(.active),.query-builder .rules-group-header .group-conditions input[name$=_cond]{display:none}.query-builder .rules-group-header .group-conditions .btn.readonly{border-radius:3px}.query-builder .rules-list{list-style:none;padding:0 0 0 15px;margin:0}.query-builder .rule-value-container{border-left:1px solid #DDD;padding-left:5px}.query-builder .rule-value-container label{margin-bottom:0;font-weight:400}.query-builder .rule-value-container label.block{display:block}.query-builder .rule-value-container input[type=number],.query-builder .rule-value-container input[type=text],.query-builder .rule-value-container select{padding:1px}.query-builder .error-container{display:none;cursor:help;color:red}.query-builder .has-error{background-color:#FDD;border-color:#F99}.query-builder .has-error .error-container{display:inline-block!important}.query-builder .rules-list>::after,.query-builder .rules-list>::before{content:'';position:absolute;left:-10px;width:10px;height:calc(50% + 4px);border-color:#CCC;border-style:solid}.query-builder .rules-list>::before{top:-4px;border-width:0 0 2px 2px}.query-builder .rules-list>::after{top:50%;border-width:0 0 0 2px}.query-builder .rules-list>:first-child::before{top:-12px;height:calc(50% + 14px)}.query-builder .rules-list>:last-child::before{border-radius:0 0 0 4px}.query-builder .rules-list>:last-child::after{display:none}.query-builder .error-container+.tooltip .tooltip-inner{color:#F99!important}.query-builder p.filter-description{margin:5px 0 0;background:#D9EDF7;border:1px solid #BCE8F1;color:#31708F;border-radius:5px;padding:2.5px 5px;font-size:.8em}.query-builder .rules-group-header [data-invert]{margin-left:5px}.query-builder .drag-handle{cursor:move;vertical-align:middle;margin-left:5px}.query-builder .dragged{opacity:.5}.query-builder .rule-placeholder{border:1px dashed #BBB;opacity:.7} \ No newline at end of file diff --git a/dist/i18n/query-builder.ar.js b/dist/i18n/query-builder.ar.js index e8444c65..edb2917e 100644 --- a/dist/i18n/query-builder.ar.js +++ b/dist/i18n/query-builder.ar.js @@ -1,8 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: Arabic (ar) * Author: Mohamed YOUNES, https://github.com/MedYOUNES - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { diff --git a/dist/i18n/query-builder.az.js b/dist/i18n/query-builder.az.js index 69e710f2..3820f42e 100644 --- a/dist/i18n/query-builder.az.js +++ b/dist/i18n/query-builder.az.js @@ -1,8 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: Azerbaijan (az) * Author: Megaplan, mborisv - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { @@ -70,7 +70,7 @@ QueryBuilder.regional['az'] = { "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": "{0} operatoru çoxlu məna daşımır" + "operator_not_multiple": "\"{1}\" operatoru çoxlu məna daşımır" }, "invert": "invert" }; 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 index fa5b8039..551bee28 100644 --- a/dist/i18n/query-builder.cs.js +++ b/dist/i18n/query-builder.cs.js @@ -1,8 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: Čeština (cs) * Author: Megaplan, mborisv - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { @@ -70,7 +70,7 @@ QueryBuilder.regional['cs'] = { "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 {0} nepodporuje mnoho hodnot" + "operator_not_multiple": "Operátor \"{1}\" nepodporuje mnoho hodnot" }, "invert": "invertní" }; diff --git a/dist/i18n/query-builder.da.js b/dist/i18n/query-builder.da.js index 05a366e0..ff6d3f77 100644 --- a/dist/i18n/query-builder.da.js +++ b/dist/i18n/query-builder.da.js @@ -1,8 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: Danish (da) * Author: Jna Borup Coyle, github@coyle.dk - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { diff --git a/dist/i18n/query-builder.de.js b/dist/i18n/query-builder.de.js index 2ca6ed4c..d92c6679 100644 --- a/dist/i18n/query-builder.de.js +++ b/dist/i18n/query-builder.de.js @@ -1,8 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: German (de) * Author: "raimu" - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { 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 index c77da5d5..a6ce0f66 100644 --- a/dist/i18n/query-builder.en.js +++ b/dist/i18n/query-builder.en.js @@ -1,8 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: English (en) * Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { @@ -66,14 +66,17 @@ QueryBuilder.regional['en'] = { "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 {0} cannot accept multiple values" + "operator_not_multiple": "Operator \"{1}\" cannot accept multiple values" }, - "invert": "Invert" + "invert": "Invert", + "NOT": "NOT" }; QueryBuilder.defaults({ lang_code: 'en' }); 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 index 3aeaa70d..ea94a3fd 100644 --- a/dist/i18n/query-builder.es.js +++ b/dist/i18n/query-builder.es.js @@ -1,8 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: Spanish (es) * Author: "pyarza", "kddlb" - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { @@ -38,6 +38,7 @@ QueryBuilder.regional['es'] = { "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", @@ -67,7 +68,12 @@ QueryBuilder.regional['es'] = { "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}" + "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" } }; diff --git a/dist/i18n/query-builder.fa-IR.js b/dist/i18n/query-builder.fa-IR.js index 6c00e9de..011758fd 100644 --- a/dist/i18n/query-builder.fa-IR.js +++ b/dist/i18n/query-builder.fa-IR.js @@ -1,8 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: Farsi (fa-ir) * Author: Behzad Sedighzade, behzad.sedighzade@gmail.com - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { @@ -38,6 +38,7 @@ QueryBuilder.regional['fa-IR'] = { "greater": "بزرگتر از", "greater_or_equal": "بزرگتر یا مساوی با", "between": "مابین", + "not_between": "مابین نباشد", "begins_with": "شروع شود با", "not_begins_with": "شروع نشود با", "contains": "شامل شود", @@ -70,7 +71,7 @@ QueryBuilder.regional['fa-IR'] = { "datetime_exceed_min": "باید بعد از {0} باشد", "datetime_exceed_max": "باید قبل از {0} باشد", "boolean_not_valid": "مقدار دودویی وارد کنید", - "operator_not_multiple": "اپراتور {0} نمی تواند چند مقدار قبول کند" + "operator_not_multiple": "اپراتور \"{1}\" نمی تواند چند مقدار قبول کند" } }; diff --git a/dist/i18n/query-builder.fr.js b/dist/i18n/query-builder.fr.js index e7c7e5b0..39dc7303 100644 --- a/dist/i18n/query-builder.fr.js +++ b/dist/i18n/query-builder.fr.js @@ -1,8 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: French (fr) * Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { @@ -29,14 +29,14 @@ QueryBuilder.regional['fr'] = { "OR": "OU" }, "operators": { - "equal": "égal", - "not_equal": "non égal", - "in": "dans", - "not_in": "pas dans", - "less": "inférieur", - "less_or_equal": "inférieur ou égal", - "greater": "supérieur", - "greater_or_equal": "supérieur ou égal", + "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", @@ -66,14 +66,17 @@ QueryBuilder.regional['fr'] = { "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 {0} ne peut utiliser plusieurs valeurs" + "operator_not_multiple": "L'opérateur \"{1}\" ne peut utiliser plusieurs valeurs" }, - "invert": "Inverser" + "invert": "Inverser", + "NOT": "NON" }; QueryBuilder.defaults({ lang_code: 'fr' }); 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 index 04542dcd..c4565e96 100644 --- a/dist/i18n/query-builder.it.js +++ b/dist/i18n/query-builder.it.js @@ -1,7 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: Italian (it) - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Author: davegraziosi, Giuseppe Lodi Rizzini + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { @@ -18,6 +19,7 @@ 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", @@ -35,6 +37,8 @@ QueryBuilder.regional['it'] = { "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", @@ -45,6 +49,31 @@ QueryBuilder.regional['it'] = { "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" } }; 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 index aef1f152..8e88ac56 100644 --- a/dist/i18n/query-builder.nl.js +++ b/dist/i18n/query-builder.nl.js @@ -1,8 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: Dutch (nl) * Author: "Roywcm" - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { @@ -38,6 +38,7 @@ QueryBuilder.regional['nl'] = { "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", diff --git a/dist/i18n/query-builder.no.js b/dist/i18n/query-builder.no.js index e4fe8fa0..1ec2d013 100644 --- a/dist/i18n/query-builder.no.js +++ b/dist/i18n/query-builder.no.js @@ -1,8 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: Norwegian (no) * Author: Jna Borup Coyle, github@coyle.dk - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { diff --git a/dist/i18n/query-builder.pl.js b/dist/i18n/query-builder.pl.js index 9b5608dc..c89d0316 100644 --- a/dist/i18n/query-builder.pl.js +++ b/dist/i18n/query-builder.pl.js @@ -1,8 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: Polish (pl) * Author: Artur Smolarek - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { @@ -25,8 +25,8 @@ QueryBuilder.regional['pl'] = { "delete_rule": "Usuń", "delete_group": "Usuń", "conditions": { - "AND": "AND", - "OR": "OR" + "AND": "ORAZ", + "OR": "LUB" }, "operators": { "equal": "równa się", @@ -71,7 +71,7 @@ QueryBuilder.regional['pl'] = { "datetime_exceed_min": "Musi być po {0}", "datetime_exceed_max": "Musi być przed {0}", "boolean_not_valid": "Niepoprawna wartość logiczna", - "operator_not_multiple": "Operator {0} nie przyjmuje wielu wartości" + "operator_not_multiple": "Operator \"{1}\" nie przyjmuje wielu wartości" }, "invert": "Odwróć" }; diff --git a/dist/i18n/query-builder.pt-BR.js b/dist/i18n/query-builder.pt-BR.js index 0f37269d..a9b4fe59 100644 --- a/dist/i18n/query-builder.pt-BR.js +++ b/dist/i18n/query-builder.pt-BR.js @@ -1,8 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: Brazilian Portuguese (pr-BR) - * Author: Leandro Gehlen, leandrogehlen@gmail.com - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Author: Leandro Gehlen, leandrogehlen@gmail.com; Marcos Ferretti, marcosvferretti@gmail.com + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { @@ -19,9 +19,9 @@ var QueryBuilder = $.fn.queryBuilder; QueryBuilder.regional['pt-BR'] = { "__locale": "Brazilian Portuguese (pr-BR)", - "__author": "Leandro Gehlen, leandrogehlen@gmail.com", + "__author": "Leandro Gehlen, leandrogehlen@gmail.com; Marcos Ferretti, marcosvferretti@gmail.com", "add_rule": "Nova Regra", - "add_group": "Novo Gruop", + "add_group": "Novo Grupo", "delete_rule": "Excluir", "delete_group": "Excluir", "conditions": { @@ -37,7 +37,8 @@ QueryBuilder.regional['pt-BR'] = { "less_or_equal": "Menor ou igual", "greater": "Maior", "greater_or_equal": "Maior ou igual", - "between": "entre", + "between": "Entre", + "not_between": "Não entre", "begins_with": "Iniciando com", "not_begins_with": "Não iniciando com", "contains": "Contém", @@ -57,7 +58,7 @@ QueryBuilder.regional['pt-BR'] = { "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 conterm mais de {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", @@ -67,8 +68,12 @@ QueryBuilder.regional['pt-BR'] = { "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_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' }); diff --git a/dist/i18n/query-builder.pt-PT.js b/dist/i18n/query-builder.pt-PT.js index 911858a2..1e0aa77f 100644 --- a/dist/i18n/query-builder.pt-PT.js +++ b/dist/i18n/query-builder.pt-PT.js @@ -1,8 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: Portuguese (pt-PT) * Author: Miguel Guerreiro, migas.csi@gmail.com - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { diff --git a/dist/i18n/query-builder.ro.js b/dist/i18n/query-builder.ro.js index 31b81127..a1ba7eda 100644 --- a/dist/i18n/query-builder.ro.js +++ b/dist/i18n/query-builder.ro.js @@ -1,8 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: Romanian (ro) - * Author: ArianServ - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Author: ArianServ, totpero + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { @@ -19,7 +19,7 @@ var QueryBuilder = $.fn.queryBuilder; QueryBuilder.regional['ro'] = { "__locale": "Romanian (ro)", - "__author": "ArianServ", + "__author": "ArianServ, totpero", "add_rule": "Adaugă regulă", "add_group": "Adaugă grup", "delete_rule": "Şterge", @@ -33,10 +33,12 @@ QueryBuilder.regional['ro'] = { "not_equal": "diferit", "in": "în", "not_in": "nu în", - "less": "mai puţin", - "less_or_equal": "mai puţin sau egal", + "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", @@ -47,6 +49,31 @@ QueryBuilder.regional['ro'] = { "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" } }; diff --git a/dist/i18n/query-builder.ru.js b/dist/i18n/query-builder.ru.js index 0ee2091d..03cc3b7c 100644 --- a/dist/i18n/query-builder.ru.js +++ b/dist/i18n/query-builder.ru.js @@ -1,7 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: Russian (ru) - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Author: + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { @@ -36,6 +37,7 @@ QueryBuilder.regional['ru'] = { "greater": "больше", "greater_or_equal": "больше или равно", "between": "между", + "not_between": "не между", "begins_with": "начинается с", "not_begins_with": "не начинается с", "contains": "содержит", @@ -50,10 +52,10 @@ QueryBuilder.regional['ru'] = { "errors": { "no_filter": "Фильтр не выбран", "empty_group": "Группа пуста", - "radio_empty": "Не выбранно значение", - "checkbox_empty": "Не выбранно значение", - "select_empty": "Не выбранно значение", - "string_empty": "Не заполненно", + "radio_empty": "Не выбрано значение", + "checkbox_empty": "Не выбрано значение", + "select_empty": "Не выбрано значение", + "string_empty": "Не заполнено", "string_exceed_min_length": "Должен содержать больше {0} символов", "string_exceed_max_length": "Должен содержать меньше {0} символов", "string_invalid_format": "Неверный формат ({0})", @@ -63,14 +65,17 @@ QueryBuilder.regional['ru'] = { "number_exceed_min": "Должно быть больше {0}", "number_exceed_max": "Должно быть меньше, чем {0}", "number_wrong_step": "Должно быть кратно {0}", - "datetime_empty": "Не заполненно", + "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": "Оператор {0} не поддерживает много значений" + "operator_not_multiple": "Оператор \"{1}\" не поддерживает много значений" }, - "invert": "Инвертировать" + "invert": "Инвертировать", + "NOT": "НЕ" }; QueryBuilder.defaults({ lang_code: 'ru' }); 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 index ba179c12..f991b12a 100644 --- a/dist/i18n/query-builder.sq.js +++ b/dist/i18n/query-builder.sq.js @@ -1,8 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: Albanian (sq) * Author: Tomor Pupovci - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { @@ -70,7 +70,7 @@ QueryBuilder.regional['sq'] = { "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 {0} nuk mund të pranojë vlera të shumëfishta" + "operator_not_multiple": "Operatori \"{1}\" nuk mund të pranojë vlera të shumëfishta" } }; 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 index dd9f9a2b..7751e423 100644 --- a/dist/i18n/query-builder.tr.js +++ b/dist/i18n/query-builder.tr.js @@ -1,8 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: Turkish (tr) * Author: Aykut Alpgiray Ateş - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { @@ -53,25 +53,27 @@ QueryBuilder.regional['tr'] = { "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ı", + "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ğilr", + "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": "Must be a multiple of {0}", + "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 {0} birden fazla değer kabul etmiyor" + "operator_not_multiple": "Operatör \"{1}\" birden fazla değer kabul etmiyor" }, "invert": "Ters Çevir" }; diff --git a/dist/i18n/query-builder.ua.js b/dist/i18n/query-builder.ua.js index fd060991..1238b26e 100644 --- a/dist/i18n/query-builder.ua.js +++ b/dist/i18n/query-builder.ua.js @@ -1,8 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: Ukrainian (ua) * Author: Megaplan, mborisv - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { @@ -70,7 +70,7 @@ QueryBuilder.regional['ua'] = { "datetime_exceed_min": "Повинне бути, після {0}", "datetime_exceed_max": "Повинне бути, до {0}", "boolean_not_valid": "Не логічне", - "operator_not_multiple": "Оператор {0} не підтримує багато значень" + "operator_not_multiple": "Оператор \"{1}\" не підтримує багато значень" }, "invert": "інвертувати" }; diff --git a/dist/i18n/query-builder.zh-CN.js b/dist/i18n/query-builder.zh-CN.js index e7b7859c..d908f33a 100644 --- a/dist/i18n/query-builder.zh-CN.js +++ b/dist/i18n/query-builder.zh-CN.js @@ -1,8 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: Simplified Chinese (zh_CN) * Author: shadowwind, shatteredwindgo@gmail.com - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Licensed under MIT (https://opensource.org/licenses/MIT) */ (function(root, factory) { @@ -71,7 +71,7 @@ QueryBuilder.regional['zh-CN'] = { "datetime_exceed_min": "必须在{0}之后", "datetime_exceed_max": "必须在{0}之前", "boolean_not_valid": "不是布尔值", - "operator_not_multiple": "选项{0}无法接受多个值" + "operator_not_multiple": "选项\"{1}\"无法接受多个值" }, "invert": "倒置" }; diff --git a/dist/js/query-builder.js b/dist/js/query-builder.js index f63bf7b9..b5b30837 100644 --- a/dist/js/query-builder.js +++ b/dist/js/query-builder.js @@ -1,44 +1,174 @@ /*! - * jQuery QueryBuilder 2.3.3 - * Copyright 2014-2016 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://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) */ -// Languages: en -// Plugins: bt-checkbox, bt-selectpicker, bt-tooltip-errors, change-filters, filter-description, invert, mongodb-support, sortable, sql-support, unique-filter (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"; -// CLASS DEFINITION -// =============================== +/** + * @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) { - this.init($el, options); -}; + $el[0].queryBuilder = this; + /** + * Element container + * @member {jQuery} + * @readonly + */ + this.$el = $el; -// EVENTS SYSTEM -// =============================== -$.extend(QueryBuilder.prototype, { - change: function(type, value) { - var event = new $.Event(type + '.queryBuilder.filter', { - builder: this, - value: value - }); + /** + * Configuration object + * @member {object} + * @readonly + */ + this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options); - this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 2)); + /** + * Internal model + * @member {Model} + * @readonly + */ + this.model = new Model(); - return event.value; - }, + /** + * 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(type + '.queryBuilder', { + var event = new $.Event(this._tojQueryEvent(type), { builder: this }); @@ -47,105 +177,76 @@ $.extend(QueryBuilder.prototype, { 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(type + '.queryBuilder', 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(type + '.queryBuilder', 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(type + '.queryBuilder', cb); + this.$el.one(this._tojQueryEvent(type), cb); return this; - } -}); - - -// PLUGINS SYSTEM -// =============================== -QueryBuilder.plugins = {}; - -/** - * Get or extend the default configuration - * @param options {object,optional} new configuration, leave undefined to get the default config - * @return {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); - } -}; - -/** - * Define a new plugin - * @param {string} - * @param {function} - * @param {object,optional} default configuration - */ -QueryBuilder.define = function(name, fct, def) { - QueryBuilder.plugins[name] = { - fct: fct, - def: def || {} - }; -}; - -/** - * Add new methods - * @param {object} - */ -QueryBuilder.extend = function(methods) { - $.extend(QueryBuilder.prototype, methods); -}; - -/** - * Init plugins for an instance - * @throws ConfigError - */ -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; + /** + * 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(' '); } - - 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); -}; +}); /** * Allowed types and their internal representation + * @type {object.} + * @readonly + * @private */ QueryBuilder.types = { 'string': 'string', @@ -159,9 +260,13 @@ QueryBuilder.types = { /** * Allowed inputs + * @type {string[]} + * @readonly + * @private */ QueryBuilder.inputs = [ 'text', + 'number', 'textarea', 'radio', 'checkbox', @@ -170,6 +275,9 @@ QueryBuilder.inputs = [ /** * Runtime modifiable options with `setOptions` method + * @type {string[]} + * @readonly + * @private */ QueryBuilder.modifiable_options = [ 'display_errors', @@ -181,8 +289,10 @@ QueryBuilder.modifiable_options = [ /** * CSS selectors for common components + * @type {object.} + * @readonly */ -var Selectors = QueryBuilder.selectors = { +QueryBuilder.selectors = { group_container: '.rules-group-container', rule_container: '.rule-container', filter_container: '.rule-filter-container', @@ -210,17 +320,23 @@ var Selectors = QueryBuilder.selectors = { }; /** - * Template strings (see `template.js`) + * Template strings (see template.js) + * @type {object.} + * @readonly */ QueryBuilder.templates = {}; /** - * Localized strings (see `i18n/`) + * 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'] }, @@ -247,6 +363,8 @@ QueryBuilder.OPERATORS = { /** * Default configuration + * @type {object} + * @readonly */ QueryBuilder.DEFAULTS = { filters: [], @@ -273,6 +391,8 @@ QueryBuilder.DEFAULTS = { default_group_flags: { condition_readonly: false, + no_add_rule: false, + no_add_group: false, no_delete: false }, @@ -280,7 +400,8 @@ QueryBuilder.DEFAULTS = { group: null, rule: null, filterSelect: null, - operatorSelect: null + operatorSelect: null, + ruleValueSelect: null }, lang_code: 'en', @@ -310,85 +431,147 @@ QueryBuilder.DEFAULTS = { ], icons: { - add_group: 'glyphicon glyphicon-plus-sign', - add_rule: 'glyphicon glyphicon-plus', - remove_group: 'glyphicon glyphicon-remove', - remove_rule: 'glyphicon glyphicon-remove', - error: 'glyphicon glyphicon-warning-sign' + 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' } }; /** - * Init the builder + * @module plugins */ -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, - updating_value: false - }; +/** + * Definition of available plugins + * @type {object.} + */ +QueryBuilder.plugins = {}; - // "allow_groups" can be boolean or int - if (this.settings.allow_groups === false) { - this.settings.allow_groups = 0; +/** + * 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 (this.settings.allow_groups === true) { - this.settings.allow_groups = -1; + 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); + } +}; - // 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; +/** + * 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 || {} + }; +}; - // translations : english << 'lang_code' << custom - if (QueryBuilder.regional['en'] === undefined) { - Utils.error('Config', '"i18n/en.js" not loaded.'); +/** + * 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; } - 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]; + 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]); } - if (typeof this.templates[tpl] == 'string') { - this.templates[tpl] = doT.template(this.templates[tpl]); + else { + Utils.error('Config', 'Unable to find plugin "{0}"', plugin); } }, 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; +/** + * 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; } - this.status.id = this.$el.attr('id'); - // INIT - this.$el.addClass('query-builder form-inline'); + if (plugin) { + if (property) { + return plugin[property]; + } + else { + return plugin; + } + } + else { + Utils.error('Config', 'Unable to find plugin "{0}"', name); + } +}; - this.filters = this.checkFilters(this.filters); - this.operators = this.checkOperators(this.operators); - this.bindEvents(); - this.initPlugins(); +/** + * 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 (options.rules) { - this.setRules(options.rules); + if (rules) { + this.setRules(rules); delete this.settings.rules; } else { @@ -398,6 +581,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) { @@ -424,7 +609,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); @@ -458,19 +643,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); } }); @@ -486,7 +700,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)); }); } } @@ -500,6 +714,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) { @@ -553,54 +769,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)); }); } @@ -610,12 +828,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(); }, @@ -623,7 +841,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); @@ -634,7 +852,7 @@ QueryBuilder.prototype.bindEvents = function() { if (node instanceof Rule) { switch (field) { case 'error': - self.displayError(node); + self.updateError(node); break; case 'flags': @@ -642,7 +860,7 @@ QueryBuilder.prototype.bindEvents = function() { break; case 'filter': - self.updateRuleFilter(node); + self.updateRuleFilter(node, oldValue); break; case 'operator': @@ -650,14 +868,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': @@ -665,7 +883,7 @@ QueryBuilder.prototype.bindEvents = function() { break; case 'condition': - self.updateGroupCondition(node); + self.updateGroupCondition(node, oldValue); break; } } @@ -674,17 +892,18 @@ 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); @@ -692,11 +911,10 @@ QueryBuilder.prototype.setRoot = function(addRule, data, flags) { 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); - this.model.root.condition = this.settings.default_condition; - if (addRule) { this.addRule(this.model.root); } @@ -705,18 +923,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; @@ -728,10 +956,22 @@ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) { 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); - model.condition = this.settings.default_condition; + /** + * After any change in the rules + * @event rulesChanged + * @memberof QueryBuilder + */ + this.trigger('rulesChanged'); if (addRule) { this.addRule(model); @@ -741,15 +981,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; @@ -758,93 +1006,147 @@ 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.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) { - model.filter = this.getFilterById(this.settings.default_filter || this.filters[0].id); + /** + * 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; }; /** - * 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; @@ -852,52 +1154,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 for a rule and init the rule operator - * @param rule {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(Selectors.operator_container).empty(); + var $operatorContainer = rule.$el.find(QueryBuilder.selectors.operator_container).empty(); if (!rule.filter) { return; } var operators = this.getOperators(rule.filter); - var $operatorSelect = $(this.getRuleOperatorSelect(rule, operators)); + var $operatorSelect = $($.parseHTML(this.getRuleOperatorSelect(rule, operators))); $operatorContainer.html($operatorSelect); // set the operator without triggering update event - rule.__.operator = operators[0]; + 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); }; /** - * Create the main input for a rule - * @param rule {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(Selectors.value_container).empty(); + var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container).empty(); rule.__.value = undefined; @@ -910,56 +1258,85 @@ QueryBuilder.prototype.createRuleInput = function(rule) { var filter = rule.filter; for (var i = 0; i < rule.operator.nb_inputs; i++) { - var $ruleInput = $(this.getRuleInput(rule, 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.show(); + $valueContainer.css('display', ''); $inputs.on('change ' + (filter.input_event || ''), function() { - self.status.updating_value = true; - rule.value = self.getRuleValue(rule); - self.status.updating_value = false; + 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 { - self.status.updating_value = true; - rule.value = self.getRuleValue(rule); - self.status.updating_value = false; + rule._updating_value = true; + rule.value = self.getRuleInputValue(rule); + rule._updating_value = false; } + + this.applyRuleFlags(rule); }; /** - * Perform action when rule's filter is changed - * @param rule {Rule} + * Performs action when a rule's filter changes + * @param {Rule} rule + * @param {object} previousFilter + * @fires QueryBuilder.afterUpdateRuleFilter + * @private */ -QueryBuilder.prototype.updateRuleFilter = function(rule) { +QueryBuilder.prototype.updateRuleFilter = function(rule, previousFilter) { this.createRuleOperators(rule); this.createRuleInput(rule); - rule.$el.find(Selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1'); + 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('afterUpdateRuleFilter', rule); + this.trigger('rulesChanged'); }; /** - * Update main visibility when rule operator changes - * @param rule {Rule} - * @param previousOperator {object} + * 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(Selectors.value_container); + var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container); if (!rule.operator || rule.operator.nb_inputs === 0) { $valueContainer.hide(); @@ -967,76 +1344,121 @@ QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) { rule.__.value = undefined; } else { - $valueContainer.show(); + $valueContainer.css('display', ''); - if ($valueContainer.is(':empty') || rule.operator.nb_inputs !== previousOperator.nb_inputs) { + 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(Selectors.rule_operator).val(rule.operator.type); + 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); } - this.trigger('afterUpdateRuleOperator', 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'); }; /** - * Perform action when rule's value is changed - * @param rule {Rule} + * Performs actions when rule's value changes + * @param {Rule} rule + * @param {object} previousValue + * @fires QueryBuilder.afterUpdateRuleValue + * @private */ -QueryBuilder.prototype.updateRuleValue = function(rule) { - if (!this.status.updating_value) { - this.setRuleValue(rule, rule.value); +QueryBuilder.prototype.updateRuleValue = function(rule, previousValue) { + if (!rule._updating_value) { + this.setRuleInputValue(rule, rule.value); } - this.trigger('afterUpdateRuleValue', rule); + /** + * After the rule value has been modified + * @event afterUpdateRuleValue + * @memberof QueryBuilder + * @param {Rule} rule + * @param {*} previousValue + */ + this.trigger('afterUpdateRuleValue', rule, previousValue); + + this.trigger('rulesChanged'); }; /** - * Change rules properties depending on flags. - * @param rule {Rule} + * 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.filter_readonly) { - rule.$el.find(Selectors.rule_filter).prop('disabled', true); - } - if (flags.operator_readonly) { - rule.$el.find(Selectors.rule_operator).prop('disabled', true); - } - if (flags.value_readonly) { - rule.$el.find(Selectors.rule_value).prop('disabled', true); - } 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); }; /** - * Change group properties depending on flags. - * @param group {Group} + * 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.condition_readonly) { - group.$el.find('>' + Selectors.group_condition).prop('disabled', true) - .parent().addClass('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); }; /** - * Clear all errors markers - * @param node {Node,optional} default is root Group + * Clears all errors markers + * @param {Node} [node] default is root Group */ QueryBuilder.prototype.clearErrors = function(node) { node = node || this.model.root; @@ -1057,38 +1479,59 @@ QueryBuilder.prototype.clearErrors = function(node) { }; /** - * Add/Remove class .has-error and update error title - * @param node {Node} + * Adds/Removes error on a Rule or Group + * @param {Node} node + * @fires QueryBuilder.changer:displayError + * @private */ -QueryBuilder.prototype.displayError = function(node) { +QueryBuilder.prototype.updateError = function(node) { if (this.settings.display_errors) { if (node.error === null) { node.$el.removeClass('has-error'); } else { - // translate the text without modifying event array - var error = $.extend([], node.error, [ - this.lang.errors[node.error[0]] || node.error[0] - ]); + 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(Selectors.error_container).eq(0) - .attr('title', Utils.fmt.apply(null, error)); + .find(QueryBuilder.selectors.error_container).eq(0) + .attr('title', errorMessage); } } }; /** - * Trigger a validation error event - * @param node {Node} - * @param error {array} - * @param value {mixed} + * 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; @@ -1097,9 +1540,15 @@ QueryBuilder.prototype.triggerValidationError = function(node, error, value) { /** - * Destroy the plugin + * 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) { @@ -1118,23 +1567,58 @@ QueryBuilder.prototype.destroy = function() { }; /** - * Reset the plugin + * 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'); }; /** - * Clear the plugin + * 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; @@ -1143,36 +1627,58 @@ QueryBuilder.prototype.clear = function() { this.model.root = null; } + /** + * After the {@link QueryBuilder#clear} method + * @event afterClear + * @memberof QueryBuilder + */ this.trigger('afterClear'); + + this.trigger('rulesChanged'); }; /** - * Modify the builder configuration + * Modifies the builder configuration.
* Only options defined in QueryBuilder.modifiable_options are modifiable - * @param {object} + * @param {object} options */ QueryBuilder.prototype.setOptions = function(options) { - // use jQuery utils to filter options keys - $.makeArray($(Object.keys(options)).filter(QueryBuilder.modifiable_options)) - .forEach(function(opt) { - this.settings[opt] = options[opt]; - }, this); + $.each(options, function(opt, value) { + if (QueryBuilder.modifiable_options.indexOf(opt) !== -1) { + this.settings[opt] = value; + } + }.bind(this)); }; /** - * Return the model associated to a DOM object, or root model - * @param {jQuery,optional} - * @return {Node} + * Returns the model associated to a DOM object, or the root model + * @param {jQuery} [target] + * @returns {Node} */ QueryBuilder.prototype.getModel = function(target) { - return !target ? this.model.root : Model(target); + if (!target) { + return this.model.root; + } + else if (target instanceof Node) { + return target; + } + else { + return $(target).data('queryBuilderModel'); + } }; /** - * Validate the whole builder - * @return {boolean} + * 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() { +QueryBuilder.prototype.validate = function(options) { + options = $.extend({ + skip_empty: false + }, options); + this.clearErrors(); var self = this; @@ -1182,12 +1688,22 @@ QueryBuilder.prototype.validate = function() { 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); @@ -1201,10 +1717,11 @@ QueryBuilder.prototype.validate = function() { done++; }, function(group) { - if (parse(group)) { + var res = parse(group); + if (res === true) { done++; } - else { + else if (res === false) { errors++; } }); @@ -1212,6 +1729,9 @@ QueryBuilder.prototype.validate = function() { 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; @@ -1221,88 +1741,145 @@ QueryBuilder.prototype.validate = function() { }(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); }; /** - * Get an object representing current rules - * @param {object} options - * - get_flags: false[default] | true(only changes from default flags) | 'all' - * @return {object} + * 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 + get_flags: false, + allow_invalid: false, + skip_empty: false }, options); - if (!this.validate()) { - return {}; + var valid = this.validate(options); + if (!valid && !options.allow_invalid) { + return null; } var self = this; var out = (function parse(group) { - var data = { + var groupData = { condition: group.condition, rules: [] }; if (group.data) { - data.data = $.extendext(true, 'replace', {}, 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)) { - data.flags = flags; + groupData.flags = flags; } } - group.each(function(model) { + group.each(function(rule) { + if (!rule.filter && options.skip_empty) { + return; + } + var value = null; - if (model.operator.nb_inputs !== 0) { - value = model.value; + if (!rule.operator || rule.operator.nb_inputs !== 0) { + value = rule.value; } - var rule = { - id: model.filter.id, - field: model.filter.field, - type: model.filter.type, - input: model.filter.input, - operator: model.operator.type, + 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 (model.filter.data || model.data) { - rule.data = $.extendext(true, 'replace', {}, model.filter.data, model.data); + 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(model.flags, options.get_flags === 'all'); + var flags = self.getRuleFlags(rule.flags, options.get_flags === 'all'); if (!$.isEmptyObject(flags)) { - rule.flags = flags; + ruleData.flags = flags; } } - data.rules.push(rule); + /** + * 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) { - data.rules.push(parse(model)); - }); + var data = parse(model); + if (data.rules.length !== 0 || !options.skip_empty) { + groupData.rules.push(data); + } + }, this); - return data; + /** + * 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); }; /** - * Set rules from object + * Sets rules from object + * @param {object} data + * @param {object} [options] + * @param {boolean} [options.allow_invalid=false] - silent-fail if the data are invalid * @throws RulesError, UndefinedConditionError - * @param data {object} + * @fires QueryBuilder.changer:setRules + * @fires QueryBuilder.changer:jsonToRule + * @fires QueryBuilder.changer:jsonToGroup + * @fires QueryBuilder.afterSetRules */ -QueryBuilder.prototype.setRules = function(data) { +QueryBuilder.prototype.setRules = function(data, options) { + options = $.extend({ + allow_invalid: false + }, options); + if ($.isArray(data)) { data = { condition: this.settings.default_condition, @@ -1317,7 +1894,15 @@ QueryBuilder.prototype.setRules = function(data) { this.clear(); this.setRoot(false, data.data, this.parseGroupFlags(data)); - data = this.change('setRules', 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; @@ -1330,17 +1915,19 @@ QueryBuilder.prototype.setRules = function(data) { data.condition = self.settings.default_condition; } else if (self.settings.conditions.indexOf(data.condition) == -1) { - Utils.error('UndefinedCondition', 'Invalid condition "{0}"', data.condition); + 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 && item.rules.length > 0) { + + 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(); - Utils.error('RulesParse', 'No more than {0} groups are allowed', self.settings.allow_groups); } else { model = self.addGroup(group, false, item.data, self.parseGroupFlags(item)); @@ -1352,37 +1939,85 @@ QueryBuilder.prototype.setRules = function(data) { } } else { - if (item.id === undefined) { - Utils.error('RulesParse', 'Missing rule field id'); - } - if (item.operator === undefined) { - item.operator = 'equal'; + 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); + model = self.addRule(group, item.data, self.parseRuleFlags(item)); if (model === null) { return; } - model.filter = self.getFilterById(item.id); - model.operator = self.getOperatorByType(item.operator); - model.flags = self.parseRuleFlags(item); + 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; + } + } - if (model.operator.nb_inputs !== 0 && item.value !== undefined) { - model.value = item.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'); }; /** - * Check if a value is correct for a filter - * @param rule {Rule} - * @param value {string|string[]|undefined} - * @return {array|true} + * 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 || {}; @@ -1392,180 +2027,206 @@ QueryBuilder.prototype.validateValue = function(rule, value) { result = validation.callback.call(this, value, rule); } else { - result = this.validateValueInternal(rule, value); + 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 - * @param rule {Rule} - * @param value {string|string[]|undefined} - * @return {array|true} + * @private */ -QueryBuilder.prototype.validateValueInternal = function(rule, value) { +QueryBuilder.prototype._validateValue = function(rule, value) { var filter = rule.filter; var operator = rule.operator; var validation = filter.validation || {}; var result = true; - var tmp; + var tmp, tempValue; if (rule.operator.nb_inputs === 1) { value = [value]; } - else { - 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) { - result = ['radio_empty']; + 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) { - result = ['checkbox_empty']; - break; - } - else if (!operator.multiple && value[i].length > 1) { - result = ['operator_not_multiple', operator.type]; + if (!validation.allow_empty_value) { + result = ['checkbox_empty']; + } break; } break; case 'select': - if (filter.multiple) { - if (value[i] === undefined || value[i].length === 0 || (filter.placeholder && value[i] == filter.placeholder_value)) { - result = ['select_empty']; - break; - } - else if (!operator.multiple && value[i].length > 1) { - result = ['operator_not_multiple', operator.type]; - break; - } - } - else { - if (value[i] === undefined || (filter.placeholder && value[i] == filter.placeholder_value)) { + 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; } break; default: - switch (QueryBuilder.types[filter.type]) { - case 'string': - if (value[i] === undefined || value[i].length === 0) { - result = ['string_empty']; - break; - } - if (validation.min !== undefined) { - if (value[i].length < parseInt(validation.min)) { - result = ['string_exceed_min_length', validation.min]; + 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.max !== undefined) { - if (value[i].length > parseInt(validation.max)) { - result = ['string_exceed_max_length', validation.max]; - 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.format) { - if (typeof validation.format == 'string') { - validation.format = new RegExp(validation.format); + 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.test(value[i])) { - result = ['string_invalid_format', validation.format]; - 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 (value[i] === undefined || isNaN(value[i])) { - result = ['number_nan']; break; - } - if (filter.type == 'integer') { - if (parseInt(value[i]) != value[i]) { - result = ['number_not_integer']; + + case 'number': + if (tempValue[j] === undefined || tempValue[j].length === 0) { + if (!validation.allow_empty_value) { + result = ['number_nan']; + } break; } - } - else { - if (parseFloat(value[i]) != value[i]) { - result = ['number_not_double']; + if (isNaN(tempValue[j])) { + result = ['number_nan']; break; } - } - if (validation.min !== undefined) { - if (value[i] < parseFloat(validation.min)) { - result = ['number_exceed_min', validation.min]; - break; + if (filter.type == 'integer') { + if (parseInt(tempValue[j]) != tempValue[j]) { + result = ['number_not_integer']; + break; + } } - } - if (validation.max !== undefined) { - if (value[i] > parseFloat(validation.max)) { - result = ['number_exceed_max', validation.max]; - 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 = (value[i] / validation.step).toPrecision(14); - if (parseInt(v) != v) { - result = ['number_wrong_step', validation.step]; - 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 (value[i] === undefined || value[i].length === 0) { - 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(value[i], validation.format); - if (!datetime.isValid()) { - result = ['datetime_invalid', validation.format]; + case 'datetime': + if (tempValue[j] === undefined || tempValue[j].length === 0) { + if (!validation.allow_empty_value) { + result = ['datetime_empty']; + } break; } - else { - if (validation.min) { - if (datetime < moment(validation.min, validation.format)) { - result = ['datetime_exceed_min', validation.min]; - break; - } + + // we need MomentJS + if (validation.format) { + if (!('moment' in window)) { + Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com'); + } + + var datetime = moment(tempValue[j], validation.format); + if (!datetime.isValid()) { + result = [this.getValidationMessage(validation, 'format', 'datetime_invalid'), validation.format]; + break; } - if (validation.max) { - if (datetime > moment(validation.max, validation.format)) { - result = ['datetime_exceed_max', validation.max]; - break; + else { + if (validation.min) { + if (datetime < moment(validation.min, validation.format)) { + result = [this.getValidationMessage(validation, 'min', 'datetime_exceed_min'), validation.min]; + break; + } + } + if (validation.max) { + if (datetime > moment(validation.max, validation.format)) { + result = [this.getValidationMessage(validation, 'max', 'datetime_exceed_max'), validation.max]; + break; + } } } } - } - break; - - case 'boolean': - tmp = value[i].trim().toLowerCase(); - if (tmp !== 'true' && tmp !== 'false' && tmp !== '1' && tmp !== '0' && value[i] !== 1 && value[i] !== 0) { - result = ['boolean_not_valid']; 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; + } } } @@ -1574,12 +2235,36 @@ QueryBuilder.prototype.validateValueInternal = function(rule, value) { } } + 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 - * @return {string} + * @returns {string} + * @private */ QueryBuilder.prototype.nextGroupId = function() { return this.status.id + '_group_' + (this.status.group_id++); @@ -1587,7 +2272,8 @@ QueryBuilder.prototype.nextGroupId = function() { /** * Returns an incremented rule ID - * @return {string} + * @returns {string} + * @private */ QueryBuilder.prototype.nextRuleId = function() { return this.status.id + '_rule_' + (this.status.rule_id++); @@ -1595,8 +2281,9 @@ QueryBuilder.prototype.nextRuleId = function() { /** * Returns the operators for a filter - * @param filter {string|object} (filter id name or filter object) - * @return {object[]} + * @param {string|object} filter - filter id or filter object + * @returns {object[]} + * @fires QueryBuilder.changer:getOperators */ QueryBuilder.prototype.getOperators = function(filter) { if (typeof filter == 'string') { @@ -1627,16 +2314,25 @@ QueryBuilder.prototype.getOperators = function(filter) { }); } + /** + * 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 - * @param filterId {string} - * @return {object|null} */ -QueryBuilder.prototype.getFilterById = function(id) { +QueryBuilder.prototype.getFilterById = function(id, doThrow) { if (id == '-1') { return null; } @@ -1647,16 +2343,19 @@ QueryBuilder.prototype.getFilterById = function(id) { } } - Utils.error('UndefinedFilter', 'Undefined filter "{0}"', id); + Utils.error(doThrow !== false, 'UndefinedFilter', 'Undefined filter "{0}"', id); + + return null; }; /** - * Return a particular operator by its type + * Returns a particular operator by its type + * @param {string} type + * @param {boolean} [doThrow=true] + * @returns {object|null} * @throws UndefinedOperatorError - * @param type {string} - * @return {object|null} */ -QueryBuilder.prototype.getOperatorByType = function(type) { +QueryBuilder.prototype.getOperatorByType = function(type, doThrow) { if (type == '-1') { return null; } @@ -1667,15 +2366,19 @@ QueryBuilder.prototype.getOperatorByType = function(type) { } } - Utils.error('UndefinedOperator', 'Undefined operator "{0}"', type); + Utils.error(doThrow !== false, 'UndefinedOperator', 'Undefined operator "{0}"', type); + + return null; }; /** - * Returns rule value - * @param rule {Rule} - * @return {mixed} + * Returns rule's current input value + * @param {Rule} rule + * @returns {*} + * @fires QueryBuilder.changer:getRuleValue + * @private */ -QueryBuilder.prototype.getRuleValue = function(rule) { +QueryBuilder.prototype.getRuleInputValue = function(rule) { var filter = rule.filter; var operator = rule.operator; var value = []; @@ -1684,7 +2387,7 @@ QueryBuilder.prototype.getRuleValue = function(rule) { value = filter.valueGetter.call(this, rule); } else { - var $value = rule.$el.find(Selectors.value_container); + 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); @@ -1721,6 +2424,21 @@ QueryBuilder.prototype.getRuleValue = function(rule) { } } + 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]; } @@ -1731,30 +2449,42 @@ QueryBuilder.prototype.getRuleValue = function(rule) { } } + /** + * 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. - * @param rule {Rule} - * @param value {mixed} + * Sets the value of a rule's input + * @param {Rule} rule + * @param {*} value + * @private */ -QueryBuilder.prototype.setRuleValue = function(rule, value) { +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(Selectors.value_container); + var $value = rule.$el.find(QueryBuilder.selectors.value_container); if (operator.nb_inputs == 1) { value = [value]; } - else { - value = value; - } for (var i = 0; i < operator.nb_inputs; i++) { var name = Utils.escapeElementId(rule.id + '_value_' + i); @@ -1774,17 +2504,24 @@ QueryBuilder.prototype.setRuleValue = function(rule, value) { 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; }; /** - * Clean rule flags. - * @param rule {object} - * @return {object} + * 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); @@ -1802,14 +2539,23 @@ QueryBuilder.prototype.parseRuleFlags = function(rule) { $.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); }; /** - * Get a copy of flags of a rule. + * Gets a copy of flags of a rule * @param {object} flags - * @param {boolean} all - true to return all flags, false to return only changes from default + * @param {boolean} [all=false] - return all flags or only changes from default flags * @returns {object} + * @private */ QueryBuilder.prototype.getRuleFlags = function(flags, all) { if (all) { @@ -1827,9 +2573,11 @@ QueryBuilder.prototype.getRuleFlags = function(flags, all) { }; /** - * Clean group flags. - * @param group {object} - * @return {object} + * 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); @@ -1837,6 +2585,8 @@ QueryBuilder.prototype.parseGroupFlags = function(group) { if (group.readonly) { $.extend(flags, { condition_readonly: true, + no_add_rule: true, + no_add_group: true, no_delete: true }); } @@ -1845,14 +2595,23 @@ QueryBuilder.prototype.parseGroupFlags = function(group) { $.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); }; /** - * Get a copy of flags of a group. + * Gets a copy of flags of a group * @param {object} flags - * @param {boolean} all - true to return all flags, false to return only changes from default + * @param {boolean} [all=false] - return all flags or only changes from default flags * @returns {object} + * @private */ QueryBuilder.prototype.getGroupFlags = function(flags, all) { if (all) { @@ -1870,358 +2629,809 @@ QueryBuilder.prototype.getGroupFlags = function(flags, all) { }; /** - * Translate a label - * @param label {string|object} - * @return string + * 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.translateLabel = function(label) { - return typeof label == 'object' ? (label[this.settings.lang_code] || label['en']) : label; +QueryBuilder.prototype.getValidationMessage = function(validation, type, def) { + return validation.messages && validation.messages[type] || def; }; -QueryBuilder.templates.group = '\ -
\ -
\ -
\ - \ - {{? it.settings.allow_groups===-1 || it.settings.allow_groups>=it.level }} \ - \ - {{?}} \ - {{? it.level>1 }} \ - \ - {{?}} \ -
\ -
\ - {{~ it.conditions: condition }} \ - \ - {{~}} \ -
\ - {{? it.settings.display_errors }} \ -
\ - {{?}} \ -
\ -
\ -
    \ -
    \ -
    '; - -QueryBuilder.templates.rule = '\ -
  • \ -
    \ -
    \ - \ -
    \ -
    \ - {{? it.settings.display_errors }} \ -
    \ - {{?}} \ -
    \ -
    \ -
    \ -
  • '; - -QueryBuilder.templates.filterSelect = '\ -{{ var optgroup = null; }} \ -'; - -QueryBuilder.templates.operatorSelect = '\ -{{ var optgroup = null; }} \ -'; - -/** - * Returns group HTML - * @param group_id {string} - * @param level {int} - * @return {string} - */ -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, - lang: this.lang, - settings: this.settings - }); +QueryBuilder.templates.group = ({ group_id, level, conditions, icons, settings, translate, builder }) => { + return ` +
    +
    +
    + + ${settings.allow_groups === -1 || settings.allow_groups >= level ? ` + + ` : ''} + ${level > 1 ? ` + + ` : ''} +
    +
    + ${conditions.map(condition => ` + + `).join('\n')} +
    + ${settings.display_errors ? ` +
    + ` : ''} +
    +
    +
    +
    +
    `; +}; + +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 ` +`; +}; - return this.change('getGroupTemplate', h, level); +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 rule HTML - * @param rule_id {string} - * @return {string} + * Returns group's HTML + * @param {string} group_id + * @param {int} level + * @returns {string} + * @fires QueryBuilder.changer:getGroupTemplate + * @private */ -QueryBuilder.prototype.getRuleTemplate = function(rule_id) { - var h = this.templates.rule({ - builder: this, - rule_id: rule_id, - icons: this.icons, - lang: this.lang, - settings: this.settings - }); +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); +}; - return this.change('getRuleTemplate', h); +/** + * 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 filter HTML - * @param rule {Rule} - * @param operators {object} - * @return {string} - */ -QueryBuilder.prototype.getRuleOperatorSelect = function(rule, operators) { - var h = this.templates.operatorSelect({ - builder: this, - rule: rule, - operators: operators, - icons: this.icons, - lang: this.lang, - settings: this.settings, - translate: this.translateLabel - }); + * 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); +}; - return this.change('getRuleOperatorSelect', h, 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 += ' ' + val + ' '; + }); + break; + + case 'select': + h = this.getRuleValueSelect(name, rule); + break; + + case 'textarea': + h += ''; - break; + 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 + } +}; - default: - switch (QueryBuilder.types[filter.type]) { - case 'number': - h+= '
    + * 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); + } + } + }); + }); }; -// Model CLASS -// =============================== /** - * Main object storing data model and emitting events - * --------- - * Access Node object stored in jQuery objects - * @param el {jQuery|Node} - * @return {Node} + * Main object storing data model and emitting model events + * @constructor */ -function Model(el) { - if (!(this instanceof Model)) { - return Model.getModel(el); - } - +function Model() { + /** + * @member {Group} + * @readonly + */ this.root = null; + + /** + * Base for event emitting + * @member {jQuery} + * @readonly + * @private + */ this.$ = $(this); } -$.extend(Model.prototype, { +$.extend(Model.prototype, /** @lends Model.prototype */ { + /** + * Triggers an event on the model + * @param {string} type + * @returns {$.Event} + */ trigger: function(type) { - this.$.triggerHandler(type, Array.prototype.slice.call(arguments, 1)); - return this; + 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; } }); -/** - * Access Node object stored in jQuery objects - * @param el {jQuery|Node} - * @return {Node} - */ -Model.getModel = function(el) { - if (!el) { - return null; - } - else if (el instanceof Node) { - return el; - } - else { - return $(el).data('queryBuilderModel'); - } -}; - -/* - * Define Node properties with getter and setter - * Update events are emitted in the setter through root Model (if any) - */ -function defineModelProperties(obj, fields) { - fields.forEach(function(field) { - Object.defineProperty(obj.prototype, field, { - enumerable: true, - get: function() { - return this.__[field]; - }, - set: function(value) { - var oldValue = (this.__[field] !== null && typeof this.__[field] == 'object') ? - $.extend({}, this.__[field]) : - this.__[field]; - - this.__[field] = value; - - if (this.model !== null) { - this.model.trigger('update', this, field, value, oldValue); - } - } - }); - }); -} - -// Node abstract CLASS -// =============================== /** - * @param {Node} - * @param {jQuery} + * Root abstract object + * @constructor + * @param {Node} [parent] + * @param {jQuery} $el */ var Node = function(parent, $el) { if (!(this instanceof Node)) { - return new 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; }; -defineModelProperties(Node, ['level', 'error', 'data', 'flags']); +Utils.defineModelProperties(Node, ['level', 'error', 'data', 'flags']); Object.defineProperty(Node.prototype, 'parent', { enumerable: true, @@ -2236,16 +3446,16 @@ Object.defineProperty(Node.prototype, 'parent', { }); /** - * Check if this Node is the root - * @return {boolean} + * Checks if this Node is the root + * @returns {boolean} */ Node.prototype.isRoot = function() { return (this.level === 1); }; /** - * Return node position inside parent - * @return {int} + * Returns the node position inside its parent + * @returns {int} */ Node.prototype.getPos = function() { if (this.isRoot()) { @@ -2257,87 +3467,107 @@ Node.prototype.getPos = function() { }; /** - * Delete self + * Deletes self + * @fires Model.model:drop */ Node.prototype.drop = function() { var model = this.model; - if (!this.isRoot()) { - this.parent._removeNode(this); + 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); } }; /** - * Move itself after another Node - * @param {Node} - * @return {Node} self + * Moves itself after another Node + * @param {Node} target + * @fires Model.model:move */ -Node.prototype.moveAfter = function(node) { - if (this.isRoot()) return; - - this._move(node.parent, node.getPos() + 1); - - return this; +Node.prototype.moveAfter = function(target) { + if (!this.isRoot()) { + this.move(target.parent, target.getPos() + 1); + } }; /** - * Move itself at the beginning of parent or another Group - * @param {Group,optional} - * @return {Node} self + * 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()) return; + if (!this.isRoot()) { + if (target === undefined) { + target = this.parent; + } - if (target === undefined) { - target = this.parent; + this.move(target, 0); } - - this._move(target, 0); - - return this; }; /** - * Move itself at the end of parent or another Group - * @param {Group,optional} - * @return {Node} self + * 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()) return; + if (!this.isRoot()) { + if (target === undefined) { + target = this.parent; + } - if (target === undefined) { - target = this.parent; + this.move(target, target.length() === 0 ? 0 : target.length() - 1); } - - this._move(target, target.length() - 1); - - return this; }; /** - * Move itself at specific position of Group - * @param {Group} - * @param {int} + * Moves itself at specific position of Group + * @param {Group} target + * @param {int} index + * @fires Model.model:move */ -Node.prototype._move = function(group, index) { - this.parent._removeNode(this); - group._appendNode(this, index, false); +Node.prototype.move = function(target, index) { + if (!this.isRoot()) { + if (typeof target === 'number') { + index = target; + target = this.parent; + } - if (this.model !== null) { - this.model.trigger('move', this, group, index); + 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 CLASS -// =============================== /** - * @param {Group} - * @param {jQuery} + * Group object + * @constructor + * @extends Node + * @param {Group} [parent] + * @param {jQuery} $el */ var Group = function(parent, $el) { if (!(this instanceof Group)) { @@ -2346,17 +3576,28 @@ var Group = function(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; -defineModelProperties(Group, ['condition']); +Utils.defineModelProperties(Group, ['condition']); /** - * Empty the Group + * Removes group's content */ Group.prototype.empty = function() { this.each('reverse', function(rule) { @@ -2367,7 +3608,7 @@ Group.prototype.empty = function() { }; /** - * Delete self + * Deletes self */ Group.prototype.drop = function() { this.empty(); @@ -2375,21 +3616,22 @@ Group.prototype.drop = function() { }; /** - * Return the number of children - * @return {int} + * Returns the number of children + * @returns {int} */ Group.prototype.length = function() { return this.rules.length; }; /** - * Add a Node at specified index - * @param {Node} - * @param {int,optional} - * @param {boolean,optional} - * @return {Node} the inserted node + * 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._appendNode = function(node, index, trigger) { +Group.prototype.insertNode = function(node, index, trigger) { if (index === undefined) { index = this.length(); } @@ -2398,65 +3640,79 @@ Group.prototype._appendNode = function(node, index, trigger) { node.parent = this; if (trigger && this.model !== null) { - this.model.trigger('add', node, index); + /** + * 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; }; /** - * Add a Group by jQuery element at specified index - * @param {jQuery} - * @param {int,optional} - * @return {Group} the inserted group + * 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._appendNode(new Group(this, $el), index, true); + return this.insertNode(new Group(this, $el), index, true); }; /** - * Add a Rule by jQuery element at specified index - * @param {jQuery} - * @param {int,optional} - * @return {Rule} the inserted rule + * 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._appendNode(new Rule(this, $el), index, true); + return this.insertNode(new Rule(this, $el), index, true); }; /** - * Delete a specific Node - * @param {Node} - * @return {Group} self + * Deletes a specific Node + * @param {Node} node */ -Group.prototype._removeNode = function(node) { +Group.prototype.removeNode = function(node) { var index = this.getNodePos(node); if (index !== -1) { node.parent = null; this.rules.splice(index, 1); } - - return this; }; /** - * Return position of a child Node - * @param {Node} - * @return {int} + * 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,optional} iterate in reverse order, required if you delete nodes - * @param {function} callback for Rules - * @param {function,optional} callback for Groups - * @return {boolean} + * @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 == 'function') { + if (typeof reverse !== 'boolean' && typeof reverse !== 'string') { context = cbGroup; cbGroup = cbRule; cbRule = reverse; @@ -2467,16 +3723,18 @@ Group.prototype.each = function(reverse, cbRule, cbGroup, 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 next = function() { + return reverse ? i >= l : i <= l; + }; var stop = false; - for (; next(); i+= c) { + for (; next(); i += c) { if (this.rules[i] instanceof Group) { - if (cbGroup !== undefined) { + if (!!cbGroup) { stop = cbGroup.call(context, this.rules[i]) === false; } } - else { + else if (!!cbRule) { stop = cbRule.call(context, this.rules[i]) === false; } @@ -2489,21 +3747,21 @@ Group.prototype.each = function(reverse, cbRule, cbGroup, context) { }; /** - * Return true if the group contains a particular Node - * @param {Node} - * @param {boolean,optional} recursive search - * @return {boolean} + * Checks if the group contains a particular Node + * @param {Node} node + * @param {boolean} [recursive=false] + * @returns {boolean} */ -Group.prototype.contains = function(node, deep) { +Group.prototype.contains = function(node, recursive) { if (this.getNodePos(node) !== -1) { return true; } - else if (!deep) { + else if (!recursive) { return false; } else { // the loop will return with false as soon as the Node is found - return !this.each(function(rule) { + return !this.each(function() { return true; }, function(group) { return !group.contains(node, true); @@ -2512,11 +3770,12 @@ Group.prototype.contains = function(node, deep) { }; -// RULE CLASS -// =============================== /** - * @param {Group} - * @param {jQuery} + * Rule object + * @constructor + * @extends Node + * @param {Group} parent + * @param {jQuery} $el */ var Rule = function(parent, $el) { if (!(this instanceof Rule)) { @@ -2525,188 +3784,84 @@ var Rule = function(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; - this.__.flags = {}; + + /** + * @name value + * @member {*} + * @memberof Rule + * @instance + */ this.__.value = undefined; }; Rule.prototype = Object.create(Node.prototype); Rule.prototype.constructor = Rule; -defineModelProperties(Rule, ['filter', 'operator', 'value']); - - -// EXPORT -// =============================== -QueryBuilder.Group = Group; -QueryBuilder.Rule = Rule; - - -var Utils = QueryBuilder.utils = {}; - -/** - * Utility to iterate over radio/checkbox/selection options. - * it accept three formats: array of values, map, array of 1-element maps - * - * @param options {object|array} - * @param tpl {callable} (takes key and text) - */ -Utils.iterateOptions = function(options, tpl) { - if (options) { - if ($.isArray(options)) { - options.forEach(function(entry) { - // array of one-element maps - if ($.isPlainObject(entry)) { - $.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); - }); - } - } -}; +Utils.defineModelProperties(Rule, ['filter', 'operator', 'value']); /** - * Replaces {0}, {1}, ... in a string - * @param str {string} - * @param args,... {mixed} - * @return {string} + * Checks if this Node is the root + * @returns {boolean} always false */ -Utils.fmt = function(str/*, args*/) { - var args = Array.prototype.slice.call(arguments, 1); - - return str.replace(/{([0-9]+)}/g, function(m, i) { - return args[parseInt(i)]; - }); +Rule.prototype.isRoot = function() { + return false; }; -/** - * Throw an Error object with custom name - * @param type {string} - * @param message {string} - * @param args,... {mixed} - */ -Utils.error = function(type, message/*, args*/) { - var err = new Error(Utils.fmt.apply(null, Array.prototype.slice.call(arguments, 1))); - err.name = type + 'Error'; - err.args = Array.prototype.slice.call(arguments, 2); - throw err; -}; /** - * Change type of a value to int or float - * @param value {mixed} - * @param type {string} 'integer', 'double' or anything else - * @param boolAsInt {boolean} return 0 or 1 for booleans - * @return {mixed} + * @member {function} + * @memberof QueryBuilder + * @see Group */ -Utils.changeType = function(value, type, boolAsInt) { - switch (type) { - case 'integer': return parseInt(value); - case 'double': return parseFloat(value); - case 'boolean': - var bool = value.trim().toLowerCase() === 'true' || value.trim() === '1' || value === 1; - return boolAsInt ? (bool ? 1 : 0) : bool; - default: return value; - } -}; +QueryBuilder.Group = Group; /** - * Escape string like mysql_real_escape_string - * @param value {string} - * @return {string} + * @member {function} + * @memberof QueryBuilder + * @see Rule */ -Utils.escapeString = function(value) { - if (typeof value != 'string') { - return value; - } - - return value - .replace(/[\0\n\r\b\\\'\"]/g, function(s) { - switch (s) { - case '\0': return '\\0'; - case '\n': return '\\n'; - case '\r': return '\\r'; - case '\b': return '\\b'; - default: return '\\' + s; - } - }) - // uglify compliant - .replace(/\t/g, '\\t') - .replace(/\x1a/g, '\\Z'); -}; +QueryBuilder.Rule = Rule; -/** - * Escape value for use in regex - * @param value {string} - * @return {string} - */ -Utils.escapeRegExp = function(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); -}; /** - * Escape HTML element id - * @param value {string} - * @return {string} + * The {@link http://learn.jquery.com/plugins/|jQuery Plugins} namespace + * @external "jQuery.fn" */ -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; -}; /** - * Sort objects by grouping them by {key}, preserving initial order when possible - * @param {object[]} items - * @param {string} key - * @returns {object[]} + * 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); */ -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; -}; - - $.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'); } @@ -2718,7 +3873,9 @@ $.fn.queryBuilder = function(option) { return this; } if (!data) { - this.data('queryBuilder', new QueryBuilder(this, options)); + 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)); @@ -2727,32 +3884,53 @@ $.fn.queryBuilder = function(option) { 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; -/*! - * jQuery QueryBuilder Awesome Bootstrap Checkbox - * Applies Awesome Bootstrap Checkbox for checkbox and radio inputs. +/** + * @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 == 'glyphicons') { - var injectCSS = document.createElement('style'); - injectCSS.innerHTML = '\ -.checkbox input[type=checkbox]:checked + label:after { \ - font-family: "Glyphicons Halflings"; \ - content: "\\e013"; \ -} \ -.checkbox label:after { \ - padding-left: 4px; \ - padding-top: 2px; \ - font-size: 9px; \ -}'; - document.body.appendChild(injectCSS); + if (options.font === 'bootstrap-icons') { + this.$el.addClass('bt-checkbox-bootstrap-icons'); } this.on('getRuleInput.filter', function(h, rule, name) { @@ -2775,87 +3953,44 @@ QueryBuilder.define('bt-checkbox', function(options) { var color = filter.colors[key] || filter.colors._def_ || options.color; var id = name + '_' + (i++); - h.value+= '\ - \ - \ - \ -'; + h.value += `
    `; }); } }); }, { - font: 'glyphicons', + font: 'bootstrap-icons', color: 'default' }); -/*! - * jQuery QueryBuilder Bootstrap Selectpicker - * Applies Bootstrap Select on filters and operators combo-boxes. - */ - -/** - * @throws ConfigError - */ -QueryBuilder.define('bt-selectpicker', function(options) { - if (!$.fn.selectpicker || !$.fn.selectpicker.Constructor) { - Utils.error('MissingLibrary', 'Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select'); - } - - // init selectpicker - this.on('afterCreateRuleFilters', function(e, rule) { - rule.$el.find(Selectors.rule_filter).removeClass('form-control').selectpicker(options); - }); - - this.on('afterCreateRuleOperators', function(e, rule) { - rule.$el.find(Selectors.rule_operator).removeClass('form-control').selectpicker(options); - }); - - // update selectpicker on change - this.on('afterUpdateRuleFilter', function(e, rule) { - rule.$el.find(Selectors.rule_filter).selectpicker('render'); - }); - - this.on('afterUpdateRuleOperator', function(e, rule) { - rule.$el.find(Selectors.rule_operator).selectpicker('render'); - }); -}, { - container: 'body', - style: 'btn-inverse btn-xs', - width: 'auto', - showIcon: false -}); - - -/*! - * jQuery QueryBuilder Bootstrap Tooltip errors - * Applies Bootstrap Tooltips on validation error messages. - */ - /** - * @throws ConfigError + * @class BtTooltipErrors + * @memberof module:plugins + * @description Applies Bootstrap Tooltips on validation error messages. + * @param {object} [options] + * @param {string} [options.placement='right'] + * @throws MissingLibraryError */ QueryBuilder.define('bt-tooltip-errors', function(options) { - if (!$.fn.tooltip || !$.fn.tooltip.Constructor || !$.fn.tooltip.Constructor.prototype.fixTitle) { - Utils.error('MissingLibrary', 'Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com'); + 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 = $(h.value); - $h.find(Selectors.error_container).attr('data-toggle', 'tooltip'); + 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(Selectors.error_container).eq(0) - .tooltip(options) - .tooltip('hide') - .tooltip('fixTitle'); + node.$el.find(QueryBuilder.selectors.error_container).eq(0) + .attr('data-bs-original-title',options).attr('data-bs-title',options).tooltip(); } }); }, { @@ -2863,27 +3998,38 @@ QueryBuilder.define('bt-tooltip-errors', function(options) { }); -/*! - * jQuery QueryBuilder Change Filters - * Allows to change available filters after plugin initialization. +/** + * @class ChangeFilters + * @memberof module:plugins + * @description Allows to change available filters after plugin initialization. */ -QueryBuilder.extend({ +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 - * @param {boolean,optional} delete rules using old filters - * @param {object[]} new filters */ - setFilters: function(delete_orphans, filters) { + setFilters: function(deleteOrphans, filters) { var self = this; if (filters === undefined) { - filters = delete_orphans; - delete_orphans = false; + 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) { @@ -2891,7 +4037,7 @@ QueryBuilder.extend({ }); // check for orphans - if (!delete_orphans) { + if (!deleteOrphans) { (function checkOrphans(node) { node.each( function(rule) { @@ -2910,17 +4056,20 @@ QueryBuilder.extend({ // apply on existing DOM (function updateBuilder(node) { node.each(true, - function(rule) { - if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) { - rule.drop(); - } - else { - self.createRuleFilters(rule); - - rule.$el.find(Selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1'); - } - }, - updateBuilder + 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)); @@ -2930,7 +4079,7 @@ QueryBuilder.extend({ this.updateDisabledFilters(); } if (this.settings.plugins['bt-selectpicker']) { - this.$el.find(Selectors.rule_filter).selectpicker('render'); + this.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render'); } } @@ -2944,15 +4093,24 @@ QueryBuilder.extend({ } } + /** + * 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 {object|object[]} the new filter - * @param {mixed,optional} numeric index or '#start' or '#end' + * @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(new_filters, position) { + addFilter: function(newFilters, position) { if (position === undefined || position == '#end') { position = this.filters.length; } @@ -2960,29 +4118,30 @@ QueryBuilder.extend({ position = 0; } - if (!$.isArray(new_filters)) { - new_filters = [new_filters]; + 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(new_filters)); + 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(new_filters)); + 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, new_filters); + Array.prototype.push.apply(filters, newFilters); } } @@ -2991,94 +4150,140 @@ QueryBuilder.extend({ /** * Removes a filter from the builder - * @param {string|string[]} the filter id - * @param {boolean,optional} delete rules using old filters + * @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(filter_ids, delete_orphans) { + removeFilter: function(filterIds, deleteOrphans) { var filters = $.extend(true, [], this.filters); - if (typeof filter_ids === 'string') { - filter_ids = [filter_ids]; + if (typeof filterIds === 'string') { + filterIds = [filterIds]; } filters = filters.filter(function(filter) { - return filter_ids.indexOf(filter.id) === -1; + return filterIds.indexOf(filter.id) === -1; }); - this.setFilters(delete_orphans, filters); + this.setFilters(deleteOrphans, filters); } }); -/*! - * jQuery QueryBuilder Filter Description - * Provides three ways to display a description about a filter: inline, Bootsrap Popover or Bootbox. +/** + * @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 - */ + // INLINE if (options.mode === 'inline') { - this.on('afterUpdateRuleFilter', function(e, rule) { + this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) { var $p = rule.$el.find('p.filter-description'); + var description = e.builder.getFilterDescription(rule.filter, rule); - if (!rule.filter || !rule.filter.description) { + if (!description) { $p.hide(); } else { if ($p.length === 0) { - $p = $('

    '); + $p = $($.parseHTML('

    ')); $p.appendTo(rule.$el); } else { - $p.show(); + $p.css('display', ''); } - $p.html(' ' + rule.filter.description); + $p.html(' ' + description); } }); } - /** - * POPOVER - */ + // 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', function(e, rule) { + this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) { var $b = rule.$el.find('button.filter-description'); + var description = e.builder.getFilterDescription(rule.filter, rule); - if (!rule.filter || !rule.filter.description) { + if (!description) { $b.hide(); - if ($b.data('bs.popover')) { + if ($b.data('bs-popover')) { $b.popover('hide'); } } else { if ($b.length === 0) { - $b = $(''); - $b.prependTo(rule.$el.find(Selectors.rule_actions)); - - $b.popover({ + $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() { - $b.popover('hide'); + popover('hide'); }); } else { - $b.show(); + $b.css('display', ''); } - $b.data('bs.popover').options.content = rule.filter.description; + $b.data('bs-popover').options.content = description; if ($b.attr('aria-describedby')) { $b.popover('show'); @@ -3086,44 +4291,125 @@ QueryBuilder.define('filter-description', function(options) { } }); } - /** - * BOOTBOX - */ + // 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', function(e, rule) { + this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) { var $b = rule.$el.find('button.filter-description'); + var description = e.builder.getFilterDescription(rule.filter, rule); - if (!rule.filter || !rule.filter.description) { + if (!description) { $b.hide(); } else { if ($b.length === 0) { - $b = $(''); - $b.prependTo(rule.$el.find(Selectors.rule_actions)); + $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', rule.filter.description); + $b.data('description', description); } }); } }, { - icon: 'glyphicon glyphicon-info-sign', + 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; + } + } +}); + -/*! - * jQuery QueryBuilder Invert - * Allows to invert a rule operator, a group condition or the entire builder. +/** + * @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: { @@ -3155,56 +4441,13 @@ QueryBuilder.defaults({ } }); -QueryBuilder.define('invert', function(options) { - var self = this; - - /** - * Bind events - */ - this.on('afterInit', function() { - self.$el.on('click.queryBuilder', '[data-invert=group]', function() { - var $group = $(this).closest(Selectors.group_container); - self.invert(Model($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(Model($rule), options); - }); - } - }); - - /** - * Modify templates - */ - this.on('getGroupTemplate.filter', function(h, level) { - var $h = $(h.value); - $h.find(Selectors.condition_container).after(''); - h.value = $h.prop('outerHTML'); - }); - - if (options.display_rules_button && options.invert_rules) { - this.on('getRuleTemplate.filter', function(h) { - var $h = $(h.value); - $h.find(Selectors.rule_actions).prepend(''); - h.value = $h.prop('outerHTML'); - }); - } -}, { - icon: 'glyphicon glyphicon-random', - recursive: true, - invert_rules: true, - display_rules_button: false, - silent_fail: false -}); - -QueryBuilder.extend({ +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 - * @param {Node,optional} - * @param {object,optional} */ invert: function(node, options) { if (!(node instanceof Node)) { @@ -3257,21 +4500,30 @@ QueryBuilder.extend({ } 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'); } } }); -/*! - * jQuery QueryBuilder MongoDB Support - * Allows to export rules as a MongoDB find object as well as populating the builder from a MongoDB object. +/** + * @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. */ -// DEFAULT CONFIG -// =============================== 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 }; }, @@ -3292,20 +4544,21 @@ QueryBuilder.defaults({ is_not_empty: function(v) { return { '$ne': '' }; }, is_null: function(v) { return null; }, is_not_null: function(v) { return { '$ne': null }; } + // @formatter:on }, mongoRuleOperators: { - $ne: function(v) { - v = v.$ne; + $eq: function(v) { return { 'val': v, - 'op': v === null ? 'is_not_null' : (v === '' ? 'is_not_empty' : 'not_equal') + 'op': v === null ? 'is_null' : (v === '' ? 'is_empty' : 'equal') }; }, - eq: function(v) { + $ne: function(v) { + v = v.$ne; return { 'val': v, - 'op': v === null ? 'is_null' : (v === '' ? 'is_empty' : 'equal') + 'op': v === null ? 'is_not_null' : (v === '' ? 'is_not_empty' : 'not_equal') }; }, $regex: function(v) { @@ -3329,54 +4582,73 @@ QueryBuilder.defaults({ return { 'val': v, 'op': 'contains' }; } }, - between: function(v) { return { 'val': [v.$gte, v.$lte], 'op': 'between' }; }, - not_between: function(v) { return { 'val': [v.$lt, v.$gt], 'op': 'not_between' }; }, - $in: function(v) { return { 'val': v.$in, 'op': 'in' }; }, - $nin: function(v) { return { 'val': v.$nin, 'op': 'not_in' }; }, - $lt: function(v) { return { 'val': v.$lt, 'op': 'less' }; }, - $lte: function(v) { return { 'val': v.$lte, 'op': 'less_or_equal' }; }, - $gt: function(v) { return { 'val': v.$gt, 'op': 'greater' }; }, - $gte: function(v) { return { 'val': v.$gte, 'op': 'greater_or_equal' }; } + between: function(v) { + return { 'val': [v.$gte, v.$lte], 'op': 'between' }; + }, + not_between: function(v) { + return { 'val': [v.$lt, v.$gt], 'op': 'not_between' }; + }, + $in: function(v) { + return { 'val': v.$in, 'op': 'in' }; + }, + $nin: function(v) { + return { 'val': v.$nin, 'op': 'not_in' }; + }, + $lt: function(v) { + return { 'val': v.$lt, 'op': 'less' }; + }, + $lte: function(v) { + return { 'val': v.$lte, 'op': 'less_or_equal' }; + }, + $gt: function(v) { + return { 'val': v.$gt, 'op': 'greater' }; + }, + $gte: function(v) { + return { 'val': v.$gte, 'op': 'greater_or_equal' }; + } } }); - -// PUBLIC METHODS -// =============================== -QueryBuilder.extend({ +QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ { /** - * Get rules as MongoDB query + * Returns rules as a MongoDB query + * @param {object} [data] - current rules by default + * @returns {object} + * @fires module:plugins.MongoDbSupport.changer:getMongoDBField + * @fires module:plugins.MongoDbSupport.changer:ruleToMongo + * @fires module:plugins.MongoDbSupport.changer:groupToMongo * @throws UndefinedMongoConditionError, UndefinedMongoOperatorError - * @param data {object} (optional) rules - * @return {object} */ getMongo: function(data) { data = (data === undefined) ? this.getRules() : data; + if (!data) { + return null; + } + var self = this; - return (function parse(data) { - if (!data.condition) { - data.condition = self.settings.default_condition; + return (function parse(group) { + if (!group.condition) { + group.condition = self.settings.default_condition; } - if (['AND', 'OR'].indexOf(data.condition.toUpperCase()) === -1) { - Utils.error('UndefinedMongoCondition', 'Unable to build MongoDB query with condition "{0}"', data.condition); + if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) { + Utils.error('UndefinedMongoCondition', 'Unable to build MongoDB query with condition "{0}"', group.condition); } - if (!data.rules) { + if (!group.rules) { return {}; } var parts = []; - data.rules.forEach(function(rule) { + group.rules.forEach(function(rule) { if (rule.rules && rule.rules.length > 0) { parts.push(parse(rule)); } else { var mdb = self.settings.mongoOperators[rule.operator]; var ope = self.getOperatorByType(rule.operator); - var values = []; if (mdb === undefined) { Utils.error('UndefinedMongoOperator', 'Unknown MongoDB operation for operator "{0}"', rule.operator); @@ -3386,344 +4658,696 @@ QueryBuilder.extend({ if (!(rule.value instanceof Array)) { rule.value = [rule.value]; } + } - rule.value.forEach(function(v) { - values.push(Utils.changeType(v, rule.type, false)); - }); + /** + * 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 part = {}; - part[rule.field] = mdb.call(self, values); - parts.push(part); - } - }); + 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; + }); - var res = {}; - if (parts.length > 0) { - res['$' + data.condition.toLowerCase()] = parts; - } - return res; - }(data)); - }, + // Read "not" from JSON + this.on('jsonToGroup.filter', function(e, json) { + e.value.not = !!json.not; + }); - /** - * Convert MongoDB object to rules - * @throws MongoParseError, UndefinedMongoConditionError, UndefinedMongoOperatorError - * @param data {object} query object - * @return {object} - */ - getRulesFromMongo: function(data) { - if (data === undefined || data === null) { - return null; + // Export "not" to SQL + this.on('groupToSQL.filter', function(e, group) { + if (group.not) { + e.value = 'NOT ( ' + e.value + ' )'; } + }); - var self = this; - var conditions = { - '$and': 'AND', - '$or': 'OR' - }; + // 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 + ); + } - return (function parse(data) { - var topKeys = Object.keys(data); + e.value.not = true; + } + }); - if (topKeys.length > 1) { - Utils.error('MongoParse', 'Invalid MongoDB query format'); - } - if (!conditions[topKeys[0].toLowerCase()]) { - Utils.error('UndefinedMongoCondition', 'Unable to build MongoDB query with condition "{0}"', topKeys[0]); - } + // 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; + } + }); - var rules = data[topKeys[0]]; - var parts = []; + // Read "not" from parsed SQL + this.on('sqlToGroup.filter', function(e, data) { + e.value.not = !!data.not; + }); - rules.forEach(function(rule) { - var keys = Object.keys(rule); + // 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] }; + } + }); - if (conditions[keys[0].toLowerCase()]) { - parts.push(parse(rule)); - } - else { - var field = keys[0]; - var value = rule[field]; + // Parse "$nor" operator from Mongo + this.on('parseMongoNode.filter', function(e) { + var keys = Object.keys(e.value); - var operator = determineMongoOperator(value, field); - if (operator === undefined) { - Utils.error('MongoParse', 'Invalid MongoDB query format'); - } + if (keys[0] == '$nor') { + e.value = e.value[keys[0]][0]; + e.value.not = true; + } + }); - var mdbrl = self.settings.mongoRuleOperators[operator]; - if (mdbrl === undefined) { - Utils.error('UndefinedMongoOperator', 'JSON Rule operation unknown for operator "{0}"', operator); - } + // 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 +}); - var opVal = mdbrl.call(self, value); - parts.push({ - id: self.change('getMongoDBFieldID', field, value), - field: field, - operator: opVal.op, - value: opVal.val - }); - } - }); +/** + * From {@link module:plugins.NotGroup} + * @name not + * @member {boolean} + * @memberof Group + * @instance + */ +Utils.defineModelProperties(Group, ['not']); - var res = {}; - if (parts.length > 0) { - res.condition = conditions[topKeys[0].toLowerCase()]; - res.rules = parts; - } - return res; - }(data)); - }, +QueryBuilder.selectors.group_not = QueryBuilder.selectors.group_header + ' [data-not=group]'; +QueryBuilder.extend(/** @lends module:plugins.NotGroup.prototype */ { /** - * Set rules from MongoDB object - * @param data {object} + * Performs actions when a group's not changes + * @param {Group} group + * @fires module:plugins.NotGroup.afterUpdateGroupNot + * @private */ - setRulesFromMongo: function(data) { - this.setRules(this.getRulesFromMongo(data)); + 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'); } }); + /** - * Find which operator is used in a MongoDB sub-object - * @param {mixed} value - * @param {string} field - * @return {string|undefined} + * @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 */ -function determineMongoOperator(value, field) { - if (value !== null && typeof value == 'object') { - var subkeys = Object.keys(value); - - if (subkeys.length === 1) { - return subkeys[0]; - } - else { - if (value.$gte !== undefined && value.$lte !== undefined) { - return 'between'; - } - if (value.$lt !== undefined && value.$gt !== undefined) { - return 'not_between'; - } - else if (value.$regex !== undefined) { // optional $options - return '$regex'; - } - else { - return; - } - } +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'); } - else { - return 'eq'; + + 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); -/*! - * jQuery QueryBuilder Sortable - * Enables drag & drop sort of rules. - */ + // set move threshold to 10px + interact.pointerMoveTolerance(10); -Selectors.rule_and_group_containers = Selectors.rule_container + ', ' + Selectors.group_container; + var placeholder; + var ghost; + var src; + var moved; -QueryBuilder.define('sortable', function(options) { - /** - * Init HTML5 drag and drop - */ - this.on('afterInit', function(e) { - // configure jQuery to use dataTransfer - $.event.props.push('dataTransfer'); + // Init drag and drop + this.on('afterAddRule afterAddGroup', function(e, node) { + if (node == placeholder) { + return; + } - var placeholder; - var src; var self = e.builder; - // only add "draggable" attribute when hovering drag handle - // preventing text select bug in Firefox - self.$el.on('mouseover.queryBuilder', '.drag-handle', function() { - self.$el.find(Selectors.rule_and_group_containers).attr('draggable', true); - }); - self.$el.on('mouseout.queryBuilder', '.drag-handle', function() { - self.$el.find(Selectors.rule_and_group_containers).removeAttr('draggable'); - }); - - // dragstart: create placeholder and hide current element - self.$el.on('dragstart.queryBuilder', '[draggable]', function(e) { - e.stopPropagation(); - - // notify drag and drop (only dummy text) - e.dataTransfer.setData('text', 'drag'); - - src = Model(e.target); - - // Chrome glitchs - // - helper invisible if hidden immediately - // - "dragend" is called immediately if we modify the DOM directly - setTimeout(function() { - var ph = $('
     
    '); - ph.css('min-height', src.$el.height()); + // 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; + } - placeholder = src.parent.addRule(ph, src.getPos()); + // Configure drag + if (!node.flags.no_sortable) { + interact(node.$el[0]) + .draggable({ + allowFrom: QueryBuilder.selectors.drag_handle, + onstart: function(event) { + moved = false; - src.$el.hide(); - }, 0); - }); + // get model of dragged element + src = self.getModel(event.target); - // dragenter: move the placeholder - self.$el.on('dragenter.queryBuilder', '[draggable]', function(e) { - e.preventDefault(); - e.stopPropagation(); + // create ghost + ghost = src.$el.clone() + .appendTo(src.$el.parent()) + .width(src.$el.outerWidth()) + .addClass('dragging'); - if (placeholder) { - moveSortableToTarget(placeholder, $(e.target)); - } - }); + // create drop placeholder + var ph = $($.parseHTML('
     
    ')) + .height(src.$el.outerHeight()); - // dragover: prevent glitches - self.$el.on('dragover.queryBuilder', '[draggable]', function(e) { - e.preventDefault(); - e.stopPropagation(); - }); + placeholder = src.parent.addRule(ph, src.getPos()); - // drop: move current element - self.$el.on('drop.queryBuilder', function(e) { - e.preventDefault(); - e.stopPropagation(); + // 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; + } - moveSortableToTarget(src, $(e.target)); - }); + // remove ghost + ghost.remove(); + ghost = undefined; - // dragend: show current element and delete placeholder - self.$el.on('dragend.queryBuilder', '[draggable]', function(e) { - e.preventDefault(); - e.stopPropagation(); + // remove placeholder + placeholder.drop(); + placeholder = undefined; - src.$el.show(); - placeholder.drop(); + // show element + src.$el.css('display', ''); - self.$el.find(Selectors.rule_and_group_containers).removeAttr('draggable'); + /** + * 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('afterMove', src); + self.trigger('rulesChanged'); + } + }); + } - src = placeholder = null; - }); - }); + 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); + } + } + }); - /** - * Remove drag handle from non-sortable rules - */ - this.on('parseRuleFlags.filter', function(flags) { - if (flags.value.no_sortable === undefined) { - flags.value.no_sortable = options.default_no_sortable; + // 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); + } + } + }); + } } }); - this.on('afterApplyRuleFlags', function(e, rule) { - if (rule.flags.no_sortable) { - rule.$el.find('.drag-handle').remove(); - } - }); + // Detach interactables + this.on('beforeDeleteRule beforeDeleteGroup', function(e, node) { + if (!e.isDefaultPrevented()) { + interact(node.$el[0]).unset(); - /** - * Remove drag handle from non-sortable groups - */ - this.on('parseGroupFlags.filter', function(flags) { - if (flags.value.no_sortable === undefined) { - flags.value.no_sortable = options.default_no_sortable; + if (node instanceof Group) { + interact(node.$el.find(QueryBuilder.selectors.group_header)[0]).unset(); + } } }); - this.on('afterApplyGroupFlags', function(e, group) { - if (group.flags.no_sortable) { - group.$el.find('.drag-handle').remove(); + // 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 - */ - this.on('getGroupTemplate.filter', function(h, level) { - if (level > 1) { - var $h = $(h.value); - $h.find(Selectors.condition_container).after('
    '); - h.value = $h.prop('outerHTML'); - } - }); + // 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 = $(h.value); - $h.find(Selectors.rule_header).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'); + }); + } }, { - default_no_sortable: false, - icon: 'glyphicon glyphicon-sort' + 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 + } }); /** - * Move an element (placeholder or actual object) depending on active target - * @param {Node} - * @param {jQuery} + * 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(element, target) { - var parent; +function moveSortableToTarget(node, target, builder) { + var parent, method; + var Selectors = QueryBuilder.selectors; // on rule parent = target.closest(Selectors.rule_container); if (parent.length) { - element.moveAfter(Model(parent)); - return; + method = 'moveAfter'; } // on group header - parent = target.closest(Selectors.group_header); - if (parent.length) { - parent = target.closest(Selectors.group_container); - element.moveAtBegin(Model(parent)); - return; + if (!method) { + parent = target.closest(Selectors.group_header); + if (parent.length) { + parent = target.closest(Selectors.group_container); + method = 'moveAtBegin'; + } } // on group - parent = target.closest(Selectors.group_container); - if (parent.length) { - element.moveAtEnd(Model(parent)); - return; + 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); + } } } -/*! - * jQuery QueryBuilder SQL Support - * Allows to export rules as a SQL WHERE statement as well as populating the builder from an SQL query. +/** + * @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 +}); -// DEFAULT CONFIG -// =============================== QueryBuilder.defaults({ - /* operators for internal -> SQL conversion */ + // 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: '> ?' }, + equal: { op: '= ?' }, + not_equal: { op: '!= ?' }, + in: { op: 'IN(?)', sep: ', ' }, + not_in: { op: 'NOT IN(?)', sep: ', ' }, + less: { op: '< ?' }, + less_or_equal: { op: '<= ?' }, + greater: { op: '> ?' }, greater_or_equal: { op: '>= ?' }, - between: { op: 'BETWEEN ?', sep: ' AND ' }, - not_between: { op: 'NOT BETWEEN ?', sep: ' AND ' }, - begins_with: { op: 'LIKE(?)', mod: '{0}%' }, - not_begins_with: { op: 'NOT LIKE(?)', mod: '{0}%' }, - contains: { op: 'LIKE(?)', mod: '%{0}%' }, - not_contains: { op: 'NOT LIKE(?)', mod: '%{0}%' }, - ends_with: { op: 'LIKE(?)', mod: '%{0}' }, - not_ends_with: { op: 'NOT LIKE(?)', mod: '%{0}' }, - is_empty: { op: '= \'\'' }, - is_not_empty: { op: '!= \'\'' }, - is_null: { op: 'IS NULL' }, - is_not_null: { op: 'IS NOT NULL' } + 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 */ + // operators for SQL -> internal conversion sqlRuleOperator: { '=': function(v) { return { @@ -3760,14 +5384,53 @@ QueryBuilder.defaults({ Utils.error('SQLParse', 'Invalid value for 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' }; }, + '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'); @@ -3782,7 +5445,7 @@ QueryBuilder.defaults({ } }, - /* statements for internal -> SQL conversion */ + // statements for internal -> SQL conversion sqlStatements: { 'question_mark': function() { var params = []; @@ -3831,7 +5494,7 @@ QueryBuilder.defaults({ } }, - /* statements for SQL -> internal conversion */ + // statements for SQL -> internal conversion sqlRuleStatement: { 'question_mark': function(values) { var index = 0; @@ -3862,7 +5525,7 @@ QueryBuilder.defaults({ 'named': function(values, char) { if (!char || char.length > 1) char = ':'; var regex1 = new RegExp('^\\' + char); - var regex2 = new RegExp('\\' + char + '(' + Object.keys(values).join('|') + ')', 'g'); + var regex2 = new RegExp('\\' + char + '(' + Object.keys(values).join('|') + ')\\b', 'g'); return { parse: function(v) { return regex1.test(v) ? values[v.slice(1)] : v; @@ -3875,23 +5538,38 @@ QueryBuilder.defaults({ } }); +/** + * @typedef {object} SqlQuery + * @memberof module:plugins.SqlSupport + * @property {string} sql + * @property {object} params + */ -// PUBLIC METHODS -// =============================== -QueryBuilder.extend({ +QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ { /** - * Get rules as SQL query + * 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 - * @param stmt {boolean|string} use prepared statements - false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)' - * @param nl {bool} output with new lines - * @param data {object} (optional) rules - * @return {object} */ getSQL: function(stmt, nl, data) { data = (data === undefined) ? this.getRules() : data; - nl = (nl === true) ? '\n' : ' '; - if (stmt === true) stmt = 'question_mark'; + 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]); @@ -3899,21 +5577,21 @@ QueryBuilder.extend({ var self = this; - var sql = (function parse(data) { - if (!data.condition) { - data.condition = self.settings.default_condition; + var sql = (function parse(group) { + if (!group.condition) { + group.condition = self.settings.default_condition; } - if (['AND', 'OR'].indexOf(data.condition.toUpperCase()) === -1) { - Utils.error('UndefinedSQLCondition', 'Unable to build SQL query with condition "{0}"', data.condition); + if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) { + Utils.error('UndefinedSQLCondition', 'Unable to build SQL query with condition "{0}"', group.condition); } - if (!data.rules) { + if (!group.rules) { return ''; } var parts = []; - data.rules.forEach(function(rule) { + group.rules.forEach(function(rule) { if (rule.rules && rule.rules.length > 0) { parts.push('(' + nl + parse(rule) + nl + ')' + nl); } @@ -3933,14 +5611,14 @@ QueryBuilder.extend({ rule.value.forEach(function(v, i) { if (i > 0) { - value+= sql.sep; + value += sql.sep; } - if (rule.type == 'integer' || rule.type == 'double' || rule.type == 'boolean') { - v = Utils.changeType(v, rule.type, true); + if (rule.type == 'boolean' && boolean_as_integer) { + v = v ? 1 : 0; } - else if (!stmt) { - v = Utils.escapeString(v); + else if (!stmt && rule.type !== 'integer' && rule.type !== 'double' && rule.type !== 'boolean') { + v = Utils.escapeString(v, sql.escape); } if (sql.mod) { @@ -3948,23 +5626,61 @@ QueryBuilder.extend({ } if (stmt) { - value+= stmt.add(rule, v); + value += stmt.add(rule, v); } else { if (typeof v == 'string') { v = '\'' + v + '\''; } - value+= v; + value += v; } }); } - parts.push(rule.field + ' ' + sql.op.replace(/\?/, value)); + 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)); } }); - return parts.join(' ' + data.condition + nl); + 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) { @@ -3981,60 +5697,135 @@ QueryBuilder.extend({ }, /** - * Convert SQL to rules - * @throws ConfigError, SQLParseError, UndefinedSQLOperatorError - * @param data {object} query object - * @param stmt {boolean|string} use prepared statements - false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)' - * @return {object} + * 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(data, stmt) { + 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 data == 'string') { - data = { sql: data }; + 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]](data.params, config[2]); + stmt = this.settings.sqlRuleStatement[config[1]](query.params, config[2]); } if (stmt) { - data.sql = stmt.esc(data.sql); + query.sql = stmt.esc(query.sql); } - if (data.sql.toUpperCase().indexOf('SELECT') !== 0) { - data.sql = 'SELECT * FROM table WHERE ' + data.sql; + if (query.sql.toUpperCase().indexOf('SELECT') !== 0) { + query.sql = 'SELECT * FROM table WHERE ' + query.sql; } - var parsed = SQLParser.parse(data.sql); + var parsed = SQLParser.parse(query.sql); if (!parsed.where) { Utils.error('SQLParse', 'No WHERE clause found'); } - var out = { + /** + * 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 - if (i > 0 && curr.condition != data.operation.toUpperCase()) { - curr.rules.push({ + + /** + * 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 = curr.rules[curr.rules.length - 1]; + curr.rules.push(group); + curr = group; } curr.condition = data.operation.toUpperCase(); @@ -4049,10 +5840,6 @@ QueryBuilder.extend({ } // it's a leaf else { - if (data.left.value === undefined || data.right.value === undefined) { - Utils.error('SQLParse', 'Missing field and/or value'); - } - if ($.isPlainObject(data.right.value)) { Utils.error('SQLParse', 'Value format not supported for {0}.', data.left.value); } @@ -4080,46 +5867,115 @@ QueryBuilder.extend({ // convert operator var operator = data.operation.toUpperCase(); - if (operator == '<>') operator = '!='; - - var sqlrl; - if (operator == 'NOT LIKE') { - sqlrl = self.settings.sqlRuleOperator['LIKE']; - } - else { - sqlrl = self.settings.sqlRuleOperator[operator]; + 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); - if (operator == 'NOT LIKE') opVal.op = 'not_' + opVal.op; - var left_value = data.left.values.join('.'); + // 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]; + }); + } - curr.rules.push({ - id: self.change('getSQLFieldID', left_value, value), - field: left_value, + var id = self.getSQLFieldID(field, value); + + /** + * Modifies the rule generated from the SQL expression + * @event changer:sqlToRule + * @memberof module:plugins.SqlSupport + * @param {object} rule + * @param {object} AST + * @returns {object} + */ + var rule = self.change('sqlToRule', { + id: id, + field: field, operator: opVal.op, - value: opVal.val - }); + value: finalValue + }, data); + + curr.rules.push(rule); } - }(parsed.where.conditions, 0)); + }(data, 0)); return out; }, /** - * Set rules from SQL - * @param data {object} + * 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 */ - setRulesFromSQL: function(data, stmt) { - this.setRules(this.getRulesFromSQL(data, stmt)); + 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]; @@ -4127,11 +5983,11 @@ function getStmtConfig(stmt) { } -/*! - * jQuery QueryBuilder Unique Filter - * Allows to define some filters as "unique": ie which can be used for only one rule, globally or in the same group. +/** + * @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 = {}; @@ -4140,9 +5996,35 @@ QueryBuilder.define('unique-filter', function() { 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({ +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; @@ -4171,6 +6053,11 @@ QueryBuilder.extend({ self.applyDisabledFilters(e); }, + /** + * Clear the list of used filters + * @param {$.Event} [e] + * @private + */ clearDisabledFilters: function(e) { var self = e ? e.builder : this; @@ -4179,21 +6066,26 @@ QueryBuilder.extend({ 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(Selectors.filter_container + ' option').prop('disabled', false); + 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(Selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true); + 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(Selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true); + rule.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true); }); }); } @@ -4201,17 +6093,17 @@ QueryBuilder.extend({ // update Selectpicker if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) { - self.$el.find(Selectors.rule_filter).selectpicker('render'); + self.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render'); } } }); /*! - * jQuery QueryBuilder 2.3.3 + * jQuery QueryBuilder 3.0.0 * Locale: English (en) * Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr - * Licensed under MIT (http://opensource.org/licenses/MIT) + * Licensed under MIT (https://opensource.org/licenses/MIT) */ QueryBuilder.regional['en'] = { @@ -4263,15 +6155,20 @@ QueryBuilder.regional['en'] = { "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 {0} cannot accept multiple values" + "operator_not_multiple": "Operator \"{1}\" cannot accept multiple values" }, - "invert": "Invert" + "invert": "Invert", + "NOT": "NOT" }; QueryBuilder.defaults({ lang_code: 'en' }); -})); \ No newline at end of file +return QueryBuilder; + +})); diff --git a/dist/js/query-builder.min.js b/dist/js/query-builder.min.js deleted file mode 100644 index ad279385..00000000 --- a/dist/js/query-builder.min.js +++ /dev/null @@ -1,8 +0,0 @@ -/*! - * jQuery QueryBuilder 2.3.3 - * Copyright 2014-2016 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) - */ - -!function(a,b){"function"==typeof define&&define.amd?define(["jquery","doT","jQuery.extendext"],b):b(a.jQuery,a.doT)}(this,function($,a){"use strict";function b(a){return this instanceof b?(this.root=null,void(this.$=$(this))):b.getModel(a)}function c(a,b){b.forEach(function(b){Object.defineProperty(a.prototype,b,{enumerable:!0,get:function(){return this.__[b]},set:function(a){var c=null!==this.__[b]&&"object"==typeof this.__[b]?$.extend({},this.__[b]):this.__[b];this.__[b]=a,null!==this.model&&this.model.trigger("update",this,b,a,c)}})})}function d(a,b){if(null!==a&&"object"==typeof a){var c=Object.keys(a);return 1===c.length?c[0]:void 0!==a.$gte&&void 0!==a.$lte?"between":void 0!==a.$lt&&void 0!==a.$gt?"not_between":void 0!==a.$regex?"$regex":void 0}return"eq"}function e(a,c){var d;return d=c.closest(h.rule_container),d.length?void a.moveAfter(b(d)):(d=c.closest(h.group_header),d.length?(d=c.closest(h.group_container),void a.moveAtBegin(b(d))):(d=c.closest(h.group_container),d.length?void a.moveAtEnd(b(d)):void 0))}function f(a){var b=a.match(/(question_mark|numbered|named)(?:\((.)\))?/);return b||(b=[null,"question_mark",void 0]),b}var g=function(a,b){this.init(a,b)};$.extend(g.prototype,{change:function(a,b){var c=new $.Event(a+".queryBuilder.filter",{builder:this,value:b});return this.$el.triggerHandler(c,Array.prototype.slice.call(arguments,2)),c.value},trigger:function(a){var b=new $.Event(a+".queryBuilder",{builder:this});return this.$el.triggerHandler(b,Array.prototype.slice.call(arguments,1)),b},on:function(a,b){return this.$el.on(a+".queryBuilder",b),this},off:function(a,b){return this.$el.off(a+".queryBuilder",b),this},once:function(a,b){return this.$el.one(a+".queryBuilder",b),this}}),g.plugins={},g.defaults=function(a){return"object"!=typeof a?"string"==typeof a?"object"==typeof g.DEFAULTS[a]?$.extend(!0,{},g.DEFAULTS[a]):g.DEFAULTS[a]:$.extend(!0,{},g.DEFAULTS):void $.extendext(!0,"replace",g.DEFAULTS,a)},g.define=function(a,b,c){g.plugins[a]={fct:b,def:c||{}}},g.extend=function(a){$.extend(g.prototype,a)},g.prototype.initPlugins=function(){if(this.plugins){if($.isArray(this.plugins)){var a={};this.plugins.forEach(function(b){a[b]=null}),this.plugins=a}Object.keys(this.plugins).forEach(function(a){a in g.plugins?(this.plugins[a]=$.extend(!0,{},g.plugins[a].def,this.plugins[a]||{}),g.plugins[a].fct.call(this,this.plugins[a])):l.error("Config",'Unable to find plugin "{0}"',a)},this)}},g.types={string:"string",integer:"number","double":"number",date:"datetime",time:"datetime",datetime:"datetime","boolean":"boolean"},g.inputs=["text","textarea","radio","checkbox","select"],g.modifiable_options=["display_errors","allow_groups","allow_empty","default_condition","default_filter"];var h=g.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]"};g.templates={},g.regional={},g.OPERATORS={equal:{type:"equal",nb_inputs:1,multiple:!1,apply_to:["string","number","datetime","boolean"]},not_equal:{type:"not_equal",nb_inputs:1,multiple:!1,apply_to:["string","number","datetime","boolean"]},"in":{type:"in",nb_inputs:1,multiple:!0,apply_to:["string","number","datetime"]},not_in:{type:"not_in",nb_inputs:1,multiple:!0,apply_to:["string","number","datetime"]},less:{type:"less",nb_inputs:1,multiple:!1,apply_to:["number","datetime"]},less_or_equal:{type:"less_or_equal",nb_inputs:1,multiple:!1,apply_to:["number","datetime"]},greater:{type:"greater",nb_inputs:1,multiple:!1,apply_to:["number","datetime"]},greater_or_equal:{type:"greater_or_equal",nb_inputs:1,multiple:!1,apply_to:["number","datetime"]},between:{type:"between",nb_inputs:2,multiple:!1,apply_to:["number","datetime"]},not_between:{type:"not_between",nb_inputs:2,multiple:!1,apply_to:["number","datetime"]},begins_with:{type:"begins_with",nb_inputs:1,multiple:!1,apply_to:["string"]},not_begins_with:{type:"not_begins_with",nb_inputs:1,multiple:!1,apply_to:["string"]},contains:{type:"contains",nb_inputs:1,multiple:!1,apply_to:["string"]},not_contains:{type:"not_contains",nb_inputs:1,multiple:!1,apply_to:["string"]},ends_with:{type:"ends_with",nb_inputs:1,multiple:!1,apply_to:["string"]},not_ends_with:{type:"not_ends_with",nb_inputs:1,multiple:!1,apply_to:["string"]},is_empty:{type:"is_empty",nb_inputs:0,multiple:!1,apply_to:["string"]},is_not_empty:{type:"is_not_empty",nb_inputs:0,multiple:!1,apply_to:["string"]},is_null:{type:"is_null",nb_inputs:0,multiple:!1,apply_to:["string","number","datetime","boolean"]},is_not_null:{type:"is_not_null",nb_inputs:0,multiple:!1,apply_to:["string","number","datetime","boolean"]}},g.DEFAULTS={filters:[],plugins:[],sort_filters:!1,display_errors:!0,allow_groups:-1,allow_empty:!1,conditions:["AND","OR"],default_condition:"AND",inputs_separator:" , ",select_placeholder:"------",display_empty_filter:!0,default_filter:null,optgroups:{},default_rule_flags:{filter_readonly:!1,operator_readonly:!1,value_readonly:!1,no_delete:!1},default_group_flags:{condition_readonly:!1,no_delete:!1},templates:{group:null,rule:null,filterSelect:null,operatorSelect:null},lang_code:"en",lang:{},operators:["equal","not_equal","in","not_in","less","less_or_equal","greater","greater_or_equal","between","not_between","begins_with","not_begins_with","contains","not_contains","ends_with","not_ends_with","is_empty","is_not_empty","is_null","is_not_null"],icons:{add_group:"glyphicon glyphicon-plus-sign",add_rule:"glyphicon glyphicon-plus",remove_group:"glyphicon glyphicon-remove",remove_rule:"glyphicon glyphicon-remove",error:"glyphicon glyphicon-warning-sign"}},g.prototype.init=function(c,d){c[0].queryBuilder=this,this.$el=c,this.settings=$.extendext(!0,"replace",{},g.DEFAULTS,d),this.model=new b,this.status={group_id:0,rule_id:0,generated_id:!1,has_optgroup:!1,has_operator_oprgroup:!1,id:null,updating_value:!1},this.settings.allow_groups===!1?this.settings.allow_groups=0:this.settings.allow_groups===!0&&(this.settings.allow_groups=-1),this.filters=this.settings.filters,this.icons=this.settings.icons,this.operators=this.settings.operators,this.templates=this.settings.templates,this.plugins=this.settings.plugins,void 0===g.regional.en&&l.error("Config",'"i18n/en.js" not loaded.'),this.lang=$.extendext(!0,"replace",{},g.regional.en,g.regional[this.settings.lang_code],this.settings.lang),Object.keys(this.templates).forEach(function(b){this.templates[b]||(this.templates[b]=g.templates[b]),"string"==typeof this.templates[b]&&(this.templates[b]=a.template(this.templates[b]))},this),this.$el.attr("id")||(this.$el.attr("id","qb_"+Math.floor(99999*Math.random())),this.status.generated_id=!0),this.status.id=this.$el.attr("id"),this.$el.addClass("query-builder form-inline"),this.filters=this.checkFilters(this.filters),this.operators=this.checkOperators(this.operators),this.bindEvents(),this.initPlugins(),this.trigger("afterInit"),d.rules?(this.setRules(d.rules),delete this.settings.rules):this.setRoot(!0)},g.prototype.checkFilters=function(a){var b=[];if(a&&0!==a.length||l.error("Config","Missing filters list"),a.forEach(function(a,c){switch(a.id||l.error("Config","Missing filter {0} id",c),-1!=b.indexOf(a.id)&&l.error("Config",'Filter "{0}" already defined',a.id),b.push(a.id),a.type?g.types[a.type]||l.error("Config",'Invalid type "{0}"',a.type):a.type="string",a.input?"function"!=typeof a.input&&-1==g.inputs.indexOf(a.input)&&l.error("Config",'Invalid input "{0}"',a.input):a.input="text",a.operators&&a.operators.forEach(function(a){"string"!=typeof a&&l.error("Config","Filter operators must be global operators types (string)")}),a.field||(a.field=a.id),a.label||(a.label=a.field),a.optgroup?(this.status.has_optgroup=!0,this.settings.optgroups[a.optgroup]||(this.settings.optgroups[a.optgroup]=a.optgroup)):a.optgroup=null,a.input){case"radio":case"checkbox":(!a.values||a.values.length<1)&&l.error("Config",'Missing filter "{0}" values',a.id);break;case"select":a.placeholder&&(void 0===a.placeholder_value&&(a.placeholder_value=-1),l.iterateOptions(a.values,function(b){b==a.placeholder_value&&l.error("Config",'Placeholder of filter "{0}" overlaps with one of its values',a.id)}))}},this),this.settings.sort_filters)if("function"==typeof this.settings.sort_filters)a.sort(this.settings.sort_filters);else{var c=this;a.sort(function(a,b){return c.translateLabel(a.label).localeCompare(c.translateLabel(b.label))})}return this.status.has_optgroup&&(a=l.groupSort(a,"optgroup")),a},g.prototype.checkOperators=function(a){var b=[];return a.forEach(function(c,d){"string"==typeof c?(g.OPERATORS[c]||l.error("Config",'Unknown operator "{0}"',c),a[d]=c=$.extendext(!0,"replace",{},g.OPERATORS[c])):(c.type||l.error("Config",'Missing "type" for operator {0}',d),g.OPERATORS[c.type]&&(a[d]=c=$.extendext(!0,"replace",{},g.OPERATORS[c.type],c)),void 0!==c.nb_inputs&&void 0!==c.apply_to||l.error("Config",'Missing "nb_inputs" and/or "apply_to" for operator "{0}"',c.type)),-1!=b.indexOf(c.type)&&l.error("Config",'Operator "{0}" already defined',c.type),b.push(c.type),c.optgroup?(this.status.has_operator_optgroup=!0,this.settings.optgroups[c.optgroup]||(this.settings.optgroups[c.optgroup]=c.optgroup)):c.optgroup=null},this),this.status.has_operator_optgroup&&(a=l.groupSort(a,"optgroup")),a},g.prototype.bindEvents=function(){var a=this;this.$el.on("change.queryBuilder",h.group_condition,function(){if($(this).is(":checked")){var a=$(this).closest(h.group_container);b(a).condition=$(this).val()}}),this.$el.on("change.queryBuilder",h.rule_filter,function(){var c=$(this).closest(h.rule_container);b(c).filter=a.getFilterById($(this).val())}),this.$el.on("change.queryBuilder",h.rule_operator,function(){var c=$(this).closest(h.rule_container);b(c).operator=a.getOperatorByType($(this).val())}),this.$el.on("click.queryBuilder",h.add_rule,function(){var c=$(this).closest(h.group_container);a.addRule(b(c))}),this.$el.on("click.queryBuilder",h.delete_rule,function(){var c=$(this).closest(h.rule_container);a.deleteRule(b(c))}),0!==this.settings.allow_groups&&(this.$el.on("click.queryBuilder",h.add_group,function(){var c=$(this).closest(h.group_container);a.addGroup(b(c))}),this.$el.on("click.queryBuilder",h.delete_group,function(){var c=$(this).closest(h.group_container);a.deleteGroup(b(c))})),this.model.on({drop:function(b,c){c.$el.remove(),a.refreshGroupsConditions()},add:function(b,c,d){0===d?c.$el.prependTo(c.parent.$el.find(">"+h.rules_list)):c.$el.insertAfter(c.parent.rules[d-1].$el),a.refreshGroupsConditions()},move:function(b,c,d,e){c.$el.detach(),0===e?c.$el.prependTo(d.$el.find(">"+h.rules_list)):c.$el.insertAfter(d.rules[e-1].$el),a.refreshGroupsConditions()},update:function(b,c,d,e,f){if(c instanceof k)switch(d){case"error":a.displayError(c);break;case"flags":a.applyRuleFlags(c);break;case"filter":a.updateRuleFilter(c);break;case"operator":a.updateRuleOperator(c,f);break;case"value":a.updateRuleValue(c)}else switch(d){case"error":a.displayError(c);break;case"flags":a.applyGroupFlags(c);break;case"condition":a.updateGroupCondition(c)}}})},g.prototype.setRoot=function(a,b,c){a=void 0===a||a===!0;var d=this.nextGroupId(),e=$(this.getGroupTemplate(d,1));return this.$el.append(e),this.model.root=new j(null,e),this.model.root.model=this.model,this.model.root.data=b,this.model.root.flags=$.extend({},this.settings.default_group_flags,c),this.trigger("afterAddGroup",this.model.root),this.model.root.condition=this.settings.default_condition,a&&this.addRule(this.model.root),this.model.root},g.prototype.addGroup=function(a,b,c,d){b=void 0===b||b===!0;var e=a.level+1,f=this.trigger("beforeAddGroup",a,b,e);if(f.isDefaultPrevented())return null;var g=this.nextGroupId(),h=$(this.getGroupTemplate(g,e)),i=a.addGroup(h);return i.data=c,i.flags=$.extend({},this.settings.default_group_flags,d),this.trigger("afterAddGroup",i),i.condition=this.settings.default_condition,b&&this.addRule(i),i},g.prototype.deleteGroup=function(a){if(a.isRoot())return!1;var b=this.trigger("beforeDeleteGroup",a);if(b.isDefaultPrevented())return!1;var c=!0;return a.each("reverse",function(a){c&=this.deleteRule(a)},function(a){c&=this.deleteGroup(a)},this),c&&(a.drop(),this.trigger("afterDeleteGroup")),c},g.prototype.updateGroupCondition=function(a){a.$el.find(">"+h.group_condition).each(function(){var b=$(this);b.prop("checked",b.val()===a.condition),b.parent().toggleClass("active",b.val()===a.condition)}),this.trigger("afterUpdateGroupCondition",a)},g.prototype.refreshGroupsConditions=function(){!function a(b){(!b.flags||b.flags&&!b.flags.condition_readonly)&&b.$el.find(">"+h.group_condition).prop("disabled",b.rules.length<=1).parent().toggleClass("disabled",b.rules.length<=1),b.each(function(a){},function(b){a(b)},this)}(this.model.root)},g.prototype.addRule=function(a,b,c){var d=this.trigger("beforeAddRule",a);if(d.isDefaultPrevented())return null;var e=this.nextRuleId(),f=$(this.getRuleTemplate(e)),g=a.addRule(f);return void 0!==b&&(g.data=b),g.flags=$.extend({},this.settings.default_rule_flags,c),this.trigger("afterAddRule",g),this.createRuleFilters(g),!this.settings.default_filter&&this.settings.display_empty_filter||(g.filter=this.getFilterById(this.settings.default_filter||this.filters[0].id)),g},g.prototype.deleteRule=function(a){if(a.flags.no_delete)return!1;var b=this.trigger("beforeDeleteRule",a);return b.isDefaultPrevented()?!1:(a.drop(),this.trigger("afterDeleteRule"),!0)},g.prototype.createRuleFilters=function(a){var b=this.change("getRuleFilters",this.filters,a),c=$(this.getRuleFilterSelect(a,b));a.$el.find(h.filter_container).html(c),this.trigger("afterCreateRuleFilters",a)},g.prototype.createRuleOperators=function(a){var b=a.$el.find(h.operator_container).empty();if(a.filter){var c=this.getOperators(a.filter),d=$(this.getRuleOperatorSelect(a,c));b.html(d),a.__.operator=c[0],this.trigger("afterCreateRuleOperators",a,c)}},g.prototype.createRuleInput=function(a){var b=a.$el.find(h.value_container).empty();if(a.__.value=void 0,a.filter&&a.operator&&0!==a.operator.nb_inputs){for(var c=this,d=$(),e=a.filter,f=0;f0&&b.append(this.settings.inputs_separator),b.append(g),d=d.add(g)}b.show(),d.on("change "+(e.input_event||""),function(){c.status.updating_value=!0,a.value=c.getRuleValue(a),c.status.updating_value=!1}),e.plugin&&d[e.plugin](e.plugin_config||{}),this.trigger("afterCreateRuleInput",a),void 0!==e.default_value?a.value=e.default_value:(c.status.updating_value=!0,a.value=c.getRuleValue(a),c.status.updating_value=!1)}},g.prototype.updateRuleFilter=function(a){this.createRuleOperators(a),this.createRuleInput(a),a.$el.find(h.rule_filter).val(a.filter?a.filter.id:"-1"),this.trigger("afterUpdateRuleFilter",a)},g.prototype.updateRuleOperator=function(a,b){var c=a.$el.find(h.value_container);a.operator&&0!==a.operator.nb_inputs?(c.show(),(c.is(":empty")||a.operator.nb_inputs!==b.nb_inputs)&&this.createRuleInput(a)):(c.hide(),a.__.value=void 0),a.operator&&a.$el.find(h.rule_operator).val(a.operator.type),this.trigger("afterUpdateRuleOperator",a)},g.prototype.updateRuleValue=function(a){this.status.updating_value||this.setRuleValue(a,a.value),this.trigger("afterUpdateRuleValue",a)},g.prototype.applyRuleFlags=function(a){var b=a.flags;b.filter_readonly&&a.$el.find(h.rule_filter).prop("disabled",!0),b.operator_readonly&&a.$el.find(h.rule_operator).prop("disabled",!0),b.value_readonly&&a.$el.find(h.rule_value).prop("disabled",!0),b.no_delete&&a.$el.find(h.delete_rule).remove(),this.trigger("afterApplyRuleFlags",a)},g.prototype.applyGroupFlags=function(a){var b=a.flags;b.condition_readonly&&a.$el.find(">"+h.group_condition).prop("disabled",!0).parent().addClass("readonly"),b.no_delete&&a.$el.find(h.delete_group).remove(),this.trigger("afterApplyGroupFlags",a)},g.prototype.clearErrors=function(a){a=a||this.model.root,a&&(a.error=null,a instanceof j&&a.each(function(a){a.error=null},function(a){this.clearErrors(a)},this))},g.prototype.displayError=function(a){if(this.settings.display_errors)if(null===a.error)a.$el.removeClass("has-error");else{var b=$.extend([],a.error,[this.lang.errors[a.error[0]]||a.error[0]]);a.$el.addClass("has-error").find(h.error_container).eq(0).attr("title",l.fmt.apply(null,b))}},g.prototype.triggerValidationError=function(a,b,c){$.isArray(b)||(b=[b]);var d=this.trigger("validationError",a,b,c);d.isDefaultPrevented()||(a.error=b)},g.prototype.destroy=function(){this.trigger("beforeDestroy"),this.status.generated_id&&this.$el.removeAttr("id"),this.clear(),this.model=null,this.$el.off(".queryBuilder").removeClass("query-builder").removeData("queryBuilder"),delete this.$el[0].queryBuilder},g.prototype.reset=function(){this.status.group_id=1,this.status.rule_id=0,this.model.root.empty(),this.addRule(this.model.root),this.trigger("afterReset")},g.prototype.clear=function(){this.status.group_id=0,this.status.rule_id=0,this.model.root&&(this.model.root.drop(),this.model.root=null),this.trigger("afterClear")},g.prototype.setOptions=function(a){$.makeArray($(Object.keys(a)).filter(g.modifiable_options)).forEach(function(b){this.settings[b]=a[b]},this)},g.prototype.getModel=function(a){return a?b(a):this.model.root},g.prototype.validate=function(){this.clearErrors();var a=this,b=function c(b){var d=0,e=0;return b.each(function(b){if(!b.filter)return a.triggerValidationError(b,"no_filter",null),void e++;if(0!==b.operator.nb_inputs){var c=a.validateValue(b,b.value);if(c!==!0)return a.triggerValidationError(b,c,b.value),void e++}d++},function(a){c(a)?d++:e++}),e>0?!1:0!==d||a.settings.allow_empty&&b.isRoot()?!0:(a.triggerValidationError(b,"empty_group",null),!1)}(this.model.root);return this.change("validate",b)},g.prototype.getRules=function(a){if(a=$.extend({get_flags:!1},a),!this.validate())return{};var b=this,c=function d(c){var e={condition:c.condition,rules:[]};if(c.data&&(e.data=$.extendext(!0,"replace",{},c.data)),a.get_flags){var f=b.getGroupFlags(c.flags,"all"===a.get_flags);$.isEmptyObject(f)||(e.flags=f)}return c.each(function(c){var d=null;0!==c.operator.nb_inputs&&(d=c.value);var f={id:c.filter.id,field:c.filter.field,type:c.filter.type,input:c.filter.input,operator:c.operator.type,value:d};if((c.filter.data||c.data)&&(f.data=$.extendext(!0,"replace",{},c.filter.data,c.data)),a.get_flags){var g=b.getRuleFlags(c.flags,"all"===a.get_flags);$.isEmptyObject(g)||(f.flags=g)}e.rules.push(f)},function(a){e.rules.push(d(a))}),e}(this.model.root);return this.change("getRules",c)},g.prototype.setRules=function(a){$.isArray(a)&&(a={condition:this.settings.default_condition,rules:a}),a&&a.rules&&(0!==a.rules.length||this.settings.allow_empty)||l.error("RulesParse","Incorrect data object passed"),this.clear(),this.setRoot(!1,a.data,this.parseGroupFlags(a)),a=this.change("setRules",a);var b=this;!function c(a,d){null!==d&&(void 0===a.condition?a.condition=b.settings.default_condition:-1==b.settings.conditions.indexOf(a.condition)&&l.error("UndefinedCondition",'Invalid condition "{0}"',a.condition),d.condition=a.condition,a.rules.forEach(function(a){var e;if(a.rules&&a.rules.length>0)if(-1!==b.settings.allow_groups&&b.settings.allow_groups1){h=["operator_not_multiple",e.type];break}break;case"select":if(d.multiple){if(void 0===b[i]||0===b[i].length||d.placeholder&&b[i]==d.placeholder_value){h=["select_empty"];break}if(!e.multiple&&b[i].length>1){h=["operator_not_multiple",e.type];break}}else if(void 0===b[i]||d.placeholder&&b[i]==d.placeholder_value){h=["select_empty"];break}break;default:switch(g.types[d.type]){case"string":if(void 0===b[i]||0===b[i].length){h=["string_empty"];break}if(void 0!==f.min&&b[i].lengthparseInt(f.max)){h=["string_exceed_max_length",f.max];break}if(f.format&&("string"==typeof f.format&&(f.format=new RegExp(f.format)),!f.format.test(b[i]))){h=["string_invalid_format",f.format];break}break;case"number":if(void 0===b[i]||isNaN(b[i])){h=["number_nan"];break}if("integer"==d.type){if(parseInt(b[i])!=b[i]){h=["number_not_integer"];break}}else if(parseFloat(b[i])!=b[i]){h=["number_not_double"];break}if(void 0!==f.min&&b[i]parseFloat(f.max)){h=["number_exceed_max",f.max];break}if(void 0!==f.step&&"any"!==f.step){var j=(b[i]/f.step).toPrecision(14);if(parseInt(j)!=j){h=["number_wrong_step",f.step];break}}break;case"datetime":if(void 0===b[i]||0===b[i].length){h=["datetime_empty"];break}if(f.format){"moment"in window||l.error("MissingLibrary","MomentJS is required for Date/Time validation. Get it here http://momentjs.com");var k=moment(b[i],f.format);if(!k.isValid()){h=["datetime_invalid",f.format];break}if(f.min&&kmoment(f.max,f.format)){h=["datetime_exceed_max",f.max];break}}break;case"boolean":if(c=b[i].trim().toLowerCase(),"true"!==c&&"false"!==c&&"1"!==c&&"0"!==c&&1!==b[i]&&0!==b[i]){h=["boolean_not_valid"];break}}}if(h!==!0)break}return h},g.prototype.nextGroupId=function(){return this.status.id+"_group_"+this.status.group_id++},g.prototype.nextRuleId=function(){return this.status.id+"_rule_"+this.status.rule_id++},g.prototype.getOperators=function(a){"string"==typeof a&&(a=this.getFilterById(a));for(var b=[],c=0,d=this.operators.length;d>c;c++){if(a.operators){if(-1==a.operators.indexOf(this.operators[c].type))continue}else if(-1==this.operators[c].apply_to.indexOf(g.types[a.type]))continue;b.push(this.operators[c])}return a.operators&&b.sort(function(b,c){return a.operators.indexOf(b.type)-a.operators.indexOf(c.type)}),this.change("getOperators",b,a)},g.prototype.getFilterById=function(a){if("-1"==a)return null;for(var b=0,c=this.filters.length;c>b;b++)if(this.filters[b].id==a)return this.filters[b];l.error("UndefinedFilter",'Undefined filter "{0}"',a)},g.prototype.getOperatorByType=function(a){if("-1"==a)return null;for(var b=0,c=this.operators.length;c>b;b++)if(this.operators[b].type==a)return this.operators[b];l.error("UndefinedOperator",'Undefined operator "{0}"',a)},g.prototype.getRuleValue=function(a){var b=a.filter,c=a.operator,d=[];if(b.valueGetter)d=b.valueGetter.call(this,a);else{for(var e=a.$el.find(h.value_container),f=0;f '+b+" "});break;case"select":h+='";break;case"textarea":h+=''; + break; + + case 'number': + h += ' ' + val + ' '; - }); - break; + console.error(type + 'Error: ' + Utils.fmt(message, args)); + } +}; - case 'select': - h+= ''; - break; +/** + * 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; + } - case 'textarea': - h+= '";break;default:switch(g.types[c.type]){case"number":h+='=f:f>=e},i=!1;h()&&(this.rules[e]instanceof j?void 0!==c&&(i=c.call(d,this.rules[e])===!1):i=b.call(d,this.rules[e])===!1,!i);e+=g);return!i},j.prototype.contains=function(a,b){return-1!==this.getNodePos(a)?!0:b?!this.each(function(a){return!0},function(b){return!b.contains(a,!0)}):!1};var k=function(a,b){return this instanceof k?(i.call(this,a,b),this.__.filter=null,this.__.operator=null,this.__.flags={},void(this.__.value=void 0)):new k(a,b)};k.prototype=Object.create(i.prototype),k.prototype.constructor=k,c(k,["filter","operator","value"]),g.Group=j,g.Rule=k;var l=g.utils={};l.iterateOptions=function(a,b){a&&($.isArray(a)?a.forEach(function(a){$.isPlainObject(a)?$.each(a,function(a,c){return b(a,c),!1}):b(a,a)}):$.each(a,function(a,c){b(a,c)}))},l.fmt=function(a){var b=Array.prototype.slice.call(arguments,1);return a.replace(/{([0-9]+)}/g,function(a,c){return b[parseInt(c)]})},l.error=function(a,b){var c=new Error(l.fmt.apply(null,Array.prototype.slice.call(arguments,1)));throw c.name=a+"Error",c.args=Array.prototype.slice.call(arguments,2),c},l.changeType=function(a,b,c){switch(b){case"integer":return parseInt(a);case"double":return parseFloat(a);case"boolean":var d="true"===a.trim().toLowerCase()||"1"===a.trim()||1===a;return c?d?1:0:d;default:return a}},l.escapeString=function(a){return"string"!=typeof a?a:a.replace(/[\0\n\r\b\\\'\"]/g,function(a){switch(a){case"\x00":return"\\0";case"\n":return"\\n";case"\r":return"\\r";case"\b":return"\\b";default:return"\\"+a}}).replace(/\t/g,"\\t").replace(/\x1a/g,"\\Z")},l.escapeRegExp=function(a){return a.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},l.escapeElementId=function(a){return a?a.replace(/(\\)?([:.\[\],])/g,function(a,b,c){return b?a:"\\"+c}):a},l.groupSort=function(a,b){var c=[],d=[];return a.forEach(function(a){var e;a[b]?(e=c.lastIndexOf(a[b]),-1==e?e=c.length:e++):e=c.length,c.splice(e,0,a[b]),d.splice(e,0,a)}),d},$.fn.queryBuilder=function(a){this.length>1&&l.error("Config","Unable to initialize on multiple target");var b=this.data("queryBuilder"),c="object"==typeof a&&a||{};return b||"destroy"!=a?(b||this.data("queryBuilder",new g(this,c)),"string"==typeof a?b[a].apply(b,Array.prototype.slice.call(arguments,1)):this):this},$.fn.queryBuilder.constructor=g,$.fn.queryBuilder.defaults=g.defaults,$.fn.queryBuilder.extend=g.extend,$.fn.queryBuilder.define=g.define,$.fn.queryBuilder.regional=g.regional,g.define("bt-checkbox",function(a){if("glyphicons"==a.font){var b=document.createElement("style");b.innerHTML='.checkbox input[type=checkbox]:checked + label:after { font-family: "Glyphicons Halflings"; content: "\\e013"; } .checkbox label:after { padding-left: 4px; padding-top: 2px; font-size: 9px; }',document.body.appendChild(b)}this.on("getRuleInput.filter",function(b,c,d){var e=c.filter;if(("radio"===e.input||"checkbox"===e.input)&&!e.plugin){b.value="",e.colors||(e.colors={}),e.color&&(e.colors._def_=e.color);var f=e.vertical?' style="display:block"':"",g=0;l.iterateOptions(e.values,function(c,h){var i=e.colors[c]||e.colors._def_||a.color,j=d+"_"+g++;b.value+=" "})}})},{font:"glyphicons",color:"default"}),g.define("bt-selectpicker",function(a){$.fn.selectpicker&&$.fn.selectpicker.Constructor||l.error("MissingLibrary",'Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select'),this.on("afterCreateRuleFilters",function(b,c){c.$el.find(h.rule_filter).removeClass("form-control").selectpicker(a)}),this.on("afterCreateRuleOperators",function(b,c){c.$el.find(h.rule_operator).removeClass("form-control").selectpicker(a)}),this.on("afterUpdateRuleFilter",function(a,b){b.$el.find(h.rule_filter).selectpicker("render")}),this.on("afterUpdateRuleOperator",function(a,b){b.$el.find(h.rule_operator).selectpicker("render")})},{container:"body",style:"btn-inverse btn-xs",width:"auto",showIcon:!1}),g.define("bt-tooltip-errors",function(a){$.fn.tooltip&&$.fn.tooltip.Constructor&&$.fn.tooltip.Constructor.prototype.fixTitle||l.error("MissingLibrary",'Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com');var b=this;this.on("getRuleTemplate.filter getGroupTemplate.filter",function(a){var b=$(a.value);b.find(h.error_container).attr("data-toggle","tooltip"),a.value=b.prop("outerHTML")}),this.model.on("update",function(c,d,e){"error"==e&&b.settings.display_errors&&d.$el.find(h.error_container).eq(0).tooltip(a).tooltip("hide").tooltip("fixTitle")})},{placement:"right"}),g.extend({setFilters:function(a,b){var c=this;void 0===b&&(b=a,a=!1),b=this.checkFilters(b),b=this.change("setFilters",b);var d=b.map(function(a){return a.id});if(a||!function f(a){a.each(function(a){a.filter&&-1===d.indexOf(a.filter.id)&&l.error("ChangeFilter",'A rule is using filter "{0}"',a.filter.id)},f)}(this.model.root),this.filters=b,function g(a){a.each(!0,function(a){a.filter&&-1===d.indexOf(a.filter.id)?a.drop():(c.createRuleFilters(a),a.$el.find(h.rule_filter).val(a.filter?a.filter.id:"-1"))},g)}(this.model.root),this.settings.plugins&&(this.settings.plugins["unique-filter"]&&this.updateDisabledFilters(),this.settings.plugins["bt-selectpicker"]&&this.$el.find(h.rule_filter).selectpicker("render")),this.settings.default_filter)try{this.getFilterById(this.settings.default_filter)}catch(e){this.settings.default_filter=null}this.trigger("afterSetFilters",b)},addFilter:function(a,b){void 0===b||"#end"==b?b=this.filters.length:"#start"==b&&(b=0),$.isArray(a)||(a=[a]);var c=$.extend(!0,[],this.filters);parseInt(b)==b?Array.prototype.splice.apply(c,[b,0].concat(a)):this.filters.some(function(a,c){return a.id==b?(b=c+1,!0):void 0})?Array.prototype.splice.apply(c,[b,0].concat(a)):Array.prototype.push.apply(c,a),this.setFilters(c)},removeFilter:function(a,b){var c=$.extend(!0,[],this.filters);"string"==typeof a&&(a=[a]),c=c.filter(function(b){return-1===a.indexOf(b.id)}),this.setFilters(b,c)}}),g.define("filter-description",function(a){"inline"===a.mode?this.on("afterUpdateRuleFilter",function(b,c){var d=c.$el.find("p.filter-description");c.filter&&c.filter.description?(0===d.length?(d=$('

    '),d.appendTo(c.$el)):d.show(),d.html(' '+c.filter.description)):d.hide()}):"popover"===a.mode?($.fn.popover&&$.fn.popover.Constructor&&$.fn.popover.Constructor.prototype.fixTitle||l.error("MissingLibrary",'Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com'),this.on("afterUpdateRuleFilter",function(b,c){var d=c.$el.find("button.filter-description");c.filter&&c.filter.description?(0===d.length?(d=$(''),d.prependTo(c.$el.find(h.rule_actions)),d.popover({placement:"left",container:"body",html:!0}),d.on("mouseout",function(){d.popover("hide")})):d.show(),d.data("bs.popover").options.content=c.filter.description,d.attr("aria-describedby")&&d.popover("show")):(d.hide(),d.data("bs.popover")&&d.popover("hide"))})):"bootbox"===a.mode&&("bootbox"in window||l.error("MissingLibrary",'Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com'),this.on("afterUpdateRuleFilter",function(b,c){var d=c.$el.find("button.filter-description");c.filter&&c.filter.description?(0===d.length&&(d=$(''),d.prependTo(c.$el.find(h.rule_actions)),d.on("click",function(){bootbox.alert(d.data("description"))})),d.data("description",c.filter.description)):d.hide()}))},{icon:"glyphicon glyphicon-info-sign",mode:"popover"}),g.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"}}),g.define("invert",function(a){var c=this;this.on("afterInit",function(){c.$el.on("click.queryBuilder","[data-invert=group]",function(){var d=$(this).closest(h.group_container);c.invert(b(d),a)}),a.display_rules_button&&a.invert_rules&&c.$el.on("click.queryBuilder","[data-invert=rule]",function(){var d=$(this).closest(h.rule_container);c.invert(b(d),a)})}),this.on("getGroupTemplate.filter",function(b,d){var e=$(b.value);e.find(h.condition_container).after('"),b.value=e.prop("outerHTML")}),a.display_rules_button&&a.invert_rules&&this.on("getRuleTemplate.filter",function(b){var d=$(b.value);d.find(h.rule_actions).prepend('"),b.value=d.prop("outerHTML")})},{icon:"glyphicon glyphicon-random",recursive:!0,invert_rules:!0,display_rules_button:!1,silent_fail:!1}),g.extend({invert:function(a,b){if(!(a instanceof i)){if(!this.model.root)return;b=a,a=this.model.root}if("object"!=typeof b&&(b={}),void 0===b.recursive&&(b.recursive=!0),void 0===b.invert_rules&&(b.invert_rules=!0),void 0===b.silent_fail&&(b.silent_fail=!1),void 0===b.trigger&&(b.trigger=!0),a instanceof j){if(this.settings.conditionOpposites[a.condition]?a.condition=this.settings.conditionOpposites[a.condition]:b.silent_fail||l.error("InvertCondition",'Unknown inverse of condition "{0}"',a.condition),b.recursive){var c=$.extend({},b,{trigger:!1});a.each(function(a){b.invert_rules&&this.invert(a,c)},function(a){this.invert(a,c)},this)}}else if(a instanceof k&&a.operator&&!a.filter.no_invert)if(this.settings.operatorOpposites[a.operator.type]){var d=this.settings.operatorOpposites[a.operator.type];a.filter.operators&&-1==a.filter.operators.indexOf(d)||(a.operator=this.getOperatorByType(d))}else b.silent_fail||l.error("InvertOperator",'Unknown inverse of operator "{0}"',a.operator.type);b.trigger&&this.trigger("afterInvert",a,b)}}),g.defaults({mongoOperators:{equal:function(a){return a[0]},not_equal:function(a){return{$ne:a[0]}},"in":function(a){return{$in:a}},not_in:function(a){return{$nin:a}},less:function(a){return{$lt:a[0]}},less_or_equal:function(a){return{$lte:a[0]}},greater:function(a){return{$gt:a[0]}},greater_or_equal:function(a){return{$gte:a[0]}},between:function(a){return{$gte:a[0],$lte:a[1]}},not_between:function(a){return{$lt:a[0],$gt:a[1]}},begins_with:function(a){return{$regex:"^"+l.escapeRegExp(a[0])}},not_begins_with:function(a){return{$regex:"^(?!"+l.escapeRegExp(a[0])+")"}},contains:function(a){return{$regex:l.escapeRegExp(a[0])}},not_contains:function(a){return{$regex:"^((?!"+l.escapeRegExp(a[0])+").)*$",$options:"s"}},ends_with:function(a){return{$regex:l.escapeRegExp(a[0])+"$"}},not_ends_with:function(a){return{$regex:"(?0)d.push(c(a));else{var e=b.settings.mongoOperators[a.operator],f=b.getOperatorByType(a.operator),g=[];void 0===e&&l.error("UndefinedMongoOperator",'Unknown MongoDB operation for operator "{0}"',a.operator),0!==f.nb_inputs&&(a.value instanceof Array||(a.value=[a.value]),a.value.forEach(function(b){g.push(l.changeType(b,a.type,!1))}));var h={};h[a.field]=e.call(b,g),d.push(h)}});var e={};return d.length>0&&(e["$"+a.condition.toLowerCase()]=d),e}(a)},getRulesFromMongo:function(a){if(void 0===a||null===a)return null;var b=this,c={$and:"AND",$or:"OR"};return function e(a){var f=Object.keys(a);f.length>1&&l.error("MongoParse","Invalid MongoDB query format"),c[f[0].toLowerCase()]||l.error("UndefinedMongoCondition",'Unable to build MongoDB query with condition "{0}"',f[0]);var g=a[f[0]],h=[];g.forEach(function(a){var f=Object.keys(a);if(c[f[0].toLowerCase()])h.push(e(a));else{var g=f[0],i=a[g],j=d(i,g);void 0===j&&l.error("MongoParse","Invalid MongoDB query format");var k=b.settings.mongoRuleOperators[j];void 0===k&&l.error("UndefinedMongoOperator",'JSON Rule operation unknown for operator "{0}"',j);var m=k.call(b,i);h.push({id:b.change("getMongoDBFieldID",g,i),field:g,operator:m.op,value:m.val})}});var i={};return h.length>0&&(i.condition=c[f[0].toLowerCase()],i.rules=h),i}(a)},setRulesFromMongo:function(a){this.setRules(this.getRulesFromMongo(a))}}),h.rule_and_group_containers=h.rule_container+", "+h.group_container,g.define("sortable",function(a){this.on("afterInit",function(a){$.event.props.push("dataTransfer");var c,d,f=a.builder;f.$el.on("mouseover.queryBuilder",".drag-handle",function(){f.$el.find(h.rule_and_group_containers).attr("draggable",!0)}),f.$el.on("mouseout.queryBuilder",".drag-handle",function(){f.$el.find(h.rule_and_group_containers).removeAttr("draggable")}),f.$el.on("dragstart.queryBuilder","[draggable]",function(a){a.stopPropagation(),a.dataTransfer.setData("text","drag"),d=b(a.target),setTimeout(function(){var a=$('
     
    ');a.css("min-height",d.$el.height()),c=d.parent.addRule(a,d.getPos()),d.$el.hide()},0)}),f.$el.on("dragenter.queryBuilder","[draggable]",function(a){a.preventDefault(),a.stopPropagation(),c&&e(c,$(a.target))}),f.$el.on("dragover.queryBuilder","[draggable]",function(a){a.preventDefault(),a.stopPropagation()}),f.$el.on("drop.queryBuilder",function(a){a.preventDefault(),a.stopPropagation(),e(d,$(a.target))}),f.$el.on("dragend.queryBuilder","[draggable]",function(a){a.preventDefault(),a.stopPropagation(),d.$el.show(),c.drop(),f.$el.find(h.rule_and_group_containers).removeAttr("draggable"),f.trigger("afterMove",d),d=c=null})}),this.on("parseRuleFlags.filter",function(b){void 0===b.value.no_sortable&&(b.value.no_sortable=a.default_no_sortable)}),this.on("afterApplyRuleFlags",function(a,b){b.flags.no_sortable&&b.$el.find(".drag-handle").remove()}),this.on("parseGroupFlags.filter",function(b){void 0===b.value.no_sortable&&(b.value.no_sortable=a.default_no_sortable)}),this.on("afterApplyGroupFlags",function(a,b){b.flags.no_sortable&&b.$el.find(".drag-handle").remove()}),this.on("getGroupTemplate.filter",function(b,c){if(c>1){var d=$(b.value);d.find(h.condition_container).after('
    '),b.value=d.prop("outerHTML")}}),this.on("getRuleTemplate.filter",function(b){var c=$(b.value);c.find(h.rule_header).after('
    '),b.value=c.prop("outerHTML")})},{default_no_sortable:!1,icon:"glyphicon glyphicon-sort"}),g.defaults({sqlOperators:{equal:{op:"= ?"},not_equal:{op:"!= ?"},"in":{op:"IN(?)",sep:", "},not_in:{op:"NOT IN(?)",sep:", "},less:{op:"< ?"},less_or_equal:{op:"<= ?"},greater:{op:"> ?"},greater_or_equal:{op:">= ?"},between:{op:"BETWEEN ?",sep:" AND "},not_between:{op:"NOT BETWEEN ?",sep:" AND "},begins_with:{op:"LIKE(?)",mod:"{0}%"},not_begins_with:{op:"NOT LIKE(?)",mod:"{0}%"},contains:{op:"LIKE(?)",mod:"%{0}%"},not_contains:{op:"NOT LIKE(?)",mod:"%{0}%"},ends_with:{op:"LIKE(?)",mod:"%{0}"},not_ends_with:{op:"NOT LIKE(?)",mod:"%{0}"},is_empty:{op:"= ''"},is_not_empty:{op:"!= ''"},is_null:{op:"IS NULL"},is_not_null:{op:"IS NOT NULL"}},sqlRuleOperator:{"=":function(a){return{val:a,op:""===a?"is_empty":"equal"}},"!=":function(a){return{val:a,op:""===a?"is_not_empty":"not_equal"}},LIKE:function(a){return"%"==a.slice(0,1)&&"%"==a.slice(-1)?{val:a.slice(1,-1),op:"contains"}:"%"==a.slice(0,1)?{val:a.slice(1),op:"ends_with"}:"%"==a.slice(-1)?{val:a.slice(0,-1),op:"begins_with"}:void l.error("SQLParse",'Invalid value for LIKE operator "{0}"',a)},IN:function(a){return{val:a,op:"in"}},"NOT IN":function(a){return{val:a,op:"not_in"}},"<":function(a){return{val:a,op:"less"}},"<=":function(a){return{val:a,op:"less_or_equal"}},">":function(a){return{val:a,op:"greater"}},">=":function(a){return{val:a,op:"greater_or_equal"}},BETWEEN:function(a){return{val:a,op:"between"}},"NOT BETWEEN":function(a){return{val:a,op:"not_between"}},IS:function(a){return null!==a&&l.error("SQLParse","Invalid value for IS operator"),{val:null,op:"is_null"}},"IS NOT":function(a){return null!==a&&l.error("SQLParse","Invalid value for IS operator"),{val:null,op:"is_not_null"}}},sqlStatements:{question_mark:function(){var a=[];return{add:function(b,c){return a.push(c),"?"},run:function(){return a}}},numbered:function(a){(!a||a.length>1)&&(a="$");var b=0,c=[];return{add:function(d,e){return c.push(e),b++,a+b},run:function(){return c}}},named:function(a){(!a||a.length>1)&&(a=":");var b={},c={};return{add:function(d,e){b[d.field]||(b[d.field]=1);var f=d.field+"_"+b[d.field]++;return c[f]=e,a+f},run:function(){return c}}}},sqlRuleStatement:{question_mark:function(a){var b=0;return{parse:function(c){return"?"==c?a[b++]:c},esc:function(a){return a.replace(/\?/g,"'?'")}}},numbered:function(a,b){(!b||b.length>1)&&(b="$");var c=new RegExp("^\\"+b+"[0-9]+$"),d=new RegExp("\\"+b+"([0-9]+)","g");return{parse:function(b){return c.test(b)?a[b.slice(1)-1]:b},esc:function(a){return a.replace(d,"'"+("$"==b?"$$":b)+"$1'")}}},named:function(a,b){(!b||b.length>1)&&(b=":");var c=new RegExp("^\\"+b),d=new RegExp("\\"+b+"("+Object.keys(a).join("|")+")","g");return{parse:function(b){return c.test(b)?a[b.slice(1)]:b},esc:function(a){return a.replace(d,"'"+("$"==b?"$$":b)+"$1'")}}}}}),g.extend({getSQL:function(a,b,c){if(c=void 0===c?this.getRules():c,b=b===!0?"\n":" ",a===!0&&(a="question_mark"),"string"==typeof a){var d=f(a);a=this.settings.sqlStatements[d[1]](d[2])}var e=this,g=function h(c){if(c.condition||(c.condition=e.settings.default_condition),-1===["AND","OR"].indexOf(c.condition.toUpperCase())&&l.error("UndefinedSQLCondition",'Unable to build SQL query with condition "{0}"',c.condition),!c.rules)return"";var d=[];return c.rules.forEach(function(c){if(c.rules&&c.rules.length>0)d.push("("+b+h(c)+b+")"+b);else{var f=e.settings.sqlOperators[c.operator],g=e.getOperatorByType(c.operator),i="";void 0===f&&l.error("UndefinedSQLOperator",'Unknown SQL operation for operator "{0}"',c.operator),0!==g.nb_inputs&&(c.value instanceof Array||(c.value=[c.value]),c.value.forEach(function(b,d){d>0&&(i+=f.sep),"integer"==c.type||"double"==c.type||"boolean"==c.type?b=l.changeType(b,c.type,!0):a||(b=l.escapeString(b)),f.mod&&(b=l.fmt(f.mod,b)),a?i+=a.add(c,b):("string"==typeof b&&(b="'"+b+"'"),i+=b)})),d.push(c.field+" "+f.op.replace(/\?/,i))}}),d.join(" "+c.condition+b)}(c);return a?{sql:g,params:a.run()}:{sql:g}},getRulesFromSQL:function(a,b){"SQLParser"in window||l.error("MissingLibrary","SQLParser is required to parse SQL queries. Get it here https://github.com/mistic100/sql-parser");var c=this;if("string"==typeof a&&(a={sql:a}),b===!0&&(b="question_mark"),"string"==typeof b){var d=f(b);b=this.settings.sqlRuleStatement[d[1]](a.params,d[2])}b&&(a.sql=b.esc(a.sql)),0!==a.sql.toUpperCase().indexOf("SELECT")&&(a.sql="SELECT * FROM table WHERE "+a.sql);var e=SQLParser.parse(a.sql);e.where||l.error("SQLParse","No WHERE clause found");var g={condition:this.settings.default_condition,rules:[]},h=g;return function i(a,d){if(-1!==["AND","OR"].indexOf(a.operation.toUpperCase())){d>0&&h.condition!=a.operation.toUpperCase()&&(h.rules.push({condition:c.settings.default_condition,rules:[]}),h=h.rules[h.rules.length-1]),h.condition=a.operation.toUpperCase(),d++;var e=h;i(a.left,d),h=e,i(a.right,d)}else{void 0!==a.left.value&&void 0!==a.right.value||l.error("SQLParse","Missing field and/or value"),$.isPlainObject(a.right.value)&&l.error("SQLParse","Value format not supported for {0}.",a.left.value);var f;f=$.isArray(a.right.value)?a.right.value.map(function(a){return a.value}):a.right.value,b&&(f=$.isArray(f)?f.map(b.parse):b.parse(f));var g=a.operation.toUpperCase();"<>"==g&&(g="!=");var j;j="NOT LIKE"==g?c.settings.sqlRuleOperator.LIKE:c.settings.sqlRuleOperator[g],void 0===j&&l.error("UndefinedSQLOperator",'Invalid SQL operation "{0}".',a.operation);var k=j.call(this,f,a.operation);"NOT LIKE"==g&&(k.op="not_"+k.op);var m=a.left.values.join(".");h.rules.push({id:c.change("getSQLFieldID",m,f),field:m,operator:k.op,value:k.val})}}(e.where.conditions,0),g},setRulesFromSQL:function(a,b){this.setRules(this.getRulesFromSQL(a,b))}}),g.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)}),g.extend({updateDisabledFilters:function(a){var b=a?a.builder:this;b.status.used_filters={},b.model&&(!function c(a){a.each(function(a){a.filter&&a.filter.unique&&(b.status.used_filters[a.filter.id]||(b.status.used_filters[a.filter.id]=[]),"group"==a.filter.unique&&b.status.used_filters[a.filter.id].push(a.parent))},function(a){c(a)})}(b.model.root),b.applyDisabledFilters(a))},clearDisabledFilters:function(a){var b=a?a.builder:this;b.status.used_filters={},b.applyDisabledFilters(a)},applyDisabledFilters:function(a){var b=a?a.builder:this;b.$el.find(h.filter_container+" option").prop("disabled",!1),$.each(b.status.used_filters,function(a,c){0===c.length?b.$el.find(h.filter_container+' option[value="'+a+'"]:not(:selected)').prop("disabled",!0):c.forEach(function(b){b.each(function(b){b.$el.find(h.filter_container+' option[value="'+a+'"]:not(:selected)').prop("disabled",!0)})})}),b.settings.plugins&&b.settings.plugins["bt-selectpicker"]&&b.$el.find(h.rule_filter).selectpicker("render")}}),g.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}",datetime_empty:"Empty value",datetime_invalid:"Invalid date format ({0})",datetime_exceed_min:"Must be after {0}",datetime_exceed_max:"Must be before {0}",boolean_not_valid:"Not a boolean",operator_not_multiple:"Operator {0} cannot accept multiple values"},invert:"Invert"},g.defaults({lang_code:"en"})}); \ No newline at end of file diff --git a/dist/scss/dark.scss b/dist/scss/dark.scss index 7deb194b..f9c5ecde 100644 --- a/dist/scss/dark.scss +++ b/dist/scss/dark.scss @@ -1,9 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 - * Copyright 2014-2016 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://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) */ - $theme-name: dark; $group-background-color: rgba(50, 70, 80, .5); diff --git a/dist/scss/default.scss b/dist/scss/default.scss index 49b4ebd7..8c2b9c0d 100644 --- a/dist/scss/default.scss +++ b/dist/scss/default.scss @@ -1,9 +1,8 @@ /*! - * jQuery QueryBuilder 2.3.3 - * Copyright 2014-2016 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://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) */ - $theme-name: default !default; // common @@ -68,8 +67,16 @@ $ticks-position: 5px, 10px !default; .group-conditions { .btn.readonly:not(.active), - input[name$=_cond] { - display: none; + 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; } .btn.readonly { @@ -107,10 +114,6 @@ $ticks-position: 5px, 10px !default; display: block; } } - - select, input[type=text], input[type=number] { - padding: 1px; - } } // ERRORS @@ -168,8 +171,8 @@ $ticks-position: 5px, 10px !default; } } - -@import 'plugins/bt-tooltip-errors'; -@import 'plugins/filter-description'; -@import 'plugins/invert'; -@import 'plugins/sortable'; \ No newline at end of file +@import "plugins/bt-checkbox"; +@import "plugins/bt-tooltip-errors"; +@import "plugins/filter-description"; +@import "plugins/invert"; +@import "plugins/sortable"; \ No newline at end of file diff --git a/dist/scss/plugins/bt-checkbox.scss b/dist/scss/plugins/bt-checkbox.scss new file mode 100644 index 00000000..22e21eed --- /dev/null +++ b/dist/scss/plugins/bt-checkbox.scss @@ -0,0 +1,10 @@ +.query-builder.bt-checkbox-bootstrap-icons { + .checkbox input[type='checkbox'] + label::before { + outline: 0; + } + + .checkbox input[type='checkbox']:checked + label::after { + font-family: 'bootstrap-icons'; + content: '\F633'; // https://icons.getbootstrap.com/icons/check-lg/ + } +} diff --git a/dist/scss/plugins/filter-description.scss b/dist/scss/plugins/filter-description.scss index 80a2af71..41498718 100644 --- a/dist/scss/plugins/filter-description.scss +++ b/dist/scss/plugins/filter-description.scss @@ -16,6 +16,6 @@ $description-border: 1px solid $description-border-color; border: $description-border; color: $description-text-color; border-radius: $item-border-radius; - padding: #{$rule-padding / 2} $rule-padding; + padding: #{$rule-padding * .5} $rule-padding; font-size: .8em; } diff --git a/dist/scss/plugins/sortable.scss b/dist/scss/plugins/sortable.scss index 657e66f6..ac902fe1 100644 --- a/dist/scss/plugins/sortable.scss +++ b/dist/scss/plugins/sortable.scss @@ -9,8 +9,15 @@ $placeholder-border: 1px dashed $placeholder-border-color; margin-left: 5px; } - .dragged { + .dragging { + position: fixed; opacity: .5; + z-index: 100; + + &::before, + &::after { + display: none; + } } .rule-placeholder { 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 740d901c..180652b8 100644 --- a/examples/index.html +++ b/examples/index.html @@ -1,71 +1,70 @@ - + jQuery QueryBuilder Example - - - - - - - - - - + + + + + + +
    -
    +
    - -
    - - + +
    - + + + + + + + + + + + + + + + + + + + + + + + +
    @@ -73,7 +72,9 @@

    jQuery QueryBuilder Example

    - +
    @@ -97,465 +98,558 @@

    Output

    - - - - - - - - - - - - + + + + + + + + + + + + - diff --git a/package.json b/package.json index ce6c6227..ed1b750a 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,40 @@ { "name": "jQuery-QueryBuilder", - "version": "2.3.3", + "version": "3.0.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": { + "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", @@ -37,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" @@ -46,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 a5bc2d01..bcb7c912 100644 --- a/src/core.js +++ b/src/core.js @@ -1,73 +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, - updating_value: false - }; - - // "allow_groups" can be boolean or int - if (this.settings.allow_groups === false) { - this.settings.allow_groups = 0; - } - else if (this.settings.allow_groups === true) { - this.settings.allow_groups = -1; - } - - // 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 { @@ -77,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) { @@ -103,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); @@ -137,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); } }); @@ -165,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)); }); } } @@ -179,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) { @@ -232,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)); }); } @@ -289,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(); }, @@ -302,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); @@ -313,7 +294,7 @@ QueryBuilder.prototype.bindEvents = function() { if (node instanceof Rule) { switch (field) { case 'error': - self.displayError(node); + self.updateError(node); break; case 'flags': @@ -321,7 +302,7 @@ QueryBuilder.prototype.bindEvents = function() { break; case 'filter': - self.updateRuleFilter(node); + self.updateRuleFilter(node, oldValue); break; case 'operator': @@ -329,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': @@ -344,7 +325,7 @@ QueryBuilder.prototype.bindEvents = function() { break; case 'condition': - self.updateGroupCondition(node); + self.updateGroupCondition(node, oldValue); break; } } @@ -353,17 +334,18 @@ 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); @@ -371,11 +353,10 @@ QueryBuilder.prototype.setRoot = function(addRule, data, flags) { 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); - this.model.root.condition = this.settings.default_condition; - if (addRule) { this.addRule(this.model.root); } @@ -384,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; @@ -407,10 +398,22 @@ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) { 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); - model.condition = this.settings.default_condition; + /** + * After any change in the rules + * @event rulesChanged + * @memberof QueryBuilder + */ + this.trigger('rulesChanged'); if (addRule) { this.addRule(model); @@ -420,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; @@ -437,93 +448,147 @@ 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.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) { - model.filter = this.getFilterById(this.settings.default_filter || this.filters[0].id); + /** + * 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; }; /** - * 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; @@ -531,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 for a rule and init the rule operator - * @param rule {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(Selectors.operator_container).empty(); + var $operatorContainer = rule.$el.find(QueryBuilder.selectors.operator_container).empty(); if (!rule.filter) { return; } var operators = this.getOperators(rule.filter); - var $operatorSelect = $(this.getRuleOperatorSelect(rule, operators)); + var $operatorSelect = $($.parseHTML(this.getRuleOperatorSelect(rule, operators))); $operatorContainer.html($operatorSelect); // set the operator without triggering update event - rule.__.operator = operators[0]; + 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); }; /** - * Create the main input for a rule - * @param rule {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(Selectors.value_container).empty(); + var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container).empty(); rule.__.value = undefined; @@ -589,56 +700,85 @@ QueryBuilder.prototype.createRuleInput = function(rule) { var filter = rule.filter; for (var i = 0; i < rule.operator.nb_inputs; i++) { - var $ruleInput = $(this.getRuleInput(rule, 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.show(); + $valueContainer.css('display', ''); $inputs.on('change ' + (filter.input_event || ''), function() { - self.status.updating_value = true; - rule.value = self.getRuleValue(rule); - self.status.updating_value = false; + 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 { - self.status.updating_value = true; - rule.value = self.getRuleValue(rule); - self.status.updating_value = false; + rule._updating_value = true; + rule.value = self.getRuleInputValue(rule); + rule._updating_value = false; } + + this.applyRuleFlags(rule); }; /** - * Perform action when rule's filter is changed - * @param rule {Rule} + * Performs action when a rule's filter changes + * @param {Rule} rule + * @param {object} previousFilter + * @fires QueryBuilder.afterUpdateRuleFilter + * @private */ -QueryBuilder.prototype.updateRuleFilter = function(rule) { +QueryBuilder.prototype.updateRuleFilter = function(rule, previousFilter) { this.createRuleOperators(rule); this.createRuleInput(rule); - rule.$el.find(Selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1'); + rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1'); - this.trigger('afterUpdateRuleFilter', rule); + // 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'); }; /** - * Update main visibility when rule operator changes - * @param rule {Rule} - * @param previousOperator {object} + * 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(Selectors.value_container); + var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container); if (!rule.operator || rule.operator.nb_inputs === 0) { $valueContainer.hide(); @@ -646,66 +786,99 @@ QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) { rule.__.value = undefined; } else { - $valueContainer.show(); + $valueContainer.css('display', ''); - if ($valueContainer.is(':empty') || rule.operator.nb_inputs !== previousOperator.nb_inputs) { + 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(Selectors.rule_operator).val(rule.operator.type); + 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); } - this.trigger('afterUpdateRuleOperator', 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'); }; /** - * Perform action when rule's value is changed - * @param rule {Rule} + * Performs actions when rule's value changes + * @param {Rule} rule + * @param {object} previousValue + * @fires QueryBuilder.afterUpdateRuleValue + * @private */ -QueryBuilder.prototype.updateRuleValue = function(rule) { - if (!this.status.updating_value) { - this.setRuleValue(rule, rule.value); +QueryBuilder.prototype.updateRuleValue = function(rule, previousValue) { + if (!rule._updating_value) { + this.setRuleInputValue(rule, rule.value); } - this.trigger('afterUpdateRuleValue', rule); + /** + * After the rule value has been modified + * @event afterUpdateRuleValue + * @memberof QueryBuilder + * @param {Rule} rule + * @param {*} previousValue + */ + this.trigger('afterUpdateRuleValue', rule, previousValue); + + this.trigger('rulesChanged'); }; /** - * Change rules properties depending on flags. - * @param rule {Rule} + * 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.filter_readonly) { - rule.$el.find(Selectors.rule_filter).prop('disabled', true); - } - if (flags.operator_readonly) { - rule.$el.find(Selectors.rule_operator).prop('disabled', true); - } - if (flags.value_readonly) { - rule.$el.find(Selectors.rule_value).prop('disabled', true); - } 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); }; /** - * Change group properties depending on flags. - * @param group {Group} + * 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.condition_readonly) { - group.$el.find('>' + Selectors.group_condition).prop('disabled', true) - .parent().addClass('readonly'); - } if (flags.no_add_rule) { group.$el.find(Selectors.add_rule).remove(); } @@ -716,12 +889,18 @@ QueryBuilder.prototype.applyGroupFlags = function(group) { 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); }; /** - * Clear all errors markers - * @param node {Node,optional} default is root Group + * Clears all errors markers + * @param {Node} [node] default is root Group */ QueryBuilder.prototype.clearErrors = function(node) { node = node || this.model.root; @@ -742,38 +921,59 @@ QueryBuilder.prototype.clearErrors = function(node) { }; /** - * Add/Remove class .has-error and update error title - * @param node {Node} + * Adds/Removes error on a Rule or Group + * @param {Node} node + * @fires QueryBuilder.changer:displayError + * @private */ -QueryBuilder.prototype.displayError = function(node) { +QueryBuilder.prototype.updateError = function(node) { if (this.settings.display_errors) { if (node.error === null) { node.$el.removeClass('has-error'); } else { - // translate the text without modifying event array - var error = $.extend([], node.error, [ - this.lang.errors[node.error[0]] || node.error[0] - ]); + 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(Selectors.error_container).eq(0) - .attr('title', Utils.fmt.apply(null, error)); + .find(QueryBuilder.selectors.error_container).eq(0) + .attr('title', errorMessage); } } }; /** - * Trigger a validation error event - * @param node {Node} - * @param error {array} - * @param value {mixed} + * 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; diff --git a/src/data.js b/src/data.js index 2f1752dc..7f466940 100644 --- a/src/data.js +++ b/src/data.js @@ -1,10 +1,9 @@ -/*jshint loopfunc:true */ - /** - * Check if a value is correct for a filter - * @param rule {Rule} - * @param value {string|string[]|undefined} - * @return {array|true} + * 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 || {}; @@ -14,180 +13,206 @@ QueryBuilder.prototype.validateValue = function(rule, value) { result = validation.callback.call(this, value, rule); } else { - result = this.validateValueInternal(rule, value); + 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 - * @param rule {Rule} - * @param value {string|string[]|undefined} - * @return {array|true} + * @private */ -QueryBuilder.prototype.validateValueInternal = function(rule, value) { +QueryBuilder.prototype._validateValue = function(rule, value) { var filter = rule.filter; var operator = rule.operator; var validation = filter.validation || {}; var result = true; - var tmp; + var tmp, tempValue; if (rule.operator.nb_inputs === 1) { value = [value]; } - else { - 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) { - result = ['radio_empty']; + 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) { - result = ['checkbox_empty']; - break; - } - else if (!operator.multiple && value[i].length > 1) { - result = ['operator_not_multiple', operator.type]; + if (!validation.allow_empty_value) { + result = ['checkbox_empty']; + } break; } break; case 'select': - if (filter.multiple) { - if (value[i] === undefined || value[i].length === 0 || (filter.placeholder && value[i] == filter.placeholder_value)) { - result = ['select_empty']; - break; - } - else if (!operator.multiple && value[i].length > 1) { - result = ['operator_not_multiple', operator.type]; - break; - } - } - else { - if (value[i] === undefined || (filter.placeholder && value[i] == filter.placeholder_value)) { + 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; } break; default: - switch (QueryBuilder.types[filter.type]) { - case 'string': - if (value[i] === undefined || value[i].length === 0) { - result = ['string_empty']; - break; - } - if (validation.min !== undefined) { - if (value[i].length < parseInt(validation.min)) { - result = ['string_exceed_min_length', validation.min]; + 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.max !== undefined) { - if (value[i].length > parseInt(validation.max)) { - result = ['string_exceed_max_length', validation.max]; - 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.format) { - if (typeof validation.format == 'string') { - validation.format = new RegExp(validation.format); + 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.test(value[i])) { - result = ['string_invalid_format', validation.format]; - 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 (value[i] === undefined || isNaN(value[i])) { - result = ['number_nan']; break; - } - if (filter.type == 'integer') { - if (parseInt(value[i]) != value[i]) { - result = ['number_not_integer']; + + case 'number': + if (tempValue[j] === undefined || tempValue[j].length === 0) { + if (!validation.allow_empty_value) { + result = ['number_nan']; + } break; } - } - else { - if (parseFloat(value[i]) != value[i]) { - result = ['number_not_double']; + if (isNaN(tempValue[j])) { + result = ['number_nan']; break; } - } - if (validation.min !== undefined) { - if (value[i] < parseFloat(validation.min)) { - result = ['number_exceed_min', validation.min]; - break; + if (filter.type == 'integer') { + if (parseInt(tempValue[j]) != tempValue[j]) { + result = ['number_not_integer']; + break; + } } - } - if (validation.max !== undefined) { - if (value[i] > parseFloat(validation.max)) { - result = ['number_exceed_max', validation.max]; - break; + else { + if (parseFloat(tempValue[j]) != tempValue[j]) { + result = ['number_not_double']; + break; + } } - } - if (validation.step !== undefined && validation.step !== 'any') { - var v = (value[i] / validation.step).toPrecision(14); - if (parseInt(v) != v) { - result = ['number_wrong_step', validation.step]; - break; + if (validation.min !== undefined) { + if (tempValue[j] < parseFloat(validation.min)) { + result = [this.getValidationMessage(validation, 'min', 'number_exceed_min'), validation.min]; + break; + } } - } - break; - - case 'datetime': - if (value[i] === undefined || value[i].length === 0) { - 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'); + 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; - var datetime = moment(value[i], validation.format); - if (!datetime.isValid()) { - result = ['datetime_invalid', validation.format]; + case 'datetime': + if (tempValue[j] === undefined || tempValue[j].length === 0) { + if (!validation.allow_empty_value) { + result = ['datetime_empty']; + } break; } - else { - if (validation.min) { - if (datetime < moment(validation.min, validation.format)) { - result = ['datetime_exceed_min', validation.min]; - break; - } + + // we need MomentJS + if (validation.format) { + if (!('moment' in window)) { + Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com'); + } + + var datetime = moment(tempValue[j], validation.format); + if (!datetime.isValid()) { + result = [this.getValidationMessage(validation, 'format', 'datetime_invalid'), validation.format]; + break; } - if (validation.max) { - if (datetime > moment(validation.max, validation.format)) { - result = ['datetime_exceed_max', validation.max]; - break; + else { + if (validation.min) { + if (datetime < moment(validation.min, validation.format)) { + result = [this.getValidationMessage(validation, 'min', 'datetime_exceed_min'), validation.min]; + break; + } + } + if (validation.max) { + if (datetime > moment(validation.max, validation.format)) { + result = [this.getValidationMessage(validation, 'max', 'datetime_exceed_max'), validation.max]; + break; + } } } } - } - break; - - case 'boolean': - tmp = value[i].trim().toLowerCase(); - if (tmp !== 'true' && tmp !== 'false' && tmp !== '1' && tmp !== '0' && value[i] !== 1 && value[i] !== 0) { - result = ['boolean_not_valid']; 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; + } } } @@ -196,12 +221,36 @@ QueryBuilder.prototype.validateValueInternal = function(rule, value) { } } + 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 - * @return {string} + * @returns {string} + * @private */ QueryBuilder.prototype.nextGroupId = function() { return this.status.id + '_group_' + (this.status.group_id++); @@ -209,7 +258,8 @@ QueryBuilder.prototype.nextGroupId = function() { /** * Returns an incremented rule ID - * @return {string} + * @returns {string} + * @private */ QueryBuilder.prototype.nextRuleId = function() { return this.status.id + '_rule_' + (this.status.rule_id++); @@ -217,8 +267,9 @@ QueryBuilder.prototype.nextRuleId = function() { /** * Returns the operators for a filter - * @param filter {string|object} (filter id name or filter object) - * @return {object[]} + * @param {string|object} filter - filter id or filter object + * @returns {object[]} + * @fires QueryBuilder.changer:getOperators */ QueryBuilder.prototype.getOperators = function(filter) { if (typeof filter == 'string') { @@ -249,16 +300,25 @@ QueryBuilder.prototype.getOperators = function(filter) { }); } + /** + * 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 - * @param filterId {string} - * @return {object|null} */ -QueryBuilder.prototype.getFilterById = function(id) { +QueryBuilder.prototype.getFilterById = function(id, doThrow) { if (id == '-1') { return null; } @@ -269,16 +329,19 @@ QueryBuilder.prototype.getFilterById = function(id) { } } - Utils.error('UndefinedFilter', 'Undefined filter "{0}"', id); + Utils.error(doThrow !== false, 'UndefinedFilter', 'Undefined filter "{0}"', id); + + return null; }; /** - * Return a particular operator by its type + * Returns a particular operator by its type + * @param {string} type + * @param {boolean} [doThrow=true] + * @returns {object|null} * @throws UndefinedOperatorError - * @param type {string} - * @return {object|null} */ -QueryBuilder.prototype.getOperatorByType = function(type) { +QueryBuilder.prototype.getOperatorByType = function(type, doThrow) { if (type == '-1') { return null; } @@ -289,15 +352,19 @@ QueryBuilder.prototype.getOperatorByType = function(type) { } } - Utils.error('UndefinedOperator', 'Undefined operator "{0}"', type); + Utils.error(doThrow !== false, 'UndefinedOperator', 'Undefined operator "{0}"', type); + + return null; }; /** - * Returns rule value - * @param rule {Rule} - * @return {mixed} + * Returns rule's current input value + * @param {Rule} rule + * @returns {*} + * @fires QueryBuilder.changer:getRuleValue + * @private */ -QueryBuilder.prototype.getRuleValue = function(rule) { +QueryBuilder.prototype.getRuleInputValue = function(rule) { var filter = rule.filter; var operator = rule.operator; var value = []; @@ -306,7 +373,7 @@ QueryBuilder.prototype.getRuleValue = function(rule) { value = filter.valueGetter.call(this, rule); } else { - var $value = rule.$el.find(Selectors.value_container); + 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); @@ -343,6 +410,21 @@ QueryBuilder.prototype.getRuleValue = function(rule) { } } + 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]; } @@ -353,30 +435,42 @@ QueryBuilder.prototype.getRuleValue = function(rule) { } } + /** + * 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. - * @param rule {Rule} - * @param value {mixed} + * Sets the value of a rule's input + * @param {Rule} rule + * @param {*} value + * @private */ -QueryBuilder.prototype.setRuleValue = function(rule, value) { +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(Selectors.value_container); + var $value = rule.$el.find(QueryBuilder.selectors.value_container); if (operator.nb_inputs == 1) { value = [value]; } - else { - value = value; - } for (var i = 0; i < operator.nb_inputs; i++) { var name = Utils.escapeElementId(rule.id + '_value_' + i); @@ -396,17 +490,24 @@ QueryBuilder.prototype.setRuleValue = function(rule, value) { 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; }; /** - * Clean rule flags. - * @param rule {object} - * @return {object} + * 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); @@ -424,14 +525,23 @@ QueryBuilder.prototype.parseRuleFlags = function(rule) { $.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); }; /** - * Get a copy of flags of a rule. + * Gets a copy of flags of a rule * @param {object} flags - * @param {boolean} all - true to return all flags, false to return only changes from default + * @param {boolean} [all=false] - return all flags or only changes from default flags * @returns {object} + * @private */ QueryBuilder.prototype.getRuleFlags = function(flags, all) { if (all) { @@ -449,9 +559,11 @@ QueryBuilder.prototype.getRuleFlags = function(flags, all) { }; /** - * Clean group flags. - * @param group {object} - * @return {object} + * 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); @@ -469,14 +581,23 @@ QueryBuilder.prototype.parseGroupFlags = function(group) { $.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); }; /** - * Get a copy of flags of a group. + * Gets a copy of flags of a group * @param {object} flags - * @param {boolean} all - true to return all flags, false to return only changes from default + * @param {boolean} [all=false] - return all flags or only changes from default flags * @returns {object} + * @private */ QueryBuilder.prototype.getGroupFlags = function(flags, all) { if (all) { @@ -494,10 +615,46 @@ QueryBuilder.prototype.getGroupFlags = function(flags, all) { }; /** - * Translate a label - * @param label {string|object} - * @return string + * 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.translateLabel = function(label) { - return typeof label == 'object' ? (label[this.settings.lang_code] || label['en']) : label; +QueryBuilder.prototype.getValidationMessage = function(validation, type, def) { + return validation.messages && validation.messages[type] || def; }; diff --git a/src/defaults.js b/src/defaults.js index fffde6f5..b63b1251 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -1,5 +1,8 @@ /** * Allowed types and their internal representation + * @type {object.} + * @readonly + * @private */ QueryBuilder.types = { 'string': 'string', @@ -13,9 +16,13 @@ QueryBuilder.types = { /** * Allowed inputs + * @type {string[]} + * @readonly + * @private */ QueryBuilder.inputs = [ 'text', + 'number', 'textarea', 'radio', 'checkbox', @@ -24,6 +31,9 @@ QueryBuilder.inputs = [ /** * Runtime modifiable options with `setOptions` method + * @type {string[]} + * @readonly + * @private */ QueryBuilder.modifiable_options = [ 'display_errors', @@ -35,8 +45,10 @@ QueryBuilder.modifiable_options = [ /** * CSS selectors for common components + * @type {object.} + * @readonly */ -var Selectors = QueryBuilder.selectors = { +QueryBuilder.selectors = { group_container: '.rules-group-container', rule_container: '.rule-container', filter_container: '.rule-filter-container', @@ -64,17 +76,23 @@ var Selectors = QueryBuilder.selectors = { }; /** - * Template strings (see `template.js`) + * Template strings (see template.js) + * @type {object.} + * @readonly */ QueryBuilder.templates = {}; /** - * Localized strings (see `i18n/`) + * 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'] }, @@ -101,6 +119,8 @@ QueryBuilder.OPERATORS = { /** * Default configuration + * @type {object} + * @readonly */ QueryBuilder.DEFAULTS = { filters: [], @@ -136,7 +156,8 @@ QueryBuilder.DEFAULTS = { group: null, rule: null, filterSelect: null, - operatorSelect: null + operatorSelect: null, + ruleValueSelect: null }, lang_code: 'en', @@ -166,10 +187,10 @@ QueryBuilder.DEFAULTS = { ], icons: { - add_group: 'glyphicon glyphicon-plus-sign', - add_rule: 'glyphicon glyphicon-plus', - remove_group: 'glyphicon glyphicon-remove', - remove_rule: 'glyphicon glyphicon-remove', - error: 'glyphicon glyphicon-warning-sign' + 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' } }; diff --git a/src/i18n/az.json b/src/i18n/az.json index 3cc13816..5e12334f 100644 --- a/src/i18n/az.json +++ b/src/i18n/az.json @@ -52,6 +52,6 @@ "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": "{0} operatoru çoxlu məna daşımır" + "operator_not_multiple": "\"{1}\" operatoru çoxlu məna daşımır" } } \ No newline at end of file diff --git a/src/i18n/bg.json b/src/i18n/bg.json new file mode 100644 index 00000000..095d01cc --- /dev/null +++ b/src/i18n/bg.json @@ -0,0 +1,61 @@ +{ + "__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}\" не може да приеме множество стойности" + } +} diff --git a/src/i18n/cs.json b/src/i18n/cs.json index 522e6978..448cc97d 100644 --- a/src/i18n/cs.json +++ b/src/i18n/cs.json @@ -52,6 +52,6 @@ "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 {0} nepodporuje mnoho hodnot" + "operator_not_multiple": "Operátor \"{1}\" nepodporuje mnoho hodnot" } } \ No newline at end of file diff --git a/src/i18n/el.json b/src/i18n/el.json new file mode 100644 index 00000000..0c988473 --- /dev/null +++ b/src/i18n/el.json @@ -0,0 +1,57 @@ +{ + "__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}\" δεν μπορεί να δεχθεί πολλαπλές τιμές" + } +} \ No newline at end of file diff --git a/src/i18n/en.json b/src/i18n/en.json index b5828139..15bbb139 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -51,11 +51,13 @@ "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 {0} cannot accept multiple values" + "operator_not_multiple": "Operator \"{1}\" cannot accept multiple values" } -} \ No newline at end of file +} diff --git a/src/i18n/eo.json b/src/i18n/eo.json new file mode 100644 index 00000000..b3c9166d --- /dev/null +++ b/src/i18n/eo.json @@ -0,0 +1,63 @@ +{ + "__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" + } +} diff --git a/src/i18n/es.json b/src/i18n/es.json index 3db3e098..3374aae1 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -22,6 +22,7 @@ "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", @@ -52,6 +53,11 @@ "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}" + "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" } } diff --git a/src/i18n/fa-IR.json b/src/i18n/fa-IR.json index a6da0959..55a6e456 100644 --- a/src/i18n/fa-IR.json +++ b/src/i18n/fa-IR.json @@ -21,7 +21,8 @@ "less_or_equal": "کمتر یا مساوی با", "greater": "بزرگتر از", "greater_or_equal": "بزرگتر یا مساوی با", - "between": "مابین", + "between": "مابین", + "not_between": "مابین نباشد", "begins_with": "شروع شود با", "not_begins_with": "شروع نشود با", "contains": "شامل شود", @@ -55,6 +56,6 @@ "datetime_exceed_min": "باید بعد از {0} باشد", "datetime_exceed_max": "باید قبل از {0} باشد", "boolean_not_valid": "مقدار دودویی وارد کنید", - "operator_not_multiple": "اپراتور {0} نمی تواند چند مقدار قبول کند" + "operator_not_multiple": "اپراتور \"{1}\" نمی تواند چند مقدار قبول کند" } -} \ No newline at end of file +} diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 9d73e09d..33b27bb6 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -13,14 +13,14 @@ }, "operators": { - "equal": "égal", - "not_equal": "non égal", - "in": "dans", - "not_in": "pas dans", - "less": "inférieur", - "less_or_equal": "inférieur ou égal", - "greater": "supérieur", - "greater_or_equal": "supérieur ou égal", + "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", @@ -51,11 +51,13 @@ "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 {0} ne peut utiliser plusieurs valeurs" + "operator_not_multiple": "L'opérateur \"{1}\" ne peut utiliser plusieurs valeurs" } -} \ No newline at end of file +} diff --git a/src/i18n/he.json b/src/i18n/he.json new file mode 100644 index 00000000..d1009042 --- /dev/null +++ b/src/i18n/he.json @@ -0,0 +1,61 @@ +{ + "__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}\" לא יכול לקבל ערכים מרובים" + } +} \ No newline at end of file diff --git a/src/i18n/hu.json b/src/i18n/hu.json new file mode 100644 index 00000000..22fa5e4c --- /dev/null +++ b/src/i18n/hu.json @@ -0,0 +1,63 @@ +{ + "__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" + } +} diff --git a/src/i18n/it.json b/src/i18n/it.json index 015cf3aa..076cc172 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -1,5 +1,6 @@ { "__locale": "Italian (it)", + "__author": "davegraziosi, Giuseppe Lodi Rizzini", "add_rule": "Aggiungi regola", "add_group": "Aggiungi gruppo", @@ -20,6 +21,8 @@ "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", @@ -30,5 +33,31 @@ "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" } -} \ No newline at end of file +} diff --git a/src/i18n/lt.json b/src/i18n/lt.json new file mode 100644 index 00000000..c95c4f08 --- /dev/null +++ b/src/i18n/lt.json @@ -0,0 +1,63 @@ +{ + "__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ų" + } +} diff --git a/src/i18n/nl.json b/src/i18n/nl.json index f2390971..06199640 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -22,6 +22,7 @@ "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", @@ -54,4 +55,4 @@ "datetime_exceed_min": "Dient na {0}", "datetime_exceed_max": "Dient voor {0}" } -} \ No newline at end of file +} diff --git a/src/i18n/pl.json b/src/i18n/pl.json index 09060e10..9cddba15 100644 --- a/src/i18n/pl.json +++ b/src/i18n/pl.json @@ -8,8 +8,8 @@ "delete_group": "Usuń", "conditions": { - "AND": "AND", - "OR": "OR" + "AND": "ORAZ", + "OR": "LUB" }, "operators": { @@ -56,6 +56,6 @@ "datetime_exceed_min": "Musi być po {0}", "datetime_exceed_max": "Musi być przed {0}", "boolean_not_valid": "Niepoprawna wartość logiczna", - "operator_not_multiple": "Operator {0} nie przyjmuje wielu wartości" + "operator_not_multiple": "Operator \"{1}\" nie przyjmuje wielu wartości" } } \ No newline at end of file diff --git a/src/i18n/pt-BR.json b/src/i18n/pt-BR.json index 36c4306e..f822aa83 100644 --- a/src/i18n/pt-BR.json +++ b/src/i18n/pt-BR.json @@ -1,9 +1,9 @@ { "__locale": "Brazilian Portuguese (pr-BR)", - "__author": "Leandro Gehlen, leandrogehlen@gmail.com", + "__author": "Leandro Gehlen, leandrogehlen@gmail.com; Marcos Ferretti, marcosvferretti@gmail.com", "add_rule": "Nova Regra", - "add_group": "Novo Gruop", + "add_group": "Novo Grupo", "delete_rule": "Excluir", "delete_group": "Excluir", @@ -21,7 +21,8 @@ "less_or_equal": "Menor ou igual", "greater": "Maior", "greater_or_equal": "Maior ou igual", - "between": "entre", + "between": "Entre", + "not_between": "Não entre", "begins_with": "Iniciando com", "not_begins_with": "Não iniciando com", "contains": "Contém", @@ -42,7 +43,7 @@ "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 conterm mais de {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", @@ -52,6 +53,9 @@ "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_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" } } \ No newline at end of file diff --git a/src/i18n/ro.json b/src/i18n/ro.json index a0e1fc06..b2a6eae3 100644 --- a/src/i18n/ro.json +++ b/src/i18n/ro.json @@ -1,6 +1,6 @@ { "__locale": "Romanian (ro)", - "__author": "ArianServ", + "__author": "ArianServ, totpero", "add_rule": "Adaugă regulă", "add_group": "Adaugă grup", @@ -17,10 +17,12 @@ "not_equal": "diferit", "in": "în", "not_in": "nu în", - "less": "mai puţin", - "less_or_equal": "mai puţin sau egal", + "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", @@ -31,5 +33,31 @@ "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" } -} \ No newline at end of file +} diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 9248db0f..9ccfefcc 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -21,6 +21,7 @@ "greater": "больше", "greater_or_equal": "больше или равно", "between": "между", + "not_between": "не между", "begins_with": "начинается с", "not_begins_with": "не начинается с", "contains": "содержит", @@ -36,10 +37,10 @@ "errors": { "no_filter": "Фильтр не выбран", "empty_group": "Группа пуста", - "radio_empty": "Не выбранно значение", - "checkbox_empty": "Не выбранно значение", - "select_empty": "Не выбранно значение", - "string_empty": "Не заполненно", + "radio_empty": "Не выбрано значение", + "checkbox_empty": "Не выбрано значение", + "select_empty": "Не выбрано значение", + "string_empty": "Не заполнено", "string_exceed_min_length": "Должен содержать больше {0} символов", "string_exceed_max_length": "Должен содержать меньше {0} символов", "string_invalid_format": "Неверный формат ({0})", @@ -49,11 +50,13 @@ "number_exceed_min": "Должно быть больше {0}", "number_exceed_max": "Должно быть меньше, чем {0}", "number_wrong_step": "Должно быть кратно {0}", - "datetime_empty": "Не заполненно", + "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": "Оператор {0} не поддерживает много значений" + "operator_not_multiple": "Оператор \"{1}\" не поддерживает много значений" } } \ No newline at end of file diff --git a/src/i18n/sk.json b/src/i18n/sk.json new file mode 100644 index 00000000..0a5926c4 --- /dev/null +++ b/src/i18n/sk.json @@ -0,0 +1,63 @@ +{ + "__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" + } +} diff --git a/src/i18n/sq.json b/src/i18n/sq.json index 508a5cdd..415244bd 100644 --- a/src/i18n/sq.json +++ b/src/i18n/sq.json @@ -55,6 +55,6 @@ "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 {0} nuk mund të pranojë vlera të shumëfishta" + "operator_not_multiple": "Operatori \"{1}\" nuk mund të pranojë vlera të shumëfishta" } } \ No newline at end of file diff --git a/src/i18n/sv.json b/src/i18n/sv.json new file mode 100644 index 00000000..a3b06e84 --- /dev/null +++ b/src/i18n/sv.json @@ -0,0 +1,63 @@ +{ + "__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" + } +} diff --git a/src/i18n/sw.json b/src/i18n/sw.json new file mode 100644 index 00000000..e215a84f --- /dev/null +++ b/src/i18n/sw.json @@ -0,0 +1,63 @@ +{ + "__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" + } +} diff --git a/src/i18n/tr.json b/src/i18n/tr.json index fe353177..25ecbf78 100644 --- a/src/i18n/tr.json +++ b/src/i18n/tr.json @@ -38,24 +38,26 @@ "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ı", + "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ğilr", + "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": "Must be a multiple of {0}", + "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 {0} birden fazla değer kabul etmiyor" + "operator_not_multiple": "Operatör \"{1}\" birden fazla değer kabul etmiyor" } } diff --git a/src/i18n/ua.json b/src/i18n/ua.json index 5276347b..2d1c5c2d 100644 --- a/src/i18n/ua.json +++ b/src/i18n/ua.json @@ -52,6 +52,6 @@ "datetime_exceed_min": "Повинне бути, після {0}", "datetime_exceed_max": "Повинне бути, до {0}", "boolean_not_valid": "Не логічне", - "operator_not_multiple": "Оператор {0} не підтримує багато значень" + "operator_not_multiple": "Оператор \"{1}\" не підтримує багато значень" } } \ No newline at end of file diff --git a/src/i18n/zh-CN.json b/src/i18n/zh-CN.json index 51ddfe39..666ca685 100644 --- a/src/i18n/zh-CN.json +++ b/src/i18n/zh-CN.json @@ -56,6 +56,6 @@ "datetime_exceed_min": "必须在{0}之后", "datetime_exceed_max": "必须在{0}之前", "boolean_not_valid": "不是布尔值", - "operator_not_multiple": "选项{0}无法接受多个值" + "operator_not_multiple": "选项\"{1}\"无法接受多个值" } } \ No newline at end of file diff --git a/src/jquery.js b/src/jquery.js index fa902998..941ea675 100644 --- a/src/jquery.js +++ b/src/jquery.js @@ -1,4 +1,24 @@ +/** + * 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'); } @@ -10,7 +30,9 @@ $.fn.queryBuilder = function(option) { return this; } if (!data) { - this.data('queryBuilder', new QueryBuilder(this, options)); + 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)); @@ -19,8 +41,37 @@ $.fn.queryBuilder = function(option) { 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; diff --git a/src/main.js b/src/main.js index db166ace..0659d288 100644 --- a/src/main.js +++ b/src/main.js @@ -1,26 +1,155 @@ -// CLASS DEFINITION -// =============================== +/** + * @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) { - this.init($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); -// EVENTS SYSTEM -// =============================== -$.extend(QueryBuilder.prototype, { - change: function(type, value) { - var event = new $.Event(type + '.queryBuilder.filter', { - builder: this, - value: value - }); + // "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; + } - this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 2)); + // 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); - return event.value; - }, + // 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(type + '.queryBuilder', { + var event = new $.Event(this._tojQueryEvent(type), { builder: this }); @@ -29,98 +158,66 @@ $.extend(QueryBuilder.prototype, { 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(type + '.queryBuilder', 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(type + '.queryBuilder', 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(type + '.queryBuilder', cb); + this.$el.one(this._tojQueryEvent(type), cb); return this; - } -}); - - -// PLUGINS SYSTEM -// =============================== -QueryBuilder.plugins = {}; - -/** - * Get or extend the default configuration - * @param options {object,optional} new configuration, leave undefined to get the default config - * @return {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); - } -}; - -/** - * Define a new plugin - * @param {string} - * @param {function} - * @param {object,optional} default configuration - */ -QueryBuilder.define = function(name, fct, def) { - QueryBuilder.plugins[name] = { - fct: fct, - def: def || {} - }; -}; - -/** - * Add new methods - * @param {object} - */ -QueryBuilder.extend = function(methods) { - $.extend(QueryBuilder.prototype, methods); -}; - -/** - * Init plugins for an instance - * @throws ConfigError - */ -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; + /** + * 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(' '); } - - 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); -}; +}); diff --git a/src/model.js b/src/model.js index f93dc8e3..a397c98f 100644 --- a/src/model.js +++ b/src/model.js @@ -1,112 +1,145 @@ -// Model CLASS -// =============================== /** - * Main object storing data model and emitting events - * --------- - * Access Node object stored in jQuery objects - * @param el {jQuery|Node} - * @return {Node} + * Main object storing data model and emitting model events + * @constructor */ -function Model(el) { - if (!(this instanceof Model)) { - return Model.getModel(el); - } - +function Model() { + /** + * @member {Group} + * @readonly + */ this.root = null; + + /** + * Base for event emitting + * @member {jQuery} + * @readonly + * @private + */ this.$ = $(this); } -$.extend(Model.prototype, { +$.extend(Model.prototype, /** @lends Model.prototype */ { + /** + * Triggers an event on the model + * @param {string} type + * @returns {$.Event} + */ trigger: function(type) { - this.$.triggerHandler(type, Array.prototype.slice.call(arguments, 1)); - return this; + 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; } }); -/** - * Access Node object stored in jQuery objects - * @param el {jQuery|Node} - * @return {Node} - */ -Model.getModel = function(el) { - if (!el) { - return null; - } - else if (el instanceof Node) { - return el; - } - else { - return $(el).data('queryBuilderModel'); - } -}; -/* - * Define Node properties with getter and setter - * Update events are emitted in the setter through root Model (if any) - */ -function defineModelProperties(obj, fields) { - fields.forEach(function(field) { - Object.defineProperty(obj.prototype, field, { - enumerable: true, - get: function() { - return this.__[field]; - }, - set: function(value) { - var oldValue = (this.__[field] !== null && typeof this.__[field] == 'object') ? - $.extend({}, this.__[field]) : - this.__[field]; - - this.__[field] = value; - - if (this.model !== null) { - this.model.trigger('update', this, field, value, oldValue); - } - } - }); - }); -} - - -// Node abstract CLASS -// =============================== /** - * @param {Node} - * @param {jQuery} + * Root abstract object + * @constructor + * @param {Node} [parent] + * @param {jQuery} $el */ var Node = function(parent, $el) { if (!(this instanceof Node)) { - return new 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; }; -defineModelProperties(Node, ['level', 'error', 'data', 'flags']); +Utils.defineModelProperties(Node, ['level', 'error', 'data', 'flags']); Object.defineProperty(Node.prototype, 'parent', { enumerable: true, @@ -121,16 +154,16 @@ Object.defineProperty(Node.prototype, 'parent', { }); /** - * Check if this Node is the root - * @return {boolean} + * Checks if this Node is the root + * @returns {boolean} */ Node.prototype.isRoot = function() { return (this.level === 1); }; /** - * Return node position inside parent - * @return {int} + * Returns the node position inside its parent + * @returns {int} */ Node.prototype.getPos = function() { if (this.isRoot()) { @@ -142,87 +175,107 @@ Node.prototype.getPos = function() { }; /** - * Delete self + * Deletes self + * @fires Model.model:drop */ Node.prototype.drop = function() { var model = this.model; - if (!this.isRoot()) { - this.parent._removeNode(this); + 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); } }; /** - * Move itself after another Node - * @param {Node} - * @return {Node} self + * Moves itself after another Node + * @param {Node} target + * @fires Model.model:move */ -Node.prototype.moveAfter = function(node) { - if (this.isRoot()) return; - - this._move(node.parent, node.getPos() + 1); - - return this; +Node.prototype.moveAfter = function(target) { + if (!this.isRoot()) { + this.move(target.parent, target.getPos() + 1); + } }; /** - * Move itself at the beginning of parent or another Group - * @param {Group,optional} - * @return {Node} self + * 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()) return; + if (!this.isRoot()) { + if (target === undefined) { + target = this.parent; + } - if (target === undefined) { - target = this.parent; + this.move(target, 0); } - - this._move(target, 0); - - return this; }; /** - * Move itself at the end of parent or another Group - * @param {Group,optional} - * @return {Node} self + * 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()) return; + if (!this.isRoot()) { + if (target === undefined) { + target = this.parent; + } - if (target === undefined) { - target = this.parent; + this.move(target, target.length() === 0 ? 0 : target.length() - 1); } - - this._move(target, target.length() === 0 ? 0 : target.length() - 1); - - return this; }; /** - * Move itself at specific position of Group - * @param {Group} - * @param {int} + * Moves itself at specific position of Group + * @param {Group} target + * @param {int} index + * @fires Model.model:move */ -Node.prototype._move = function(group, index) { - this.parent._removeNode(this); - group._appendNode(this, index, false); +Node.prototype.move = function(target, index) { + if (!this.isRoot()) { + if (typeof target === 'number') { + index = target; + target = this.parent; + } - if (this.model !== null) { - this.model.trigger('move', this, group, index); + 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 CLASS -// =============================== /** - * @param {Group} - * @param {jQuery} + * Group object + * @constructor + * @extends Node + * @param {Group} [parent] + * @param {jQuery} $el */ var Group = function(parent, $el) { if (!(this instanceof Group)) { @@ -231,17 +284,28 @@ var Group = function(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; -defineModelProperties(Group, ['condition']); +Utils.defineModelProperties(Group, ['condition']); /** - * Empty the Group + * Removes group's content */ Group.prototype.empty = function() { this.each('reverse', function(rule) { @@ -252,7 +316,7 @@ Group.prototype.empty = function() { }; /** - * Delete self + * Deletes self */ Group.prototype.drop = function() { this.empty(); @@ -260,21 +324,22 @@ Group.prototype.drop = function() { }; /** - * Return the number of children - * @return {int} + * Returns the number of children + * @returns {int} */ Group.prototype.length = function() { return this.rules.length; }; /** - * Add a Node at specified index - * @param {Node} - * @param {int,optional} - * @param {boolean,optional} - * @return {Node} the inserted node + * 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._appendNode = function(node, index, trigger) { +Group.prototype.insertNode = function(node, index, trigger) { if (index === undefined) { index = this.length(); } @@ -283,65 +348,79 @@ Group.prototype._appendNode = function(node, index, trigger) { node.parent = this; if (trigger && this.model !== null) { - this.model.trigger('add', node, index); + /** + * 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; }; /** - * Add a Group by jQuery element at specified index - * @param {jQuery} - * @param {int,optional} - * @return {Group} the inserted group + * 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._appendNode(new Group(this, $el), index, true); + return this.insertNode(new Group(this, $el), index, true); }; /** - * Add a Rule by jQuery element at specified index - * @param {jQuery} - * @param {int,optional} - * @return {Rule} the inserted rule + * 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._appendNode(new Rule(this, $el), index, true); + return this.insertNode(new Rule(this, $el), index, true); }; /** - * Delete a specific Node - * @param {Node} - * @return {Group} self + * Deletes a specific Node + * @param {Node} node */ -Group.prototype._removeNode = function(node) { +Group.prototype.removeNode = function(node) { var index = this.getNodePos(node); if (index !== -1) { node.parent = null; this.rules.splice(index, 1); } - - return this; }; /** - * Return position of a child Node - * @param {Node} - * @return {int} + * 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,optional} iterate in reverse order, required if you delete nodes - * @param {function} callback for Rules - * @param {function,optional} callback for Groups - * @return {boolean} + * @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 == 'function') { + if (typeof reverse !== 'boolean' && typeof reverse !== 'string') { context = cbGroup; cbGroup = cbRule; cbRule = reverse; @@ -352,16 +431,18 @@ Group.prototype.each = function(reverse, cbRule, cbGroup, 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 next = function() { + return reverse ? i >= l : i <= l; + }; var stop = false; - for (; next(); i+= c) { + for (; next(); i += c) { if (this.rules[i] instanceof Group) { - if (cbGroup !== undefined) { + if (!!cbGroup) { stop = cbGroup.call(context, this.rules[i]) === false; } } - else { + else if (!!cbRule) { stop = cbRule.call(context, this.rules[i]) === false; } @@ -374,21 +455,21 @@ Group.prototype.each = function(reverse, cbRule, cbGroup, context) { }; /** - * Return true if the group contains a particular Node - * @param {Node} - * @param {boolean,optional} recursive search - * @return {boolean} + * Checks if the group contains a particular Node + * @param {Node} node + * @param {boolean} [recursive=false] + * @returns {boolean} */ -Group.prototype.contains = function(node, deep) { +Group.prototype.contains = function(node, recursive) { if (this.getNodePos(node) !== -1) { return true; } - else if (!deep) { + else if (!recursive) { return false; } else { // the loop will return with false as soon as the Node is found - return !this.each(function(rule) { + return !this.each(function() { return true; }, function(group) { return !group.contains(node, true); @@ -397,11 +478,12 @@ Group.prototype.contains = function(node, deep) { }; -// RULE CLASS -// =============================== /** - * @param {Group} - * @param {jQuery} + * Rule object + * @constructor + * @extends Node + * @param {Group} parent + * @param {jQuery} $el */ var Rule = function(parent, $el) { if (!(this instanceof Rule)) { @@ -410,19 +492,58 @@ var Rule = function(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; - this.__.flags = {}; + + /** + * @name value + * @member {*} + * @memberof Rule + * @instance + */ this.__.value = undefined; }; Rule.prototype = Object.create(Node.prototype); Rule.prototype.constructor = Rule; -defineModelProperties(Rule, ['filter', 'operator', 'value']); +Utils.defineModelProperties(Rule, ['filter', 'operator', 'value']); + +/** + * Checks if this Node is the root + * @returns {boolean} always false + */ +Rule.prototype.isRoot = function() { + return false; +}; -// EXPORT -// =============================== +/** + * @member {function} + * @memberof QueryBuilder + * @see Group + */ QueryBuilder.Group = Group; + +/** + * @member {function} + * @memberof QueryBuilder + * @see Rule + */ QueryBuilder.Rule = Rule; diff --git a/src/plugins.js b/src/plugins.js new file mode 100644 index 00000000..036f9cc4 --- /dev/null +++ b/src/plugins.js @@ -0,0 +1,114 @@ +/** + * @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); + } +}; diff --git a/src/plugins/bt-checkbox/plugin.js b/src/plugins/bt-checkbox/plugin.js index a35abd0e..26b0998f 100644 --- a/src/plugins/bt-checkbox/plugin.js +++ b/src/plugins/bt-checkbox/plugin.js @@ -1,22 +1,14 @@ -/*! - * jQuery QueryBuilder Awesome Bootstrap Checkbox - * Applies Awesome Bootstrap Checkbox for checkbox and radio inputs. +/** + * @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 == 'glyphicons') { - var injectCSS = document.createElement('style'); - injectCSS.innerHTML = '\ -.checkbox input[type=checkbox]:checked + label:after { \ - font-family: "Glyphicons Halflings"; \ - content: "\\e013"; \ -} \ -.checkbox label:after { \ - padding-left: 4px; \ - padding-top: 2px; \ - font-size: 9px; \ -}'; - document.body.appendChild(injectCSS); + if (options.font === 'bootstrap-icons') { + this.$el.addClass('bt-checkbox-bootstrap-icons'); } this.on('getRuleInput.filter', function(h, rule, name) { @@ -39,15 +31,11 @@ QueryBuilder.define('bt-checkbox', function(options) { var color = filter.colors[key] || filter.colors._def_ || options.color; var id = name + '_' + (i++); - h.value+= '\ - \ - \ - \ -
    '; + h.value += `
    `; }); } }); }, { - font: 'glyphicons', + font: 'bootstrap-icons', color: 'default' }); diff --git a/src/plugins/bt-checkbox/plugin.scss b/src/plugins/bt-checkbox/plugin.scss new file mode 100644 index 00000000..22e21eed --- /dev/null +++ b/src/plugins/bt-checkbox/plugin.scss @@ -0,0 +1,10 @@ +.query-builder.bt-checkbox-bootstrap-icons { + .checkbox input[type='checkbox'] + label::before { + outline: 0; + } + + .checkbox input[type='checkbox']:checked + label::after { + font-family: 'bootstrap-icons'; + content: '\F633'; // https://icons.getbootstrap.com/icons/check-lg/ + } +} diff --git a/src/plugins/bt-selectpicker/plugin.js b/src/plugins/bt-selectpicker/plugin.js deleted file mode 100644 index e4ad3be6..00000000 --- a/src/plugins/bt-selectpicker/plugin.js +++ /dev/null @@ -1,36 +0,0 @@ -/*! - * jQuery QueryBuilder Bootstrap Selectpicker - * Applies Bootstrap Select on filters and operators combo-boxes. - */ - -/** - * @throws ConfigError - */ -QueryBuilder.define('bt-selectpicker', function(options) { - if (!$.fn.selectpicker || !$.fn.selectpicker.Constructor) { - Utils.error('MissingLibrary', 'Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select'); - } - - // init selectpicker - this.on('afterCreateRuleFilters', function(e, rule) { - rule.$el.find(Selectors.rule_filter).removeClass('form-control').selectpicker(options); - }); - - this.on('afterCreateRuleOperators', function(e, rule) { - rule.$el.find(Selectors.rule_operator).removeClass('form-control').selectpicker(options); - }); - - // update selectpicker on change - this.on('afterUpdateRuleFilter', function(e, rule) { - rule.$el.find(Selectors.rule_filter).selectpicker('render'); - }); - - this.on('afterUpdateRuleOperator', function(e, rule) { - rule.$el.find(Selectors.rule_operator).selectpicker('render'); - }); -}, { - container: 'body', - style: 'btn-inverse btn-xs', - width: 'auto', - showIcon: false -}); diff --git a/src/plugins/bt-tooltip-errors/plugin.js b/src/plugins/bt-tooltip-errors/plugin.js index 0d97c0b9..52f4830d 100644 --- a/src/plugins/bt-tooltip-errors/plugin.js +++ b/src/plugins/bt-tooltip-errors/plugin.js @@ -1,32 +1,31 @@ -/*! - * jQuery QueryBuilder Bootstrap Tooltip errors - * Applies Bootstrap Tooltips on validation error messages. - */ - /** - * @throws ConfigError + * @class BtTooltipErrors + * @memberof module:plugins + * @description Applies Bootstrap Tooltips on validation error messages. + * @param {object} [options] + * @param {string} [options.placement='right'] + * @throws MissingLibraryError */ QueryBuilder.define('bt-tooltip-errors', function(options) { - if (!$.fn.tooltip || !$.fn.tooltip.Constructor || !$.fn.tooltip.Constructor.prototype.fixTitle) { - Utils.error('MissingLibrary', 'Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com'); + 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 = $(h.value); - $h.find(Selectors.error_container).attr('data-toggle', 'tooltip'); + 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(Selectors.error_container).eq(0) - .tooltip(options) - .tooltip('hide') - .tooltip('fixTitle'); + node.$el.find(QueryBuilder.selectors.error_container).eq(0) + .attr('data-bs-original-title',options).attr('data-bs-title',options).tooltip(); } }); }, { diff --git a/src/plugins/change-filters/plugin.js b/src/plugins/change-filters/plugin.js index a4e65069..f44934ab 100644 --- a/src/plugins/change-filters/plugin.js +++ b/src/plugins/change-filters/plugin.js @@ -1,24 +1,35 @@ -/*! - * jQuery QueryBuilder Change Filters - * Allows to change available filters after plugin initialization. +/** + * @class ChangeFilters + * @memberof module:plugins + * @description Allows to change available filters after plugin initialization. */ -QueryBuilder.extend({ +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 - * @param {boolean,optional} delete rules using old filters - * @param {object[]} new filters */ - setFilters: function(delete_orphans, filters) { + setFilters: function(deleteOrphans, filters) { var self = this; if (filters === undefined) { - filters = delete_orphans; - delete_orphans = false; + 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) { @@ -26,7 +37,7 @@ QueryBuilder.extend({ }); // check for orphans - if (!delete_orphans) { + if (!deleteOrphans) { (function checkOrphans(node) { node.each( function(rule) { @@ -45,17 +56,20 @@ QueryBuilder.extend({ // apply on existing DOM (function updateBuilder(node) { node.each(true, - function(rule) { - if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) { - rule.drop(); - } - else { - self.createRuleFilters(rule); - - rule.$el.find(Selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1'); - } - }, - updateBuilder + 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)); @@ -65,7 +79,7 @@ QueryBuilder.extend({ this.updateDisabledFilters(); } if (this.settings.plugins['bt-selectpicker']) { - this.$el.find(Selectors.rule_filter).selectpicker('render'); + this.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render'); } } @@ -79,15 +93,24 @@ QueryBuilder.extend({ } } + /** + * 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 {object|object[]} the new filter - * @param {mixed,optional} numeric index or '#start' or '#end' + * @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(new_filters, position) { + addFilter: function(newFilters, position) { if (position === undefined || position == '#end') { position = this.filters.length; } @@ -95,29 +118,30 @@ QueryBuilder.extend({ position = 0; } - if (!$.isArray(new_filters)) { - new_filters = [new_filters]; + 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(new_filters)); + 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(new_filters)); + 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, new_filters); + Array.prototype.push.apply(filters, newFilters); } } @@ -126,19 +150,22 @@ QueryBuilder.extend({ /** * Removes a filter from the builder - * @param {string|string[]} the filter id - * @param {boolean,optional} delete rules using old filters + * @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(filter_ids, delete_orphans) { + removeFilter: function(filterIds, deleteOrphans) { var filters = $.extend(true, [], this.filters); - if (typeof filter_ids === 'string') { - filter_ids = [filter_ids]; + if (typeof filterIds === 'string') { + filterIds = [filterIds]; } filters = filters.filter(function(filter) { - return filter_ids.indexOf(filter.id) === -1; + return filterIds.indexOf(filter.id) === -1; }); - this.setFilters(delete_orphans, filters); + this.setFilters(deleteOrphans, filters); } }); diff --git a/src/plugins/chosen-selectpicker/plugin.js b/src/plugins/chosen-selectpicker/plugin.js new file mode 100644 index 00000000..e18c8573 --- /dev/null +++ b/src/plugins/chosen-selectpicker/plugin.js @@ -0,0 +1,44 @@ +/** + * @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'); + }); +}); diff --git a/src/plugins/filter-description/plugin.js b/src/plugins/filter-description/plugin.js index 868ac9db..3027e761 100644 --- a/src/plugins/filter-description/plugin.js +++ b/src/plugins/filter-description/plugin.js @@ -1,73 +1,70 @@ -/*! - * jQuery QueryBuilder Filter Description - * Provides three ways to display a description about a filter: inline, Bootsrap Popover or Bootbox. - */ - /** + * @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 - */ + // INLINE if (options.mode === 'inline') { - this.on('afterUpdateRuleFilter', function(e, rule) { + this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) { var $p = rule.$el.find('p.filter-description'); + var description = e.builder.getFilterDescription(rule.filter, rule); - if (!rule.filter || !rule.filter.description) { + if (!description) { $p.hide(); } else { if ($p.length === 0) { - $p = $('

    '); + $p = $($.parseHTML('

    ')); $p.appendTo(rule.$el); } else { - $p.show(); + $p.css('display', ''); } - $p.html(' ' + rule.filter.description); + $p.html(' ' + description); } }); } - /** - * POPOVER - */ + // 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', function(e, rule) { + this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) { var $b = rule.$el.find('button.filter-description'); + var description = e.builder.getFilterDescription(rule.filter, rule); - if (!rule.filter || !rule.filter.description) { + if (!description) { $b.hide(); - if ($b.data('bs.popover')) { + if ($b.data('bs-popover')) { $b.popover('hide'); } } else { if ($b.length === 0) { - $b = $(''); - $b.prependTo(rule.$el.find(Selectors.rule_actions)); - - $b.popover({ + $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() { - $b.popover('hide'); + popover('hide'); }); } else { - $b.show(); + $b.css('display', ''); } - $b.data('bs.popover').options.content = rule.filter.description; + $b.data('bs-popover').options.content = description; if ($b.attr('aria-describedby')) { $b.popover('show'); @@ -75,35 +72,58 @@ QueryBuilder.define('filter-description', function(options) { } }); } - /** - * BOOTBOX - */ + // 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', function(e, rule) { + this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) { var $b = rule.$el.find('button.filter-description'); + var description = e.builder.getFilterDescription(rule.filter, rule); - if (!rule.filter || !rule.filter.description) { + if (!description) { $b.hide(); } else { if ($b.length === 0) { - $b = $(''); - $b.prependTo(rule.$el.find(Selectors.rule_actions)); + $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', rule.filter.description); + $b.data('description', description); } }); } }, { - icon: 'glyphicon glyphicon-info-sign', + 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; + } + } +}); diff --git a/src/plugins/filter-description/plugin.scss b/src/plugins/filter-description/plugin.scss index 80a2af71..41498718 100644 --- a/src/plugins/filter-description/plugin.scss +++ b/src/plugins/filter-description/plugin.scss @@ -16,6 +16,6 @@ $description-border: 1px solid $description-border-color; border: $description-border; color: $description-text-color; border-radius: $item-border-radius; - padding: #{$rule-padding / 2} $rule-padding; + padding: #{$rule-padding * .5} $rule-padding; font-size: .8em; } diff --git a/src/plugins/invert/i18n/el.json b/src/plugins/invert/i18n/el.json new file mode 100644 index 00000000..cc9db1fd --- /dev/null +++ b/src/plugins/invert/i18n/el.json @@ -0,0 +1,3 @@ +{ + "invert": "Εναλλαγή" +} \ No newline at end of file diff --git a/src/plugins/invert/i18n/eo.json b/src/plugins/invert/i18n/eo.json new file mode 100644 index 00000000..e5ddde54 --- /dev/null +++ b/src/plugins/invert/i18n/eo.json @@ -0,0 +1,3 @@ +{ + "invert": "Inversigi" +} diff --git a/src/plugins/invert/i18n/he.json b/src/plugins/invert/i18n/he.json new file mode 100644 index 00000000..c279ee3d --- /dev/null +++ b/src/plugins/invert/i18n/he.json @@ -0,0 +1,3 @@ +{ + "invert": "הפוך שאילתא" +} diff --git a/src/plugins/invert/i18n/hu.json b/src/plugins/invert/i18n/hu.json new file mode 100644 index 00000000..83b08887 --- /dev/null +++ b/src/plugins/invert/i18n/hu.json @@ -0,0 +1,3 @@ +{ + "invert": "Megfordítás (Invertálás)" +} diff --git a/src/plugins/invert/i18n/lt.json b/src/plugins/invert/i18n/lt.json new file mode 100644 index 00000000..6913901d --- /dev/null +++ b/src/plugins/invert/i18n/lt.json @@ -0,0 +1,3 @@ +{ + "invert": "Invertuoti" +} diff --git a/src/plugins/invert/i18n/pt-BR.json b/src/plugins/invert/i18n/pt-BR.json new file mode 100644 index 00000000..24761e72 --- /dev/null +++ b/src/plugins/invert/i18n/pt-BR.json @@ -0,0 +1,3 @@ +{ + "invert": "Inverter" +} \ No newline at end of file diff --git a/src/plugins/invert/i18n/sk.json b/src/plugins/invert/i18n/sk.json new file mode 100644 index 00000000..b9d3274b --- /dev/null +++ b/src/plugins/invert/i18n/sk.json @@ -0,0 +1,3 @@ +{ + "invert": "Invertný" +} diff --git a/src/plugins/invert/i18n/sv.json b/src/plugins/invert/i18n/sv.json new file mode 100644 index 00000000..aa932882 --- /dev/null +++ b/src/plugins/invert/i18n/sv.json @@ -0,0 +1,3 @@ +{ + "invert": "Invertera" +} diff --git a/src/plugins/invert/i18n/sw.json b/src/plugins/invert/i18n/sw.json new file mode 100644 index 00000000..7ee2e3fd --- /dev/null +++ b/src/plugins/invert/i18n/sw.json @@ -0,0 +1,3 @@ +{ + "invert": "Pindua" +} diff --git a/src/plugins/invert/plugin.js b/src/plugins/invert/plugin.js index a4818e3e..8b0ab7ef 100644 --- a/src/plugins/invert/plugin.js +++ b/src/plugins/invert/plugin.js @@ -1,7 +1,65 @@ -/*! - * jQuery QueryBuilder Invert - * Allows to invert a rule operator, a group condition or the entire builder. +/** + * @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: { @@ -33,56 +91,13 @@ QueryBuilder.defaults({ } }); -QueryBuilder.define('invert', function(options) { - var self = this; - - /** - * Bind events - */ - this.on('afterInit', function() { - self.$el.on('click.queryBuilder', '[data-invert=group]', function() { - var $group = $(this).closest(Selectors.group_container); - self.invert(Model($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(Model($rule), options); - }); - } - }); - - /** - * Modify templates - */ - this.on('getGroupTemplate.filter', function(h, level) { - var $h = $(h.value); - $h.find(Selectors.condition_container).after(''); - h.value = $h.prop('outerHTML'); - }); - - if (options.display_rules_button && options.invert_rules) { - this.on('getRuleTemplate.filter', function(h) { - var $h = $(h.value); - $h.find(Selectors.rule_actions).prepend(''); - h.value = $h.prop('outerHTML'); - }); - } -}, { - icon: 'glyphicon glyphicon-random', - recursive: true, - invert_rules: true, - display_rules_button: false, - silent_fail: false -}); - -QueryBuilder.extend({ +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 - * @param {Node,optional} - * @param {object,optional} */ invert: function(node, options) { if (!(node instanceof Node)) { @@ -135,7 +150,16 @@ QueryBuilder.extend({ } 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'); } } }); diff --git a/src/plugins/mongodb-support/plugin.js b/src/plugins/mongodb-support/plugin.js index 1344badc..5df30db4 100644 --- a/src/plugins/mongodb-support/plugin.js +++ b/src/plugins/mongodb-support/plugin.js @@ -1,12 +1,12 @@ -/*! - * jQuery QueryBuilder MongoDB Support - * Allows to export rules as a MongoDB find object as well as populating the builder from a MongoDB object. +/** + * @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. */ -// DEFAULT CONFIG -// =============================== 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 }; }, @@ -27,20 +27,21 @@ QueryBuilder.defaults({ is_not_empty: function(v) { return { '$ne': '' }; }, is_null: function(v) { return null; }, is_not_null: function(v) { return { '$ne': null }; } + // @formatter:on }, mongoRuleOperators: { - $ne: function(v) { - v = v.$ne; + $eq: function(v) { return { 'val': v, - 'op': v === null ? 'is_not_null' : (v === '' ? 'is_not_empty' : 'not_equal') + 'op': v === null ? 'is_null' : (v === '' ? 'is_empty' : 'equal') }; }, - eq: function(v) { + $ne: function(v) { + v = v.$ne; return { 'val': v, - 'op': v === null ? 'is_null' : (v === '' ? 'is_empty' : 'equal') + 'op': v === null ? 'is_not_null' : (v === '' ? 'is_not_empty' : 'not_equal') }; }, $regex: function(v) { @@ -64,54 +65,73 @@ QueryBuilder.defaults({ return { 'val': v, 'op': 'contains' }; } }, - between: function(v) { return { 'val': [v.$gte, v.$lte], 'op': 'between' }; }, - not_between: function(v) { return { 'val': [v.$lt, v.$gt], 'op': 'not_between' }; }, - $in: function(v) { return { 'val': v.$in, 'op': 'in' }; }, - $nin: function(v) { return { 'val': v.$nin, 'op': 'not_in' }; }, - $lt: function(v) { return { 'val': v.$lt, 'op': 'less' }; }, - $lte: function(v) { return { 'val': v.$lte, 'op': 'less_or_equal' }; }, - $gt: function(v) { return { 'val': v.$gt, 'op': 'greater' }; }, - $gte: function(v) { return { 'val': v.$gte, 'op': 'greater_or_equal' }; } + between: function(v) { + return { 'val': [v.$gte, v.$lte], 'op': 'between' }; + }, + not_between: function(v) { + return { 'val': [v.$lt, v.$gt], 'op': 'not_between' }; + }, + $in: function(v) { + return { 'val': v.$in, 'op': 'in' }; + }, + $nin: function(v) { + return { 'val': v.$nin, 'op': 'not_in' }; + }, + $lt: function(v) { + return { 'val': v.$lt, 'op': 'less' }; + }, + $lte: function(v) { + return { 'val': v.$lte, 'op': 'less_or_equal' }; + }, + $gt: function(v) { + return { 'val': v.$gt, 'op': 'greater' }; + }, + $gte: function(v) { + return { 'val': v.$gte, 'op': 'greater_or_equal' }; + } } }); - -// PUBLIC METHODS -// =============================== -QueryBuilder.extend({ +QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ { /** - * Get rules as MongoDB query + * Returns rules as a MongoDB query + * @param {object} [data] - current rules by default + * @returns {object} + * @fires module:plugins.MongoDbSupport.changer:getMongoDBField + * @fires module:plugins.MongoDbSupport.changer:ruleToMongo + * @fires module:plugins.MongoDbSupport.changer:groupToMongo * @throws UndefinedMongoConditionError, UndefinedMongoOperatorError - * @param data {object} (optional) rules - * @return {object} */ getMongo: function(data) { data = (data === undefined) ? this.getRules() : data; + if (!data) { + return null; + } + var self = this; - return (function parse(data) { - if (!data.condition) { - data.condition = self.settings.default_condition; + return (function parse(group) { + if (!group.condition) { + group.condition = self.settings.default_condition; } - if (['AND', 'OR'].indexOf(data.condition.toUpperCase()) === -1) { - Utils.error('UndefinedMongoCondition', 'Unable to build MongoDB query with condition "{0}"', data.condition); + if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) { + Utils.error('UndefinedMongoCondition', 'Unable to build MongoDB query with condition "{0}"', group.condition); } - if (!data.rules) { + if (!group.rules) { return {}; } var parts = []; - data.rules.forEach(function(rule) { + group.rules.forEach(function(rule) { if (rule.rules && rule.rules.length > 0) { parts.push(parse(rule)); } else { var mdb = self.settings.mongoOperators[rule.operator]; var ope = self.getOperatorByType(rule.operator); - var values = []; if (mdb === undefined) { Utils.error('UndefinedMongoOperator', 'Unknown MongoDB operation for operator "{0}"', rule.operator); @@ -121,67 +141,123 @@ QueryBuilder.extend({ if (!(rule.value instanceof Array)) { rule.value = [rule.value]; } - - rule.value.forEach(function(v) { - values.push(Utils.changeType(v, rule.type, false)); - }); } - var part = {}; - part[rule.field] = mdb.call(self, values); - parts.push(part); + /** + * 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 res = {}; - if (parts.length > 0) { - res['$' + data.condition.toLowerCase()] = parts; - } - return res; + 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)); }, /** - * Convert MongoDB object to rules + * 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 - * @param data {object} query object - * @return {object} */ - getRulesFromMongo: function(data) { - if (data === undefined || data === null) { + getRulesFromMongo: function(query) { + if (query === undefined || query === null) { return null; } var self = this; - var conditions = { - '$and': 'AND', - '$or': 'OR' - }; - return (function parse(data) { - var topKeys = Object.keys(data); + /** + * 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); - if (topKeys.length > 1) { - Utils.error('MongoParse', 'Invalid MongoDB query format'); - } - if (!conditions[topKeys[0].toLowerCase()]) { - Utils.error('UndefinedMongoCondition', 'Unable to build MongoDB query with condition "{0}"', topKeys[0]); - } + // a plugin returned a group + if ('rules' in query && 'condition' in query) { + return query; + } - var rules = data[topKeys[0]]; + // 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(rule) { - var keys = Object.keys(rule); + rules.forEach(function(data) { + // allow plugins to manually parse or handle special cases + data = self.change('parseMongoNode', data); - if (conditions[keys[0].toLowerCase()]) { - parts.push(parse(rule)); + // 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 = keys[0]; - var value = rule[field]; + var field = Object.keys(data)[0]; + var value = data[field]; - var operator = determineMongoOperator(value, field); + var operator = self.getMongoOperator(value); if (operator === undefined) { Utils.error('MongoParse', 'Invalid MongoDB query format'); } @@ -192,62 +268,126 @@ QueryBuilder.extend({ } var opVal = mdbrl.call(self, value); - parts.push({ - id: self.change('getMongoDBFieldID', field, 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); } }); - var res = {}; - if (parts.length > 0) { - res.condition = conditions[topKeys[0].toLowerCase()]; - res.rules = parts; - } - return res; - }(data)); + /** + * 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)); }, /** - * Set rules from MongoDB object - * @param data {object} + * Sets rules a from MongoDB query + * @see module:plugins.MongoDbSupport.getRulesFromMongo */ - setRulesFromMongo: function(data) { - this.setRules(this.getRulesFromMongo(data)); - } -}); + setRulesFromMongo: function(query) { + this.setRules(this.getRulesFromMongo(query)); + }, -/** - * Find which operator is used in a MongoDB sub-object - * @param {mixed} value - * @param {string} field - * @return {string|undefined} - */ -function determineMongoOperator(value, field) { - if (value !== null && typeof value == 'object') { - var subkeys = Object.keys(value); + /** + * 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; + }); - if (subkeys.length === 1) { - return subkeys[0]; + var id; + if (matchingFilters.length === 1) { + id = matchingFilters[0].id; } else { - if (value.$gte !== undefined && value.$lte !== undefined) { + /** + * 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 (value.$lt !== undefined && value.$gt !== undefined) { + if (data.$lt !== undefined && data.$gt !== undefined) { return 'not_between'; } - else if (value.$regex !== undefined) { // optional $options - return '$regex'; + + var knownKeys = Object.keys(data).filter(function(key) { + return !!this.settings.mongoRuleOperators[key]; + }.bind(this)); + + if (knownKeys.length === 1) { + return knownKeys[0]; } - else { - return; + } + 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]; } } } - else { - return 'eq'; - } -} +}); diff --git a/src/plugins/not-group/i18n/en.json b/src/plugins/not-group/i18n/en.json new file mode 100644 index 00000000..2894c4da --- /dev/null +++ b/src/plugins/not-group/i18n/en.json @@ -0,0 +1,3 @@ +{ + "NOT": "NOT" +} diff --git a/src/plugins/not-group/i18n/eo.json b/src/plugins/not-group/i18n/eo.json new file mode 100644 index 00000000..8025e4e8 --- /dev/null +++ b/src/plugins/not-group/i18n/eo.json @@ -0,0 +1,3 @@ +{ + "NOT": "NE" +} diff --git a/src/plugins/not-group/i18n/fr.json b/src/plugins/not-group/i18n/fr.json new file mode 100644 index 00000000..78937d3c --- /dev/null +++ b/src/plugins/not-group/i18n/fr.json @@ -0,0 +1,3 @@ +{ + "NOT": "NON" +} diff --git a/src/plugins/not-group/i18n/he.json b/src/plugins/not-group/i18n/he.json new file mode 100644 index 00000000..ac76d192 --- /dev/null +++ b/src/plugins/not-group/i18n/he.json @@ -0,0 +1,3 @@ +{ + "NOT": "לא" +} diff --git a/src/plugins/not-group/i18n/hu.json b/src/plugins/not-group/i18n/hu.json new file mode 100644 index 00000000..4399728c --- /dev/null +++ b/src/plugins/not-group/i18n/hu.json @@ -0,0 +1,3 @@ +{ + "NOT": "NEM" +} diff --git a/src/plugins/not-group/i18n/lt.json b/src/plugins/not-group/i18n/lt.json new file mode 100644 index 00000000..8025e4e8 --- /dev/null +++ b/src/plugins/not-group/i18n/lt.json @@ -0,0 +1,3 @@ +{ + "NOT": "NE" +} diff --git a/src/plugins/not-group/i18n/ru.json b/src/plugins/not-group/i18n/ru.json new file mode 100644 index 00000000..923d6069 --- /dev/null +++ b/src/plugins/not-group/i18n/ru.json @@ -0,0 +1,3 @@ +{ + "NOT": "НЕ" +} diff --git a/src/plugins/not-group/i18n/sk.json b/src/plugins/not-group/i18n/sk.json new file mode 100644 index 00000000..9da29bd5 --- /dev/null +++ b/src/plugins/not-group/i18n/sk.json @@ -0,0 +1,3 @@ +{ + "NOT": "NIE" +} diff --git a/src/plugins/not-group/i18n/sv.json b/src/plugins/not-group/i18n/sv.json new file mode 100644 index 00000000..2778b4da --- /dev/null +++ b/src/plugins/not-group/i18n/sv.json @@ -0,0 +1,3 @@ +{ + "NOT": "INTE" +} diff --git a/src/plugins/not-group/i18n/sw.json b/src/plugins/not-group/i18n/sw.json new file mode 100644 index 00000000..36f9902b --- /dev/null +++ b/src/plugins/not-group/i18n/sw.json @@ -0,0 +1,3 @@ +{ + "NOT": "SIO" +} diff --git a/src/plugins/not-group/plugin.js b/src/plugins/not-group/plugin.js new file mode 100644 index 00000000..be98bb00 --- /dev/null +++ b/src/plugins/not-group/plugin.js @@ -0,0 +1,154 @@ +/** + * @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'); + } +}); diff --git a/src/plugins/sortable/plugin.js b/src/plugins/sortable/plugin.js index ae5bcdaf..84b29e1f 100644 --- a/src/plugins/sortable/plugin.js +++ b/src/plugins/sortable/plugin.js @@ -1,171 +1,241 @@ -/*! - * jQuery QueryBuilder Sortable - * Enables drag & drop sort of rules. +/** + * @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 */ - -Selectors.rule_and_group_containers = Selectors.rule_container + ', ' + Selectors.group_container; - QueryBuilder.define('sortable', function(options) { - /** - * Init HTML5 drag and drop - */ - this.on('afterInit', function(e) { - // configure jQuery to use dataTransfer - $.event.props.push('dataTransfer'); - - var placeholder; - var src; - var self = e.builder; + if (!('interact' in window)) { + Utils.error('MissingLibrary', 'interact.js is required to use "sortable" plugin. Get it here: http://interactjs.io'); + } - // only add "draggable" attribute when hovering drag handle - // preventing text select bug in Firefox - self.$el.on('mouseover.queryBuilder', '.drag-handle', function() { - self.$el.find(Selectors.rule_and_group_containers).attr('draggable', true); - }); - self.$el.on('mouseout.queryBuilder', '.drag-handle', function() { - self.$el.find(Selectors.rule_and_group_containers).removeAttr('draggable'); - }); + 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; + } - // dragstart: create placeholder and hide current element - self.$el.on('dragstart.queryBuilder', '[draggable]', function(e) { - e.stopPropagation(); + // recompute drop-zones during drag (when a rule is hidden) + interact.dynamicDrop(true); - // notify drag and drop (only dummy text) - e.dataTransfer.setData('text', 'drag'); + // set move threshold to 10px + interact.pointerMoveTolerance(10); - src = Model(e.target); + var placeholder; + var ghost; + var src; + var moved; - // Chrome glitchs - // - helper invisible if hidden immediately - // - "dragend" is called immediately if we modify the DOM directly - setTimeout(function() { - var ph = $('
     
    '); - ph.css('min-height', src.$el.height()); + // Init drag and drop + this.on('afterAddRule afterAddGroup', function(e, node) { + if (node == placeholder) { + return; + } - placeholder = src.parent.addRule(ph, src.getPos()); + var self = e.builder; - src.$el.hide(); - }, 0); - }); + // 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; + } - // dragenter: move the placeholder - self.$el.on('dragenter.queryBuilder', '[draggable]', function(e) { - e.preventDefault(); - e.stopPropagation(); + // 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 (placeholder) { - moveSortableToTarget(placeholder, $(e.target)); + 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); + } + } + }); } - }); - - // dragover: prevent glitches - self.$el.on('dragover.queryBuilder', '[draggable]', function(e) { - e.preventDefault(); - e.stopPropagation(); - }); - - // drop: move current element - self.$el.on('drop.queryBuilder', function(e) { - e.preventDefault(); - e.stopPropagation(); - - moveSortableToTarget(src, $(e.target)); - }); - - // dragend: show current element and delete placeholder - self.$el.on('dragend.queryBuilder', '[draggable]', function(e) { - e.preventDefault(); - e.stopPropagation(); - - src.$el.show(); - placeholder.drop(); - - self.$el.find(Selectors.rule_and_group_containers).removeAttr('draggable'); - - self.trigger('afterMove', src); - - src = placeholder = null; - }); - }); - - /** - * Remove drag handle from non-sortable rules - */ - this.on('parseRuleFlags.filter', function(flags) { - if (flags.value.no_sortable === undefined) { - flags.value.no_sortable = options.default_no_sortable; } }); - this.on('afterApplyRuleFlags', function(e, rule) { - if (rule.flags.no_sortable) { - rule.$el.find('.drag-handle').remove(); - } - }); + // Detach interactables + this.on('beforeDeleteRule beforeDeleteGroup', function(e, node) { + if (!e.isDefaultPrevented()) { + interact(node.$el[0]).unset(); - /** - * Remove drag handle from non-sortable groups - */ - this.on('parseGroupFlags.filter', function(flags) { - if (flags.value.no_sortable === undefined) { - flags.value.no_sortable = options.default_no_sortable; + if (node instanceof Group) { + interact(node.$el.find(QueryBuilder.selectors.group_header)[0]).unset(); + } } }); - this.on('afterApplyGroupFlags', function(e, group) { - if (group.flags.no_sortable) { - group.$el.find('.drag-handle').remove(); + // 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 - */ - this.on('getGroupTemplate.filter', function(h, level) { - if (level > 1) { - var $h = $(h.value); - $h.find(Selectors.condition_container).after('
    '); - h.value = $h.prop('outerHTML'); - } - }); + // 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 = $(h.value); - $h.find(Selectors.rule_header).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'); + }); + } }, { - default_no_sortable: false, - icon: 'glyphicon glyphicon-sort' + 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 + } }); /** - * Move an element (placeholder or actual object) depending on active target - * @param {Node} - * @param {jQuery} + * 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(element, target) { - var parent; +function moveSortableToTarget(node, target, builder) { + var parent, method; + var Selectors = QueryBuilder.selectors; // on rule parent = target.closest(Selectors.rule_container); if (parent.length) { - element.moveAfter(Model(parent)); - return; + method = 'moveAfter'; } // on group header - parent = target.closest(Selectors.group_header); - if (parent.length) { - parent = target.closest(Selectors.group_container); - element.moveAtBegin(Model(parent)); - return; + if (!method) { + parent = target.closest(Selectors.group_header); + if (parent.length) { + parent = target.closest(Selectors.group_container); + method = 'moveAtBegin'; + } } // on group - parent = target.closest(Selectors.group_container); - if (parent.length) { - element.moveAtEnd(Model(parent)); - return; + 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); + } } } diff --git a/src/plugins/sortable/plugin.scss b/src/plugins/sortable/plugin.scss index 657e66f6..ac902fe1 100644 --- a/src/plugins/sortable/plugin.scss +++ b/src/plugins/sortable/plugin.scss @@ -9,8 +9,15 @@ $placeholder-border: 1px dashed $placeholder-border-color; margin-left: 5px; } - .dragged { + .dragging { + position: fixed; opacity: .5; + z-index: 100; + + &::before, + &::after { + display: none; + } } .rule-placeholder { diff --git a/src/plugins/sql-support/plugin.js b/src/plugins/sql-support/plugin.js index 500d965b..4bfb996a 100644 --- a/src/plugins/sql-support/plugin.js +++ b/src/plugins/sql-support/plugin.js @@ -1,36 +1,42 @@ -/*! - * jQuery QueryBuilder SQL Support - * Allows to export rules as a SQL WHERE statement as well as populating the builder from an SQL query. +/** + * @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 +}); -// DEFAULT CONFIG -// =============================== QueryBuilder.defaults({ - /* operators for internal -> SQL conversion */ + // 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: '> ?' }, + equal: { op: '= ?' }, + not_equal: { op: '!= ?' }, + in: { op: 'IN(?)', sep: ', ' }, + not_in: { op: 'NOT IN(?)', sep: ', ' }, + less: { op: '< ?' }, + less_or_equal: { op: '<= ?' }, + greater: { op: '> ?' }, greater_or_equal: { op: '>= ?' }, - between: { op: 'BETWEEN ?', sep: ' AND ' }, - not_between: { op: 'NOT BETWEEN ?', sep: ' AND ' }, - begins_with: { op: 'LIKE(?)', mod: '{0}%' }, - not_begins_with: { op: 'NOT LIKE(?)', mod: '{0}%' }, - contains: { op: 'LIKE(?)', mod: '%{0}%' }, - not_contains: { op: 'NOT LIKE(?)', mod: '%{0}%' }, - ends_with: { op: 'LIKE(?)', mod: '%{0}' }, - not_ends_with: { op: 'NOT LIKE(?)', mod: '%{0}' }, - is_empty: { op: '= \'\'' }, - is_not_empty: { op: '!= \'\'' }, - is_null: { op: 'IS NULL' }, - is_not_null: { op: 'IS NOT NULL' } + 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 */ + // operators for SQL -> internal conversion sqlRuleOperator: { '=': function(v) { return { @@ -67,14 +73,53 @@ QueryBuilder.defaults({ Utils.error('SQLParse', 'Invalid value for 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' }; }, + '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'); @@ -89,7 +134,7 @@ QueryBuilder.defaults({ } }, - /* statements for internal -> SQL conversion */ + // statements for internal -> SQL conversion sqlStatements: { 'question_mark': function() { var params = []; @@ -138,7 +183,7 @@ QueryBuilder.defaults({ } }, - /* statements for SQL -> internal conversion */ + // statements for SQL -> internal conversion sqlRuleStatement: { 'question_mark': function(values) { var index = 0; @@ -169,7 +214,7 @@ QueryBuilder.defaults({ 'named': function(values, char) { if (!char || char.length > 1) char = ':'; var regex1 = new RegExp('^\\' + char); - var regex2 = new RegExp('\\' + char + '(' + Object.keys(values).join('|') + ')', 'g'); + var regex2 = new RegExp('\\' + char + '(' + Object.keys(values).join('|') + ')\\b', 'g'); return { parse: function(v) { return regex1.test(v) ? values[v.slice(1)] : v; @@ -182,23 +227,38 @@ QueryBuilder.defaults({ } }); +/** + * @typedef {object} SqlQuery + * @memberof module:plugins.SqlSupport + * @property {string} sql + * @property {object} params + */ -// PUBLIC METHODS -// =============================== -QueryBuilder.extend({ +QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ { /** - * Get rules as SQL query + * 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 - * @param stmt {boolean|string} use prepared statements - false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)' - * @param nl {bool} output with new lines - * @param data {object} (optional) rules - * @return {object} */ getSQL: function(stmt, nl, data) { data = (data === undefined) ? this.getRules() : data; - nl = (nl === true) ? '\n' : ' '; - if (stmt === true) stmt = 'question_mark'; + 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]); @@ -206,21 +266,21 @@ QueryBuilder.extend({ var self = this; - var sql = (function parse(data) { - if (!data.condition) { - data.condition = self.settings.default_condition; + var sql = (function parse(group) { + if (!group.condition) { + group.condition = self.settings.default_condition; } - if (['AND', 'OR'].indexOf(data.condition.toUpperCase()) === -1) { - Utils.error('UndefinedSQLCondition', 'Unable to build SQL query with condition "{0}"', data.condition); + if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) { + Utils.error('UndefinedSQLCondition', 'Unable to build SQL query with condition "{0}"', group.condition); } - if (!data.rules) { + if (!group.rules) { return ''; } var parts = []; - data.rules.forEach(function(rule) { + group.rules.forEach(function(rule) { if (rule.rules && rule.rules.length > 0) { parts.push('(' + nl + parse(rule) + nl + ')' + nl); } @@ -240,14 +300,14 @@ QueryBuilder.extend({ rule.value.forEach(function(v, i) { if (i > 0) { - value+= sql.sep; + value += sql.sep; } - if (rule.type == 'integer' || rule.type == 'double' || rule.type == 'boolean') { - v = Utils.changeType(v, rule.type, true); + if (rule.type == 'boolean' && boolean_as_integer) { + v = v ? 1 : 0; } - else if (!stmt) { - v = Utils.escapeString(v); + else if (!stmt && rule.type !== 'integer' && rule.type !== 'double' && rule.type !== 'boolean') { + v = Utils.escapeString(v, sql.escape); } if (sql.mod) { @@ -255,23 +315,61 @@ QueryBuilder.extend({ } if (stmt) { - value+= stmt.add(rule, v); + value += stmt.add(rule, v); } else { if (typeof v == 'string') { v = '\'' + v + '\''; } - value+= v; + value += v; } }); } - parts.push(rule.field + ' ' + sql.op.replace(/\?/, value)); + 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)); } }); - return parts.join(' ' + data.condition + nl); + 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) { @@ -288,60 +386,135 @@ QueryBuilder.extend({ }, /** - * Convert SQL to rules - * @throws ConfigError, SQLParseError, UndefinedSQLOperatorError - * @param data {object} query object - * @param stmt {boolean|string} use prepared statements - false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)' - * @return {object} + * 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(data, stmt) { + 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 data == 'string') { - data = { sql: data }; + 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]](data.params, config[2]); + stmt = this.settings.sqlRuleStatement[config[1]](query.params, config[2]); } if (stmt) { - data.sql = stmt.esc(data.sql); + query.sql = stmt.esc(query.sql); } - if (data.sql.toUpperCase().indexOf('SELECT') !== 0) { - data.sql = 'SELECT * FROM table WHERE ' + data.sql; + if (query.sql.toUpperCase().indexOf('SELECT') !== 0) { + query.sql = 'SELECT * FROM table WHERE ' + query.sql; } - var parsed = SQLParser.parse(data.sql); + var parsed = SQLParser.parse(query.sql); if (!parsed.where) { Utils.error('SQLParse', 'No WHERE clause found'); } - var out = { + /** + * 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 - if (i > 0 && curr.condition != data.operation.toUpperCase()) { - curr.rules.push({ + + /** + * 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 = curr.rules[curr.rules.length - 1]; + curr.rules.push(group); + curr = group; } curr.condition = data.operation.toUpperCase(); @@ -356,10 +529,6 @@ QueryBuilder.extend({ } // it's a leaf else { - if (data.left.value === undefined || data.right.value === undefined) { - Utils.error('SQLParse', 'Missing field and/or value'); - } - if ($.isPlainObject(data.right.value)) { Utils.error('SQLParse', 'Value format not supported for {0}.', data.left.value); } @@ -387,46 +556,115 @@ QueryBuilder.extend({ // convert operator var operator = data.operation.toUpperCase(); - if (operator == '<>') operator = '!='; - - var sqlrl; - if (operator == 'NOT LIKE') { - sqlrl = self.settings.sqlRuleOperator['LIKE']; - } - else { - sqlrl = self.settings.sqlRuleOperator[operator]; + 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); - if (operator == 'NOT LIKE') opVal.op = 'not_' + opVal.op; - var left_value = data.left.values.join('.'); + // 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)); + } - curr.rules.push({ - id: self.change('getSQLFieldID', left_value, value), - field: left_value, + // 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: opVal.val - }); + value: finalValue + }, data); + + curr.rules.push(rule); } - }(parsed.where.conditions, 0)); + }(data, 0)); return out; }, /** - * Set rules from SQL - * @param data {object} + * Sets the builder's rules from a SQL query + * @see module:plugins.SqlSupport.getRulesFromSQL */ - setRulesFromSQL: function(data, stmt) { - this.setRules(this.getRulesFromSQL(data, stmt)); + 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]; diff --git a/src/plugins/unique-filter/plugin.js b/src/plugins/unique-filter/plugin.js index 6c77831a..5b6fb802 100644 --- a/src/plugins/unique-filter/plugin.js +++ b/src/plugins/unique-filter/plugin.js @@ -1,8 +1,8 @@ -/*! - * jQuery QueryBuilder Unique Filter - * Allows to define some filters as "unique": ie which can be used for only one rule, globally or in the same group. +/** + * @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 = {}; @@ -11,9 +11,35 @@ QueryBuilder.define('unique-filter', function() { 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({ +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; @@ -42,6 +68,11 @@ QueryBuilder.extend({ self.applyDisabledFilters(e); }, + /** + * Clear the list of used filters + * @param {$.Event} [e] + * @private + */ clearDisabledFilters: function(e) { var self = e ? e.builder : this; @@ -50,21 +81,26 @@ QueryBuilder.extend({ 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(Selectors.filter_container + ' option').prop('disabled', false); + 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(Selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true); + 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(Selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true); + rule.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true); }); }); } @@ -72,7 +108,7 @@ QueryBuilder.extend({ // update Selectpicker if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) { - self.$el.find(Selectors.rule_filter).selectpicker('render'); + self.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render'); } } }); diff --git a/src/public.js b/src/public.js index 47e4c2bf..8e8117af 100644 --- a/src/public.js +++ b/src/public.js @@ -1,7 +1,13 @@ /** - * Destroy the plugin + * 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) { @@ -20,23 +26,58 @@ QueryBuilder.prototype.destroy = function() { }; /** - * Reset the plugin + * 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'); }; /** - * Clear the plugin + * 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; @@ -45,36 +86,58 @@ QueryBuilder.prototype.clear = function() { this.model.root = null; } + /** + * After the {@link QueryBuilder#clear} method + * @event afterClear + * @memberof QueryBuilder + */ this.trigger('afterClear'); + + this.trigger('rulesChanged'); }; /** - * Modify the builder configuration + * Modifies the builder configuration.
    * Only options defined in QueryBuilder.modifiable_options are modifiable - * @param {object} + * @param {object} options */ QueryBuilder.prototype.setOptions = function(options) { - // use jQuery utils to filter options keys - $.makeArray($(Object.keys(options)).filter(QueryBuilder.modifiable_options)) - .forEach(function(opt) { - this.settings[opt] = options[opt]; - }, this); + $.each(options, function(opt, value) { + if (QueryBuilder.modifiable_options.indexOf(opt) !== -1) { + this.settings[opt] = value; + } + }.bind(this)); }; /** - * Return the model associated to a DOM object, or root model - * @param {jQuery,optional} - * @return {Node} + * Returns the model associated to a DOM object, or the root model + * @param {jQuery} [target] + * @returns {Node} */ QueryBuilder.prototype.getModel = function(target) { - return !target ? this.model.root : Model(target); + if (!target) { + return this.model.root; + } + else if (target instanceof Node) { + return target; + } + else { + return $(target).data('queryBuilderModel'); + } }; /** - * Validate the whole builder - * @return {boolean} + * 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() { +QueryBuilder.prototype.validate = function(options) { + options = $.extend({ + skip_empty: false + }, options); + this.clearErrors(); var self = this; @@ -84,12 +147,22 @@ QueryBuilder.prototype.validate = function() { 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); @@ -103,10 +176,11 @@ QueryBuilder.prototype.validate = function() { done++; }, function(group) { - if (parse(group)) { + var res = parse(group); + if (res === true) { done++; } - else { + else if (res === false) { errors++; } }); @@ -114,6 +188,9 @@ QueryBuilder.prototype.validate = function() { 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; @@ -123,88 +200,145 @@ QueryBuilder.prototype.validate = function() { }(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); }; /** - * Get an object representing current rules - * @param {object} options - * - get_flags: false[default] | true(only changes from default flags) | 'all' - * @return {object} + * 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 + get_flags: false, + allow_invalid: false, + skip_empty: false }, options); - if (!this.validate()) { - return {}; + var valid = this.validate(options); + if (!valid && !options.allow_invalid) { + return null; } var self = this; var out = (function parse(group) { - var data = { + var groupData = { condition: group.condition, rules: [] }; if (group.data) { - data.data = $.extendext(true, 'replace', {}, 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)) { - data.flags = flags; + groupData.flags = flags; } } - group.each(function(model) { + group.each(function(rule) { + if (!rule.filter && options.skip_empty) { + return; + } + var value = null; - if (model.operator.nb_inputs !== 0) { - value = model.value; + if (!rule.operator || rule.operator.nb_inputs !== 0) { + value = rule.value; } - var rule = { - id: model.filter.id, - field: model.filter.field, - type: model.filter.type, - input: model.filter.input, - operator: model.operator.type, + 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 (model.filter.data || model.data) { - rule.data = $.extendext(true, 'replace', {}, model.filter.data, model.data); + 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(model.flags, options.get_flags === 'all'); + var flags = self.getRuleFlags(rule.flags, options.get_flags === 'all'); if (!$.isEmptyObject(flags)) { - rule.flags = flags; + ruleData.flags = flags; } } - data.rules.push(rule); + /** + * 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) { - data.rules.push(parse(model)); - }); + var data = parse(model); + if (data.rules.length !== 0 || !options.skip_empty) { + groupData.rules.push(data); + } + }, this); - return data; + /** + * 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); }; /** - * Set rules from object + * Sets rules from object + * @param {object} data + * @param {object} [options] + * @param {boolean} [options.allow_invalid=false] - silent-fail if the data are invalid * @throws RulesError, UndefinedConditionError - * @param data {object} + * @fires QueryBuilder.changer:setRules + * @fires QueryBuilder.changer:jsonToRule + * @fires QueryBuilder.changer:jsonToGroup + * @fires QueryBuilder.afterSetRules */ -QueryBuilder.prototype.setRules = function(data) { +QueryBuilder.prototype.setRules = function(data, options) { + options = $.extend({ + allow_invalid: false + }, options); + if ($.isArray(data)) { data = { condition: this.settings.default_condition, @@ -219,7 +353,15 @@ QueryBuilder.prototype.setRules = function(data) { this.clear(); this.setRoot(false, data.data, this.parseGroupFlags(data)); - data = this.change('setRules', 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; @@ -232,17 +374,19 @@ QueryBuilder.prototype.setRules = function(data) { data.condition = self.settings.default_condition; } else if (self.settings.conditions.indexOf(data.condition) == -1) { - Utils.error('UndefinedCondition', 'Invalid condition "{0}"', data.condition); + 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 && item.rules.length > 0) { + + 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(); - Utils.error('RulesParse', 'No more than {0} groups are allowed', self.settings.allow_groups); } else { model = self.addGroup(group, false, item.data, self.parseGroupFlags(item)); @@ -254,27 +398,74 @@ QueryBuilder.prototype.setRules = function(data) { } } else { - if (item.id === undefined) { - Utils.error('RulesParse', 'Missing rule field id'); - } - if (item.operator === undefined) { - item.operator = 'equal'; + 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); + model = self.addRule(group, item.data, self.parseRuleFlags(item)); if (model === null) { return; } - model.filter = self.getFilterById(item.id); - model.operator = self.getOperatorByType(item.operator); - model.flags = self.parseRuleFlags(item); + 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.nb_inputs !== 0 && item.value !== undefined) { - model.value = item.value; + 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'); }; diff --git a/src/scss/default.scss b/src/scss/default.scss index 1008a7fd..9091634c 100644 --- a/src/scss/default.scss +++ b/src/scss/default.scss @@ -62,8 +62,16 @@ $ticks-position: 5px, 10px !default; .group-conditions { .btn.readonly:not(.active), - input[name$=_cond] { - display: none; + 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; } .btn.readonly { @@ -101,10 +109,6 @@ $ticks-position: 5px, 10px !default; display: block; } } - - select, input[type=text], input[type=number] { - padding: 1px; - } } // ERRORS diff --git a/src/template.js b/src/template.js index 89692601..219aa06e 100644 --- a/src/template.js +++ b/src/template.js @@ -1,232 +1,336 @@ -QueryBuilder.templates.group = '\ -
    \ -
    \ -
    \ - \ - {{? it.settings.allow_groups===-1 || it.settings.allow_groups>=it.level }} \ - \ - {{?}} \ - {{? it.level>1 }} \ - \ - {{?}} \ -
    \ -
    \ - {{~ it.conditions: condition }} \ - \ - {{~}} \ -
    \ - {{? it.settings.display_errors }} \ -
    \ - {{?}} \ -
    \ -
    \ -
      \ -
      \ -
      '; - -QueryBuilder.templates.rule = '\ -
    • \ -
      \ -
      \ - \ -
      \ -
      \ - {{? it.settings.display_errors }} \ -
      \ - {{?}} \ -
      \ -
      \ -
      \ -
    • '; - -QueryBuilder.templates.filterSelect = '\ -{{ var optgroup = null; }} \ -'; - -QueryBuilder.templates.operatorSelect = '\ -{{ var optgroup = null; }} \ -'; +QueryBuilder.templates.group = ({ group_id, level, conditions, icons, settings, translate, builder }) => { + return ` +
      +
      +
      + + ${settings.allow_groups === -1 || settings.allow_groups >= level ? ` + + ` : ''} + ${level > 1 ? ` + + ` : ''} +
      +
      + ${conditions.map(condition => ` + + `).join('\n')} +
      + ${settings.display_errors ? ` +
      + ` : ''} +
      +
      +
      +
      +
      `; +}; + +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 group HTML - * @param group_id {string} - * @param level {int} - * @return {string} + * Returns rule's HTML + * @param {string} rule_id + * @returns {string} + * @fires QueryBuilder.changer:getRuleTemplate + * @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, - lang: this.lang, - settings: this.settings - }); - - return this.change('getGroupTemplate', h, level); +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 HTML - * @param rule_id {string} - * @return {string} + * Returns rule's filter HTML + * @param {Rule} rule + * @param {object[]} filters + * @returns {string} + * @fires QueryBuilder.changer:getRuleFilterTemplate + * @private */ -QueryBuilder.prototype.getRuleTemplate = function(rule_id) { - var h = this.templates.rule({ - builder: this, - rule_id: rule_id, - icons: this.icons, - lang: this.lang, - settings: this.settings - }); - - return this.change('getRuleTemplate', h); +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 filter HTML - * @param rule {Rule} - * @param operators {object} - * @return {string} + * Returns the rule's value select HTML + * @param {string} name + * @param {Rule} rule + * @returns {string} + * @fires QueryBuilder.changer:getRuleValueSelect + * @private */ -QueryBuilder.prototype.getRuleOperatorSelect = function(rule, operators) { - var h = this.templates.operatorSelect({ - builder: this, - rule: rule, - operators: operators, - icons: this.icons, - lang: this.lang, - settings: this.settings, - translate: this.translateLabel - }); - - return this.change('getRuleOperatorSelect', h, rule); +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); }; /** - * Return the rule value HTML - * @param rule {Rule} - * @param filter {object} - * @param value_id {int} - * @return {string} + * Returns the rule's value HTML + * @param {Rule} rule + * @param {int} value_id + * @returns {string} + * @fires QueryBuilder.changer:getRuleInput + * @private */ -QueryBuilder.prototype.getRuleInput = function(rule, value_id) { - var filter = rule.filter; - var validation = rule.filter.validation || {}; - var name = rule.id + '_value_' + value_id; - var c = filter.vertical ? ' class=block' : ''; - var h = ''; - - if (typeof filter.input == 'function') { - h = filter.input.call(this, rule, name); - } - else { - switch (filter.input) { - case 'radio': case 'checkbox': - Utils.iterateOptions(filter.values, function(key, val) { - h+= ' ' + val + ' '; - }); - break; - - case 'select': - h+= ''; - break; - - case 'textarea': - h+= ''; + break; + + case 'number': + h += '
      + * 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); + } + } + }); + }); +}; diff --git a/tests/common.js b/tests/common.js index 31e5dd85..c4473ed7 100644 --- a/tests/common.js +++ b/tests/common.js @@ -15,7 +15,7 @@ QUnit.begin(function() { */ QUnit.begin(function(){ $('#qunit-header').append( - '
      ' + + '
      ' + '' + '' + '' + @@ -43,6 +43,10 @@ QUnit.assert.rulesMatch = function(actual, expected, message) { var ok = (function match(a, b){ var ok = true; + if (a.hasOwnProperty('valid') && b.hasOwnProperty('valid')) { + ok = QUnit.equiv(a.valid, b.valid); + } + if (b.hasOwnProperty('data')) { if (!a.hasOwnProperty('data')) { ok = false; @@ -100,7 +104,12 @@ QUnit.assert.rulesMatch = function(actual, expected, message) { return ok; }(actual, expected)); - this.push(ok, actual, expected, message); + this.pushResult({ + result: ok, + actual: actual, + expected: expected, + message: message + }); }; /** @@ -154,7 +163,12 @@ QUnit.assert.optionsMatch = function($target, expected, message) { * Custom assert to test a regex */ QUnit.assert.match = function(actual, regex, message) { - this.push(regex.test(actual), actual, regex, message); + this.pushResult({ + result: regex.test(actual), + actual: actual, + expected: regex, + message: message + }); }; @@ -244,7 +258,8 @@ QUnit.assert.match = function(actual, regex, message) { var basic_filters = [{ id: 'name', label: 'Name', - type: 'string' + type: 'string', + value_separator: ',' }, { id: 'category', label: 'Category', @@ -286,8 +301,18 @@ var basic_filters = [{ placeholder: '____-____-____', operators: ['equal', 'not_equal'], validation: { - format: /^.{4}-.{4}-.{4}$/ + format: /^.{4}-.{4}-.{4}$/, + messages: { + format: 'Custom format error message' + } } +}, { + id: 'age', + label: 'Age', + type: 'integer', + input: 'text', + value_separator: '|', + default_operator: 'in' }]; var basic_rules = { diff --git a/tests/core.module.js b/tests/core.module.js index 37dcc7df..84ee916b 100644 --- a/tests/core.module.js +++ b/tests/core.module.js @@ -86,9 +86,9 @@ $(function(){ ); }); - assert.deepEqual( + assert.equal( $b.queryBuilder('getRules'), - {}, + null, 'Should return empty object' ); @@ -98,7 +98,7 @@ $(function(){ assert.deepEqual( $b.queryBuilder('getRules'), - { condition: 'AND', rules: [] }, + { condition: 'AND', rules: [], valid: true}, 'Should return object with no rules' ); }); @@ -412,33 +412,33 @@ $(function(){ } }); - assert.ok( - $('#builder_group_0>.rules-group-header input:not(:disabled)').length == 0, + assert.equal( + $('#builder_group_0>.rules-group-header input:not(:disabled)').length, 0, 'Should disable group condition radio buttons' ); - assert.ok( - $('#builder_rule_0 [data-delete=rule]').length == 0, + assert.equal( + $('#builder_rule_0 [data-delete=rule]').length, 0, 'Should hide delete button of "no_delete" rule' ); - assert.ok( - $('#builder_rule_0').find('input:disabled, select:disabled').length == 0, + assert.equal( + $('#builder_rule_0').find('input:disabled, select:disabled').length, 0, 'Should not disable inputs of "no_delete" rule' ); - assert.ok( - $('#builder_rule_1 [data-delete=rule]').length == 0, + assert.equal( + $('#builder_rule_1 [data-delete=rule]').length, 0, 'Should hide delete button of "readonly" rule' ); - assert.ok( - $('#builder_rule_1').find('input:disabled, select:disabled').length == 3, + assert.equal( + $('#builder_rule_1').find('input:disabled, select:disabled').length, 3, 'Should disable inputs of "readonly" rule' ); - assert.ok( - $('#builder_group_2').find('[data-delete=group], [data-add=rule], [data-add=group]').length ==0, + assert.equal( + $('#builder_group_2').find('[data-delete=group], [data-add=rule], [data-add=group]').length, 0, 'Should hide all buttons of "readonly" group' ); @@ -689,6 +689,47 @@ $(function(){ ); }); + /** + * Test custom error messages + */ + QUnit.test('Custom error messages', function(assert) { + $b.queryBuilder({ + filters: basic_filters, + rules: [ + { + id: 'id', + operator: 'equal', + value: 'azerty' + }, + { + id: 'price', + operator: 'equal', + value: -10 + } + ] + }); + + $b.on('displayError.queryBuilder.filter', function(e, error, node) { + if (node.filter.id == 'price' && error[0] == 'number_exceed_min') { + e.value = 'Custom min error message'; + } + }); + + $b.queryBuilder('validate'); + + assert.equal( + $b.find('.error-container').eq(1).attr('title'), + 'Custom format error message', + 'Should apply custom message from config' + ); + + assert.equal( + $b.find('.error-container').eq(2).attr('title'), + 'Custom min error message', + 'Should apply custom message from event' + ); + }); + /** * Test access to defaults */ @@ -721,7 +762,9 @@ $(function(){ filter_readonly: true, operator_readonly: false, value_readonly: true, - no_delete: false + no_delete: false, + no_sortable: true, + no_drop: false }; QueryBuilder.defaults({ default_rule_flags: flags }); diff --git a/tests/data.module.js b/tests/data.module.js index 8034abaf..41e7b48d 100644 --- a/tests/data.module.js +++ b/tests/data.module.js @@ -1,4 +1,4 @@ -$(function(){ +$(function() { var $b = $('#builder'); QUnit.module('data', { @@ -31,9 +31,9 @@ $(function(){ type: 'string', input: 'select', values: [ - {one: 'One'}, - {two: 'Two'}, - {three: 'Three'} + { one: 'One' }, + { two: 'Two' }, + { three: 'Three' } ] }], rules: { @@ -155,11 +155,6 @@ $(function(){ /number_not_integer/ ); - assert.validationError($b, - { id: 'double', value: 'abc' }, - /number_not_double/ - ); - assert.validationError($b, { id: 'integer', value: -15 }, /number_exceed_min/ @@ -175,6 +170,11 @@ $(function(){ /number_wrong_step/ ); + assert.validationError($b, + { id: 'integer', operator: 'between', value: [5, 1] }, + /number_between_invalid/ + ); + assert.validationError($b, { id: 'date' }, /datetime_empty/ @@ -195,6 +195,11 @@ $(function(){ /datetime_exceed_max/ ); + assert.validationError($b, + { id: 'date', operator: 'between', value: ['2015/01/01', '2014/01/01'] }, + /datetime_between_invalid/ + ); + assert.validationError($b, { id: 'boolean', value: 'oui' }, /boolean_not_valid/ @@ -212,7 +217,7 @@ $(function(){ QUnit.test('custom data', function(assert) { var rules = { condition: 'AND', - data: [1,2,3], + data: [1, 2, 3], rules: [{ id: 'name', value: 'Mistic', @@ -244,6 +249,46 @@ $(function(){ ); }); + /** + * Set an empty rule + */ + QUnit.test('set empty rule', function(assert) { + var rules = [{ + id: 'name', + value: 'Mistic' + }, { + empty: true + }, { + condition: 'OR', + rules: [] + }]; + + $b.queryBuilder({ + filters: basic_filters, + rules: rules + }); + + assert.validationError($b, + null, + /no_filter/ + ); + + assert.equal( + $b[0].queryBuilder.model.root.rules.length, 3, + 'Should have two rules and one group' + ); + + assert.equal( + $b[0].queryBuilder.model.root.rules[2].rules.length, 0, + 'Group should be empty' + ); + + assert.equal( + $('[name=builder_rule_1_filter]').val(), '-1', + 'Second rule should be empty' + ); + }); + /** * Test get flags with getRules */ @@ -296,28 +341,232 @@ $(function(){ condition_readonly: true, no_add_rule: false, no_add_group: false, - no_delete: false + no_delete: false, + no_sortable: false, + no_drop: false }; rules_all_flags.rules[0].flags = { filter_readonly: false, operator_readonly: false, value_readonly: false, - no_delete: true + no_delete: true, + no_sortable: false, + no_drop: false + }; + rules_all_flags.rules[1].flags = { + condition_readonly: true, + no_add_rule: true, + no_add_group: true, + no_delete: true, + no_sortable: false, + no_drop: false + }; + rules_all_flags.rules[1].rules[0].flags = { + filter_readonly: true, + operator_readonly: true, + value_readonly: true, + no_delete: true, + no_sortable: false, + no_drop: false }; assert.rulesMatch( - $b.queryBuilder('getRules', {get_flags: true}), + $b.queryBuilder('getRules', { get_flags: true }), rules_changed_flags, 'Should export rules with changed flags' ); assert.rulesMatch( - $b.queryBuilder('getRules', {get_flags: 'all'}), + $b.queryBuilder('getRules', { get_flags: 'all' }), rules_all_flags, 'Should export rules with all flags' ); }); + /** + * Test value separator + */ + QUnit.test('value separator', function(assert) { + $b.queryBuilder({ + filters: basic_filters, + rules: [{ + id: 'name', + operator: 'equal', + value: 'Mistic,Damien' + }, { + id: 'age', + operator: 'not_equal', + value: '16|17|18' + }] + }); + + $('[name=builder_rule_0_operator]').val('in').trigger('change'); + $('[name=builder_rule_1_operator]').val('not_in').trigger('change'); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + { + condition: 'AND', + rules: [{ + id: 'name', + operator: 'in', + value: ['Mistic', 'Damien'] + }, { + id: 'age', + operator: 'not_in', + value: [16, 17, 18] + }] + }, + 'Should split values on comma and pipe' + ); + }); + + /** + * Test default operator + */ + QUnit.test('default operator', function(assert) { + $b.queryBuilder({ + filters: basic_filters + }); + + $('[name=builder_rule_0_filter]').val('age').trigger('change'); + + assert.equal( + $('[name=builder_rule_0_operator]').val(), + 'in', + 'Should set "age" operator to "in" by default' + ); + }); + + /** + * Test allow_invalid option + */ + QUnit.test('allow invalid', function(assert) { + $b.queryBuilder({ + filters: basic_filters + }); + + $b.queryBuilder('setRules', { + condition: 'XOR', + rules: [{ + id: 'name', + operator: 'unkown_ope', + value: 'Mistic' + }, { + id: 'unknown_id', + operator: 'equal', + value: 123 + }] + }, { + allow_invalid: true + }); + + assert.rulesMatch( + $b.queryBuilder('getRules', { + allow_invalid: true + }), + { + valid: false, + condition: 'AND', + rules: [{ + id: 'name', + operator: 'equal', + value: 'Mistic' + }, { + id: null, + operator: null, + value: null + }] + }, + 'Should allow invalid rules for setRules and getRules' + ); + }); + + /** + * Test skip_empty option + */ + QUnit.test('skip empty', function(assert) { + $b.queryBuilder({ + filters: basic_filters + }); + + $b.queryBuilder('setRules', { + condition: 'AND', + rules: [{ + id: 'name', + operator: 'equal', + value: 'Mistic' + }, { + empty: true + }] + }); + + assert.rulesMatch( + $b.queryBuilder('getRules', { + skip_empty: true + }), + { + condition: 'AND', + rules: [{ + id: 'name', + operator: 'equal', + value: 'Mistic' + }] + }, + 'Should skip empty rules for getRules' + ); + }); + + QUnit.test('apply default value', function(assert) { + $b.queryBuilder({ + filters: [ + { + id: 'name', + default_value: 'Mistic' + } + ], + rules: [ + { + id: 'name' + } + ] + }); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + { + condition: 'AND', + rules: [{ + id: 'name', + operator: 'equal', + value: 'Mistic' + }] + }, + 'Should have used the filter default value' + ); + }); + + /** + * Test allow_empty_value option + */ + QUnit.test('allow empty value', function(assert) { + var filters = $.extend(true, [], basic_filters); + filters.forEach(function(filter) { + filter.validation = $.extend({ allow_empty_value: true }, filter.validation); + }); + + $b.queryBuilder({ + filters: filters, + rules: empty_rules + }); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + empty_rules, + 'Should allow empty value for all filters' + ); + }); + var validation_filters = [{ id: 'radio', @@ -389,4 +638,25 @@ $(function(){ } } }]; + + var empty_rules = { + condition: 'AND', + rules: [{ + id: 'name', + operator: 'equal', + value: '' + }, { + id: 'category', + operator: 'equal', + value: [] + }, { + id: 'in_stock', + operator: 'equal', + value: undefined + }, { + id: 'price', + operator: 'equal', + value: '' + }] + }; }); diff --git a/tests/index.html b/tests/index.html index 4d6d8fea..e322dcfd 100644 --- a/tests/index.html +++ b/tests/index.html @@ -4,10 +4,10 @@ jQuery-QueryBuilder - - - - + + + + #qunit-modulefilter-container { float:none; } @@ -16,50 +16,55 @@ - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + diff --git a/tests/plugins-gui.module.js b/tests/plugins-gui.module.js index 042719b0..a6ac0d22 100644 --- a/tests/plugins-gui.module.js +++ b/tests/plugins-gui.module.js @@ -79,21 +79,22 @@ $(function(){ }); /** - * Test bt-selectpicker + * Test chosen-selectpicker */ - QUnit.test('bt-selectpicker', function(assert) { + QUnit.test('chosen-selectpicker', function(assert) { $b.queryBuilder({ - plugins: ['bt-selectpicker'], + plugins: ['chosen-selectpicker'], filters: basic_filters, rules: basic_rules }); assert.ok( - $b.find('.bootstrap-select').length == 8, - 'Should have initialized Bootstrap Select on all filters and operators selectors' + $b.find('.chosen-single').length == 8, + 'Should have initialized chosen on all filters and operators selectors' ); }); + /** * Test bt-tooltip-errors */ @@ -114,9 +115,9 @@ $(function(){ $b.queryBuilder('validate'); assert.equal( - $('#builder_group_0 .error-container').eq(0).data('toggle'), + $('#builder_group_0 .error-container').eq(0).data('bs-toggle'), 'tooltip', - 'Should have added data-toggle="tooltip" in the template' + 'Should have added data-bs-toggle="tooltip" in the template' ); assert.equal( @@ -134,6 +135,12 @@ $(function(){ id: 'name', type: 'string', description: 'Lorem Ipsum sit amet.' + }, { + id: 'age', + type: 'integer', + description: function(rule) { + return 'Description of operator ' + rule.operator.type; + } }]; var rules = { @@ -141,6 +148,9 @@ $(function(){ rules: [{ id: 'name', value: 'Mistic' + }, { + id: 'age', + value: 25 }] }; @@ -154,10 +164,16 @@ $(function(){ assert.match( $('#builder_rule_0 p.filter-description').html(), - new RegExp(filters[0].description), + new RegExp('Lorem Ipsum sit amet.'), 'Paragraph should contain filter description' ); + assert.match( + $('#builder_rule_1 p.filter-description').html(), + new RegExp('Description of operator equal'), + 'Paragraph should contain filter description after function execution' + ); + $b.queryBuilder('destroy'); $b.queryBuilder({ @@ -169,7 +185,7 @@ $(function(){ }); assert.ok( - $('#builder_rule_0 button.filter-description').data('toggle') == 'popover', + $('#builder_rule_0 button.filter-description').data('bs-toggle') == 'popover', 'Rule should contain a new button enabled with Popover' ); @@ -184,7 +200,7 @@ $(function(){ }); assert.ok( - $('#builder_rule_0 button.filter-description').data('toggle') == 'bootbox', + $('#builder_rule_0 button.filter-description').data('bs-toggle') == 'bootbox', 'Rule should contain a new button enabled with Bootbox' ); }); @@ -193,31 +209,30 @@ $(function(){ * Test sortable */ QUnit.test('sortable', function(assert) { - var sorted_rules = $.extend(true, {}, basic_rules); - sorted_rules.rules.splice(2, 0, sorted_rules.rules[2].rules.pop()); + $b.queryBuilder({ + filters: basic_filters, + rules: basic_rules, + plugins: ['sortable'] + }); + + assert.ok( + $b.find('.drag-handle').length > 0, + 'Should add the drag handles' + ); - assert.expect(1); - var done = assert.async(); + $b.queryBuilder('destroy'); $b.queryBuilder({ - plugins: ['sortable'], + plugins: { + 'sortable': {disable_template: true} + }, filters: basic_filters, rules: basic_rules }); - $('#builder_rule_3').simulateDragDrop({ - dropTarget: $('#builder_rule_1'), - start: function() { - $(this).find('.drag-handle').trigger('mouseover'); - }, - done: function() { - assert.rulesMatch( - $b.queryBuilder('getRules'), - sorted_rules, - 'Should have moved "Identifier" rule' - ); - done(); - } - }); + assert.ok( + $b.find('.drag-handle').length === 0, + 'Should not have added the handles with disable_template=true' + ); }); -}); \ No newline at end of file +}); diff --git a/tests/plugins.module.js b/tests/plugins.module.js index bc404b5d..c0b4fbf6 100644 --- a/tests/plugins.module.js +++ b/tests/plugins.module.js @@ -87,15 +87,12 @@ $(function(){ '"Identifier" filter should be disabled everywhere' ); - /* - FIXME: the test always fails in Grunt assert.ok( $('select[name=builder_rule_1_filter] option[value=price]').is(':disabled') && !$('select[name=builder_rule_2_filter] option[value=price]').is(':disabled') && !$('select[name=builder_rule_3_filter] option[value=price]').is(':disabled'), '"Price" filter should be disabled in his group only' ); - */ }); /** @@ -141,6 +138,21 @@ $(function(){ }, 'Should have inverted all conditions and operators' ); + + $b.queryBuilder('destroy'); + + $b.queryBuilder({ + plugins: { + invert: {disable_template: true} + }, + filters: basic_filters, + rules: basic_rules + }); + + assert.ok( + $b.find('[data-invert="group"]').length === 0, + 'Should not have added the button with disable_template=true' + ); }); /** @@ -213,4 +225,4 @@ $(function(){ 'Should have added filter "a" after "c"' ); }); -}); \ No newline at end of file +}); diff --git a/tests/plugins.mongo-support.module.js b/tests/plugins.mongo-support.module.js new file mode 100644 index 00000000..61ea36d8 --- /dev/null +++ b/tests/plugins.mongo-support.module.js @@ -0,0 +1,217 @@ +$(function(){ + var $b = $('#builder'); + + QUnit.module('plugins.mongo-support', { + afterEach: function() { + $b.queryBuilder('destroy'); + } + }); + + QUnit.test('Basics', function(assert) { + var basic_rules_mongodb = { + '$and': [ + {'price': {'$lt': 10.25}}, + {'name': null}, + { + '$or': [ + {'category': {'$in': ['mo', 'mu']}}, + {'id': {'$ne': '1234-azer-5678'}} + ] + } + ] + }; + + $b.queryBuilder({ + filters: basic_filters, + rules: basic_rules + }); + + assert.deepEqual( + $b.queryBuilder('getMongo'), + basic_rules_mongodb, + 'Should create MongoDB query' + ); + + assert.deepEqual( + $b.queryBuilder('getRulesFromMongo', basic_rules_mongodb), + basic_rules, + 'Should return rules object from MongoDB query' + ); + }); + + QUnit.test('All operators', function(assert) { + $b.queryBuilder({ + filters: basic_filters, + rules: all_operators_rules + }); + + assert.deepEqual( + $b.queryBuilder('getMongo'), + all_operators_rules_mongodb, + 'Should successfully convert all kind of operators to MongoDB' + ); + + $b.queryBuilder('reset'); + + $b.queryBuilder('setRulesFromMongo', all_operators_rules_mongodb); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + all_operators_rules, + 'Should successfully parse all kind of operators from MongoDB' + ); + }); + + QUnit.test('Automatically use filter from field', function(assert) { + var rules = { + condition: 'AND', + rules: [ + { + id: 'name', + operator: 'equal', + value: 'Mistic' + } + ] + }; + + var mongo = { + $and: [{ + username: 'Mistic' + }] + }; + + $b.queryBuilder({ + filters: [ + { + id: 'name', + field: 'username', + type: 'string' + }, + { + id: 'last_days', + field: 'display_date', + type: 'integer' + } + ] + }); + + $b.queryBuilder('setRulesFromMongo', mongo); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + rules, + 'Should use "name" filter from "username" field' + ); + }); + + + var all_operators_rules = { + condition: 'AND', + rules: [{ + id: 'name', + operator: 'equal', + value: 'foo' + }, { + id: 'name', + operator: 'not_equal', + value: 'foo' + }, { + id: 'category', + operator: 'in', + value: ['bk','mo'] + }, { + id: 'category', + operator: 'not_in', + value: ['bk','mo'] + }, { + id: 'price', + operator: 'less', + value: 5 + }, { + id: 'price', + operator: 'less_or_equal', + value: 5 + }, { + id: 'price', + operator: 'greater', + value: 4 + }, { + id: 'price', + operator: 'greater_or_equal', + value: 4 + }, { + id: 'price', + operator: 'between', + value: [4,5] + }, { + id: 'price', + operator: 'not_between', + value: [4,5] + }, { + id: 'name', + operator: 'begins_with', + value: 'foo' + }, { + id: 'name', + operator: 'not_begins_with', + value: 'foo' + }, { + id: 'name', + operator: 'contains', + value: 'foo' + }, { + id: 'name', + operator: 'not_contains', + value: 'foo' + }, { + id: 'name', + operator: 'ends_with', + value: 'foo' + }, { + id: 'name', + operator: 'not_ends_with', + value: 'foo' + }, { + id: 'name', + operator: 'is_empty', + value: null + }, { + id: 'name', + operator: 'is_not_empty', + value: null + }, { + id: 'name', + operator: 'is_null', + value: null + }, { + id: 'name', + operator: 'is_not_null', + value: null + }] + }; + + var all_operators_rules_mongodb = { + $and: [ + { name: 'foo' }, + { name: {$ne: 'foo'} }, + { category: { $in: ['bk','mo'] }}, + { category: { $nin: ['bk','mo'] }}, + { price: {$lt: 5} }, + { price: {$lte: 5} }, + { price: {$gt: 4} }, + { price: {$gte: 4} }, + { price: {$gte: 4, $lte: 5} }, + { price: {$lt: 4, $gt: 5} }, + { name: {$regex: '^foo'} }, + { name: {$regex: '^(?!foo)'} }, + { name: {$regex: 'foo'} }, + { name: {$regex: '^((?!foo).)*$', $options: 's'} }, + { name: {$regex: 'foo$'} }, + { name: {$regex: '(? 0, + 'Should add "not" buttons"' + ); + + $('#builder_group_0>.rules-group-header [data-not=group]').trigger('click'); + + assert.ok( + $b.queryBuilder('getModel').not, + 'The root group should have "not" flag set to true' + ); + + assert.ok( + $b.queryBuilder('getRules').not, + 'The root json should have "not" flag set to true' + ); + + $b.queryBuilder('destroy'); + + $b.queryBuilder({ + plugins: { + 'not-group': {disable_template: true} + }, + filters: basic_filters, + rules: basic_rules + }); + + assert.ok( + $b.find('[data-not="group"]').length === 0, + 'Should not have added the button with disable_template=true' + ); + }); + + QUnit.test('SQL export', function (assert) { + $b.queryBuilder({ + filters: basic_filters, + rules: rules, + plugins: ['not-group'] + }); + + assert.equal( + $b.queryBuilder('getSQL').sql, + sql, + 'Should export SQL with NOT function' + ); + }); + + QUnit.test('SQL import', function (assert) { + $b.queryBuilder({ + filters: basic_filters, + plugins: ['not-group'] + }); + + $b.queryBuilder('setRulesFromSQL', sql); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + rules, + 'Should parse NOT SQL function' + ); + + $b.queryBuilder('setRulesFromSQL', sql2); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + rules2, + 'Should parse NOT SQL function with only one rule' + ); + + $b.queryBuilder('setRulesFromSQL', sql3); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + rules3, + 'Should parse NOT SQL function with same operation' + ); + + $b.queryBuilder('setRulesFromSQL', sql4); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + rules4, + 'Should parse NOT SQL function with negated root group' + ); + + $b.queryBuilder('setRulesFromSQL', sql5); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + rules5, + 'Should parse NOT SQL function with double negated root group' + ); + }); + + QUnit.test('Mongo export', function (assert) { + $b.queryBuilder({ + filters: basic_filters, + rules: rules, + plugins: ['not-group'] + }); + + assert.deepEqual( + $b.queryBuilder('getMongo'), + mongo, + 'Should export MongoDB with $nor function' + ); + + $b.queryBuilder('reset'); + + $b.queryBuilder('setRulesFromMongo', mongo); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + rules, + 'Should parse $nor MongoDB function' + ); + }); + + var rules = { + condition: 'OR', + not: false, + rules: [{ + id: 'name', + operator: 'equal', + value: 'Mistic' + }, { + condition: 'AND', + not: true, + rules: [{ + id: 'price', + operator: 'less', + value: 10.25 + }, { + id: 'category', + field: 'category', + operator: 'in', + value: ['mo', 'mu'] + }] + }] + }; + + var sql = 'name = \'Mistic\' OR ( NOT ( price < 10.25 AND category IN(\'mo\', \'mu\') ) ) '; + + var rules2 = { + condition: 'OR', + not: false, + rules: [{ + id: 'name', + operator: 'equal', + value: 'Mistic' + }, { + condition: 'AND', + not: true, + rules: [{ + id: 'price', + operator: 'less', + value: 10.25 + }] + }] + }; + + var sql2 = 'name = \'Mistic\' OR ( NOT ( price < 10.25 ) ) '; + + var rules3 = { + condition: 'OR', + not: false, + rules: [{ + id: 'name', + operator: 'equal', + value: 'Mistic' + }, { + condition: 'OR', + not: true, + rules: [{ + id: 'price', + operator: 'less', + value: 10.25 + }, { + id: 'category', + field: 'category', + operator: 'in', + value: ['mo', 'mu'] + }] + }] + }; + + var sql3 = 'name = \'Mistic\' OR ( NOT ( price < 10.25 OR category IN(\'mo\', \'mu\') ) ) '; + + var rules4 = { + condition: 'AND', + not: true, + rules: [{ + id: 'price', + operator: 'less', + value: 10.25 + }] + }; + + var sql4 = 'NOT ( price < 10.25 )'; + + var rules5 = { + condition: 'AND', + not: false, + rules: [{ + condition: 'AND', + not: true, + rules: [{ + id: 'price', + operator: 'less', + value: 10.25 + }] + }, { + condition: 'AND', + not: true, + rules: [{ + id: 'price', + operator: 'greater', + value: 20.5 + }] + }] + }; + + var sql5 = 'NOT ( price < 10.25 ) AND NOT ( price > 20.5 )'; + + var mongo = { + "$or": [{ + "name": "Mistic" + }, + { + "$nor": [{ + "$and": [{ + "price": {"$lt": 10.25} + }, { + "category": {"$in": ["mo", "mu"]} + }] + }] + }] + }; + +}); diff --git a/tests/plugins-data.module.js b/tests/plugins.sql-support.module.js similarity index 56% rename from tests/plugins-data.module.js rename to tests/plugins.sql-support.module.js index 490b66d6..ee347e57 100644 --- a/tests/plugins-data.module.js +++ b/tests/plugins.sql-support.module.js @@ -1,55 +1,13 @@ -$(function(){ +$(function() { var $b = $('#builder'); - QUnit.module('plugins-data', { + QUnit.module('plugins.sql-support', { afterEach: function() { $b.queryBuilder('destroy'); } }); - /** - * SQL import/export - */ - QUnit.test('sql-support', function(assert) { - var basic_rules_sql_raw = { - sql: 'price < 10.25 AND name IS NULL AND ( category IN(\'mo\', \'mu\') OR id != \'1234-azer-5678\' ) ' - }; - - var basic_rules_sql_stmt = { - sql: 'price < ? AND name IS NULL AND ( category IN(?, ?) OR id != ? ) ', - params: [10.25, 'mo', 'mu', '1234-azer-5678'] - }; - - var basic_rules_sql_stmt_num = { - sql: 'price < $1 AND name IS NULL AND ( category IN($2, $3) OR id != $4 ) ', - params: [10.25, 'mo', 'mu', '1234-azer-5678'] - }; - - var basic_rules_sql_stmt_num_at = { - sql: 'price < @1 AND name IS NULL AND ( category IN(@2, @3) OR id != @4 ) ', - params: [10.25, 'mo', 'mu', '1234-azer-5678'] - }; - - var basic_rules_sql_stmt_named = { - sql: 'price < :price_1 AND name IS NULL AND ( category IN(:category_1, :category_2) OR id != :id_1 ) ', - params: { - price_1: 10.25, - category_1: 'mo', - category_2: 'mu', - id_1: '1234-azer-5678' - } - }; - - var basic_rules_sql_stmt_named_at = { - sql: 'price < @price_1 AND name IS NULL AND ( category IN(@category_1, @category_2) OR id != @id_1 ) ', - params: { - price_1: 10.25, - category_1: 'mo', - category_2: 'mu', - id_1: '1234-azer-5678' - } - }; - + QUnit.test('Raw SQL', function(assert) { $b.queryBuilder({ filters: basic_filters, rules: basic_rules @@ -61,12 +19,46 @@ $(function(){ 'Should create SQL query' ); + $b.queryBuilder('reset'); + + $b.queryBuilder('setRulesFromSQL', basic_rules_sql_raw); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + basic_rules, + 'Should parse SQL query' + ); + }); + + QUnit.test('Placeholder SQL', function(assert) { + $b.queryBuilder({ + filters: basic_filters, + rules: basic_rules + }); + assert.deepEqual( $b.queryBuilder('getSQL', 'question_mark'), basic_rules_sql_stmt, 'Should create SQL query with statements (?)' ); + $b.queryBuilder('reset'); + + $b.queryBuilder('setRulesFromSQL', basic_rules_sql_stmt, 'question_mark'); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + basic_rules, + 'Should parse SQL query with statements (?)' + ); + }); + + QUnit.test('Numbered SQL', function(assert) { + $b.queryBuilder({ + filters: basic_filters, + rules: basic_rules + }); + assert.deepEqual( $b.queryBuilder('getSQL', 'numbered'), basic_rules_sql_stmt_num, @@ -79,61 +71,96 @@ $(function(){ 'Should create SQL query with statements (@ numbered)' ); - assert.deepEqual( - $b.queryBuilder('getSQL', 'named'), - basic_rules_sql_stmt_named, - 'Should create SQL query with statements (: named)' - ); + $b.queryBuilder('reset'); - assert.deepEqual( - $b.queryBuilder('getSQL', 'named(@)'), - basic_rules_sql_stmt_named_at, - 'Should create SQL query with statements (@ named)' - ); + $b.queryBuilder('setRulesFromSQL', basic_rules_sql_stmt_num, 'numbered'); - $b.queryBuilder('setRulesFromSQL', basic_rules_sql_raw); assert.rulesMatch( $b.queryBuilder('getRules'), basic_rules, - 'Should parse SQL query' + 'Should parse SQL query with statements ($ numbered)' ); - $b.queryBuilder('setRulesFromSQL', basic_rules_sql_stmt, 'question_mark'); + $b.queryBuilder('reset'); + + $b.queryBuilder('setRulesFromSQL', basic_rules_sql_stmt_num_at, 'numbered(@)'); + assert.rulesMatch( $b.queryBuilder('getRules'), basic_rules, - 'Should parse SQL query with statements (?)' + 'Should parse SQL query with statements (@ numbered)' ); + }); - $b.queryBuilder('setRulesFromSQL', basic_rules_sql_stmt_num, 'numbered'); - assert.rulesMatch( - $b.queryBuilder('getRules'), - basic_rules, - 'Should parse SQL query with statements ($ numbered)' + QUnit.test('Named SQL', function(assert) { + $b.queryBuilder({ + filters: basic_filters, + rules: basic_rules + }); + + assert.deepEqual( + $b.queryBuilder('getSQL', 'named'), + basic_rules_sql_stmt_named, + 'Should create SQL query with statements (: named)' ); - $b.queryBuilder('setRulesFromSQL', basic_rules_sql_stmt_num_at, 'numbered(@)'); - assert.rulesMatch( - $b.queryBuilder('getRules'), - basic_rules, - 'Should parse SQL query with statements (@ numbered)' + assert.deepEqual( + $b.queryBuilder('getSQL', 'named(@)'), + basic_rules_sql_stmt_named_at, + 'Should create SQL query with statements (@ named)' ); + $b.queryBuilder('reset'); + $b.queryBuilder('setRulesFromSQL', basic_rules_sql_stmt_named, 'named'); + assert.rulesMatch( $b.queryBuilder('getRules'), basic_rules, 'Should parse SQL query with statements (: named)' ); + $b.queryBuilder('reset'); + $b.queryBuilder('setRulesFromSQL', basic_rules_sql_stmt_named_at, 'named(@)'); + assert.rulesMatch( $b.queryBuilder('getRules'), basic_rules, 'Should parse SQL query with statements (@ named)' ); + }); + + QUnit.test('Special chars', function(assert) { + // PhantomJS is broken https://github.com/ariya/phantomjs/issues/14921 + if (!!window._phantom) { + assert.ok(true, 'Test ignore in PhantomJS'); + return; + } + + var chars = ['\'', '"', '$1', '$$', '$&', '$`', '$\'']; + + var sql = "name = '\\'' AND name = '\\\"' AND name = '$1' AND " + + "name = '$$' AND name = '$&' AND name = '$`' AND name = '$\\''"; - $b.queryBuilder('destroy'); + $b.queryBuilder({ + filters: basic_filters, + rules: chars.map(function(char) { + return { + id: 'name', + value: char + }; + }) + }); + + assert.equal( + $b.queryBuilder('getSQL').sql, + sql, + 'Should output SQL with escaped special chars' + ); + }); + + QUnit.test('All operators', function(assert) { $b.queryBuilder({ filters: basic_filters, rules: all_operators_rules @@ -145,31 +172,40 @@ $(function(){ 'Should convert all kind of operators to SQL' ); + $b.queryBuilder('reset'); + $b.queryBuilder('setRulesFromSQL', all_operators_rules_sql, 'question_mark'); + assert.rulesMatch( $b.queryBuilder('getRules'), all_operators_rules, 'Should parse all kind of operators from SQL' ); + }); + + QUnit.test('Nested rules', function(assert) { - $b.queryBuilder('destroy'); $b.queryBuilder({ filters: [ - {id: 'a', type: 'integer'}, - {id: 'b', type: 'integer'}, - {id: 'c', type: 'integer'}, - {id: 'd', type: 'integer'} + { id: 'a', type: 'integer' }, + { id: 'b', type: 'integer' }, + { id: 'c', type: 'integer' }, + { id: 'd', type: 'integer' } ] }); $b.queryBuilder('setRulesFromSQL', nested_rules_sql); + assert.rulesMatch( $b.queryBuilder('getRules'), nested_rules, 'Should parse SQL with deep nested rules' ); + $b.queryBuilder('reset'); + $b.queryBuilder('setRulesFromSQL', 'a = 5'); + assert.rulesMatch( $b.queryBuilder('getRules'), { @@ -184,57 +220,200 @@ $(function(){ ); }); - /** - * MongoDB import/export - */ - QUnit.test('mongo-support', function(assert) { - var basic_rules_mongodb = {'$and': [ - {'price': { '$lt': 10.25 }}, - {'name': null}, - {'$or': [ - {'category': {'$in': ['mo', 'mu']}}, - {'id': {'$ne': '1234-azer-5678'}} - ]} - ]}; + QUnit.test('Custom export/parsing', function(assert) { + var rules = { + condition: 'AND', + rules: [ + { + id: 'name', + operator: 'equal', + value: 'Mistic' + }, + { + id: 'last_days', + operator: 'greater', + value: 5 + } + ] + }; + + var sql = 'name = \'Mistic\' AND display_date > DATE_SUB(NOW(), INTERVAL 5 DAY)'; $b.queryBuilder({ - filters: basic_filters, - rules: basic_rules + filters: [ + { + id: 'name', + type: 'string' + }, + { + id: 'last_days', + field: 'display_date', + type: 'integer', + operators: ['greater'] + } + ] }); - assert.deepEqual( - $b.queryBuilder('getMongo'), - basic_rules_mongodb, - 'Should create MongoDB query' + $b.on('ruleToSQL.queryBuilder.filter', function(e, rule, sqlValue, sqlOperator) { + if (rule.id === 'last_days') { + e.value = rule.field + ' ' + sqlOperator('DATE_SUB(NOW(), INTERVAL ' + sqlValue + ' DAY)'); + } + }); + + $b.on('parseSQLNode.queryBuilder.filter', function(e) { + var data = e.value; + // left must be the field name and right must be the date_sub function + if (data.left && data.left.value == 'display_date' && data.operation == '>' && data.right && data.right.name == 'DATE_SUB') { + var right = data.right; + // 1st argument is "NOW()" and 2nd argument is a list + if (right.arguments.value.length === 2 && right.arguments.value[1].value.length === 3) { + var params = right.arguments.value[1].value; + // 1st item is "INTERVAL", 2nd item is the value, 3rd item is "DAY" + if (params[0].value == 'INTERVAL' && params[2].value == 'DAY') { + e.value = { + id: 'last_days', + operator: 'greater', + value: params[1].value + }; + } + } + } + }); + + $b.queryBuilder('setRules', rules); + + assert.equal( + $b.queryBuilder('getSQL').sql, + sql, + 'Should export custom date_sub function' ); - assert.deepEqual( - $b.queryBuilder('getRulesFromMongo', basic_rules_mongodb), - basic_rules, - 'Should return rules object from MongoDB query' + $b.queryBuilder('reset'); + $b.queryBuilder('setRulesFromSQL', sql); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + rules, + 'Should parse date_sub function' ); + }); + + QUnit.test('Automatically use filter from field', function(assert) { + var rules = { + condition: 'AND', + rules: [ + { + id: 'name', + operator: 'equal', + value: 'Mistic' + } + ] + }; + + var sql = 'username = \'Mistic\''; - $b.queryBuilder('destroy'); $b.queryBuilder({ - filters: basic_filters, - rules: all_operators_rules + filters: [ + { + id: 'name', + field: 'username', + type: 'string' + }, + { + id: 'last_days', + field: 'display_date', + type: 'integer' + } + ] }); - assert.deepEqual( - $b.queryBuilder('getMongo'), - all_operators_rules_mongodb, - 'Should successfully convert all kind of operators to MongoDB' - ); + $b.queryBuilder('setRulesFromSQL', sql); - $b.queryBuilder('setRulesFromMongo', all_operators_rules_mongodb); assert.rulesMatch( $b.queryBuilder('getRules'), - all_operators_rules, - 'Should successfully parse all kind of operators from MongoDB' + rules, + 'Should use "name" filter from "username" field' + ); + }); + + QUnit.test('Cast booleans', function(assert) { + $b.queryBuilder({ + plugins: { + 'sql-support': { + boolean_as_integer: true + } + }, + filters: [ + { + id: 'done', + type: 'boolean' + } + ], + rules: [ + { + id: 'done', + operator: 'equal', + value: true + } + ] + }); + + assert.rulesMatch( + $b.queryBuilder('getSQL'), + 'done = 1', + 'Should convert boolean value to integer' + ); + + // don't do that in real life ! + $b[0].queryBuilder.plugins['sql-support'].boolean_as_integer = false; + + assert.rulesMatch( + $b.queryBuilder('getSQL'), + 'done = true', + 'Should not convert boolean value to integer' ); }); + var basic_rules_sql_raw = { + sql: 'price < 10.25 AND name IS NULL AND ( category IN(\'mo\', \'mu\') OR id != \'1234-azer-5678\' ) ' + }; + + var basic_rules_sql_stmt = { + sql: 'price < ? AND name IS NULL AND ( category IN(?, ?) OR id != ? ) ', + params: [10.25, 'mo', 'mu', '1234-azer-5678'] + }; + + var basic_rules_sql_stmt_num = { + sql: 'price < $1 AND name IS NULL AND ( category IN($2, $3) OR id != $4 ) ', + params: [10.25, 'mo', 'mu', '1234-azer-5678'] + }; + + var basic_rules_sql_stmt_num_at = { + sql: 'price < @1 AND name IS NULL AND ( category IN(@2, @3) OR id != @4 ) ', + params: [10.25, 'mo', 'mu', '1234-azer-5678'] + }; + + var basic_rules_sql_stmt_named = { + sql: 'price < :price_1 AND name IS NULL AND ( category IN(:category_1, :category_2) OR id != :id_1 ) ', + params: { + price_1: 10.25, + category_1: 'mo', + category_2: 'mu', + id_1: '1234-azer-5678' + } + }; + + var basic_rules_sql_stmt_named_at = { + sql: 'price < @price_1 AND name IS NULL AND ( category IN(@category_1, @category_2) OR id != @id_1 ) ', + params: { + price_1: 10.25, + category_1: 'mo', + category_2: 'mu', + id_1: '1234-azer-5678' + } + }; + var all_operators_rules = { condition: 'AND', rules: [{ @@ -248,35 +427,35 @@ $(function(){ }, { id: 'category', operator: 'in', - value: ['bk','mo'] + value: ['bk', 'mo'] }, { id: 'category', operator: 'not_in', - value: ['bk','mo'] + value: ['bk', 'mo'] }, { id: 'price', operator: 'less', - value: '5' + value: 5 }, { id: 'price', operator: 'less_or_equal', - value: '5' + value: 5 }, { id: 'price', operator: 'greater', - value: '4' + value: 4 }, { id: 'price', operator: 'greater_or_equal', - value: '4' + value: 4 }, { id: 'price', operator: 'between', - value: ['4','5'] + value: [4,5] }, { id: 'price', operator: 'not_between', - value: ['4','5'] + value: [4,5] }, { id: 'name', operator: 'begins_with', @@ -321,27 +500,26 @@ $(function(){ }; var all_operators_rules_sql = { - sql: - 'name = ? ' + - 'AND name != ? ' + - 'AND category IN(?, ?) ' + - 'AND category NOT IN(?, ?) ' + - 'AND price < ? ' + - 'AND price <= ? ' + - 'AND price > ? ' + - 'AND price >= ? ' + - 'AND price BETWEEN ? AND ? ' + - 'AND price NOT BETWEEN ? AND ? ' + - 'AND name LIKE(?) ' + - 'AND name NOT LIKE(?) ' + - 'AND name LIKE(?) ' + - 'AND name NOT LIKE(?) ' + - 'AND name LIKE(?) ' + - 'AND name NOT LIKE(?) ' + - 'AND name = \'\' ' + - 'AND name != \'\' ' + - 'AND name IS NULL ' + - 'AND name IS NOT NULL', + sql: 'name = ? ' + + 'AND name != ? ' + + 'AND category IN(?, ?) ' + + 'AND category NOT IN(?, ?) ' + + 'AND price < ? ' + + 'AND price <= ? ' + + 'AND price > ? ' + + 'AND price >= ? ' + + 'AND price BETWEEN ? AND ? ' + + 'AND price NOT BETWEEN ? AND ? ' + + 'AND name LIKE(?) ' + + 'AND name NOT LIKE(?) ' + + 'AND name LIKE(?) ' + + 'AND name NOT LIKE(?) ' + + 'AND name LIKE(?) ' + + 'AND name NOT LIKE(?) ' + + 'AND name = \'\' ' + + 'AND name != \'\' ' + + 'AND name IS NULL ' + + 'AND name IS NOT NULL', params: [ 'foo', 'foo', @@ -362,31 +540,6 @@ $(function(){ ] }; - var all_operators_rules_mongodb = { - $and: [ - { name: 'foo' }, - { name: {$ne: 'foo'} }, - { category: { $in: ['bk','mo'] }}, - { category: { $nin: ['bk','mo'] }}, - { price: {$lt: 5} }, - { price: {$lte: 5} }, - { price: {$gt: 4} }, - { price: {$gte: 4} }, - { price: {$gte: 4, $lte: 5} }, - { price: {$lt: 4, $gt: 5} }, - { name: {$regex: '^foo'} }, - { name: {$regex: '^(?!foo)'} }, - { name: {$regex: 'foo'} }, - { name: {$regex: '^((?!foo).)*$', $options: 's'} }, - { name: {$regex: 'foo$'} }, - { name: {$regex: '(?=3.0.0 <4.0.0": + version "3.5.3" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chosenjs@^1.4.3: + version "1.4.3" + resolved "https://registry.npmjs.org/chosenjs/-/chosenjs-1.4.3.tgz" + integrity sha512-RMgwgPszZjQgP+PtcuTaXnhWc9bwcqfP8b4QonKU0vvFYb39Y+WRmsGp0xO0E1MYwyEX/ZcFYecprK8sF/8s9A== + dependencies: + jquery ">=1.4.4" + +clean-css@~4.1.1: + version "4.1.11" + resolved "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz" + integrity sha512-a3ZEe58u+LizPdSCHM0jIGeKu1hN+oqqXXc1i70mnV0x2Ox3/ho1pE6Y8HD6yhDts5lEQs028H9kutlihP77uQ== + dependencies: + source-map "0.5.x" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colors@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz" + integrity sha512-ENwblkFQpqqia6b++zLD/KUWafYlVY/UNnAp7oz7LY7E924wmpye416wBOmvv/HMWzl8gL1kJlfvId/1Dg176w== + +colors@1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + +commander@~2.19.0: + version "2.19.0" + resolved "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz" + integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concurrently@^8.2.0: + version "8.2.2" + resolved "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz" + integrity sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg== + dependencies: + chalk "^4.1.2" + date-fns "^2.30.0" + lodash "^4.17.21" + rxjs "^7.8.1" + shell-quote "^1.8.1" + spawn-command "0.0.2" + supports-color "^8.1.1" + tree-kill "^1.2.2" + yargs "^17.7.2" + +connect@^3.7.0: + version "3.7.0" + resolved "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" + +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cross-spawn@^7.0.0: + version "7.0.3" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +css-select@~1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz" + integrity sha512-dUQOBoqdR7QwV90WysXPLXG5LO7nhYBgiWVfxF80DKPF8zx1t/pUd2FYy73emg3zrjtM6dzmYgbHKfV2rxiHQA== + dependencies: + boolbase "~1.0.0" + css-what "2.1" + domutils "1.5.1" + nth-check "~1.0.1" + +css-what@2.1: + version "2.1.3" + resolved "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz" + integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== + +date-fns@^2.30.0: + version "2.30.0" + resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== + dependencies: + "@babel/runtime" "^7.21.0" + +dateformat@~4.6.2: + version "4.6.3" + resolved "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz" + integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== + +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +deepmerge@^2.1.0: + version "2.2.1" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz" + integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +depd@~2.0.0, depd@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-file@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz" + integrity sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q== + +dom-serializer@^1.0.1: + version "1.4.1" + resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +dom-serializer@~0.1.0, dom-serializer@0: + version "0.1.1" + resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz" + integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== + dependencies: + domelementtype "^1.3.0" + entities "^1.1.1" + +domelementtype@^1.3.0, domelementtype@^1.3.1, domelementtype@1: + version "1.3.1" + resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.3.0" + resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^2.3.0: + version "2.4.2" + resolved "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz" + integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== + dependencies: + domelementtype "1" + +domhandler@^3.0.0: + version "3.3.0" + resolved "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz" + integrity sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA== + dependencies: + domelementtype "^2.0.1" + +domhandler@^4.2.0: + version "4.3.1" + resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== + dependencies: + domelementtype "^2.2.0" + +domutils@^1.5.1, domutils@1.5.1: + version "1.5.1" + resolved "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz" + integrity sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw== + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^2.0.0: + version "2.8.0" + resolved "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + +duplexer@^0.1.1, duplexer@~0.1.1: + version "0.1.2" + resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz" + integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +entities@^1.1.1, entities@~1.1.1: + version "1.1.2" + resolved "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +entities@~2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz" + integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.2: + version "1.0.5" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-stream@4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz" + integrity sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA== + dependencies: + duplexer "^0.1.1" + from "^0.1.7" + map-stream "0.0.7" + pause-stream "^0.0.11" + split "^1.0.1" + stream-combiner "^0.2.2" + through "^2.3.8" + +eventemitter2@~0.4.13: + version "0.4.14" + resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz" + integrity sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ== + +exit@~0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expand-tilde@^2.0.0, expand-tilde@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz" + integrity sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw== + dependencies: + homedir-polyfill "^1.0.1" + +extend@^3.0.1, extend@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +faye-websocket@0.11.4: + version "0.11.4" + resolved "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +figures@^1.0.1: + version "1.7.0" + resolved "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz" + integrity sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ== + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +file-sync-cmp@^0.1.0: + version "0.1.1" + resolved "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz" + integrity sha512-0k45oWBokCqh2MOexeYKpyqmGKG+8mQ2Wd8iawx+uWd/weWJQAZ6SoPybagdCI4xFisag8iAR77WPm4h3pTfxA== + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +findup-sync@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz" + integrity sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ== + dependencies: + detect-file "^1.0.0" + is-glob "^4.0.0" + micromatch "^4.0.2" + resolve-dir "^1.0.1" + +findup-sync@~5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz" + integrity sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ== + dependencies: + detect-file "^1.0.0" + is-glob "^4.0.3" + micromatch "^4.0.4" + resolve-dir "^1.0.1" + +fined@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz" + integrity sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng== + dependencies: + expand-tilde "^2.0.2" + is-plain-object "^2.0.3" + object.defaults "^1.1.0" + object.pick "^1.2.0" + parse-filepath "^1.0.1" + +flagged-respawn@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz" + integrity sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q== + +foodoc@^0.0.9: + version "0.0.9" + resolved "https://registry.npmjs.org/foodoc/-/foodoc-0.0.9.tgz" + integrity sha512-TjswPE8Vd8Wu1AAwu/aet/g0FlxCtEfWmkbFXppMq1FmgdwvH33U/jPJkIggAM7RoLPTB5UvNB5Cgg0PII/smQ== + dependencies: + cheerio "^0.22.0" + extend "^3.0.1" + glob "^7.1.2" + grunt "^1.0.2" + grunt-contrib-clean "^1.1.0" + grunt-contrib-copy "^1.0.0" + grunt-contrib-cssmin "^2.2.1" + grunt-contrib-uglify "^3.3.0" + handlebars "^4.0.11" + handlebars-layouts "^3.1.4" + jsdoc "^3.5.5" + lunr "^1.0.0" + moment "^2.22.1" + sanitize-html "^1.18.2" + +for-in@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz" + integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ== + +for-own@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz" + integrity sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg== + dependencies: + for-in "^1.0.1" + +foreground-child@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz" + integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +from@^0.1.7: + version "0.1.7" + resolved "https://registry.npmjs.org/from/-/from-0.1.7.tgz" + integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +getobject@~1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/getobject/-/getobject-1.0.2.tgz" + integrity sha512-2zblDBaFcb3rB4rF77XVnuINOE2h2k/OnqXAiy0IrTxUfV1iFp3la33oAQVY9pCpWU268WFYVt2t71hlMuLsOg== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^10.3.1: + version "10.3.10" + resolved "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz" + integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.3.5" + minimatch "^9.0.1" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry "^1.10.1" + +glob@^7.1.2: + version "7.2.3" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@~7.1.6: + version "7.1.7" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-modules@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz" + integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== + dependencies: + global-prefix "^1.0.1" + is-windows "^1.0.1" + resolve-dir "^1.0.0" + +global-prefix@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz" + integrity sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg== + dependencies: + expand-tilde "^2.0.2" + homedir-polyfill "^1.0.1" + ini "^1.3.4" + is-windows "^1.0.1" + which "^1.2.14" + +graceful-fs@^4.1.9: + version "4.2.11" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +grunt-cli@~1.4.3: + version "1.4.3" + resolved "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.4.3.tgz" + integrity sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ== + dependencies: + grunt-known-options "~2.0.0" + interpret "~1.1.0" + liftup "~3.0.1" + nopt "~4.0.1" + v8flags "~3.2.0" + +grunt-contrib-clean@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-1.1.0.tgz" + integrity sha512-tET+TYTd8vCtKeGwbLjoH8+SdI8ngVzGbPr7vlWkewG7mYYHIccd2Ldxq+PK3DyBp5Www3ugdkfsjoNKUl5MTg== + dependencies: + async "^1.5.2" + rimraf "^2.5.1" + +grunt-contrib-copy@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz" + integrity sha512-gFRFUB0ZbLcjKb67Magz1yOHGBkyU6uL29hiEW1tdQ9gQt72NuMKIy/kS6dsCbV0cZ0maNCb0s6y+uT1FKU7jA== + dependencies: + chalk "^1.1.1" + file-sync-cmp "^0.1.0" + +grunt-contrib-cssmin@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/grunt-contrib-cssmin/-/grunt-contrib-cssmin-2.2.1.tgz" + integrity sha512-IXNomhQ5ekVZbDbj/ik5YccoD9khU6LT2fDXqO1+/Txjq8cp0tQKjVS8i8EAbHOrSDkL7/UD6A7b+xj98gqh9w== + dependencies: + chalk "^1.0.0" + clean-css "~4.1.1" + maxmin "^2.1.0" + +grunt-contrib-uglify@^3.3.0: + version "3.4.0" + resolved "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-3.4.0.tgz" + integrity sha512-UXsTpeP0pytpTYlmll3RDndsRXfdwmrf1tI/AtD/PrArQAzGmKMvj83aVt3D8egWlE6KqPjsJBLCCvfC52LI/A== + dependencies: + chalk "^1.0.0" + maxmin "^2.1.0" + uglify-js "~3.4.0" + uri-path "^1.0.0" + +grunt-known-options@~2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-2.0.0.tgz" + integrity sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA== + +grunt-legacy-log-utils@~2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz" + integrity sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw== + dependencies: + chalk "~4.1.0" + lodash "~4.17.19" + +grunt-legacy-log@~3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz" + integrity sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA== + dependencies: + colors "~1.1.2" + grunt-legacy-log-utils "~2.1.0" + hooker "~0.2.3" + lodash "~4.17.19" + +grunt-legacy-util@~2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.1.tgz" + integrity sha512-2bQiD4fzXqX8rhNdXkAywCadeqiPiay0oQny77wA2F3WF4grPJXCvAcyoWUJV+po/b15glGkxuSiQCK299UC2w== + dependencies: + async "~3.2.0" + exit "~0.1.2" + getobject "~1.0.0" + hooker "~0.2.3" + lodash "~4.17.21" + underscore.string "~3.3.5" + which "~2.0.2" + +grunt@^1.0.2, grunt@>=0.4.5: + version "1.6.1" + resolved "https://registry.npmjs.org/grunt/-/grunt-1.6.1.tgz" + integrity sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA== + dependencies: + dateformat "~4.6.2" + eventemitter2 "~0.4.13" + exit "~0.1.2" + findup-sync "~5.0.0" + glob "~7.1.6" + grunt-cli "~1.4.3" + grunt-known-options "~2.0.0" + grunt-legacy-log "~3.0.0" + grunt-legacy-util "~2.0.1" + iconv-lite "~0.6.3" + js-yaml "~3.14.0" + minimatch "~3.0.4" + nopt "~3.0.6" + +gzip-size@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz" + integrity sha512-6s8trQiK+OMzSaCSVXX+iqIcLV9tC+E73jrJrJTyS4h/AJhlxHvzFKqM1YLDJWRGgHX8uLkBeXkA0njNj39L4w== + dependencies: + duplexer "^0.1.1" + +handlebars-layouts@^3.1.4: + version "3.1.4" + resolved "https://registry.npmjs.org/handlebars-layouts/-/handlebars-layouts-3.1.4.tgz" + integrity sha512-2llBmvnj8ueOfxNHdRzJOcgalzZjYVd9+WAl93kPYmlX4WGx7FTHTzNxhK+i9YKY2OSjzfehgpLiIwP/OJr6tw== + +handlebars@^4.0.11: + version "4.7.8" + resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz" + integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.2" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz" + integrity sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg== + dependencies: + ansi-regex "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + +homedir-polyfill@^1.0.1: + version "1.0.3" + resolved "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz" + integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== + dependencies: + parse-passwd "^1.0.0" + +hooker@~0.2.3: + version "0.2.3" + resolved "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz" + integrity sha512-t+UerCsQviSymAInD01Pw+Dn/usmz1sRO+3Zk1+lx8eg+WKpD2ulcwWqHHL0+aseRBr+3+vIhiG1K1JTwaIcTA== + +htmlparser2@^3.9.1: + version "3.10.1" + resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== + dependencies: + domelementtype "^1.3.1" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^3.1.1" + +htmlparser2@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz" + integrity sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q== + dependencies: + domelementtype "^2.0.1" + domhandler "^3.0.0" + domutils "^2.0.0" + entities "^2.0.0" + +http-auth-connect@^1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/http-auth-connect/-/http-auth-connect-1.0.6.tgz" + integrity sha512-yaO0QSCPqGCjPrl3qEEHjJP+lwZ6gMpXLuCBE06eWwcXomkI5TARtu0kxf9teFuBj6iaV3Ybr15jaWUvbzNzHw== + +http-auth@4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/http-auth/-/http-auth-4.2.0.tgz" + integrity sha512-trIkGI7dgnFJ5k8YaQFSr1Q5uq9c19vK6Y9ZCjlY0zBEQgdJpXZU3Cyrmk4nwrAGy4pKJhs599o7q6eicbVnhw== + dependencies: + apache-crypt "^1.1.2" + apache-md5 "^1.0.6" + bcryptjs "^2.4.3" + uuid "^8.3.2" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + +iconv-lite@~0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + +immutable@^4.0.0: + version "4.3.4" + resolved "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz" + integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@^2.0.1, inherits@^2.0.3, inherits@2, inherits@2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +ini@^1.3.4: + version "1.3.8" + resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +interactjs@^1.3.3: + version "1.10.26" + resolved "https://registry.npmjs.org/interactjs/-/interactjs-1.10.26.tgz" + integrity sha512-5gNTNDTfEHp2EifqtWGi5VkD3CMZVJSTGmtK/IsVRd+rkOk3E63iVs5Z+IeD5K1Lr0qZpU2754VHAwf5i+Z9xg== + dependencies: + "@interactjs/types" "1.10.26" + +interpret@~1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz" + integrity sha512-CLM8SNMDu7C5psFCn6Wg/tgpj/bKAg7hc2gWqcuR9OD5Ft9PhBpIu8PLicPeis+xDd6YX2ncI8MCA64I9tftIA== + +is-absolute@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz" + integrity sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA== + dependencies: + is-relative "^1.0.0" + is-windows "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-relative@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz" + integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA== + dependencies: + is-unc-path "^1.0.0" + +is-unc-path@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz" + integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ== + dependencies: + unc-path-regex "^0.1.2" + +is-windows@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jackspeak@^2.3.5: + version "2.3.6" + resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz" + integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +jquery-extendext@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/jquery-extendext/-/jquery-extendext-1.0.0.tgz" + integrity sha512-gDJjpQ8ISd4ZMwSn0yXSuyufsPMCeWtWJ8kmqE85VromJqyVT2sdkHzS5yL0w1GK81UpOzmr2f9JgFln2sbJSg== + dependencies: + jquery ">=1.9.1" + +jquery-ui@^1.13.2: + version "1.13.2" + resolved "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.13.2.tgz" + integrity sha512-wBZPnqWs5GaYJmo1Jj0k/mrSkzdQzKDwhXNtHKcBdAcKVxMM3KNYFq+iJ2i1rwiG53Z8M4mTn3Qxrm17uH1D4Q== + dependencies: + jquery ">=1.8.0 <4.0.0" + +"jquery@^1.7.0 || ^2 || ^3", jquery@^3.5.1, jquery@>=1.4.4, "jquery@>=1.8.0 <4.0.0", jquery@>=1.9.1: + version "3.7.1" + resolved "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz" + integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg== + +js-yaml@~3.14.0: + version "3.14.1" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js2xmlparser@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz" + integrity sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA== + dependencies: + xmlcreate "^2.0.4" + +jsdoc@^3.5.5: + version "3.6.11" + resolved "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.11.tgz" + integrity sha512-8UCU0TYeIYD9KeLzEcAu2q8N/mx9O3phAGl32nmHlE0LpaJL71mMkP4d+QE5zWfNt50qheHtOZ0qoxVrsX5TUg== + dependencies: + "@babel/parser" "^7.9.4" + "@types/markdown-it" "^12.2.3" + bluebird "^3.7.2" + catharsis "^0.9.0" + escape-string-regexp "^2.0.0" + js2xmlparser "^4.0.2" + klaw "^3.0.0" + markdown-it "^12.3.2" + markdown-it-anchor "^8.4.1" + marked "^4.0.10" + mkdirp "^1.0.4" + requizzle "^0.2.3" + strip-json-comments "^3.1.0" + taffydb "2.6.2" + underscore "~1.13.2" + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +klaw@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz" + integrity sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g== + dependencies: + graceful-fs "^4.1.9" + +liftup@~3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/liftup/-/liftup-3.0.1.tgz" + integrity sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw== + dependencies: + extend "^3.0.2" + findup-sync "^4.0.0" + fined "^1.2.0" + flagged-respawn "^1.0.1" + is-plain-object "^2.0.4" + object.map "^1.0.1" + rechoir "^0.7.0" + resolve "^1.19.0" + +linkify-it@^3.0.1: + version "3.0.3" + resolved "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz" + integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ== + dependencies: + uc.micro "^1.0.1" + +lodash.assignin@^4.0.9: + version "4.2.0" + resolved "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz" + integrity sha512-yX/rx6d/UTVh7sSVWVSIMjfnz95evAgDFdb1ZozC35I9mSFCkmzptOzevxjgbQUsc78NR44LVHWjsoMQXy9FDg== + +lodash.bind@^4.1.4: + version "4.2.1" + resolved "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz" + integrity sha512-lxdsn7xxlCymgLYo1gGvVrfHmkjDiyqVv62FAeF2i5ta72BipE1SLxw8hPEPLhD4/247Ijw07UQH7Hq/chT5LA== + +lodash.defaults@^4.0.1: + version "4.2.0" + resolved "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + +lodash.filter@^4.4.0: + version "4.6.0" + resolved "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz" + integrity sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ== + +lodash.flatten@^4.2.0: + version "4.4.0" + resolved "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz" + integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g== + +lodash.foreach@^4.3.0: + version "4.5.0" + resolved "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz" + integrity sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ== + +lodash.map@^4.4.0: + version "4.6.0" + resolved "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz" + integrity sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q== + +lodash.merge@^4.4.0: + version "4.6.2" + resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.pick@^4.2.1: + version "4.4.0" + resolved "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz" + integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q== + +lodash.reduce@^4.4.0: + version "4.6.0" + resolved "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz" + integrity sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw== + +lodash.reject@^4.4.0: + version "4.6.0" + resolved "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz" + integrity sha512-qkTuvgEzYdyhiJBx42YPzPo71R1aEr0z79kAv7Ixg8wPFEjgRgJdUsGMG3Hf3OYSF/kHI79XhNlt+5Ar6OzwxQ== + +lodash.some@^4.4.0: + version "4.6.0" + resolved "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz" + integrity sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ== + +lodash@^4.17.15, lodash@^4.17.21, lodash@~4.17.19, lodash@~4.17.21: + version "4.17.21" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +"lru-cache@^9.1.1 || ^10.0.0": + version "10.1.0" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz" + integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag== + +lunr@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/lunr/-/lunr-1.0.0.tgz" + integrity sha512-vGgr9YUMBfL1izpsb4RASwPz58JSSdmcTocuCs2v0PyGU3e7CDJWuS5psl4O2m9t0CsNemeR+jhxu2xNkXCM2A== + +make-iterator@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz" + integrity sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw== + dependencies: + kind-of "^6.0.2" + +map-cache@^0.2.0: + version "0.2.2" + resolved "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz" + integrity sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg== + +map-stream@0.0.7: + version "0.0.7" + resolved "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz" + integrity sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ== + +markdown-it-anchor@^8.4.1: + version "8.6.7" + resolved "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz" + integrity sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA== + +markdown-it@*, markdown-it@^12.3.2: + version "12.3.2" + resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz" + integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg== + dependencies: + argparse "^2.0.1" + entities "~2.1.0" + linkify-it "^3.0.1" + mdurl "^1.0.1" + uc.micro "^1.0.5" + +marked@^4.0.10: + version "4.3.0" + resolved "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz" + integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== + +maxmin@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/maxmin/-/maxmin-2.1.0.tgz" + integrity sha512-NWlApBjW9az9qRPaeg7CX4sQBWwytqz32bIEo1PW9pRW+kBP9KLRfJO3UC+TV31EcQZEUq7eMzikC7zt3zPJcw== + dependencies: + chalk "^1.0.0" + figures "^1.0.1" + gzip-size "^3.0.0" + pretty-bytes "^3.0.0" + +mdurl@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz" + integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== + +micromatch@^4.0.2, micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@~2.1.17, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +minimatch@^3.0.4, minimatch@~3.0.4: + version "3.0.8" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz" + integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^9.0.1: + version "9.0.3" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.5: + version "1.2.8" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": + version "7.0.4" + resolved "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz" + integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== + +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +moment@^2.22.1, moment@^2.29.1: + version "2.30.1" + resolved "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + +morgan@^1.10.0: + version "1.10.0" + resolved "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz" + integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== + dependencies: + basic-auth "~2.0.1" + debug "2.6.9" + depd "~2.0.0" + on-finished "~2.3.0" + on-headers "~1.0.2" + +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +nodemon@^2.0.22: + version "2.0.22" + resolved "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz" + integrity sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ== + dependencies: + chokidar "^3.5.2" + debug "^3.2.7" + ignore-by-default "^1.0.1" + minimatch "^3.1.2" + pstree.remy "^1.1.8" + semver "^5.7.1" + simple-update-notifier "^1.0.7" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.5" + +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz" + integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg== + dependencies: + abbrev "1" + +nopt@~3.0.6: + version "3.0.6" + resolved "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz" + integrity sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg== + dependencies: + abbrev "1" + +nopt@~4.0.1: + version "4.0.3" + resolved "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz" + integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +nth-check@~1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz" + integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ== + +object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object.defaults@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz" + integrity sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA== + dependencies: + array-each "^1.0.1" + array-slice "^1.0.0" + for-own "^1.0.0" + isobject "^3.0.0" + +object.map@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz" + integrity sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w== + dependencies: + for-own "^1.0.0" + make-iterator "^1.0.0" + +object.pick@^1.2.0: + version "1.3.0" + resolved "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz" + integrity sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ== + dependencies: + isobject "^3.0.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +open@^8.4.2: + version "8.4.2" + resolved "https://registry.npmjs.org/open/-/open-8.4.2.tgz" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz" + integrity sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ== + +os-tmpdir@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + +osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz" + integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +parse-filepath@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz" + integrity sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q== + dependencies: + is-absolute "^1.0.0" + map-cache "^0.2.0" + path-root "^0.1.1" + +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz" + integrity sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q== + +parse-srcset@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz" + integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q== + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-root-regex@^0.1.0: + version "0.1.2" + resolved "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz" + integrity sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ== + +path-root@^0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz" + integrity sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg== + dependencies: + path-root-regex "^0.1.0" + +path-scurry@^1.10.1: + version "1.10.1" + resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz" + integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== + dependencies: + lru-cache "^9.1.1 || ^10.0.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +pause-stream@^0.0.11: + version "0.0.11" + resolved "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz" + integrity sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A== + dependencies: + through "~2.3" + +picocolors@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz" + integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +postcss@^7.0.27: + version "7.0.39" + resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz" + integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== + dependencies: + picocolors "^0.2.1" + source-map "^0.6.1" + +pretty-bytes@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-3.0.1.tgz" + integrity sha512-eb7ZAeUTgfh294cElcu51w+OTRp/6ItW758LjwJSK72LDevcuJn0P4eD71PLMDGPwwatXmAmYHTkzvpKlJE3ow== + dependencies: + number-is-nan "^1.0.0" + +proxy-middleware@^0.15.0: + version "0.15.0" + resolved "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.15.0.tgz" + integrity sha512-EGCG8SeoIRVMhsqHQUdDigB2i7qU7fCsWASwn54+nPutYO8n4q6EiwMzyfWlC+dzRFExP+kvcnDFdBDHoZBU7Q== + +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +readable-stream@^3.1.1: + version "3.6.2" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.7.0: + version "0.7.1" + resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz" + integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg== + dependencies: + resolve "^1.9.0" + +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +requizzle@^0.2.3: + version "0.2.4" + resolved "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz" + integrity sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw== + dependencies: + lodash "^4.17.21" + +resolve-dir@^1.0.0, resolve-dir@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz" + integrity sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg== + dependencies: + expand-tilde "^2.0.0" + global-modules "^1.0.0" + +resolve@^1.19.0, resolve@^1.9.0: + version "1.22.8" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rimraf@^2.5.1: + version "2.7.1" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +rxjs@^7.8.1: + version "7.8.1" + resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + dependencies: + tslib "^2.1.0" + +safe-buffer@>=5.1.0, safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sanitize-html@^1.18.2: + version "1.27.5" + resolved "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.27.5.tgz" + integrity sha512-M4M5iXDAUEcZKLXkmk90zSYWEtk5NH3JmojQxKxV371fnMh+x9t1rqdmXaGoyEHw3z/X/8vnFhKjGL5xFGOJ3A== + dependencies: + htmlparser2 "^4.1.0" + lodash "^4.17.15" + parse-srcset "^1.0.2" + postcss "^7.0.27" + +sass@^1.63.6: + version "1.70.0" + resolved "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz" + integrity sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + +semver@^5.7.1: + version "5.7.2" + resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@~7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz" + integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== + +send@^0.18.0: + version "0.18.0" + resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz" + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.8.1: + version "1.8.1" + resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +simple-update-notifier@^1.0.7: + version "1.1.0" + resolved "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz" + integrity sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg== + dependencies: + semver "~7.0.0" + +"source-map-js@>=0.6.2 <2.0.0": + version "1.0.2" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@0.5.x: + version "0.5.7" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + +spawn-command@0.0.2: + version "0.0.2" + resolved "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz" + integrity sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ== + +split@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/split/-/split-1.0.1.tgz" + integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== + dependencies: + through "2" + +sprintf-js@^1.1.1: + version "1.1.3" + resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +sql-parser-mistic@^1.2.3: + version "1.2.3" + resolved "https://registry.npmjs.org/sql-parser-mistic/-/sql-parser-mistic-1.2.3.tgz" + integrity sha512-2jyVSr7jIgbeFnPW8JO4hTMkDP5mTxbbWX+P7GcCbCwHp+ffiJeQGBK4dDLoPZvexK2Wgy0aUBfsWgc2DPhYRg== + +"statuses@>= 1.4.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +stream-combiner@^0.2.2: + version "0.2.2" + resolved "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz" + integrity sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ== + dependencies: + duplexer "~0.1.1" + through "~2.3.4" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" + integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-json-comments@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" + integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g== + +supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +taffydb@2.6.2: + version "2.6.2" + resolved "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz" + integrity sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA== + +through@^2.3.8, through@~2.3, through@~2.3.4, through@2: + version "2.3.8" + resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + +tree-kill@^1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + +tslib@^2.1.0: + version "2.6.2" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + +uc.micro@^1.0.1, uc.micro@^1.0.5: + version "1.0.6" + resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + +uglify-js@^3.1.4, uglify-js@~3.4.0: + version "3.4.10" + resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz" + integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw== + dependencies: + commander "~2.19.0" + source-map "~0.6.1" + +unc-path-regex@^0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz" + integrity sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg== + +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + +underscore.string@~3.3.5: + version "3.3.6" + resolved "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz" + integrity sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ== + dependencies: + sprintf-js "^1.1.1" + util-deprecate "^1.0.2" + +underscore@~1.13.2: + version "1.13.6" + resolved "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz" + integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== + +unix-crypt-td-js@^1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/unix-crypt-td-js/-/unix-crypt-td-js-1.1.4.tgz" + integrity sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw== + +unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +uri-path@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/uri-path/-/uri-path-1.0.0.tgz" + integrity sha512-8pMuAn4KacYdGMkFaoQARicp4HSw24/DHOVKWqVRJ8LhhAwPPFpdGvdL9184JVmUwe7vz7Z9n6IqI6t5n2ELdg== + +util-deprecate@^1.0.1, util-deprecate@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +v8flags@~3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz" + integrity sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg== + dependencies: + homedir-polyfill "^1.0.1" + +vary@^1: + version "1.1.2" + resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +websocket-driver@>=0.5.1: + version "0.7.4" + resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +which@^1.2.14: + version "1.3.1" + resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1, which@~2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" + integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +xmlcreate@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz" + integrity sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1"