diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..7715ae28 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.json] +indent_size = 2 +insert_final_newline = false + +[*.scss] +indent_size = 2 + +[*.html] +indent_size = 2 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..be3320fd --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,21 @@ +# Guidelines for contributing + +## Work on `dev` +Any merge request should be created from and issued to the `dev` branch. + +Do not add the `dist` files to your pull request. The directory is ignored for a reason: it is generated and pushed only when doing a release on `master`. + +## Core vs Plugins +I want to keep the core clean of extra (and certainly awesome) functionalities. That includes, but is not limited to, export/import plugins, visual aids, etc. + +Check the doc about [creating plugins](http://querybuilder.js.org/dev/plugins.html) and [use events](http://querybuilder.js.org/dev/events.html). + +I reserve the right to refuse any plugin I think is not useful for many people. Particularly, only import/export plugins for mainstream data storages will be integrated in the main repository. Others should be in a separated repository. But it's totally possible to add a link to your repository in the documentation. + +## Unit tests +Any big feature must have it's own QUnit tests suite. Of course existing tests must still pass after changes. + +I won't merge any branch not passing the TravisCI build, including JShint/JSCS/SCSSlint compliance. + +## Translations +Source language files are plain JSON files which will be converted to executable JS files by the build task. The `__locale` key must be filled with the international name of the language + 2-chars code and the `__author` key can be used to give information about the translator. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..7a8c011b --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,6 @@ +**Issues guidelines** + +- Please search in the [documentation](http://querybuilder.js.org) before asking. +- Any issue without enough details won't get any answer and will be closed. +- Help requests must be exhaustive, precise and come with some code explaining the need (use Markdown code highlight). +- Bug reports must come with a simple test case, preferably on jsFiddle, Plunker, etc. (QueryBuilder is available on [jsDelivr](https://cdn.jsdelivr.net/npm/jQuery-QueryBuilder/dist/) and [unpkg](https://unpkg.com/jQuery-QueryBuilder/dist/) to be used on such platforms). diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..1131297f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ +**Merge request checklist** + +- [ ] I read the [guidelines for contributing](https://github.com/mistic100/jQuery-QueryBuilder/blob/master/.github/CONTRIBUTING.md) +- [ ] I created my branch from `dev` and I am issuing the PR to `dev` +- [ ] I didn't pushed the `dist` directory +- [ ] If it's a new feature, I added the necessary unit tests +- [ ] If it's a new language, I filled the `__locale` and `__author` fields diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..0648b398 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,21 @@ +name: CI + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: '16' + cache: 'yarn' + + - name: build + run: | + yarn install + yarn build diff --git a/.gitignore b/.gitignore index 7bf6eb18..a952958a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ -bower_components node_modules +doc +.sass-cache +.idea +*.iml +package-lock.json diff --git a/.jsdoc.json b/.jsdoc.json new file mode 100644 index 00000000..d8a3fd67 --- /dev/null +++ b/.jsdoc.json @@ -0,0 +1,39 @@ +{ + "opts": { + "private": false, + "template": "node_modules/foodoc/template", + "readme": "build/jsdoc.md" + }, + "plugins": [ + "plugins/markdown" + ], + "templates": { + "systemName": "jQuery QueryBuilder API", + "systemSummary": "jQuery plugin for user friendly query/filter creator", + "systemColor": "#004482", + "copyright": "Licensed under MIT License, documentation under CC BY 3.0.", + "includeDate": false, + "inverseNav": false, + "cleverLinks": true, + "sort": "longname, version, since", + "analytics": { + "ua": "UA-28192323-3", + "domain": "auto" + }, + "navMembers": [ + {"kind": "class", "title": "Classes", "summary": "All documented classes."}, + {"kind": "external", "title": "Externals", "summary": "All documented external members."}, + {"kind": "global", "title": "Globals", "summary": "All documented globals."}, + {"kind": "mixin", "title": "Mixins", "summary": "All documented mixins."}, + {"kind": "interface", "title": "Interfaces", "summary": "All documented interfaces."}, + {"kind": "module", "title": "Modules", "summary": "All documented modules."}, + {"kind": "event", "title": "Events", "summary": "All documented events."}, + {"kind": "namespace", "title": "Namespaces", "summary": "All documented namespaces."}, + {"kind": "tutorial", "title": "Tutorials", "summary": "All available tutorials."} + ], + "scripts": [ + "https://cdnjs.cloudflare.com/ajax/libs/trianglify/1.0.1/trianglify.min.js", + "js/custom.js" + ] + } +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5628af08..00000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: node_js -node_js: - - "0.10" -before_script: - - npm install -g grunt-cli - - npm install -g bower - - bower install -install: npm install diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index b22642da..00000000 --- a/Gruntfile.js +++ /dev/null @@ -1,310 +0,0 @@ -module.exports = function(grunt) { - // list available modules and languages - var modules = {}, - langs = {}, - js_files_to_load = [], - css_files_to_load = [], - files_for_standalone = [ - 'bower_components/microevent-mistic100/microevent.js', - 'bower_components/jquery-extendext/jQuery.extendext.js', - 'dist/query-builder.js' - ], - loaded_modules = [], - loaded_lang = ''; - - grunt.file.expand('src/plugins/*/plugin.js') - .forEach(function(f) { - modules[f.split('/')[2]] = f; - }); - - grunt.file.expandMapping('src/i18n/*.js', '', { - flatten: true, ext: '' - }) - .forEach(function(f) { - langs[f.dest] = f.src[0]; - }); - - // parse 'modules' parameter - var arg_modules = grunt.option('modules'); - if (typeof arg_modules === 'string') { - arg_modules.split(',').forEach(function(m) { - m = m.trim(); - if (modules[m]) { - js_files_to_load.push(modules[m]); - loaded_modules.push(m); - } - else if (m !== 'none') { - grunt.fail.warn('Module '+ m +' unknown'); - } - }); - } - else if (arg_modules === undefined) { - for (var m in modules) { - js_files_to_load.push(modules[m]); - loaded_modules.push(m); - } - } - - // parse 'lang' parameter - var arg_lang = grunt.option('lang'); - if (typeof arg_lang === 'string') { - if (langs[arg_lang]) { - if (arg_lang != 'en') { - js_files_to_load.push(langs[arg_lang]); - } - loaded_lang = arg_lang; - } - else { - grunt.fail.warn('Lang '+ arg_lang +' unknown'); - } - } - - // get css files for loaded mofules - js_files_to_load.forEach(function(js_file) { - var css_file = js_file.replace(/js$/, 'css'); - if (grunt.file.exists(css_file)) { - css_files_to_load.push(css_file); - } - }); - - - function removeJshint(src) { - return src - .replace(/\/\*jshint [a-z:]+ \*\/\r?\n/g, '') - .replace(/\/\*jshint -[EWI]{1}[0-9]{3} \*\/\r?\n/g, ''); - } - - function removeWrapper(src) { - return src - .replace(/\(function\(\$\){\r?\n/g, '') - .replace(/\r?\n}\(jQuery\)\);/g, '') - .replace(/[ \t]*"use strict";\r?\n/g, '') - .replace(/\r?\n( *\/\/ [^\r\n]*\r?\n)+ *\/\/ =+/g, ''); - } - - - 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'+ - ' */', - - // copy i18n - copy: { - i18n: { - files: [{ - expand: true, - flatten: true, - src: ['src/i18n/*.js'], - dest: 'dist/i18n' - }] - } - }, - - // copy src - concat: { - options: { - separator: '\n', - stripBanners: { - block: true - } - }, - css: { - src: ['src/query-builder.css'].concat(css_files_to_load), - dest: 'dist/query-builder.css', - options: { - banner: '<%= banner %>\n', - // remove sections comments - process: function(src) { - return src.replace(/\/\* [^\r\n]* \*\/\r?\n/g, ''); - } - } - }, - js: { - src: ['src/query-builder.js'].concat(js_files_to_load), - dest: 'dist/query-builder.js', - options: { - // remove wrappers, use strict, jshint directives, sections comments - process: function(src) { - return removeWrapper(removeJshint(src)); - } - } - } - }, - - // add AMD wrapper - wrap: { - dist: { - src: ['dist/query-builder.js'], - dest: '', - options: { - separator: '', - wrapper: function() { - var wrapper = grunt.file.read('src/.wrapper.js').split('@@js\n'); - - if (loaded_modules.length) { - wrapper[0] = '// Modules: ' + loaded_modules.join(', ') + '\n' + wrapper[0]; - } - if (loaded_lang.length) { - wrapper[0] = '// Language: ' + loaded_lang + '\n' + wrapper[0]; - } - wrapper[0] = grunt.template.process('<%= banner %>\n\n') + wrapper[0]; - - return wrapper; - } - } - } - }, - - // compress js - uglify: { - options: { - banner: '<%= banner %>\n', - mangle: { except: ['$'] } - }, - dist: { - files: { - 'dist/query-builder.min.js': [ - 'dist/query-builder.js' - ], - 'dist/query-builder.standalone.min.js': [ - 'dist/query-builder.standalone.js' - ] - } - } - }, - - // compress css - cssmin: { - options: { - banner: '<%= banner %>', - keepSpecialComments: 0 - }, - dist: { - files: { - 'dist/query-builder.min.css': [ - 'dist/query-builder.css' - ] - } - } - }, - - // jshint tests - jshint: { - lib: { - files: { - src: ['src/query-builder.js'].concat(js_files_to_load) - } - } - }, - - // qunit test suite - qunit: { - all: ['tests/*.html'] - } - }); - - // from https://github.com/brianreavis/selectize.js/blob/master/Gruntfile.js - grunt.registerTask('build_standalone', '', function() { - var files = [], - modules = []; - - // get sources with named definitions - for (var i=0, n=files_for_standalone.length; i= 1.9 - * [jQuery.extendext](https://github.com/mistic100/jQuery.extendext) - * [MicroEvent](https://github.com/mistic100/microevent.js) (mistic100 version) - * (optional) MomentJS - * (optional) any widgets library like jQuery UI -($.extendext and MicroEvent are directly included in the [standalone](https://github.com/mistic100/jQuery-QueryBuilder/blob/master/dist/query-builder.standalone.js) file) -### Browser support - * Internet Explorer >= 10 - * Mozilla FireFox ?? - * Google Chrome ?? - * Opera ?? - * Safari ?? +## Install -### Build -Run `grunt` in root directory to generate minified files inside `dist`. +#### Manually -You can choose which plugins to include with `--modules` -```bash -# include "sql-support" plugin -grunt --modules=sql-support +[Download the latest release](https://github.com/mistic100/jQuery-QueryBuilder/releases) -# disable all modules -grunt --modules=false -``` -All plugins are included by default. +#### With npm -You can also include ONE language with `--lang` ```bash -# include French translation -grunt --lang=fr +$ npm install jQuery-QueryBuilder ``` -Run `grunt test` to run jsHint and the Mocha test suite. +#### Via CDN + +jQuery-QueryBuilder is available on [jsDelivr](https://www.jsdelivr.com/package/npm/jQuery-QueryBuilder). +### Dependencies + * [jQuery 3](https://jquery.com) + * [Bootstrap 5](https://getbootstrap.com/docs/5.3/) CSS and bundle.js which includes `Popper` for tooltips and popovers + * [Bootstrap Icons](https://icons.getbootstrap.com/) + * [jQuery.extendext](https://github.com/mistic100/jQuery.extendext) + * [MomentJS](https://momentjs.com) (optional, for Date/Time validation) + * [SQL Parser](https://github.com/mistic100/sql-parser) (optional, for SQL methods) + * Other Bootstrap/jQuery plugins used by plugins + +($.extendext is directly included in the [standalone](https://github.com/mistic100/jQuery-QueryBuilder/blob/master/dist/js/query-builder.standalone.js) file) + + + +## Developement + +Install Node dependencies with `npm install`. + +#### Build + +Run `npm run build` in the root directory to generate production files inside `dist`. -Run `grunt list_modules` to get the list of available plugins and languages. +#### Serve -### Contributing -Changes have to be done only in `src` directory. The `dist` directory is updated only once in a while before a release. +Run `npm run serve` to open the example page with automatic build and livereload. -### Inspiration - * [Knockout Query Builder](http://kindohm.com/posts/2013/09/25/knockout-query-builder/) - * [jui_filter_rules](http://www.pontikis.net/labs/jui_filter_rules/) +## License +This library is available under the MIT license. diff --git a/bower.json b/bower.json deleted file mode 100644 index edbfddee..00000000 --- a/bower.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "jQuery-QueryBuilder", - "version": "1.4.1", - "authors": [{ - "name": "Damien \"Mistic\" Sorel", - "homepage": "http://www.strangeplanet.fr" - }], - "description": "jQuery plugin for user friendly query/filter creator", - "main": [ - "dist/query-builder.js", - "dist/query-builder.css" - ], - "dependencies" : { - "jquery": ">= 1.9.0", - "bootstrap": "^3.1.0", - "momentjs": "^2.6.0", - "microevent-mistic100": "^2.0.0", - "jquery-extendext": "^0.1.1" - }, - "keywords": [ - "jquery", - "query", - "builder", - "filter" - ], - "license": "MIT", - "homepage": "https://github.com/mistic100/jQuery-QueryBuilder", - "repository": { - "type": "git", - "url": "git://github.com/mistic100/jQuery-QueryBuilder.git" - }, - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "test", - "tests" - ] -} diff --git a/build/dist.mjs b/build/dist.mjs new file mode 100644 index 00000000..e413853b --- /dev/null +++ b/build/dist.mjs @@ -0,0 +1,210 @@ +import fs from 'fs'; +import path from 'path'; +import { globSync } from 'glob'; +import * as sass from 'sass'; +import pkg from '../package.json' assert { type: 'json' }; + +const DEV = process.argv[2] === '--dev'; + +const DIST = 'dist/'; + +const CORE_JS = [ + 'src/main.js', + 'src/defaults.js', + 'src/plugins.js', + 'src/core.js', + 'src/public.js', + 'src/data.js', + 'src/template.js', + 'src/utils.js', + 'src/model.js', + 'src/jquery.js', +]; + +const CORE_SASS = [ + 'src/scss/dark.scss', + 'src/scss/default.scss', +]; + +const STANDALONE_JS = { + 'jquery-extendext': 'node_modules/jquery-extendext/jquery-extendext.js', + 'query-builder': `${DIST}js/query-builder.js`, +}; + +const BANNER = () => `/*! + * jQuery QueryBuilder ${pkg.version} + * Copyright 2014-${new Date().getFullYear()} Damien "Mistic" Sorel (http://www.strangeplanet.fr) + * Licensed under MIT (https://opensource.org/licenses/MIT) + */`; + +const LANG_BANNER = (locale, author) => `/*! + * jQuery QueryBuilder ${pkg.version} + * Locale: ${locale} + * Author: ${author} + * Licensed under MIT (https://opensource.org/licenses/MIT) + */`; + +const ALL_PLUGINS_JS = glob('src/plugins/*/plugin.js') + .sort() + .reduce((all, p) => { + const n = p.split('/')[2]; + all[n] = p; + return all; + }, {}); + +const ALL_PLUGINS_SASS = glob('src/plugins/*/plugin.scss') + .sort() + .reduce((all, p) => { + const n = p.split('/')[2]; + all[n] = p; + return all; + }, {}); + +const ALL_LANGS = glob('src/i18n/*.json') + .map(p => p.split(/[\/\.]/)[2]) + .sort(); + +function glob(pattern) { + return globSync(pattern) + .map(p => p.split(path.sep).join('/')); +} + +/** + * Build lang files + */ +function buildLangs() { + const wrapper = fs.readFileSync('src/i18n/.wrapper.js', { encoding: 'utf8' }) + .split('@@js\n'); + + ALL_LANGS.forEach(lang => { + const outpath = `${DIST}i18n/query-builder.${lang}.js`; + console.log(`LANG ${lang} (${outpath})`); + fs.writeFileSync(outpath, getLang(lang, wrapper)); + }); +} + +/** + * Get the content of a single lang + */ +function getLang(lang, wrapper = ['', '']) { + const corepath = `src/i18n/${lang}.json`; + const content = JSON.parse(fs.readFileSync(corepath, { encoding: 'utf8' })); + + Object.keys(ALL_PLUGINS_JS).forEach(plugin => { + const pluginpath = `src/plugins/${plugin}/i18n/${lang}.json`; + try { + const plugincontent = JSON.parse(fs.readFileSync(pluginpath, { encoding: 'utf8' })); + Object.assign(content, plugincontent); + } catch { } + }); + + return LANG_BANNER(content.__locale || lang, content.__author || '') + + '\n\n' + + wrapper[0] + + `QueryBuilder.regional['${lang}'] = ` + + JSON.stringify(content, null, 2) + + ';\n\n' + + `QueryBuilder.defaults({ lang_code: '${lang}' });` + + wrapper[1]; +} + +/** + * Build main JS file + */ +function buildMain() { + const wrapper = fs.readFileSync('src/.wrapper.js', { encoding: 'utf8' }) + .split('@@js\n'); + + const files_to_load = [ + ...CORE_JS, + ...Object.values(ALL_PLUGINS_JS), + ]; + + const output = BANNER() + + '\n\n' + + wrapper[0] + + files_to_load.map(f => fs.readFileSync(f, { encoding: 'utf8' })).join('\n\n') + + '\n\n' + + getLang('en') + + wrapper[1]; + + const outpath = `${DIST}js/query-builder.js`; + console.log(`MAIN (${outpath})`); + fs.writeFileSync(outpath, output); +} + +/** + * Build standalone JS file + */ +function buildStandalone() { + const output = Object.entries(STANDALONE_JS) + .map(([name, file]) => { + return fs.readFileSync(file, { encoding: 'utf8' }) + .replace(/define\((.*?)\);/, `define('${name}', $1);`); + }) + .join('\n\n'); + + const outpath = `${DIST}js/query-builder.standalone.js`; + console.log(`STANDALONE (${outpath})`); + fs.writeFileSync(outpath, output); +} + +/** + * Copy SASS files + */ +function copySass() { + Object.entries(ALL_PLUGINS_SASS).forEach(([plugin, path]) => { + const outpath = `${DIST}scss/plugins/${plugin}.scss`; + console.log(`SASS ${plugin} (${path})`); + fs.copyFileSync(path, outpath); + }); + + CORE_SASS.forEach(path => { + const name = path.split('/').pop(); + + const content = fs.readFileSync(path, { encoding: 'utf8' }); + + let output = BANNER() + + '\n' + + content; + if (name === 'default.scss') { + output += '\n' + + Object.keys(ALL_PLUGINS_SASS).map(p => `@import "plugins/${p}";`).join('\n'); + } + + const outpath = `${DIST}scss/${name}`; + console.log(`SASS (${path})`); + fs.writeFileSync(outpath, output); + }); +} + +/** + * Build CSS files + */ +function buildCss() { + CORE_SASS.forEach(p => { + const path = p.replace('src/', DIST); + const name = path.split('/').pop(); + + const output = sass.compile(path); + + const outpath = `${DIST}css/query-builder.${name.split('.').shift()}.css`; + console.log(`CSS (${path})`); + fs.writeFileSync(outpath, output.css); + }); +} + +if (!DEV) { + fs.rmSync(DIST, { recursive: true, force: true }); +} +fs.mkdirSync(DIST + 'css', { recursive: true }); +fs.mkdirSync(DIST + 'i18n', { recursive: true }); +fs.mkdirSync(DIST + 'js', { recursive: true }); +fs.mkdirSync(DIST + 'scss', { recursive: true }); +fs.mkdirSync(DIST + 'scss/plugins', { recursive: true }); + +buildLangs(); +buildMain(); +buildStandalone(); +copySass(); +buildCss(); diff --git a/build/jsdoc.js b/build/jsdoc.js new file mode 100644 index 00000000..edbad8d4 --- /dev/null +++ b/build/jsdoc.js @@ -0,0 +1,12 @@ +(function() { + var header = $('.page-header'); + var pattern = Trianglify({ + width: window.screen.width | header.outerWidth(), + height: header.outerHeight(), + cell_size: 90, + seed: 'jQuery QueryBuilder', + x_colors: ['#0074d9', '#001224'] + }); + + header.css('background-image', 'url(' + pattern.png() + ')'); +}()); diff --git a/build/jsdoc.md b/build/jsdoc.md new file mode 100644 index 00000000..e329af92 --- /dev/null +++ b/build/jsdoc.md @@ -0,0 +1,11 @@ +# [Main documentation](..) + +# Entry point: [$.fn.QueryBuilder](external-_jQuery.fn_.html) + +# [QueryBuilder](QueryBuilder.html) + +# [Rule](Rule.html) & [Group](Group.html) + +# [Events](list_event.html) + +# [Plugins](module-plugins.html) diff --git a/build/liveserver.mjs b/build/liveserver.mjs new file mode 100644 index 00000000..3d60e23c --- /dev/null +++ b/build/liveserver.mjs @@ -0,0 +1,20 @@ +import liveServer from 'alive-server'; +import path from 'path'; + +const rootDir = process.cwd(); + +const EXAMPLES_DIR = 'examples'; +const DIST_DIR = 'dist'; + +liveServer.start({ + open: true, + root: path.join(rootDir, EXAMPLES_DIR), + watch: [ + path.join(rootDir, EXAMPLES_DIR), + path.join(rootDir, DIST_DIR), + ], + mount: [ + ['/node_modules', path.join(rootDir, 'node_modules')], + ['/dist', path.join(rootDir, DIST_DIR)], + ], +}); diff --git a/composer.json b/composer.json deleted file mode 100644 index c3b26444..00000000 --- a/composer.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "mistic100/jquery-querybuilder", - "version": "1.4.1", - "description": "jQuery plugin for user friendly query/filter creator", - "keywords": [ - "jquery", - "query", - "builder", - "filter" - ], - "homepage": "https://github.com/mistic100/jQuery-QueryBuilder", - "authors": [{ - "name": "Damien \"Mistic\" Sorel", - "email": "contact@git.strangeplanet.fr", - "homepage": "http://www.strangeplanet.fr" - }], - "support": { - "issues": "https://github.com/mistic100/jQuery-QueryBuilder/issues" - }, - "require": { - "yiisoft/jquery": "2.* | 1.9.*", - "moment/moment": "dev-master", - "twbs/bootstrap": "3.2.*@dev" - }, - "license": "MIT" -} diff --git a/dist/css/query-builder.dark.css b/dist/css/query-builder.dark.css new file mode 100644 index 00000000..90b5fbaa --- /dev/null +++ b/dist/css/query-builder.dark.css @@ -0,0 +1,150 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Copyright 2014-2024 Damien "Mistic" Sorel (http://www.strangeplanet.fr) + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ +/*! + * jQuery QueryBuilder 3.0.0 + * Copyright 2014-2024 Damien "Mistic" Sorel (http://www.strangeplanet.fr) + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ +.query-builder .rule-placeholder, .query-builder .rule-container, .query-builder .rules-group-container { + position: relative; + margin: 4px 0; + border-radius: 5px; + padding: 5px; + border: 1px solid #111; + background: rgba(40, 40, 40, 0.9); +} + +.query-builder .drag-handle, .query-builder .error-container, .query-builder .rule-container .rule-filter-container, +.query-builder .rule-container .rule-operator-container, +.query-builder .rule-container .rule-value-container { + display: inline-block; + margin: 0 5px 0 0; + vertical-align: middle; +} + +.query-builder .rules-group-container { + padding: 10px; + padding-bottom: 6px; + border: 1px solid #00164A; + background: rgba(50, 70, 80, 0.5); +} +.query-builder .rules-group-header { + margin-bottom: 10px; +} +.query-builder .rules-group-header .group-conditions .btn.readonly:not(.active), +.query-builder .rules-group-header .group-conditions input[name$=_cond] { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + white-space: nowrap; +} +.query-builder .rules-group-header .group-conditions .btn.readonly { + border-radius: 3px; +} +.query-builder .rules-list { + list-style: none; + padding: 0 0 0 15px; + margin: 0; +} +.query-builder .rule-value-container { + border-left: 1px solid #DDD; + padding-left: 5px; +} +.query-builder .rule-value-container label { + margin-bottom: 0; + font-weight: normal; +} +.query-builder .rule-value-container label.block { + display: block; +} +.query-builder .error-container { + display: none; + cursor: help; + color: #F00; +} +.query-builder .has-error { + background-color: #322; + border-color: #800; +} +.query-builder .has-error .error-container { + display: inline-block !important; +} +.query-builder .rules-list > *::before, .query-builder .rules-list > *::after { + content: ""; + position: absolute; + left: -10px; + width: 10px; + height: calc(50% + 4px); + border-color: #222; + border-style: solid; +} +.query-builder .rules-list > *::before { + top: -4px; + border-width: 0 0 2px 2px; +} +.query-builder .rules-list > *::after { + top: 50%; + border-width: 0 0 0 2px; +} +.query-builder .rules-list > *:first-child::before { + top: -12px; + height: calc(50% + 14px); +} +.query-builder .rules-list > *:last-child::before { + border-radius: 0 0 0 4px; +} +.query-builder .rules-list > *:last-child::after { + display: none; +} + +.query-builder.bt-checkbox-bootstrap-icons .checkbox input[type=checkbox] + label::before { + outline: 0; +} +.query-builder.bt-checkbox-bootstrap-icons .checkbox input[type=checkbox]:checked + label::after { + font-family: "bootstrap-icons"; + content: "\f633"; +} + +.query-builder .error-container + .tooltip .tooltip-inner { + color: #F22 !important; +} + +.query-builder p.filter-description { + margin: 5px 0 0 0; + background: rgba(0, 170, 255, 0.2); + border: 1px solid #346F7B; + color: #AAD1E4; + border-radius: 5px; + padding: 2.5px 5px; + font-size: 0.8em; +} + +.query-builder .rules-group-header [data-invert] { + margin-left: 5px; +} + +.query-builder .drag-handle { + cursor: move; + vertical-align: middle; + margin-left: 5px; +} +.query-builder .dragging { + position: fixed; + opacity: 0.5; + z-index: 100; +} +.query-builder .dragging::before, .query-builder .dragging::after { + display: none; +} +.query-builder .rule-placeholder { + border: 1px dashed #BBB; + opacity: 0.7; +} \ No newline at end of file diff --git a/dist/css/query-builder.default.css b/dist/css/query-builder.default.css new file mode 100644 index 00000000..b5edf68e --- /dev/null +++ b/dist/css/query-builder.default.css @@ -0,0 +1,145 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Copyright 2014-2024 Damien "Mistic" Sorel (http://www.strangeplanet.fr) + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ +.query-builder .rule-placeholder, .query-builder .rule-container, .query-builder .rules-group-container { + position: relative; + margin: 4px 0; + border-radius: 5px; + padding: 5px; + border: 1px solid #EEE; + background: rgba(255, 255, 255, 0.9); +} + +.query-builder .drag-handle, .query-builder .error-container, .query-builder .rule-container .rule-filter-container, +.query-builder .rule-container .rule-operator-container, +.query-builder .rule-container .rule-value-container { + display: inline-block; + margin: 0 5px 0 0; + vertical-align: middle; +} + +.query-builder .rules-group-container { + padding: 10px; + padding-bottom: 6px; + border: 1px solid #DCC896; + background: rgba(250, 240, 210, 0.5); +} +.query-builder .rules-group-header { + margin-bottom: 10px; +} +.query-builder .rules-group-header .group-conditions .btn.readonly:not(.active), +.query-builder .rules-group-header .group-conditions input[name$=_cond] { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + white-space: nowrap; +} +.query-builder .rules-group-header .group-conditions .btn.readonly { + border-radius: 3px; +} +.query-builder .rules-list { + list-style: none; + padding: 0 0 0 15px; + margin: 0; +} +.query-builder .rule-value-container { + border-left: 1px solid #DDD; + padding-left: 5px; +} +.query-builder .rule-value-container label { + margin-bottom: 0; + font-weight: normal; +} +.query-builder .rule-value-container label.block { + display: block; +} +.query-builder .error-container { + display: none; + cursor: help; + color: #F00; +} +.query-builder .has-error { + background-color: #FDD; + border-color: #F99; +} +.query-builder .has-error .error-container { + display: inline-block !important; +} +.query-builder .rules-list > *::before, .query-builder .rules-list > *::after { + content: ""; + position: absolute; + left: -10px; + width: 10px; + height: calc(50% + 4px); + border-color: #CCC; + border-style: solid; +} +.query-builder .rules-list > *::before { + top: -4px; + border-width: 0 0 2px 2px; +} +.query-builder .rules-list > *::after { + top: 50%; + border-width: 0 0 0 2px; +} +.query-builder .rules-list > *:first-child::before { + top: -12px; + height: calc(50% + 14px); +} +.query-builder .rules-list > *:last-child::before { + border-radius: 0 0 0 4px; +} +.query-builder .rules-list > *:last-child::after { + display: none; +} + +.query-builder.bt-checkbox-bootstrap-icons .checkbox input[type=checkbox] + label::before { + outline: 0; +} +.query-builder.bt-checkbox-bootstrap-icons .checkbox input[type=checkbox]:checked + label::after { + font-family: "bootstrap-icons"; + content: "\f633"; +} + +.query-builder .error-container + .tooltip .tooltip-inner { + color: #F99 !important; +} + +.query-builder p.filter-description { + margin: 5px 0 0 0; + background: #D9EDF7; + border: 1px solid #BCE8F1; + color: #31708F; + border-radius: 5px; + padding: 2.5px 5px; + font-size: 0.8em; +} + +.query-builder .rules-group-header [data-invert] { + margin-left: 5px; +} + +.query-builder .drag-handle { + cursor: move; + vertical-align: middle; + margin-left: 5px; +} +.query-builder .dragging { + position: fixed; + opacity: 0.5; + z-index: 100; +} +.query-builder .dragging::before, .query-builder .dragging::after { + display: none; +} +.query-builder .rule-placeholder { + border: 1px dashed #BBB; + opacity: 0.7; +} \ No newline at end of file diff --git a/dist/i18n/de.js b/dist/i18n/de.js deleted file mode 100644 index 988b7826..00000000 --- a/dist/i18n/de.js +++ /dev/null @@ -1,37 +0,0 @@ -/*! - * jQuery QueryBuilder - * German translation - */ - -jQuery.fn.queryBuilder.defaults.set({ lang: { - "add_rule": "neue Regel", - "add_group": "neue Gruppe", - "delete_rule": "löschen", - "delete_group": "löschen", - - "condition_and": "UND", - "condition_or": "ODER", - - "filter_select_placeholder": "------", - - "operators": { - "equal": "gleich", - "not_equal": "ungleich", - "in": "in", - "not_in": "nicht in", - "less": "kleiner", - "less_or_equal": "kleiner gleich", - "greater": "größer", - "greater_or_equal": "größer gleich", - "begins_with": "beginnt mit", - "not_begins_with": "beginnt nicht mit", - "contains": "enthält", - "not_contains": "enthält nicht", - "ends_with": "endet mit", - "not_ends_with": "endet nicht mit", - "is_empty": "ist leer", - "is_not_empty": "ist nicht leer", - "is_null": "ist null", - "is_not_null": "ist nicht null" - } -}}); \ No newline at end of file diff --git a/dist/i18n/es.js b/dist/i18n/es.js deleted file mode 100644 index bc3ea3a7..00000000 --- a/dist/i18n/es.js +++ /dev/null @@ -1,59 +0,0 @@ -/*! - * jQuery QueryBuilder - * Spanish translation by "pyarza" - */ - -jQuery.fn.queryBuilder.defaults.set({ lang: { - "add_rule": "Añadir regla", - "add_group": "Añadir grupo", - "delete_rule": "Borrar", - "delete_group": "Borrar", - - "condition_and": "Y", - "condition_or": "O", - - "filter_select_placeholder": "------", - - "operators": { - "equal": "igual", - "not_equal": "distinto", - "in": "en", - "not_in": "no en", - "less": "menor", - "less_or_equal": "menor o igual", - "greater": "mayor", - "greater_or_equal": "mayor o igual", - "between": "entre", - "begins_with": "empieza por", - "not_begins_with": "no empieza por", - "contains": "contiene", - "not_contains": "no contiene", - "ends_with": "acaba con", - "not_ends_with": "no acaba con", - "is_empty": "esta vacio", - "is_not_empty": "no esta vacio", - "is_null": "es nulo", - "is_not_null": "no es nulo" - }, - - "errors": { - "no_filter": "No se ha seleccionado ningun filtro", - "empty_group": "El grupo esta vacio", - "radio_empty": "Ningun valor seleccionado", - "checkbox_empty": "Ningun valor seleccionado", - "select_empty": "Ningun valor seleccionado", - "string_empty": "Cadena vacia", - "string_exceed_min_length": "Debe contener al menos {0} caracteres", - "string_exceed_max_length": "No debe contener mas de {0} caracteres", - "string_invalid_format": "Formato invalido ({0})", - "number_nan": "No es un numero", - "number_not_integer": "No es un numero entero", - "number_not_double": "No es un numero real", - "number_exceed_min": "Debe ser mayor que {0}", - "number_exceed_max": "Debe ser menot que {0}", - "number_wrong_step": "Debe ser multiplo de {0}", - "datetime_invalid": "Formato de fecha invalido ({0})", - "datetime_exceed_min": "Debe ser posterior a {0}", - "datetime_exceed_max": "Debe ser anterior a {0}" - } -}}); diff --git a/dist/i18n/fr.js b/dist/i18n/fr.js deleted file mode 100644 index 72373000..00000000 --- a/dist/i18n/fr.js +++ /dev/null @@ -1,59 +0,0 @@ -/*! - * jQuery QueryBuilder - * French translation by Damien "Mistic" Sorel - */ - -jQuery.fn.queryBuilder.defaults.set({ lang: { - "add_rule": "Ajouter une règle", - "add_group": "Ajouter un groupe", - "delete_rule": "Supprimer", - "delete_group": "Supprimer", - - "condition_and": "ET", - "condition_or": "OU", - - "filter_select_placeholder": "------", - - "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", - "between": "entre", - "begins_with": "commence par", - "not_begins_with": "ne commence pas par", - "contains": "contient", - "not_contains": "ne contient pas", - "ends_with": "finit par", - "not_ends_with": "ne finit pas par", - "is_empty": "est vide", - "is_not_empty": "n'est pas vide", - "is_null": "est nul", - "is_not_null": "n'est pas nul" - }, - - "errors": { - "no_filter": "Aucun filtre sélectionné", - "empty_group": "Le groupe est vide", - "radio_empty": "Pas de valeur selectionnée", - "checkbox_empty": "Pas de valeur selectionnée", - "select_empty": "Pas de valeur selectionnée", - "string_empty": "Valeur vide", - "string_exceed_min_length": "Doit contenir au moins {0} caractères", - "string_exceed_max_length": "Ne doit pas contenir plus de {0} caractères", - "string_invalid_format": "Format invalide ({0})", - "number_nan": "N'est pas un nombre", - "number_not_integer": "N'est pas un entier", - "number_not_double": "N'est pas un nombre réel", - "number_exceed_min": "Doit être plus grand que {0}", - "number_exceed_max": "Doit être plus petit que {0}", - "number_wrong_step": "Doit être un multiple de {0}", - "datetime_invalid": "Fomat de date invalide ({0})", - "datetime_exceed_min": "Doit être après {0}", - "datetime_exceed_max": "Doit être avant {0}" - } -}}); \ No newline at end of file diff --git a/dist/i18n/it.js b/dist/i18n/it.js deleted file mode 100644 index 87a100b1..00000000 --- a/dist/i18n/it.js +++ /dev/null @@ -1,37 +0,0 @@ -/*! - * jQuery QueryBuilder - * Italian translation - */ - -jQuery.fn.queryBuilder.defaults.set({ lang: { - "add_rule": "Aggiungi regola", - "add_group": "Aggiungi gruppo", - "delete_rule": "Elimina", - "delete_group": "Elimina", - - "condition_and": "E", - "condition_or": "O", - - "filter_select_placeholder": "------", - - "operators": { - "equal": "uguale", - "not_equal": "non uguale", - "in": "in", - "not_in": "non in", - "less": "minore", - "less_or_equal": "minore o uguale", - "greater": "maggiore", - "greater_or_equal": "maggiore o uguale", - "begins_with": "inizia con", - "not_begins_with": "non inizia con", - "contains": "contiene", - "not_contains": "non contiene", - "ends_with": "finisce con", - "not_ends_with": "non finisce con", - "is_empty": "è vuoto", - "is_not_empty": "non è vuoto", - "is_null": "è nullo", - "is_not_null": "non è nullo" - } -}}); \ No newline at end of file diff --git a/dist/i18n/query-builder.ar.js b/dist/i18n/query-builder.ar.js new file mode 100644 index 00000000..edb2917e --- /dev/null +++ b/dist/i18n/query-builder.ar.js @@ -0,0 +1,80 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Arabic (ar) + * Author: Mohamed YOUNES, https://github.com/MedYOUNES + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['ar'] = { + "__locale": "Arabic (ar)", + "__author": "Mohamed YOUNES, https://github.com/MedYOUNES", + "add_rule": "إضافة حُكم", + "add_group": "إضافة زُمْرَة", + "delete_rule": "حذف", + "delete_group": "حذف", + "conditions": { + "AND": "و", + "OR": "أو" + }, + "operators": { + "equal": "يساوي", + "not_equal": "غير مساوٍ", + "in": "في", + "not_in": "ليس في", + "less": "أقل من", + "less_or_equal": "أصغر أو مساو", + "greater": "أكبر", + "greater_or_equal": "أكبر أو مساو", + "between": "محصور بين", + "not_between": "غير محصور بين", + "begins_with": "يبدأ بـ", + "not_begins_with": "لا يبدأ بـ", + "contains": "يحتوي على", + "not_contains": "لا يحتوي على", + "ends_with": "ينتهي بـ", + "not_ends_with": "لا ينتهي بـ", + "is_empty": "فارغ", + "is_not_empty": "غير فارغ", + "is_null": "صفر", + "is_not_null": "ليس صفرا" + }, + "errors": { + "no_filter": "لم تحدد أي مرشح", + "empty_group": "الزمرة فارغة", + "radio_empty": "لم تحدد أي قيمة", + "checkbox_empty": "لم تحدد أي قيمة", + "select_empty": "لم تحدد أي قيمة", + "string_empty": "النص فارغ", + "string_exceed_min_length": "النص دون الأدنى المسموح به", + "string_exceed_max_length": "النص فوق الأقصى المسموح به", + "string_invalid_format": "تركيبة غير صحيحة", + "number_nan": "ليس عددا", + "number_not_integer": "ليس عددا صحيحا", + "number_not_double": "ليس عددا كسريا", + "number_exceed_min": "العدد أصغر من الأدنى المسموح به", + "number_exceed_max": "العدد أكبر من الأقصى المسموح به", + "number_wrong_step": "أخطأت في حساب مضاعفات العدد", + "datetime_empty": "لم تحدد التاريخ", + "datetime_invalid": "صيغة التاريخ غير صحيحة", + "datetime_exceed_min": "التاريخ دون الأدنى المسموح به", + "datetime_exceed_max": "التاريخ أكبر من الأقصى المسموح به", + "boolean_not_valid": "ليست قيمة منطقية ثنائية", + "operator_not_multiple": "العامل ليس متعدد القيَم" + }, + "invert": "قَلْبُ" +}; + +QueryBuilder.defaults({ lang_code: 'ar' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.az.js b/dist/i18n/query-builder.az.js new file mode 100644 index 00000000..3820f42e --- /dev/null +++ b/dist/i18n/query-builder.az.js @@ -0,0 +1,79 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Azerbaijan (az) + * Author: Megaplan, mborisv + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['az'] = { + "__locale": "Azerbaijan (az)", + "__author": "Megaplan, mborisv ", + "add_rule": "Əlavə etmək", + "add_group": "Qrup əlavə etmək", + "delete_rule": "Silmək", + "delete_group": "Silmək", + "conditions": { + "AND": "VƏ", + "OR": "VƏ YA" + }, + "operators": { + "equal": "bərabərdir", + "not_equal": "bərabər deyil", + "in": "qeyd edilmişlərdən", + "not_in": "qeyd olunmamışlardan", + "less": "daha az", + "less_or_equal": "daha az və ya bərabər", + "greater": "daha çox", + "greater_or_equal": "daha çox və ya bərabər", + "between": "arasında", + "begins_with": "başlayır", + "not_begins_with": "başlamır", + "contains": "ibarətdir", + "not_contains": "yoxdur", + "ends_with": "başa çatır", + "not_ends_with": "başa çatmır", + "is_empty": "boş sətir", + "is_not_empty": "boş olmayan sətir", + "is_null": "boşdur", + "is_not_null": "boş deyil" + }, + "errors": { + "no_filter": "Filterlər seçilməyib", + "empty_group": "Qrup boşdur", + "radio_empty": "Məna seçilməyib", + "checkbox_empty": "Məna seçilməyib", + "select_empty": "Məna seçilməyib", + "string_empty": "Doldurulmayıb", + "string_exceed_min_length": "{0} daha çox simvol olmalıdır", + "string_exceed_max_length": "{0} daha az simvol olmalıdır", + "string_invalid_format": "Yanlış format ({0})", + "number_nan": "Rəqəm deyil", + "number_not_integer": "Rəqəm deyil", + "number_not_double": "Rəqəm deyil", + "number_exceed_min": "{0} daha çox olmalıdır", + "number_exceed_max": "{0} daha az olmalıdır", + "number_wrong_step": "{0} bölünən olmalıdır", + "datetime_empty": "Doldurulmayıb", + "datetime_invalid": "Yanlış tarix formatı ({0})", + "datetime_exceed_min": "{0} sonra olmalıdır", + "datetime_exceed_max": "{0} əvvəl olmalıdır", + "boolean_not_valid": "Loqik olmayan", + "operator_not_multiple": "\"{1}\" operatoru çoxlu məna daşımır" + }, + "invert": "invert" +}; + +QueryBuilder.defaults({ lang_code: 'az' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.bg.js b/dist/i18n/query-builder.bg.js new file mode 100644 index 00000000..3fa9dc74 --- /dev/null +++ b/dist/i18n/query-builder.bg.js @@ -0,0 +1,79 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Bulgarian (bg) + * Author: Valentin Hristov + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['bg'] = { + "__locale": "Bulgarian (bg)", + "__author": "Valentin Hristov", + "add_rule": "Добави правило", + "add_group": "Добави група", + "delete_rule": "Изтрий", + "delete_group": "Изтрий", + "conditions": { + "AND": "И", + "OR": "ИЛИ" + }, + "operators": { + "equal": "равно", + "not_equal": "различно", + "in": "в", + "not_in": "не е в", + "less": "по-малко", + "less_or_equal": "по-малко или равно", + "greater": "по-голям", + "greater_or_equal": "по-голям или равно", + "between": "между", + "not_between": "не е между", + "begins_with": "започва с", + "not_begins_with": "не започва с", + "contains": "съдържа", + "not_contains": "не съдържа", + "ends_with": "завършва с", + "not_ends_with": "не завършва с", + "is_empty": "е празно", + "is_not_empty": "не е празно", + "is_null": "е нищо", + "is_not_null": "различно от нищо" + }, + "errors": { + "no_filter": "Не е избран филтър", + "empty_group": "Групата е празна", + "radio_empty": "Не е селектирана стойност", + "checkbox_empty": "Не е селектирана стойност", + "select_empty": "Не е селектирана стойност", + "string_empty": "Празна стойност", + "string_exceed_min_length": "Необходимо е да съдържа поне {0} символа", + "string_exceed_max_length": "Необходимо е да съдържа повече от {0} символа", + "string_invalid_format": "Невалиден формат ({0})", + "number_nan": "Не е число", + "number_not_integer": "Не е цяло число", + "number_not_double": "Не е реално число", + "number_exceed_min": "Трябва да е по-голямо от {0}", + "number_exceed_max": "Трябва да е по-малко от {0}", + "number_wrong_step": "Трябва да е кратно на {0}", + "datetime_empty": "Празна стойност", + "datetime_invalid": "Невалиден формат на дата ({0})", + "datetime_exceed_min": "Трябва да е след {0}", + "datetime_exceed_max": "Трябва да е преди {0}", + "boolean_not_valid": "Не е булева", + "operator_not_multiple": "Оператора \"{1}\" не може да приеме множество стойности" + } +}; + +QueryBuilder.defaults({ lang_code: 'bg' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.cs.js b/dist/i18n/query-builder.cs.js new file mode 100644 index 00000000..551bee28 --- /dev/null +++ b/dist/i18n/query-builder.cs.js @@ -0,0 +1,79 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Čeština (cs) + * Author: Megaplan, mborisv + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['cs'] = { + "__locale": "Čeština (cs)", + "__author": "Megaplan, mborisv ", + "add_rule": "Přidat", + "add_group": "Přidat skupinu", + "delete_rule": "Odstranit", + "delete_group": "Odstranit skupinu", + "conditions": { + "AND": "I", + "OR": "NEBO" + }, + "operators": { + "equal": "stejně", + "not_equal": "liší se", + "in": "z uvedených", + "not_in": "ne z uvedených", + "less": "méně", + "less_or_equal": "méně nebo stejně", + "greater": "více", + "greater_or_equal": "více nebo stejně", + "between": "mezi", + "begins_with": "začíná z", + "not_begins_with": "nezačíná z", + "contains": "obsahuje", + "not_contains": "neobsahuje", + "ends_with": "končí na", + "not_ends_with": "nekončí na", + "is_empty": "prázdný řádek", + "is_not_empty": "neprázdný řádek", + "is_null": "prázdno", + "is_not_null": "plno" + }, + "errors": { + "no_filter": "není vybraný filtr", + "empty_group": "prázdná skupina", + "radio_empty": "Není udaná hodnota", + "checkbox_empty": "Není udaná hodnota", + "select_empty": "Není udaná hodnota", + "string_empty": "Nevyplněno", + "string_exceed_min_length": "Musí obsahovat více {0} symbolů", + "string_exceed_max_length": "Musí obsahovat méně {0} symbolů", + "string_invalid_format": "Nesprávný formát ({0})", + "number_nan": "Žádné číslo", + "number_not_integer": "Žádné číslo", + "number_not_double": "Žádné číslo", + "number_exceed_min": "Musí být více {0}", + "number_exceed_max": "Musí být méně {0}", + "number_wrong_step": "Musí být násobkem {0}", + "datetime_empty": "Nevyplněno", + "datetime_invalid": "Nesprávný formát datumu ({0})", + "datetime_exceed_min": "Musí být po {0}", + "datetime_exceed_max": "Musí být do {0}", + "boolean_not_valid": "Nelogické", + "operator_not_multiple": "Operátor \"{1}\" nepodporuje mnoho hodnot" + }, + "invert": "invertní" +}; + +QueryBuilder.defaults({ lang_code: 'cs' }); +})); \ No newline at end of file diff --git a/src/i18n/da.js b/dist/i18n/query-builder.da.js similarity index 55% rename from src/i18n/da.js rename to dist/i18n/query-builder.da.js index 15630102..ff6d3f77 100644 --- a/src/i18n/da.js +++ b/dist/i18n/query-builder.da.js @@ -1,19 +1,35 @@ /*! - * jQuery QueryBuilder - * Oversat af Jna Borup Coyle, github@coyle.dk + * jQuery QueryBuilder 3.0.0 + * Locale: Danish (da) + * Author: Jna Borup Coyle, github@coyle.dk + * Licensed under MIT (https://opensource.org/licenses/MIT) */ -jQuery.fn.queryBuilder.defaults.set({ lang: { +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['da'] = { + "__locale": "Danish (da)", + "__author": "Jna Borup Coyle, github@coyle.dk", "add_rule": "Tilføj regel", "add_group": "Tilføj gruppe", "delete_rule": "Slet regel", "delete_group": "Slet gruppe", - + "conditions": { + "AND": "OG", + "OR": "ELLER" + }, "condition_and": "OG", "condition_or": "ELLER", - - "filter_select_placeholder": "------", - "operators": { "equal": "lig med", "not_equal": "ikke lige med", @@ -34,4 +50,7 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "is_null": "er null", "is_not_null": "er ikke null" } -}}); \ No newline at end of file +}; + +QueryBuilder.defaults({ lang_code: 'da' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.de.js b/dist/i18n/query-builder.de.js new file mode 100644 index 00000000..d92c6679 --- /dev/null +++ b/dist/i18n/query-builder.de.js @@ -0,0 +1,76 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: German (de) + * Author: "raimu" + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['de'] = { + "__locale": "German (de)", + "__author": "\"raimu\"", + "add_rule": "neue Regel", + "add_group": "neue Gruppe", + "delete_rule": "löschen", + "delete_group": "löschen", + "conditions": { + "AND": "UND", + "OR": "ODER" + }, + "operators": { + "equal": "gleich", + "not_equal": "ungleich", + "in": "in", + "not_in": "nicht in", + "less": "kleiner", + "less_or_equal": "kleiner gleich", + "greater": "größer", + "greater_or_equal": "größer gleich", + "between": "zwischen", + "not_between": "nicht zwischen", + "begins_with": "beginnt mit", + "not_begins_with": "beginnt nicht mit", + "contains": "enthält", + "not_contains": "enthält nicht", + "ends_with": "endet mit", + "not_ends_with": "endet nicht mit", + "is_empty": "ist leer", + "is_not_empty": "ist nicht leer", + "is_null": "ist null", + "is_not_null": "ist nicht null" + }, + "errors": { + "no_filter": "Kein Filter ausgewählt", + "empty_group": "Die Gruppe ist leer", + "radio_empty": "Kein Wert ausgewählt", + "checkbox_empty": "Kein Wert ausgewählt", + "select_empty": "Kein Wert ausgewählt", + "string_empty": "Leerer Wert", + "string_exceed_min_length": "Muss mindestens {0} Zeichen enthalten", + "string_exceed_max_length": "Darf nicht mehr als {0} Zeichen enthalten", + "string_invalid_format": "Ungültiges Format ({0})", + "number_nan": "Keine Zahl", + "number_not_integer": "Keine Ganzzahl", + "number_not_double": "Keine Dezimalzahl", + "number_exceed_min": "Muss größer als {0} sein", + "number_exceed_max": "Muss kleiner als {0} sein", + "number_wrong_step": "Muss ein Vielfaches von {0} sein", + "datetime_invalid": "Ungültiges Datumsformat ({0})", + "datetime_exceed_min": "Muss nach dem {0} sein", + "datetime_exceed_max": "Muss vor dem {0} sein" + } +}; + +QueryBuilder.defaults({ lang_code: 'de' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.el.js b/dist/i18n/query-builder.el.js new file mode 100644 index 00000000..701d2045 --- /dev/null +++ b/dist/i18n/query-builder.el.js @@ -0,0 +1,80 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Greek (el) + * Author: Stelios Patsatzis, https://www.linkedin.com/in/stelios-patsatzis-89841561 + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['el'] = { + "__locale": "Greek (el)", + "__author": "Stelios Patsatzis, https://www.linkedin.com/in/stelios-patsatzis-89841561", + "add_rule": "Προσθήκη Συνθήκης", + "add_group": "Προσθήκη Ομάδας", + "delete_rule": "Διαγραφή", + "delete_group": "Διαγραφή", + "conditions": { + "AND": "Λογικό ΚΑΙ", + "OR": "Λογικό Η" + }, + "operators": { + "equal": "Ισούται με", + "not_equal": "Διάφορο από ", + "in": "Περιέχει", + "not_in": "Δεν Περιέχει", + "less": "Λιγότερο από", + "less_or_equal": "Λιγότερο ή Ίσο", + "greater": "Μεγαλύτερο από", + "greater_or_equal": "Μεγαλύτερο ή Ίσο", + "between": "Μεταξύ", + "not_between": "Εκτός", + "begins_with": "Αρχίζει με", + "not_begins_with": "Δεν αρχίζει με", + "contains": "Περιέχει", + "not_contains": "Δεν περιέχει", + "ends_with": "Τελειώνει σε", + "not_ends_with": "Δεν τελειώνει σε", + "is_empty": "Είναι άδειο", + "is_not_empty": "Δεν είναι άδειο", + "is_null": "Είναι NULL", + "is_not_null": "Δεν είναι NULL" + }, + "errors": { + "no_filter": "Χωρίς φίλτρα", + "empty_group": "Άδεια ομάδα", + "radio_empty": "Χωρίς τιμή", + "checkbox_empty": "Χωρίς τιμή", + "select_empty": "Χωρίς τιμή", + "string_empty": "Χωρίς τιμή", + "string_exceed_min_length": "Ελάχιστο όριο {0} χαρακτήρων", + "string_exceed_max_length": "Μέγιστο όριο {0} χαρακτήρων", + "string_invalid_format": "Λανθασμένη μορφή ({0})", + "number_nan": "Δεν είναι αριθμός", + "number_not_integer": "Δεν είναι ακέραιος αριθμός", + "number_not_double": "Δεν είναι πραγματικός αριθμός", + "number_exceed_min": "Πρέπει να είναι μεγαλύτερο απο {0}", + "number_exceed_max": "Πρέπει να είναι μικρότερο απο {0}", + "number_wrong_step": "Πρέπει να είναι πολλαπλάσιο του {0}", + "datetime_empty": "Χωρίς τιμή", + "datetime_invalid": "Λανθασμένη μορφή ημερομηνίας ({0})", + "datetime_exceed_min": "Νεότερο από {0}", + "datetime_exceed_max": "Παλαιότερο από {0}", + "boolean_not_valid": "Δεν είναι BOOLEAN", + "operator_not_multiple": "Η συνθήκη \"{1}\" δεν μπορεί να δεχθεί πολλαπλές τιμές" + }, + "invert": "Εναλλαγή" +}; + +QueryBuilder.defaults({ lang_code: 'el' }); +})); \ No newline at end of file diff --git a/src/i18n/en.js b/dist/i18n/query-builder.en.js similarity index 58% rename from src/i18n/en.js rename to dist/i18n/query-builder.en.js index 513e915a..a6ce0f66 100644 --- a/src/i18n/en.js +++ b/dist/i18n/query-builder.en.js @@ -1,19 +1,33 @@ /*! - * jQuery QueryBuilder - * Reference language file + * jQuery QueryBuilder 3.0.0 + * Locale: English (en) + * Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr + * Licensed under MIT (https://opensource.org/licenses/MIT) */ -jQuery.fn.queryBuilder.defaults.set({ lang: { +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['en'] = { + "__locale": "English (en)", + "__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr", "add_rule": "Add rule", "add_group": "Add group", "delete_rule": "Delete", "delete_group": "Delete", - - "condition_and": "AND", - "condition_or": "OR", - - "filter_select_placeholder": "------", - + "conditions": { + "AND": "AND", + "OR": "OR" + }, "operators": { "equal": "equal", "not_equal": "not equal", @@ -24,6 +38,7 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "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", @@ -35,7 +50,6 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "is_null": "is null", "is_not_null": "is not null" }, - "errors": { "no_filter": "No filter selected", "empty_group": "The group is empty", @@ -52,8 +66,18 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "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}" - } -}}); \ No newline at end of file + "datetime_exceed_max": "Must be before {0}", + "datetime_between_invalid": "Invalid values, {0} is greater than {1}", + "boolean_not_valid": "Not a boolean", + "operator_not_multiple": "Operator \"{1}\" cannot accept multiple values" + }, + "invert": "Invert", + "NOT": "NOT" +}; + +QueryBuilder.defaults({ lang_code: 'en' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.eo.js b/dist/i18n/query-builder.eo.js new file mode 100644 index 00000000..6e66521d --- /dev/null +++ b/dist/i18n/query-builder.eo.js @@ -0,0 +1,83 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Esperanto (eo) + * Author: Robin van der Vliet, https://robinvandervliet.com/ + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['eo'] = { + "__locale": "Esperanto (eo)", + "__author": "Robin van der Vliet, https://robinvandervliet.com/", + "add_rule": "Aldoni regulon", + "add_group": "Aldoni grupon", + "delete_rule": "Forigi", + "delete_group": "Forigi", + "conditions": { + "AND": "KAJ", + "OR": "AŬ" + }, + "operators": { + "equal": "estas egala al", + "not_equal": "ne estas egala al", + "in": "estas en", + "not_in": "ne estas en", + "less": "estas malpli ol", + "less_or_equal": "estas malpli ol aŭ egala al", + "greater": "estas pli ol", + "greater_or_equal": "estas pli ol aŭ egala al", + "between": "estas inter", + "not_between": "ne estas inter", + "begins_with": "komenciĝas per", + "not_begins_with": "ne komenciĝas per", + "contains": "enhavas", + "not_contains": "ne enhavas", + "ends_with": "finiĝas per", + "not_ends_with": "ne finiĝas per", + "is_empty": "estas malplena", + "is_not_empty": "ne estas malplena", + "is_null": "estas senvalora", + "is_not_null": "ne estas senvalora" + }, + "errors": { + "no_filter": "Neniu filtrilo elektita", + "empty_group": "La grupo estas malplena", + "radio_empty": "Neniu valoro elektita", + "checkbox_empty": "Neniu valoro elektita", + "select_empty": "Neniu valoro elektita", + "string_empty": "Malplena valoro", + "string_exceed_min_length": "Devas enhavi pli ol {0} signojn", + "string_exceed_max_length": "Devas ne enhavi pli ol {0} signojn", + "string_invalid_format": "Nevalida strukturo ({0})", + "number_nan": "Ne estas nombro", + "number_not_integer": "Ne estas entjera nombro", + "number_not_double": "Ne estas reela nombro", + "number_exceed_min": "Devas esti pli ol {0}", + "number_exceed_max": "Devas esti malpli ol {0}", + "number_wrong_step": "Devas esti oblo de {0}", + "number_between_invalid": "Nevalidaj valoroj, {0} estas pli ol {1}", + "datetime_empty": "Malplena valoro", + "datetime_invalid": "Nevalida dato ({0})", + "datetime_exceed_min": "Devas esti post {0}", + "datetime_exceed_max": "Devas esti antaŭ {0}", + "datetime_between_invalid": "Nevalidaj valoroj, {0} estas post {1}", + "boolean_not_valid": "Ne estas bulea valoro", + "operator_not_multiple": "La operacio \"{1}\" ne akceptas plurajn valorojn" + }, + "invert": "Inversigi", + "NOT": "NE" +}; + +QueryBuilder.defaults({ lang_code: 'eo' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.es.js b/dist/i18n/query-builder.es.js new file mode 100644 index 00000000..ea94a3fd --- /dev/null +++ b/dist/i18n/query-builder.es.js @@ -0,0 +1,81 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Spanish (es) + * Author: "pyarza", "kddlb" + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['es'] = { + "__locale": "Spanish (es)", + "__author": "\"pyarza\", \"kddlb\"", + "add_rule": "Añadir regla", + "add_group": "Añadir grupo", + "delete_rule": "Borrar", + "delete_group": "Borrar", + "conditions": { + "AND": "Y", + "OR": "O" + }, + "operators": { + "equal": "igual", + "not_equal": "distinto", + "in": "en", + "not_in": "no en", + "less": "menor", + "less_or_equal": "menor o igual", + "greater": "mayor", + "greater_or_equal": "mayor o igual", + "between": "entre", + "not_between": "no está entre", + "begins_with": "empieza por", + "not_begins_with": "no empieza por", + "contains": "contiene", + "not_contains": "no contiene", + "ends_with": "acaba con", + "not_ends_with": "no acaba con", + "is_empty": "está vacío", + "is_not_empty": "no está vacío", + "is_null": "es nulo", + "is_not_null": "no es nulo" + }, + "errors": { + "no_filter": "No se ha seleccionado ningún filtro", + "empty_group": "El grupo está vacío", + "radio_empty": "Ningún valor seleccionado", + "checkbox_empty": "Ningún valor seleccionado", + "select_empty": "Ningún valor seleccionado", + "string_empty": "Cadena vacía", + "string_exceed_min_length": "Debe contener al menos {0} caracteres", + "string_exceed_max_length": "No debe contener más de {0} caracteres", + "string_invalid_format": "Formato inválido ({0})", + "number_nan": "No es un número", + "number_not_integer": "No es un número entero", + "number_not_double": "No es un número real", + "number_exceed_min": "Debe ser mayor que {0}", + "number_exceed_max": "Debe ser menor que {0}", + "number_wrong_step": "Debe ser múltiplo de {0}", + "datetime_invalid": "Formato de fecha inválido ({0})", + "datetime_exceed_min": "Debe ser posterior a {0}", + "datetime_exceed_max": "Debe ser anterior a {0}", + "number_between_invalid": "Valores Inválidos, {0} es mayor que {1}", + "datetime_empty": "Campo vacio", + "datetime_between_invalid": "Valores Inválidos, {0} es mayor que {1}", + "boolean_not_valid": "No es booleano", + "operator_not_multiple": "El operador \"{1}\" no puede aceptar valores multiples" + } +}; + +QueryBuilder.defaults({ lang_code: 'es' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.fa-IR.js b/dist/i18n/query-builder.fa-IR.js new file mode 100644 index 00000000..011758fd --- /dev/null +++ b/dist/i18n/query-builder.fa-IR.js @@ -0,0 +1,79 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Farsi (fa-ir) + * Author: Behzad Sedighzade, behzad.sedighzade@gmail.com + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['fa-IR'] = { + "__locale": "Farsi (fa-ir)", + "__author": "Behzad Sedighzade, behzad.sedighzade@gmail.com", + "add_rule": "افزودن قاعده", + "add_group": "افزودن گروه", + "delete_rule": "حذف قاعده", + "delete_group": "حذف گروه", + "conditions": { + "AND": "و", + "OR": "یا" + }, + "operators": { + "equal": "برابر با", + "not_equal": "مخالف", + "in": "شامل مجموعه شود", + "not_in": "شامل مجموعه نشود", + "less": "کمتر از", + "less_or_equal": "کمتر یا مساوی با", + "greater": "بزرگتر از", + "greater_or_equal": "بزرگتر یا مساوی با", + "between": "مابین", + "not_between": "مابین نباشد", + "begins_with": "شروع شود با", + "not_begins_with": "شروع نشود با", + "contains": "شامل شود", + "not_contains": "شامل نشود", + "ends_with": "خاتمه یابد با", + "not_ends_with": "خاتمه نیابد با", + "is_empty": "خالی باشد", + "is_not_empty": "خالی نباشد", + "is_null": "باشد ( null ) پوچ", + "is_not_null": "نباشد( null ) پوچ " + }, + "errors": { + "no_filter": "هیچ قاعده ای انتخاب نشده است", + "empty_group": "گروه خالی است", + "radio_empty": "مقداری انتخاب نشده است", + "checkbox_empty": "مقداری انتخاب نشده است", + "select_empty": "مقداری انتخاب نشده است", + "string_empty": "مقدار متنی خالی است", + "string_exceed_min_length": "رشته حداقل باید {0} عدد حرف داشته باشد", + "string_exceed_max_length": "رشته حداکثر {0} عدد حرف می تواند قبول کند", + "string_invalid_format": "قالب رشته {0} نامعتبر ست", + "number_nan": "عدد وارد کنید", + "number_not_integer": "مقدار صحیح وارد کنید", + "number_not_double": "مقدار اعشاری وارد کنید", + "number_exceed_min": "باید از {0} بزرگتر باشد", + "number_exceed_max": "باید از {0} کمتر باشد", + "number_wrong_step": "باید مضربی از {0} باشد", + "datetime_empty": "مقدار تاریخ خالی وارد شده!", + "datetime_invalid": "قالب تاریخ ( {0} ) اشتباه است", + "datetime_exceed_min": "باید بعد از {0} باشد", + "datetime_exceed_max": "باید قبل از {0} باشد", + "boolean_not_valid": "مقدار دودویی وارد کنید", + "operator_not_multiple": "اپراتور \"{1}\" نمی تواند چند مقدار قبول کند" + } +}; + +QueryBuilder.defaults({ lang_code: 'fa-IR' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.fr.js b/dist/i18n/query-builder.fr.js new file mode 100644 index 00000000..39dc7303 --- /dev/null +++ b/dist/i18n/query-builder.fr.js @@ -0,0 +1,83 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: French (fr) + * Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['fr'] = { + "__locale": "French (fr)", + "__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr", + "add_rule": "Ajouter une règle", + "add_group": "Ajouter un groupe", + "delete_rule": "Supprimer", + "delete_group": "Supprimer", + "conditions": { + "AND": "ET", + "OR": "OU" + }, + "operators": { + "equal": "est égal à", + "not_equal": "n'est pas égal à", + "in": "est compris dans", + "not_in": "n'est pas compris dans", + "less": "est inférieur à", + "less_or_equal": "est inférieur ou égal à", + "greater": "est supérieur à", + "greater_or_equal": "est supérieur ou égal à", + "between": "est entre", + "not_between": "n'est pas entre", + "begins_with": "commence par", + "not_begins_with": "ne commence pas par", + "contains": "contient", + "not_contains": "ne contient pas", + "ends_with": "finit par", + "not_ends_with": "ne finit pas par", + "is_empty": "est vide", + "is_not_empty": "n'est pas vide", + "is_null": "est nul", + "is_not_null": "n'est pas nul" + }, + "errors": { + "no_filter": "Aucun filtre sélectionné", + "empty_group": "Le groupe est vide", + "radio_empty": "Pas de valeur selectionnée", + "checkbox_empty": "Pas de valeur selectionnée", + "select_empty": "Pas de valeur selectionnée", + "string_empty": "Valeur vide", + "string_exceed_min_length": "Doit contenir au moins {0} caractères", + "string_exceed_max_length": "Ne doit pas contenir plus de {0} caractères", + "string_invalid_format": "Format invalide ({0})", + "number_nan": "N'est pas un nombre", + "number_not_integer": "N'est pas un entier", + "number_not_double": "N'est pas un nombre réel", + "number_exceed_min": "Doit être plus grand que {0}", + "number_exceed_max": "Doit être plus petit que {0}", + "number_wrong_step": "Doit être un multiple de {0}", + "number_between_invalid": "Valeurs invalides, {0} est plus grand que {1}", + "datetime_empty": "Valeur vide", + "datetime_invalid": "Fomat de date invalide ({0})", + "datetime_exceed_min": "Doit être après {0}", + "datetime_exceed_max": "Doit être avant {0}", + "datetime_between_invalid": "Valeurs invalides, {0} est plus grand que {1}", + "boolean_not_valid": "N'est pas un booléen", + "operator_not_multiple": "L'opérateur \"{1}\" ne peut utiliser plusieurs valeurs" + }, + "invert": "Inverser", + "NOT": "NON" +}; + +QueryBuilder.defaults({ lang_code: 'fr' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.he.js b/dist/i18n/query-builder.he.js new file mode 100644 index 00000000..2e453775 --- /dev/null +++ b/dist/i18n/query-builder.he.js @@ -0,0 +1,81 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Hebrew (he) + * Author: Kfir Stri https://github.com/kfirstri + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['he'] = { + "__locale": "Hebrew (he)", + "__author": "Kfir Stri https://github.com/kfirstri", + "add_rule": "הוסף כלל", + "add_group": "הוסף קבוצה", + "delete_rule": "מחק", + "delete_group": "מחק", + "conditions": { + "AND": "וגם", + "OR": "או" + }, + "operators": { + "equal": "שווה ל", + "not_equal": "שונה מ", + "in": "חלק מ", + "not_in": "לא חלק מ", + "less": "פחות מ", + "less_or_equal": "פחות או שווה ל", + "greater": "גדול מ", + "greater_or_equal": "גדול או שווה ל", + "between": "בין", + "not_between": "לא בין", + "begins_with": "מתחיל ב", + "not_begins_with": "לא מתחיל ב", + "contains": "מכיל", + "not_contains": "לא מכיל", + "ends_with": "מסתיים ב", + "not_ends_with": "לא מסתיים ב", + "is_empty": "ריק", + "is_not_empty": "לא ריק", + "is_null": "חסר ערך", + "is_not_null": "לא חסר ערך" + }, + "errors": { + "no_filter": "לא נבחרו מסננים", + "empty_group": "הקבוצה רירקה", + "radio_empty": "לא נבחר אף ערך", + "checkbox_empty": "לא נבחר אף ערך", + "select_empty": "לא נבחר אף ערך", + "string_empty": "חסר ערך", + "string_exceed_min_length": "המחרוזת חייבת להכיל לפחות {0} תווים", + "string_exceed_max_length": "המחרוזת לא יכולה להכיל יותר מ{0} תווים", + "string_invalid_format": "המחרוזת בפורמט שגוי ({0})", + "number_nan": "זהו לא מספר", + "number_not_integer": "המספר אינו מספר שלם", + "number_not_double": "המספר אינו מספר עשרוני", + "number_exceed_min": "המספר צריך להיות גדול מ {0}", + "number_exceed_max": "המספר צריך להיות קטן מ{0}", + "number_wrong_step": "המספר צריך להיות כפולה של {0}", + "datetime_empty": "תאריך ריק", + "datetime_invalid": "פורמט תאריך שגוי ({0})", + "datetime_exceed_min": "התאריך חייב להיות אחרי {0}", + "datetime_exceed_max": "התאריך חייב להיות לפני {0}", + "boolean_not_valid": "זהו לא בוליאני", + "operator_not_multiple": "האופרטור \"{1}\" לא יכול לקבל ערכים מרובים" + }, + "invert": "הפוך שאילתא", + "NOT": "לא" +}; + +QueryBuilder.defaults({ lang_code: 'he' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.hu.js b/dist/i18n/query-builder.hu.js new file mode 100644 index 00000000..323a9adc --- /dev/null +++ b/dist/i18n/query-builder.hu.js @@ -0,0 +1,83 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Hungarian - Magyar (hu) + * Author: Szabó Attila "Tailor993", https://www.tailor993.hu + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['hu'] = { + "__locale": "Hungarian - Magyar (hu)", + "__author": "Szabó Attila \"Tailor993\", https://www.tailor993.hu", + "add_rule": "Feltétel hozzáadása", + "add_group": "Csoport hozzáadása", + "delete_rule": "Feltétel törlése", + "delete_group": "Csoport törlése", + "conditions": { + "AND": "ÉS", + "OR": "VAGY" + }, + "operators": { + "equal": "egyenlő", + "not_equal": "nem egyenlő", + "in": "bennevan", + "not_in": "nincs benne", + "less": "kisebb", + "less_or_equal": "kisebb vagy egyenlő", + "greater": "nagyobb", + "greater_or_equal": "nagyobb vagy egyenlő", + "between": "közötte", + "not_between": "nincs közötte", + "begins_with": "ezzel kezdődik", + "not_begins_with": "ezzel nem kezdődik", + "contains": "tartalmazza", + "not_contains": "nem tartalmazza", + "ends_with": "erre végződik", + "not_ends_with": "errre nem végződik", + "is_empty": "üres", + "is_not_empty": "nem üres", + "is_null": "null", + "is_not_null": "nem null" + }, + "errors": { + "no_filter": "Nincs kiválasztott feltétel", + "empty_group": "A csoport üres", + "radio_empty": "Nincs kiválasztott érték", + "checkbox_empty": "Nincs kiválasztott érték", + "select_empty": "Nincs kiválasztott érték", + "string_empty": "Üres érték", + "string_exceed_min_length": "A megadott szöveg rövidebb a várt {0} karakternél", + "string_exceed_max_length": "A megadott szöveg nem tartalmazhat többet, mint {0} karaktert", + "string_invalid_format": "Nem megfelelő formátum ({0})", + "number_nan": "Nem szám", + "number_not_integer": "Nem egész szám (integer)", + "number_not_double": "Nem valós szám", + "number_exceed_min": "Nagyobbnak kell lennie, mint {0}", + "number_exceed_max": "Kisebbnek kell lennie, mint {0}", + "number_wrong_step": "{0} többszörösének kell lennie.", + "number_between_invalid": "INem megfelelő érték, {0} nagyobb, mint {1}", + "datetime_empty": "Üres érték", + "datetime_invalid": "nem megfelelő dátum formátum ({0})", + "datetime_exceed_min": "A dátumnak későbbinek kell lennie, mint{0}", + "datetime_exceed_max": "A dátumnak korábbinak kell lennie, mint {0}", + "datetime_between_invalid": "Nem megfelelő értékek, {0} nagyobb, mint {1}", + "boolean_not_valid": "Nem igaz/hamis (boolean)", + "operator_not_multiple": "Ez a művelet: \"{1}\" nem fogadhat el több értéket" + }, + "invert": "Megfordítás (Invertálás)", + "NOT": "NEM" +}; + +QueryBuilder.defaults({ lang_code: 'hu' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.it.js b/dist/i18n/query-builder.it.js new file mode 100644 index 00000000..c4565e96 --- /dev/null +++ b/dist/i18n/query-builder.it.js @@ -0,0 +1,81 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Italian (it) + * Author: davegraziosi, Giuseppe Lodi Rizzini + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['it'] = { + "__locale": "Italian (it)", + "__author": "davegraziosi, Giuseppe Lodi Rizzini", + "add_rule": "Aggiungi regola", + "add_group": "Aggiungi gruppo", + "delete_rule": "Elimina", + "delete_group": "Elimina", + "conditions": { + "AND": "E", + "OR": "O" + }, + "operators": { + "equal": "uguale", + "not_equal": "non uguale", + "in": "in", + "not_in": "non in", + "less": "minore", + "less_or_equal": "minore o uguale", + "greater": "maggiore", + "greater_or_equal": "maggiore o uguale", + "between": "compreso tra", + "not_between": "non compreso tra", + "begins_with": "inizia con", + "not_begins_with": "non inizia con", + "contains": "contiene", + "not_contains": "non contiene", + "ends_with": "finisce con", + "not_ends_with": "non finisce con", + "is_empty": "è vuoto", + "is_not_empty": "non è vuoto", + "is_null": "è nullo", + "is_not_null": "non è nullo" + }, + "errors": { + "no_filter": "Nessun filtro selezionato", + "empty_group": "Il gruppo è vuoto", + "radio_empty": "No value selected", + "checkbox_empty": "Nessun valore selezionato", + "select_empty": "Nessun valore selezionato", + "string_empty": "Valore vuoto", + "string_exceed_min_length": "Deve contenere almeno {0} caratteri", + "string_exceed_max_length": "Non deve contenere più di {0} caratteri", + "string_invalid_format": "Formato non valido ({0})", + "number_nan": "Non è un numero", + "number_not_integer": "Non è un intero", + "number_not_double": "Non è un numero con la virgola", + "number_exceed_min": "Deve essere maggiore di {0}", + "number_exceed_max": "Deve essere minore di {0}", + "number_wrong_step": "Deve essere multiplo di {0}", + "number_between_invalid": "Valori non validi, {0} è maggiore di {1}", + "datetime_empty": "Valore vuoto", + "datetime_invalid": "Formato data non valido ({0})", + "datetime_exceed_min": "Deve essere successivo a {0}", + "datetime_exceed_max": "Deve essere precedente a {0}", + "datetime_between_invalid": "Valori non validi, {0} è maggiore di {1}", + "boolean_not_valid": "Non è un booleano", + "operator_not_multiple": "L'Operatore {0} non può accettare valori multipli" + } +}; + +QueryBuilder.defaults({ lang_code: 'it' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.lt.js b/dist/i18n/query-builder.lt.js new file mode 100644 index 00000000..5d0324ab --- /dev/null +++ b/dist/i18n/query-builder.lt.js @@ -0,0 +1,83 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Lithuanian (lt) + * Author: Dalius Guzauskas (aka Tichij), https://lt.linkedin.com/in/daliusg + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['lt'] = { + "__locale": "Lithuanian (lt)", + "__author": "Dalius Guzauskas (aka Tichij), https://lt.linkedin.com/in/daliusg", + "add_rule": "Pridėti taisyklę", + "add_group": "Pridėti grupę", + "delete_rule": "Ištrinti", + "delete_group": "Ištrinti", + "conditions": { + "AND": "IR", + "OR": "ARBA" + }, + "operators": { + "equal": "lygu", + "not_equal": "nėra lygu", + "in": "iš nurodytų", + "not_in": "ne iš nurodytų", + "less": "mažiau", + "less_or_equal": "mažiau arba lygu", + "greater": "daugiau", + "greater_or_equal": "daugiau arba lygu", + "between": "tarp", + "not_between": "nėra tarp", + "begins_with": "prasideda", + "not_begins_with": "neprasideda", + "contains": "turi", + "not_contains": "neturi", + "ends_with": "baigiasi", + "not_ends_with": "nesibaigia", + "is_empty": "tuščia", + "is_not_empty": "ne tuščia", + "is_null": "neapibrėžta", + "is_not_null": "nėra neapibrėžta" + }, + "errors": { + "no_filter": "Nepasirinktas filtras", + "empty_group": "Grupė tuščia", + "radio_empty": "Nepasirinkta reikšmė", + "checkbox_empty": "Nepasirinkta reikšmė", + "select_empty": "Nepasirinkta reikšmė", + "string_empty": "Tuščia reikšmė", + "string_exceed_min_length": "Turi būti bent {0} simbolių", + "string_exceed_max_length": "Turi būti ne daugiau kaip {0} simbolių", + "string_invalid_format": "Klaidingas formatas ({0})", + "number_nan": "Nėra skaičius", + "number_not_integer": "Ne sveikasis skaičius", + "number_not_double": "Ne realusis skaičius", + "number_exceed_min": "Turi būti daugiau už {0}", + "number_exceed_max": "Turi būti mažiau už {0}", + "number_wrong_step": "Turi būti {0} kartotinis", + "number_between_invalid": "Klaidingos reikšmės, {0} yra daugiau už {1}", + "datetime_empty": "Tuščia reikšmė", + "datetime_invalid": "Klaidingas datos formatas ({0})", + "datetime_exceed_min": "Turi būti po {0}", + "datetime_exceed_max": "Turi būti prieš {0}", + "datetime_between_invalid": "Klaidingos reikšmės, {0} yra daugiau už {1}", + "boolean_not_valid": "Nėra loginis tipas", + "operator_not_multiple": "Operatorius \"{1}\" negali priimti kelių reikšmių" + }, + "invert": "Invertuoti", + "NOT": "NE" +}; + +QueryBuilder.defaults({ lang_code: 'lt' }); +})); \ No newline at end of file diff --git a/src/i18n/nl.js b/dist/i18n/query-builder.nl.js similarity index 73% rename from src/i18n/nl.js rename to dist/i18n/query-builder.nl.js index 3a591f1f..8e88ac56 100644 --- a/src/i18n/nl.js +++ b/dist/i18n/query-builder.nl.js @@ -1,19 +1,33 @@ /*! - * jQuery QueryBuilder - * Dutch translation by "Roywcm" + * jQuery QueryBuilder 3.0.0 + * Locale: Dutch (nl) + * Author: "Roywcm" + * Licensed under MIT (https://opensource.org/licenses/MIT) */ -jQuery.fn.queryBuilder.defaults.set({ lang: { +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['nl'] = { + "__locale": "Dutch (nl)", + "__author": "\"Roywcm\"", "add_rule": "Nieuwe regel", "add_group": "Nieuwe groep", "delete_rule": "Verwijder", "delete_group": "Verwijder", - - "condition_and": "EN", - "condition_or": "OF", - - "filter_select_placeholder": "------", - + "conditions": { + "AND": "EN", + "OR": "OF" + }, "operators": { "equal": "gelijk", "not_equal": "niet gelijk", @@ -24,6 +38,7 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "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", @@ -35,7 +50,6 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "is_null": "is null", "is_not_null": "is niet null" }, - "errors": { "no_filter": "Geen filter geselecteerd", "empty_group": "De groep is leeg", @@ -56,4 +70,7 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "datetime_exceed_min": "Dient na {0}", "datetime_exceed_max": "Dient voor {0}" } -}}); \ No newline at end of file +}; + +QueryBuilder.defaults({ lang_code: 'nl' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.no.js b/dist/i18n/query-builder.no.js new file mode 100644 index 00000000..1ec2d013 --- /dev/null +++ b/dist/i18n/query-builder.no.js @@ -0,0 +1,54 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Norwegian (no) + * Author: Jna Borup Coyle, github@coyle.dk + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['no'] = { + "__locale": "Norwegian (no)", + "__author": "Jna Borup Coyle, github@coyle.dk", + "add_rule": "Legg til regel", + "add_group": "Legg til gruppe", + "delete_rule": "Slett regel", + "delete_group": "Slett gruppe", + "conditions": { + "AND": "OG", + "OR": "ELLER" + }, + "operators": { + "equal": "er lik", + "not_equal": "er ikke lik", + "in": "finnes i", + "not_in": "finnes ikke i", + "less": "er mindre enn", + "less_or_equal": "er mindre eller lik", + "greater": "er større enn", + "greater_or_equal": "er større eller lik", + "begins_with": "begynner med", + "not_begins_with": "begynner ikke med", + "contains": "inneholder", + "not_contains": "inneholder ikke", + "ends_with": "slutter med", + "not_ends_with": "slutter ikke med", + "is_empty": "er tom", + "is_not_empty": "er ikke tom", + "is_null": "er null", + "is_not_null": "er ikke null" + } +}; + +QueryBuilder.defaults({ lang_code: 'no' }); +})); \ No newline at end of file diff --git a/src/i18n/pl.js b/dist/i18n/query-builder.pl.js similarity index 65% rename from src/i18n/pl.js rename to dist/i18n/query-builder.pl.js index 475d25e2..c89d0316 100644 --- a/src/i18n/pl.js +++ b/dist/i18n/query-builder.pl.js @@ -1,19 +1,33 @@ /*! - * jQuery QueryBuilder - * Polish translation by Artur Smolarek + * jQuery QueryBuilder 3.0.0 + * Locale: Polish (pl) + * Author: Artur Smolarek + * Licensed under MIT (https://opensource.org/licenses/MIT) */ -jQuery.fn.queryBuilder.defaults.set({ lang: { +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['pl'] = { + "__locale": "Polish (pl)", + "__author": "Artur Smolarek", "add_rule": "Dodaj regułę", "add_group": "Dodaj grupę", "delete_rule": "Usuń", "delete_group": "Usuń", - - "condition_and": "AND", - "condition_or": "OR", - - "filter_select_placeholder": "------", - + "conditions": { + "AND": "ORAZ", + "OR": "LUB" + }, "operators": { "equal": "równa się", "not_equal": "jest różne od", @@ -24,6 +38,7 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "greater": "większe", "greater_or_equal": "większe lub równe", "between": "pomiędzy", + "not_between": "nie jest pomiędzy", "begins_with": "rozpoczyna się od", "not_begins_with": "nie rozpoczyna się od", "contains": "zawiera", @@ -35,7 +50,6 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "is_null": "jest niezdefiniowane", "is_not_null": "nie jest niezdefiniowane" }, - "errors": { "no_filter": "Nie wybrano żadnego filtra", "empty_group": "Grupa jest pusta", @@ -52,8 +66,15 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "number_exceed_min": "Musi być większe niż {0}", "number_exceed_max": "Musi być mniejsze niż {0}", "number_wrong_step": "Musi być wielokrotnością {0}", + "datetime_empty": "Nie wybrano wartości", "datetime_invalid": "Nieprawidłowy format daty ({0})", "datetime_exceed_min": "Musi być po {0}", - "datetime_exceed_max": "Musi być przed {0}" - } -}}); \ No newline at end of file + "datetime_exceed_max": "Musi być przed {0}", + "boolean_not_valid": "Niepoprawna wartość logiczna", + "operator_not_multiple": "Operator \"{1}\" nie przyjmuje wielu wartości" + }, + "invert": "Odwróć" +}; + +QueryBuilder.defaults({ lang_code: 'pl' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.pt-BR.js b/dist/i18n/query-builder.pt-BR.js new file mode 100644 index 00000000..a9b4fe59 --- /dev/null +++ b/dist/i18n/query-builder.pt-BR.js @@ -0,0 +1,80 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Brazilian Portuguese (pr-BR) + * Author: Leandro Gehlen, leandrogehlen@gmail.com; Marcos Ferretti, marcosvferretti@gmail.com + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['pt-BR'] = { + "__locale": "Brazilian Portuguese (pr-BR)", + "__author": "Leandro Gehlen, leandrogehlen@gmail.com; Marcos Ferretti, marcosvferretti@gmail.com", + "add_rule": "Nova Regra", + "add_group": "Novo Grupo", + "delete_rule": "Excluir", + "delete_group": "Excluir", + "conditions": { + "AND": "E", + "OR": "OU" + }, + "operators": { + "equal": "Igual", + "not_equal": "Diferente", + "in": "Contido", + "not_in": "Não contido", + "less": "Menor", + "less_or_equal": "Menor ou igual", + "greater": "Maior", + "greater_or_equal": "Maior ou igual", + "between": "Entre", + "not_between": "Não entre", + "begins_with": "Iniciando com", + "not_begins_with": "Não iniciando com", + "contains": "Contém", + "not_contains": "Não contém", + "ends_with": "Terminando com", + "not_ends_with": "Terminando sem", + "is_empty": "É vazio", + "is_not_empty": "Não é vazio", + "is_null": "É nulo", + "is_not_null": "Não é nulo" + }, + "errors": { + "no_filter": "Nenhum filtro selecionado", + "empty_group": "O grupo está vazio", + "radio_empty": "Nenhum valor selecionado", + "checkbox_empty": "Nenhum valor selecionado", + "select_empty": "Nenhum valor selecionado", + "string_empty": "Valor vazio", + "string_exceed_min_length": "É necessário conter pelo menos {0} caracteres", + "string_exceed_max_length": "É necessário conter mais de {0} caracteres", + "string_invalid_format": "Formato inválido ({0})", + "number_nan": "Não é um número", + "number_not_integer": "Não é um número inteiro", + "number_not_double": "Não é um número real", + "number_exceed_min": "É necessário ser maior que {0}", + "number_exceed_max": "É necessário ser menor que {0}", + "number_wrong_step": "É necessário ser múltiplo de {0}", + "datetime_invalid": "Formato de data inválido ({0})", + "datetime_exceed_min": "É necessário ser superior a {0}", + "datetime_exceed_max": "É necessário ser inferior a {0}", + "datetime_empty": "Nenhuma data selecionada", + "boolean_not_valid": "Não é um valor booleano", + "operator_not_multiple": "O operador \"{1}\" não aceita valores múltiplos" + }, + "invert": "Inverter" +}; + +QueryBuilder.defaults({ lang_code: 'pt-BR' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.pt-PT.js b/dist/i18n/query-builder.pt-PT.js new file mode 100644 index 00000000..1e0aa77f --- /dev/null +++ b/dist/i18n/query-builder.pt-PT.js @@ -0,0 +1,75 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Portuguese (pt-PT) + * Author: Miguel Guerreiro, migas.csi@gmail.com + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['pt-PT'] = { + "__locale": "Portuguese (pt-PT)", + "__author": "Miguel Guerreiro, migas.csi@gmail.com", + "add_rule": "Nova Regra", + "add_group": "Novo Grupo", + "delete_rule": "Excluir", + "delete_group": "Excluir", + "conditions": { + "AND": "E", + "OR": "OU" + }, + "operators": { + "equal": "Igual a", + "not_equal": "Diferente de", + "in": "Contido", + "not_in": "Não contido", + "less": "Menor que", + "less_or_equal": "Menor ou igual a", + "greater": "Maior que", + "greater_or_equal": "Maior ou igual que", + "between": "Entre", + "begins_with": "Começar por", + "not_begins_with": "Não a começar por", + "contains": "Contém", + "not_contains": "Não contém", + "ends_with": "Terminando com", + "not_ends_with": "Terminando sem", + "is_empty": "É vazio", + "is_not_empty": "Não é vazio", + "is_null": "É nulo", + "is_not_null": "Não é nulo" + }, + "errors": { + "no_filter": "Nenhum filtro selecionado", + "empty_group": "O grupo está vazio", + "radio_empty": "Nenhum valor selecionado", + "checkbox_empty": "Nenhum valor selecionado", + "select_empty": "Nenhum valor selecionado", + "string_empty": "Valor vazio", + "string_exceed_min_length": "É necessário conter pelo menos {0} caracteres", + "string_exceed_max_length": "É necessário conter mais de {0} caracteres", + "string_invalid_format": "Formato inválido ({0})", + "number_nan": "Não é um número", + "number_not_integer": "Não é um número inteiro", + "number_not_double": "Não é um número real", + "number_exceed_min": "É necessário ser maior que {0}", + "number_exceed_max": "É necessário ser menor que {0}", + "number_wrong_step": "É necessário ser múltiplo de {0}", + "datetime_invalid": "Formato de data inválido ({0})", + "datetime_exceed_min": "É necessário ser superior a {0}", + "datetime_exceed_max": "É necessário ser inferior a {0}" + } +}; + +QueryBuilder.defaults({ lang_code: 'pt-PT' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.ro.js b/dist/i18n/query-builder.ro.js new file mode 100644 index 00000000..a1ba7eda --- /dev/null +++ b/dist/i18n/query-builder.ro.js @@ -0,0 +1,81 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Romanian (ro) + * Author: ArianServ, totpero + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['ro'] = { + "__locale": "Romanian (ro)", + "__author": "ArianServ, totpero", + "add_rule": "Adaugă regulă", + "add_group": "Adaugă grup", + "delete_rule": "Şterge", + "delete_group": "Şterge", + "conditions": { + "AND": "ŞI", + "OR": "SAU" + }, + "operators": { + "equal": "egal", + "not_equal": "diferit", + "in": "în", + "not_in": "nu în", + "less": "mai mic", + "less_or_equal": "mai mic sau egal", + "greater": "mai mare", + "greater_or_equal": "mai mare sau egal", + "between": "între", + "not_between": "nu între", + "begins_with": "începe cu", + "not_begins_with": "nu începe cu", + "contains": "conţine", + "not_contains": "nu conţine", + "ends_with": "se termină cu", + "not_ends_with": "nu se termină cu", + "is_empty": "este gol", + "is_not_empty": "nu este gol", + "is_null": "e nul", + "is_not_null": "nu e nul" + }, + "errors": { + "no_filter": "Nici un filtru selectat", + "empty_group": "Grupul este gol", + "radio_empty": "Nici o valoare nu este selectată", + "checkbox_empty": "Nici o valoare nu este selectată", + "select_empty": "Nici o valoare nu este selectată", + "string_empty": "Valoare goală", + "string_exceed_min_length": "Trebuie să conţină mai puţin de {0} caractere", + "string_exceed_max_length": "Trebuie să conţină mai mult de {0} caractere", + "string_invalid_format": "Format invalid ({0})", + "number_nan": "Nu este număr", + "number_not_integer": "Nu este număr întreg", + "number_not_double": "Nu este număr real", + "number_exceed_min": "Trebuie să fie mai mare decât {0}", + "number_exceed_max": "Trebuie să fie mai mic decât {0}", + "number_wrong_step": "Trebuie să fie multiplu de {0}", + "number_between_invalid": "Valori invalide, {0} este mai mare decât {1}", + "datetime_empty": "Valoare goală", + "datetime_invalid": "Format dată invalid ({0})", + "datetime_exceed_min": "Trebuie să fie după {0}", + "datetime_exceed_max": "Trebuie să fie înainte {0}", + "datetime_between_invalid": "Valori invalide, {0} este mai mare decât {1}", + "boolean_not_valid": "Nu este boolean", + "operator_not_multiple": "Operatorul \"{1}\" nu poate accepta mai multe valori" + } +}; + +QueryBuilder.defaults({ lang_code: 'ro' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.ru.js b/dist/i18n/query-builder.ru.js new file mode 100644 index 00000000..03cc3b7c --- /dev/null +++ b/dist/i18n/query-builder.ru.js @@ -0,0 +1,82 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Russian (ru) + * Author: + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['ru'] = { + "__locale": "Russian (ru)", + "add_rule": "Добавить", + "add_group": "Добавить группу", + "delete_rule": "Удалить", + "delete_group": "Удалить", + "conditions": { + "AND": "И", + "OR": "ИЛИ" + }, + "operators": { + "equal": "равно", + "not_equal": "не равно", + "in": "из указанных", + "not_in": "не из указанных", + "less": "меньше", + "less_or_equal": "меньше или равно", + "greater": "больше", + "greater_or_equal": "больше или равно", + "between": "между", + "not_between": "не между", + "begins_with": "начинается с", + "not_begins_with": "не начинается с", + "contains": "содержит", + "not_contains": "не содержит", + "ends_with": "оканчивается на", + "not_ends_with": "не оканчивается на", + "is_empty": "пустая строка", + "is_not_empty": "не пустая строка", + "is_null": "пусто", + "is_not_null": "не пусто" + }, + "errors": { + "no_filter": "Фильтр не выбран", + "empty_group": "Группа пуста", + "radio_empty": "Не выбрано значение", + "checkbox_empty": "Не выбрано значение", + "select_empty": "Не выбрано значение", + "string_empty": "Не заполнено", + "string_exceed_min_length": "Должен содержать больше {0} символов", + "string_exceed_max_length": "Должен содержать меньше {0} символов", + "string_invalid_format": "Неверный формат ({0})", + "number_nan": "Не число", + "number_not_integer": "Не число", + "number_not_double": "Не число", + "number_exceed_min": "Должно быть больше {0}", + "number_exceed_max": "Должно быть меньше, чем {0}", + "number_wrong_step": "Должно быть кратно {0}", + "number_between_invalid": "Недопустимые значения, {0} больше {1}", + "datetime_empty": "Не заполнено", + "datetime_invalid": "Неверный формат даты ({0})", + "datetime_exceed_min": "Должно быть, после {0}", + "datetime_exceed_max": "Должно быть, до {0}", + "datetime_between_invalid": "Недопустимые значения, {0} больше {1}", + "boolean_not_valid": "Не логическое", + "operator_not_multiple": "Оператор \"{1}\" не поддерживает много значений" + }, + "invert": "Инвертировать", + "NOT": "НЕ" +}; + +QueryBuilder.defaults({ lang_code: 'ru' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.sk.js b/dist/i18n/query-builder.sk.js new file mode 100644 index 00000000..d8ca2623 --- /dev/null +++ b/dist/i18n/query-builder.sk.js @@ -0,0 +1,83 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Slovensky (sk) + * Author: k2s + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['sk'] = { + "__locale": "Slovensky (sk)", + "__author": "k2s", + "add_rule": "Pridať podmienku", + "add_group": "Pridať skupinu", + "delete_rule": "Zmazať", + "delete_group": "Zmazať", + "conditions": { + "AND": "A", + "OR": "ALEBO" + }, + "operators": { + "equal": "rovné", + "not_equal": "nerovné", + "in": "v", + "not_in": "nie v", + "less": "menej", + "less_or_equal": "menej alebo rovné", + "greater": "väčšie", + "greater_or_equal": "väčšie alebo rovné", + "between": "medzi", + "not_between": "nie medzi", + "begins_with": "začína na", + "not_begins_with": "nezačína na", + "contains": "obsahuje", + "not_contains": "neobsahuje", + "ends_with": "končí na", + "not_ends_with": "nekončí na", + "is_empty": "je prázdne", + "is_not_empty": "nie je prázdne", + "is_null": "je null", + "is_not_null": "nie je null" + }, + "errors": { + "no_filter": "Nie je zvolený filter", + "empty_group": "Skupina je prázdna", + "radio_empty": "Nie je označená hodnota", + "checkbox_empty": "Nie je označená hodnota", + "select_empty": "Nie je označená hodnota", + "string_empty": "Prázdna hodnota", + "string_exceed_min_length": "Musí obsahovať aspon {0} znakov", + "string_exceed_max_length": "Nesmie obsahovať viac ako {0} znakov", + "string_invalid_format": "Chybný formát ({0})", + "number_nan": "Nie je číslo", + "number_not_integer": "Nie je celé číslo", + "number_not_double": "Nie je desatinné číslo", + "number_exceed_min": "Musí byť väčšie ako {0}", + "number_exceed_max": "Musí byť menšie ako {0}", + "number_wrong_step": "Musí byť násobkom čísla {0}", + "number_between_invalid": "Chybné hodnoty, {0} je väčšie ako {1}", + "datetime_empty": "Prázdna hodnota", + "datetime_invalid": "Chybný formát dátumu ({0})", + "datetime_exceed_min": "Musí byť neskôr ako {0}", + "datetime_exceed_max": "Musí byť skôr ako {0}", + "datetime_between_invalid": "Chybné hodnoty, {0} je neskôr ako {1}", + "boolean_not_valid": "Neplatné áno/nie", + "operator_not_multiple": "Operátor '{1}' nepodporuje viacero hodnôt" + }, + "invert": "Invertný", + "NOT": "NIE" +}; + +QueryBuilder.defaults({ lang_code: 'sk' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.sq.js b/dist/i18n/query-builder.sq.js new file mode 100644 index 00000000..f991b12a --- /dev/null +++ b/dist/i18n/query-builder.sq.js @@ -0,0 +1,78 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Albanian (sq) + * Author: Tomor Pupovci + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['sq'] = { + "__locale": "Albanian (sq)", + "__author": "Tomor Pupovci", + "add_rule": "Shto rregull", + "add_group": "Shto grup", + "delete_rule": "Fshij", + "delete_group": "Fshij", + "conditions": { + "AND": "DHE", + "OR": "OSE" + }, + "operators": { + "equal": "barabartë", + "not_equal": "e ndryshme prej", + "in": "në", + "not_in": "jo në", + "less": "më e vogël", + "less_or_equal": "më e vogël ose e barabartë me", + "greater": "më e madhe", + "greater_or_equal": "më e madhe ose e barabartë", + "between": "në mes", + "begins_with": "fillon me", + "not_begins_with": "nuk fillon me", + "contains": "përmban", + "not_contains": "nuk përmban", + "ends_with": "mbaron me", + "not_ends_with": "nuk mbaron me", + "is_empty": "është e zbrazët", + "is_not_empty": "nuk është e zbrazët", + "is_null": "është null", + "is_not_null": "nuk është null" + }, + "errors": { + "no_filter": "Nuk ka filter të zgjedhur", + "empty_group": "Grupi është i zbrazët", + "radio_empty": "Nuk ka vlerë të zgjedhur", + "checkbox_empty": "Nuk ka vlerë të zgjedhur", + "select_empty": "Nuk ka vlerë të zgjedhur", + "string_empty": "Vlerë e zbrazët", + "string_exceed_min_length": "Duhet të përmbajë së paku {0} karaktere", + "string_exceed_max_length": "Nuk duhet të përmbajë më shumë se {0} karaktere", + "string_invalid_format": "Format i pasaktë ({0})", + "number_nan": "Nuk është numër", + "number_not_integer": "Nuk është numër i plotë", + "number_not_double": "Nuk është numër me presje", + "number_exceed_min": "Duhet të jetë më i madh se {0}", + "number_exceed_max": "Duhet të jetë më i vogël se {0}", + "number_wrong_step": "Duhet të jetë shumëfish i {0}", + "datetime_empty": "Vlerë e zbrazët", + "datetime_invalid": "Format i pasaktë i datës ({0})", + "datetime_exceed_min": "Duhet të jetë pas {0}", + "datetime_exceed_max": "Duhet të jetë para {0}", + "boolean_not_valid": "Nuk është boolean", + "operator_not_multiple": "Operatori \"{1}\" nuk mund të pranojë vlera të shumëfishta" + } +}; + +QueryBuilder.defaults({ lang_code: 'sq' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.sv.js b/dist/i18n/query-builder.sv.js new file mode 100644 index 00000000..715cebb5 --- /dev/null +++ b/dist/i18n/query-builder.sv.js @@ -0,0 +1,83 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Svenska (sv) + * Author: hekin1 + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['sv'] = { + "__locale": "Svenska (sv)", + "__author": "hekin1", + "add_rule": "Lägg till regel", + "add_group": "Lägg till grupp", + "delete_rule": "Ta bort", + "delete_group": "Ta bort", + "conditions": { + "AND": "OCH", + "OR": "ELLER" + }, + "operators": { + "equal": "lika med", + "not_equal": "ej lika med", + "in": "en av", + "not_in": "ej en av", + "less": "mindre", + "less_or_equal": "mindre eller lika med", + "greater": "större", + "greater_or_equal": "större eller lika med", + "between": "mellan", + "not_between": "ej mellan", + "begins_with": "börjar med", + "not_begins_with": "börjar inte med", + "contains": "innehåller", + "not_contains": "innehåller inte", + "ends_with": "slutar med", + "not_ends_with": "slutar inte med", + "is_empty": "är tom", + "is_not_empty": "är inte tom", + "is_null": "är null", + "is_not_null": "är inte null" + }, + "errors": { + "no_filter": "Inget filter valt", + "empty_group": "Gruppen är tom", + "radio_empty": "Inget värde valt", + "checkbox_empty": "Inget värde valt", + "select_empty": "Inget värde valt", + "string_empty": "Tomt värde", + "string_exceed_min_length": "Måste innehålla minst {0} tecken", + "string_exceed_max_length": "Får ej innehålla fler än {0} tecken", + "string_invalid_format": "Felaktigt format ({0})", + "number_nan": "Inte numeriskt", + "number_not_integer": "Inte en siffra", + "number_not_double": "Inte ett decimaltal", + "number_exceed_min": "Måste vara större än {0}", + "number_exceed_max": "Måste vara lägre än {0}", + "number_wrong_step": "Måste vara en mutipel av {0}", + "number_between_invalid": "Felaktiga värden, {0} är större än {1}", + "datetime_empty": "Tomt värde", + "datetime_invalid": "Felaktigt datumformat ({0})", + "datetime_exceed_min": "Måste vara efter {0}", + "datetime_exceed_max": "Måste vara före {0}", + "datetime_between_invalid": "Felaktiga värden, {0} är större än {1}", + "boolean_not_valid": "Inte en boolean", + "operator_not_multiple": "Operatorn \"{1}\" accepterar inte flera värden" + }, + "invert": "Invertera", + "NOT": "INTE" +}; + +QueryBuilder.defaults({ lang_code: 'sv' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.sw.js b/dist/i18n/query-builder.sw.js new file mode 100644 index 00000000..8829fc60 --- /dev/null +++ b/dist/i18n/query-builder.sw.js @@ -0,0 +1,83 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Swahili (sw) + * Author: Timothy Anyona + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['sw'] = { + "__locale": "Swahili (sw)", + "__author": "Timothy Anyona", + "add_rule": "Ongeza kanuni", + "add_group": "Ongeza kikundi", + "delete_rule": "Futa", + "delete_group": "Futa", + "conditions": { + "AND": "NA", + "OR": "AU" + }, + "operators": { + "equal": "ni", + "not_equal": "sio", + "in": "mojawapo ya", + "not_in": "sio mojawapo ya", + "less": "isiyozidi", + "less_or_equal": "isiyozidi au ni sawa na", + "greater": "inayozidi", + "greater_or_equal": "inayozidi au ni sawa na", + "between": "kati ya", + "not_between": "isiyo kati ya", + "begins_with": "inaanza na", + "not_begins_with": "isiyoanza na", + "contains": "ina", + "not_contains": "haina", + "ends_with": "inaisha na", + "not_ends_with": "isiyoisha na", + "is_empty": "ni tupu", + "is_not_empty": "sio tupu", + "is_null": "ni batili", + "is_not_null": "sio batili" + }, + "errors": { + "no_filter": "Chujio halijachaguliwa", + "empty_group": "Kikundi ki tupu", + "radio_empty": "Thamani haijachaguliwa", + "checkbox_empty": "Thamani haijachaguliwa", + "select_empty": "Thamani haijachaguliwa", + "string_empty": "Thamani tupu", + "string_exceed_min_length": "Lazima iwe na vibambo visiopungua {0}", + "string_exceed_max_length": "Haifai kuwa na vibambo zaidi ya {0}", + "string_invalid_format": "Fomati batili ({0})", + "number_nan": "Sio nambari", + "number_not_integer": "Sio namba kamili", + "number_not_double": "Sio namba desimali", + "number_exceed_min": "Lazima iwe zaidi ya {0}", + "number_exceed_max": "Lazima iwe chini ya {0}", + "number_wrong_step": "Lazima iwe kigawe cha {0}", + "number_between_invalid": "Thamani batili, {0} ni kubwa kuliko {1}", + "datetime_empty": "Thamani tupu", + "datetime_invalid": "Fomati tarehe batili ({0})", + "datetime_exceed_min": "Lazima iwe baada ya {0}", + "datetime_exceed_max": "Lazima iwe kabla ya {0}", + "datetime_between_invalid": "Thamani batili, {0} ni baada ya {1}", + "boolean_not_valid": "Sio buleani", + "operator_not_multiple": "Opereta \"{1}\" haikubali thamani nyingi" + }, + "invert": "Pindua", + "NOT": "SIO" +}; + +QueryBuilder.defaults({ lang_code: 'sw' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.tr.js b/dist/i18n/query-builder.tr.js new file mode 100644 index 00000000..7751e423 --- /dev/null +++ b/dist/i18n/query-builder.tr.js @@ -0,0 +1,82 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Turkish (tr) + * Author: Aykut Alpgiray Ateş + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['tr'] = { + "__locale": "Turkish (tr)", + "__author": "Aykut Alpgiray Ateş", + "add_rule": "Kural Ekle", + "add_group": "Grup Ekle", + "delete_rule": "Sil", + "delete_group": "Sil", + "conditions": { + "AND": "Ve", + "OR": "Veya" + }, + "operators": { + "equal": "eşit", + "not_equal": "eşit değil", + "in": "içinde", + "not_in": "içinde değil", + "less": "küçük", + "less_or_equal": "küçük veya eşit", + "greater": "büyük", + "greater_or_equal": "büyük veya eşit", + "between": "arasında", + "not_between": "arasında değil", + "begins_with": "ile başlayan", + "not_begins_with": "ile başlamayan", + "contains": "içeren", + "not_contains": "içermeyen", + "ends_with": "ile biten", + "not_ends_with": "ile bitmeyen", + "is_empty": "boş ise", + "is_not_empty": "boş değil ise", + "is_null": "var ise", + "is_not_null": "yok ise" + }, + "errors": { + "no_filter": "Bir filtre seçili değil", + "empty_group": "Grup bir eleman içermiyor", + "radio_empty": "Seçim yapılmalı", + "checkbox_empty": "Seçim yapılmalı", + "select_empty": "Seçim yapılmalı", + "string_empty": "Bir metin girilmeli", + "string_exceed_min_length": "En az {0} karakter girilmeli", + "string_exceed_max_length": "En fazla {0} karakter girilebilir", + "string_invalid_format": "Uyumsuz format ({0})", + "number_nan": "Sayı değil", + "number_not_integer": "Tam sayı değil", + "number_not_double": "Ondalıklı sayı değil", + "number_exceed_min": "Sayı {0}'den/dan daha büyük olmalı", + "number_exceed_max": "Sayı {0}'den/dan daha küçük olmalı", + "number_wrong_step": "{0} veya katı olmalı", + "number_between_invalid": "Geçersiz değerler, {0} değeri {1} değerinden büyük", + "datetime_empty": "Tarih Seçilmemiş", + "datetime_invalid": "Uygun olmayan tarih formatı ({0})", + "datetime_exceed_min": "{0} Tarihinden daha sonrası olmalı.", + "datetime_exceed_max": "{0} Tarihinden daha öncesi olmalı.", + "datetime_between_invalid": "Geçersiz değerler, {0} değeri {1} değerinden büyük", + "boolean_not_valid": "Değer Doğru/Yanlış(bool) olmalı", + "operator_not_multiple": "Operatör \"{1}\" birden fazla değer kabul etmiyor" + }, + "invert": "Ters Çevir" +}; + +QueryBuilder.defaults({ lang_code: 'tr' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.ua.js b/dist/i18n/query-builder.ua.js new file mode 100644 index 00000000..1238b26e --- /dev/null +++ b/dist/i18n/query-builder.ua.js @@ -0,0 +1,79 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Ukrainian (ua) + * Author: Megaplan, mborisv + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['ua'] = { + "__locale": "Ukrainian (ua)", + "__author": "Megaplan, mborisv ", + "add_rule": "Додати", + "add_group": "Додати групу", + "delete_rule": "Видалити", + "delete_group": "Видалити", + "conditions": { + "AND": "І", + "OR": "АБО" + }, + "operators": { + "equal": "дорівнює", + "not_equal": "не дорівнює", + "in": "з вказаних", + "not_in": "не з вказаних", + "less": "менше", + "less_or_equal": "менше або дорівнюж", + "greater": "більше", + "greater_or_equal": "більше або дорівнює", + "between": "між", + "begins_with": "починається з", + "not_begins_with": "не починається з", + "contains": "містить", + "not_contains": "не містить", + "ends_with": "закінчується на", + "not_ends_with": "не не закінчується на", + "is_empty": "порожній рядок", + "is_not_empty": "не порожній рядок", + "is_null": "порожньо", + "is_not_null": "не порожньо" + }, + "errors": { + "no_filter": "Фільтр не вибраний", + "empty_group": "Група порожня", + "radio_empty": "Значення не вибрано", + "checkbox_empty": "Значення не вибрано", + "select_empty": "Значення не вибрано", + "string_empty": "Не заповнено", + "string_exceed_min_length": "Повинен містити більше {0} символів", + "string_exceed_max_length": "Повинен містити менше {0} символів", + "string_invalid_format": "Невірний формат ({0})", + "number_nan": "Не число", + "number_not_integer": "Не число", + "number_not_double": "Не число", + "number_exceed_min": "Повинне бути більше {0}", + "number_exceed_max": "Повинне бути менше, ніж {0}", + "number_wrong_step": "Повинне бути кратне {0}", + "datetime_empty": "Не заповнено", + "datetime_invalid": "Невірний формат дати ({0})", + "datetime_exceed_min": "Повинне бути, після {0}", + "datetime_exceed_max": "Повинне бути, до {0}", + "boolean_not_valid": "Не логічне", + "operator_not_multiple": "Оператор \"{1}\" не підтримує багато значень" + }, + "invert": "інвертувати" +}; + +QueryBuilder.defaults({ lang_code: 'ua' }); +})); \ No newline at end of file diff --git a/dist/i18n/query-builder.zh-CN.js b/dist/i18n/query-builder.zh-CN.js new file mode 100644 index 00000000..d908f33a --- /dev/null +++ b/dist/i18n/query-builder.zh-CN.js @@ -0,0 +1,80 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: Simplified Chinese (zh_CN) + * Author: shadowwind, shatteredwindgo@gmail.com + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'query-builder'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +var QueryBuilder = $.fn.queryBuilder; + +QueryBuilder.regional['zh-CN'] = { + "__locale": "Simplified Chinese (zh_CN)", + "__author": "shadowwind, shatteredwindgo@gmail.com", + "add_rule": "添加规则", + "add_group": "添加组", + "delete_rule": "删除", + "delete_group": "删除组", + "conditions": { + "AND": "和", + "OR": "或" + }, + "operators": { + "equal": "等于", + "not_equal": "不等于", + "in": "在...之內", + "not_in": "不在...之內", + "less": "小于", + "less_or_equal": "小于或等于", + "greater": "大于", + "greater_or_equal": "大于或等于", + "between": "在...之间", + "not_between": "不在...之间", + "begins_with": "以...开始", + "not_begins_with": "不以...开始", + "contains": "包含以下内容", + "not_contains": "不包含以下内容", + "ends_with": "以...结束", + "not_ends_with": "不以...结束", + "is_empty": "为空", + "is_not_empty": "不为空", + "is_null": "为 null", + "is_not_null": "不为 null" + }, + "errors": { + "no_filter": "没有选择过滤器", + "empty_group": "该组为空", + "radio_empty": "没有选中项", + "checkbox_empty": "没有选中项", + "select_empty": "没有选中项", + "string_empty": "没有输入值", + "string_exceed_min_length": "必须至少包含{0}个字符", + "string_exceed_max_length": "必须不超过{0}个字符", + "string_invalid_format": "无效格式({0})", + "number_nan": "值不是数字", + "number_not_integer": "不是整数", + "number_not_double": "不是浮点数", + "number_exceed_min": "必须大于{0}", + "number_exceed_max": "必须小于{0}", + "number_wrong_step": "必须是{0}的倍数", + "datetime_empty": "值为空", + "datetime_invalid": "不是有效日期({0})", + "datetime_exceed_min": "必须在{0}之后", + "datetime_exceed_max": "必须在{0}之前", + "boolean_not_valid": "不是布尔值", + "operator_not_multiple": "选项\"{1}\"无法接受多个值" + }, + "invert": "倒置" +}; + +QueryBuilder.defaults({ lang_code: 'zh-CN' }); +})); \ No newline at end of file diff --git a/dist/i18n/ro.js b/dist/i18n/ro.js deleted file mode 100644 index 477d8cf6..00000000 --- a/dist/i18n/ro.js +++ /dev/null @@ -1,37 +0,0 @@ -/*! - * jQuery QueryBuilder - * Romanian translation by ArianServ - */ - -jQuery.fn.queryBuilder.defaults.set({ lang: { - "add_rule": "Adaugă regulă", - "add_group": "Adaugă grup", - "delete_rule": "Şterge", - "delete_group": "Şterge", - - "condition_and": "ŞI", - "condition_or": "SAU", - - "filter_select_placeholder": "------", - - "operators": { - "equal": "egal", - "not_equal": "diferit", - "in": "în", - "not_in": "nu în", - "less": "mai puţin", - "less_or_equal": "mai puţin sau egal", - "greater": "mai mare", - "greater_or_equal": "mai mare sau egal", - "begins_with": "începe cu", - "not_begins_with": "nu începe cu", - "contains": "conţine", - "not_contains": "nu conţine", - "ends_with": "se termină cu", - "not_ends_with": "nu se termină cu", - "is_empty": "este gol", - "is_not_empty": "nu este gol", - "is_null": "e nul", - "is_not_null": "nu e nul" - } -}}); \ No newline at end of file diff --git a/dist/js/query-builder.js b/dist/js/query-builder.js new file mode 100644 index 00000000..b5b30837 --- /dev/null +++ b/dist/js/query-builder.js @@ -0,0 +1,6174 @@ +/*! + * jQuery QueryBuilder 3.0.0 + * Copyright 2014-2024 Damien "Mistic" Sorel (http://www.strangeplanet.fr) + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof define == 'function' && define.amd) { + define(['jquery', 'jquery-extendext'], factory); + } + else if (typeof module === 'object' && module.exports) { + module.exports = factory(require('jquery'), require('jquery-extendext')); + } + else { + factory(root.jQuery); + } +}(this, function($) { +"use strict"; + +/** + * @typedef {object} Filter + * @memberof QueryBuilder + * @description See {@link http://querybuilder.js.org/index.html#filters} + */ + +/** + * @typedef {object} Operator + * @memberof QueryBuilder + * @description See {@link http://querybuilder.js.org/index.html#operators} + */ + +/** + * @param {jQuery} $el + * @param {object} options - see {@link http://querybuilder.js.org/#options} + * @constructor + */ +var QueryBuilder = function($el, options) { + $el[0].queryBuilder = this; + + /** + * Element container + * @member {jQuery} + * @readonly + */ + this.$el = $el; + + /** + * Configuration object + * @member {object} + * @readonly + */ + this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options); + + /** + * Internal model + * @member {Model} + * @readonly + */ + this.model = new Model(); + + /** + * Internal status + * @member {object} + * @property {string} id - id of the container + * @property {boolean} generated_id - if the container id has been generated + * @property {int} group_id - current group id + * @property {int} rule_id - current rule id + * @property {boolean} has_optgroup - if filters have optgroups + * @property {boolean} has_operator_optgroup - if operators have optgroups + * @readonly + * @private + */ + this.status = { + id: null, + generated_id: false, + group_id: 0, + rule_id: 0, + has_optgroup: false, + has_operator_optgroup: false + }; + + /** + * List of filters + * @member {QueryBuilder.Filter[]} + * @readonly + */ + this.filters = this.settings.filters; + + /** + * List of icons + * @member {object.} + * @readonly + */ + this.icons = this.settings.icons; + + /** + * List of operators + * @member {QueryBuilder.Operator[]} + * @readonly + */ + this.operators = this.settings.operators; + + /** + * List of templates + * @member {object.} + * @readonly + */ + this.templates = this.settings.templates; + + /** + * Plugins configuration + * @member {object.} + * @readonly + */ + this.plugins = this.settings.plugins; + + /** + * Translations object + * @member {object} + * @readonly + */ + this.lang = null; + + // translations : english << 'lang_code' << custom + if (QueryBuilder.regional['en'] === undefined) { + Utils.error('Config', '"i18n/en.js" not loaded.'); + } + this.lang = $.extendext(true, 'replace', {}, QueryBuilder.regional['en'], QueryBuilder.regional[this.settings.lang_code], this.settings.lang); + + // "allow_groups" can be boolean or int + if (this.settings.allow_groups === false) { + this.settings.allow_groups = 0; + } + else if (this.settings.allow_groups === true) { + this.settings.allow_groups = -1; + } + + // init templates + Object.keys(this.templates).forEach(function(tpl) { + if (!this.templates[tpl]) { + this.templates[tpl] = QueryBuilder.templates[tpl]; + } + if (typeof this.templates[tpl] !== 'function') { + throw new Error(`Template ${tpl} must be a function`); + } + }, this); + + // ensure we have a container id + if (!this.$el.attr('id')) { + this.$el.attr('id', 'qb_' + Math.floor(Math.random() * 99999)); + this.status.generated_id = true; + } + this.status.id = this.$el.attr('id'); + + // INIT + this.$el.addClass('query-builder'); + + this.filters = this.checkFilters(this.filters); + this.operators = this.checkOperators(this.operators); + this.bindEvents(); + this.initPlugins(); +}; + +$.extend(QueryBuilder.prototype, /** @lends QueryBuilder.prototype */ { + /** + * Triggers an event on the builder container + * @param {string} type + * @returns {$.Event} + */ + trigger: function(type) { + var event = new $.Event(this._tojQueryEvent(type), { + builder: this + }); + + this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 1)); + + return event; + }, + + /** + * Triggers an event on the builder container and returns the modified value + * @param {string} type + * @param {*} value + * @returns {*} + */ + change: function(type, value) { + var event = new $.Event(this._tojQueryEvent(type, true), { + builder: this, + value: value + }); + + this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 2)); + + return event.value; + }, + + /** + * Attaches an event listener on the builder container + * @param {string} type + * @param {function} cb + * @returns {QueryBuilder} + */ + on: function(type, cb) { + this.$el.on(this._tojQueryEvent(type), cb); + return this; + }, + + /** + * Removes an event listener from the builder container + * @param {string} type + * @param {function} [cb] + * @returns {QueryBuilder} + */ + off: function(type, cb) { + this.$el.off(this._tojQueryEvent(type), cb); + return this; + }, + + /** + * Attaches an event listener called once on the builder container + * @param {string} type + * @param {function} cb + * @returns {QueryBuilder} + */ + once: function(type, cb) { + this.$el.one(this._tojQueryEvent(type), cb); + return this; + }, + + /** + * Appends `.queryBuilder` and optionally `.filter` to the events names + * @param {string} name + * @param {boolean} [filter=false] + * @returns {string} + * @private + */ + _tojQueryEvent: function(name, filter) { + return name.split(' ').map(function(type) { + return type + '.queryBuilder' + (filter ? '.filter' : ''); + }).join(' '); + } +}); + + +/** + * Allowed types and their internal representation + * @type {object.} + * @readonly + * @private + */ +QueryBuilder.types = { + 'string': 'string', + 'integer': 'number', + 'double': 'number', + 'date': 'datetime', + 'time': 'datetime', + 'datetime': 'datetime', + 'boolean': 'boolean' +}; + +/** + * Allowed inputs + * @type {string[]} + * @readonly + * @private + */ +QueryBuilder.inputs = [ + 'text', + 'number', + 'textarea', + 'radio', + 'checkbox', + 'select' +]; + +/** + * Runtime modifiable options with `setOptions` method + * @type {string[]} + * @readonly + * @private + */ +QueryBuilder.modifiable_options = [ + 'display_errors', + 'allow_groups', + 'allow_empty', + 'default_condition', + 'default_filter' +]; + +/** + * CSS selectors for common components + * @type {object.} + * @readonly + */ +QueryBuilder.selectors = { + group_container: '.rules-group-container', + rule_container: '.rule-container', + filter_container: '.rule-filter-container', + operator_container: '.rule-operator-container', + value_container: '.rule-value-container', + error_container: '.error-container', + condition_container: '.rules-group-header .group-conditions', + + rule_header: '.rule-header', + group_header: '.rules-group-header', + group_actions: '.group-actions', + rule_actions: '.rule-actions', + + rules_list: '.rules-group-body>.rules-list', + + group_condition: '.rules-group-header [name$=_cond]', + rule_filter: '.rule-filter-container [name$=_filter]', + rule_operator: '.rule-operator-container [name$=_operator]', + rule_value: '.rule-value-container [name*=_value_]', + + add_rule: '[data-add=rule]', + delete_rule: '[data-delete=rule]', + add_group: '[data-add=group]', + delete_group: '[data-delete=group]' +}; + +/** + * Template strings (see template.js) + * @type {object.} + * @readonly + */ +QueryBuilder.templates = {}; + +/** + * Localized strings (see i18n/) + * @type {object.} + * @readonly + */ +QueryBuilder.regional = {}; + +/** + * Default operators + * @type {object.} + * @readonly + */ +QueryBuilder.OPERATORS = { + equal: { type: 'equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] }, + not_equal: { type: 'not_equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] }, + in: { type: 'in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime'] }, + not_in: { type: 'not_in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime'] }, + less: { type: 'less', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] }, + less_or_equal: { type: 'less_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] }, + greater: { type: 'greater', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] }, + greater_or_equal: { type: 'greater_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] }, + between: { type: 'between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] }, + not_between: { type: 'not_between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] }, + begins_with: { type: 'begins_with', nb_inputs: 1, multiple: false, apply_to: ['string'] }, + not_begins_with: { type: 'not_begins_with', nb_inputs: 1, multiple: false, apply_to: ['string'] }, + contains: { type: 'contains', nb_inputs: 1, multiple: false, apply_to: ['string'] }, + not_contains: { type: 'not_contains', nb_inputs: 1, multiple: false, apply_to: ['string'] }, + ends_with: { type: 'ends_with', nb_inputs: 1, multiple: false, apply_to: ['string'] }, + not_ends_with: { type: 'not_ends_with', nb_inputs: 1, multiple: false, apply_to: ['string'] }, + is_empty: { type: 'is_empty', nb_inputs: 0, multiple: false, apply_to: ['string'] }, + is_not_empty: { type: 'is_not_empty', nb_inputs: 0, multiple: false, apply_to: ['string'] }, + is_null: { type: 'is_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] }, + is_not_null: { type: 'is_not_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] } +}; + +/** + * Default configuration + * @type {object} + * @readonly + */ +QueryBuilder.DEFAULTS = { + filters: [], + plugins: [], + + sort_filters: false, + display_errors: true, + allow_groups: -1, + allow_empty: false, + conditions: ['AND', 'OR'], + default_condition: 'AND', + inputs_separator: ' , ', + select_placeholder: '------', + display_empty_filter: true, + default_filter: null, + optgroups: {}, + + default_rule_flags: { + filter_readonly: false, + operator_readonly: false, + value_readonly: false, + no_delete: false + }, + + default_group_flags: { + condition_readonly: false, + no_add_rule: false, + no_add_group: false, + no_delete: false + }, + + templates: { + group: null, + rule: null, + filterSelect: null, + operatorSelect: null, + ruleValueSelect: null + }, + + lang_code: 'en', + lang: {}, + + operators: [ + 'equal', + 'not_equal', + 'in', + 'not_in', + 'less', + 'less_or_equal', + 'greater', + 'greater_or_equal', + 'between', + 'not_between', + 'begins_with', + 'not_begins_with', + 'contains', + 'not_contains', + 'ends_with', + 'not_ends_with', + 'is_empty', + 'is_not_empty', + 'is_null', + 'is_not_null' + ], + + icons: { + add_group: 'bi-plus-circle-fill', + add_rule: 'bi-plus-lg', + remove_group: 'bi-x-lg', + remove_rule: 'bi-x-lg', + error: 'bi-exclamation-triangle' + } +}; + + +/** + * @module plugins + */ + +/** + * Definition of available plugins + * @type {object.} + */ +QueryBuilder.plugins = {}; + +/** + * Gets or extends the default configuration + * @param {object} [options] - new configuration + * @returns {undefined|object} nothing or configuration object (copy) + */ +QueryBuilder.defaults = function(options) { + if (typeof options == 'object') { + $.extendext(true, 'replace', QueryBuilder.DEFAULTS, options); + } + else if (typeof options == 'string') { + if (typeof QueryBuilder.DEFAULTS[options] == 'object') { + return $.extend(true, {}, QueryBuilder.DEFAULTS[options]); + } + else { + return QueryBuilder.DEFAULTS[options]; + } + } + else { + return $.extend(true, {}, QueryBuilder.DEFAULTS); + } +}; + +/** + * Registers a new plugin + * @param {string} name + * @param {function} fct - init function + * @param {object} [def] - default options + */ +QueryBuilder.define = function(name, fct, def) { + QueryBuilder.plugins[name] = { + fct: fct, + def: def || {} + }; +}; + +/** + * Adds new methods to QueryBuilder prototype + * @param {object.} methods + */ +QueryBuilder.extend = function(methods) { + $.extend(QueryBuilder.prototype, methods); +}; + +/** + * Initializes plugins for an instance + * @throws ConfigError + * @private + */ +QueryBuilder.prototype.initPlugins = function() { + if (!this.plugins) { + return; + } + + if ($.isArray(this.plugins)) { + var tmp = {}; + this.plugins.forEach(function(plugin) { + tmp[plugin] = null; + }); + this.plugins = tmp; + } + + Object.keys(this.plugins).forEach(function(plugin) { + if (plugin in QueryBuilder.plugins) { + this.plugins[plugin] = $.extend(true, {}, + QueryBuilder.plugins[plugin].def, + this.plugins[plugin] || {} + ); + + QueryBuilder.plugins[plugin].fct.call(this, this.plugins[plugin]); + } + else { + Utils.error('Config', 'Unable to find plugin "{0}"', plugin); + } + }, this); +}; + +/** + * Returns the config of a plugin, if the plugin is not loaded, returns the default config. + * @param {string} name + * @param {string} [property] + * @throws ConfigError + * @returns {*} + */ +QueryBuilder.prototype.getPluginOptions = function(name, property) { + var plugin; + if (this.plugins && this.plugins[name]) { + plugin = this.plugins[name]; + } + else if (QueryBuilder.plugins[name]) { + plugin = QueryBuilder.plugins[name].def; + } + + if (plugin) { + if (property) { + return plugin[property]; + } + else { + return plugin; + } + } + else { + Utils.error('Config', 'Unable to find plugin "{0}"', name); + } +}; + + +/** + * Final initialisation of the builder + * @param {object} [rules] + * @fires QueryBuilder.afterInit + * @private + */ +QueryBuilder.prototype.init = function(rules) { + /** + * When the initilization is done, just before creating the root group + * @event afterInit + * @memberof QueryBuilder + */ + this.trigger('afterInit'); + + if (rules) { + this.setRules(rules); + delete this.settings.rules; + } + else { + this.setRoot(true); + } +}; + +/** + * Checks the configuration of each filter + * @param {QueryBuilder.Filter[]} filters + * @returns {QueryBuilder.Filter[]} + * @throws ConfigError + */ +QueryBuilder.prototype.checkFilters = function(filters) { + var definedFilters = []; + + if (!filters || filters.length === 0) { + Utils.error('Config', 'Missing filters list'); + } + + filters.forEach(function(filter, i) { + if (!filter.id) { + Utils.error('Config', 'Missing filter {0} id', i); + } + if (definedFilters.indexOf(filter.id) != -1) { + Utils.error('Config', 'Filter "{0}" already defined', filter.id); + } + definedFilters.push(filter.id); + + if (!filter.type) { + filter.type = 'string'; + } + else if (!QueryBuilder.types[filter.type]) { + Utils.error('Config', 'Invalid type "{0}"', filter.type); + } + + if (!filter.input) { + filter.input = QueryBuilder.types[filter.type] === 'number' ? 'number' : 'text'; + } + else if (typeof filter.input != 'function' && QueryBuilder.inputs.indexOf(filter.input) == -1) { + Utils.error('Config', 'Invalid input "{0}"', filter.input); + } + + if (filter.operators) { + filter.operators.forEach(function(operator) { + if (typeof operator != 'string') { + Utils.error('Config', 'Filter operators must be global operators types (string)'); + } + }); + } + + if (!filter.field) { + filter.field = filter.id; + } + if (!filter.label) { + filter.label = filter.field; + } + + if (!filter.optgroup) { + filter.optgroup = null; + } + else { + this.status.has_optgroup = true; + + // register optgroup if needed + if (!this.settings.optgroups[filter.optgroup]) { + this.settings.optgroups[filter.optgroup] = filter.optgroup; + } + } + + switch (filter.input) { + case 'radio': + case 'checkbox': + if (!filter.values || filter.values.length < 1) { + Utils.error('Config', 'Missing filter "{0}" values', filter.id); + } + break; + + case 'select': + var cleanValues = []; + filter.has_optgroup = false; + + Utils.iterateOptions(filter.values, function(value, label, optgroup) { + cleanValues.push({ + value: value, + label: label, + optgroup: optgroup || null + }); + + if (optgroup) { + filter.has_optgroup = true; + + // register optgroup if needed + if (!this.settings.optgroups[optgroup]) { + this.settings.optgroups[optgroup] = optgroup; + } + } + }.bind(this)); + + if (filter.has_optgroup) { + filter.values = Utils.groupSort(cleanValues, 'optgroup'); + } + else { + filter.values = cleanValues; + } + + if (filter.placeholder) { + if (filter.placeholder_value === undefined) { + filter.placeholder_value = -1; + } + + filter.values.forEach(function(entry) { + if (entry.value == filter.placeholder_value) { + Utils.error('Config', 'Placeholder of filter "{0}" overlaps with one of its values', filter.id); + } + }); + } + break; + } + }, this); + + if (this.settings.sort_filters) { + if (typeof this.settings.sort_filters == 'function') { + filters.sort(this.settings.sort_filters); + } + else { + var self = this; + filters.sort(function(a, b) { + return self.translate(a.label).localeCompare(self.translate(b.label)); + }); + } + } + + if (this.status.has_optgroup) { + filters = Utils.groupSort(filters, 'optgroup'); + } + + return filters; +}; + +/** + * Checks the configuration of each operator + * @param {QueryBuilder.Operator[]} operators + * @returns {QueryBuilder.Operator[]} + * @throws ConfigError + */ +QueryBuilder.prototype.checkOperators = function(operators) { + var definedOperators = []; + + operators.forEach(function(operator, i) { + if (typeof operator == 'string') { + if (!QueryBuilder.OPERATORS[operator]) { + Utils.error('Config', 'Unknown operator "{0}"', operator); + } + + operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator]); + } + else { + if (!operator.type) { + Utils.error('Config', 'Missing "type" for operator {0}', i); + } + + if (QueryBuilder.OPERATORS[operator.type]) { + operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator.type], operator); + } + + if (operator.nb_inputs === undefined || operator.apply_to === undefined) { + Utils.error('Config', 'Missing "nb_inputs" and/or "apply_to" for operator "{0}"', operator.type); + } + } + + if (definedOperators.indexOf(operator.type) != -1) { + Utils.error('Config', 'Operator "{0}" already defined', operator.type); + } + definedOperators.push(operator.type); + + if (!operator.optgroup) { + operator.optgroup = null; + } + else { + this.status.has_operator_optgroup = true; + + // register optgroup if needed + if (!this.settings.optgroups[operator.optgroup]) { + this.settings.optgroups[operator.optgroup] = operator.optgroup; + } + } + }, this); + + if (this.status.has_operator_optgroup) { + operators = Utils.groupSort(operators, 'optgroup'); + } + + return operators; +}; + +/** + * Adds all events listeners to the builder + * @private + */ +QueryBuilder.prototype.bindEvents = function() { + var self = this; + var Selectors = QueryBuilder.selectors; + + // group condition change + this.$el.on('change.queryBuilder', Selectors.group_condition, function() { + if ($(this).is(':checked')) { + var $group = $(this).closest(Selectors.group_container); + self.getModel($group).condition = $(this).val(); + } + }); + + // rule filter change + this.$el.on('change.queryBuilder', Selectors.rule_filter, function() { + var $rule = $(this).closest(Selectors.rule_container); + self.getModel($rule).filter = self.getFilterById($(this).val()); + }); + + // rule operator change + this.$el.on('change.queryBuilder', Selectors.rule_operator, function() { + var $rule = $(this).closest(Selectors.rule_container); + self.getModel($rule).operator = self.getOperatorByType($(this).val()); + }); + + // add rule button + this.$el.on('click.queryBuilder', Selectors.add_rule, function() { + var $group = $(this).closest(Selectors.group_container); + self.addRule(self.getModel($group)); + }); + + // delete rule button + this.$el.on('click.queryBuilder', Selectors.delete_rule, function() { + var $rule = $(this).closest(Selectors.rule_container); + self.deleteRule(self.getModel($rule)); + }); + + if (this.settings.allow_groups !== 0) { + // add group button + this.$el.on('click.queryBuilder', Selectors.add_group, function() { + var $group = $(this).closest(Selectors.group_container); + self.addGroup(self.getModel($group)); + }); + + // delete group button + this.$el.on('click.queryBuilder', Selectors.delete_group, function() { + var $group = $(this).closest(Selectors.group_container); + self.deleteGroup(self.getModel($group)); + }); + } + + // model events + this.model.on({ + 'drop': function(e, node) { + node.$el.remove(); + self.refreshGroupsConditions(); + }, + 'add': function(e, parent, node, index) { + if (index === 0) { + node.$el.prependTo(parent.$el.find('>' + QueryBuilder.selectors.rules_list)); + } + else { + node.$el.insertAfter(parent.rules[index - 1].$el); + } + self.refreshGroupsConditions(); + }, + 'move': function(e, node, group, index) { + node.$el.detach(); + + if (index === 0) { + node.$el.prependTo(group.$el.find('>' + QueryBuilder.selectors.rules_list)); + } + else { + node.$el.insertAfter(group.rules[index - 1].$el); + } + self.refreshGroupsConditions(); + }, + 'update': function(e, node, field, value, oldValue) { + if (node instanceof Rule) { + switch (field) { + case 'error': + self.updateError(node); + break; + + case 'flags': + self.applyRuleFlags(node); + break; + + case 'filter': + self.updateRuleFilter(node, oldValue); + break; + + case 'operator': + self.updateRuleOperator(node, oldValue); + break; + + case 'value': + self.updateRuleValue(node, oldValue); + break; + } + } + else { + switch (field) { + case 'error': + self.updateError(node); + break; + + case 'flags': + self.applyGroupFlags(node); + break; + + case 'condition': + self.updateGroupCondition(node, oldValue); + break; + } + } + } + }); +}; + +/** + * Creates the root group + * @param {boolean} [addRule=true] - adds a default empty rule + * @param {object} [data] - group custom data + * @param {object} [flags] - flags to apply to the group + * @returns {Group} root group + * @fires QueryBuilder.afterAddGroup + */ +QueryBuilder.prototype.setRoot = function(addRule, data, flags) { + addRule = (addRule === undefined || addRule === true); + + var group_id = this.nextGroupId(); + var $group = $($.parseHTML(this.getGroupTemplate(group_id, 1))); + + this.$el.append($group); + this.model.root = new Group(null, $group); + this.model.root.model = this.model; + + this.model.root.data = data; + this.model.root.flags = $.extend({}, this.settings.default_group_flags, flags); + this.model.root.condition = this.settings.default_condition; + + this.trigger('afterAddGroup', this.model.root); + + if (addRule) { + this.addRule(this.model.root); + } + + return this.model.root; +}; + +/** + * Adds a new group + * @param {Group} parent + * @param {boolean} [addRule=true] - adds a default empty rule + * @param {object} [data] - group custom data + * @param {object} [flags] - flags to apply to the group + * @returns {Group} + * @fires QueryBuilder.beforeAddGroup + * @fires QueryBuilder.afterAddGroup + */ +QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) { + addRule = (addRule === undefined || addRule === true); + + var level = parent.level + 1; + + /** + * Just before adding a group, can be prevented. + * @event beforeAddGroup + * @memberof QueryBuilder + * @param {Group} parent + * @param {boolean} addRule - if an empty rule will be added in the group + * @param {int} level - nesting level of the group, 1 is the root group + */ + var e = this.trigger('beforeAddGroup', parent, addRule, level); + if (e.isDefaultPrevented()) { + return null; + } + + var group_id = this.nextGroupId(); + var $group = $(this.getGroupTemplate(group_id, level)); + var model = parent.addGroup($group); + + model.data = data; + model.flags = $.extend({}, this.settings.default_group_flags, flags); + model.condition = this.settings.default_condition; + + /** + * Just after adding a group + * @event afterAddGroup + * @memberof QueryBuilder + * @param {Group} group + */ + this.trigger('afterAddGroup', model); + + /** + * After any change in the rules + * @event rulesChanged + * @memberof QueryBuilder + */ + this.trigger('rulesChanged'); + + if (addRule) { + this.addRule(model); + } + + return model; +}; + +/** + * Tries to delete a group. The group is not deleted if at least one rule is flagged `no_delete`. + * @param {Group} group + * @returns {boolean} if the group has been deleted + * @fires QueryBuilder.beforeDeleteGroup + * @fires QueryBuilder.afterDeleteGroup + */ +QueryBuilder.prototype.deleteGroup = function(group) { + if (group.isRoot()) { + return false; + } + + /** + * Just before deleting a group, can be prevented + * @event beforeDeleteGroup + * @memberof QueryBuilder + * @param {Group} parent + */ + var e = this.trigger('beforeDeleteGroup', group); + if (e.isDefaultPrevented()) { + return false; + } + + var del = true; + + group.each('reverse', function(rule) { + del &= this.deleteRule(rule); + }, function(group) { + del &= this.deleteGroup(group); + }, this); + + if (del) { + group.drop(); + + /** + * Just after deleting a group + * @event afterDeleteGroup + * @memberof QueryBuilder + */ + this.trigger('afterDeleteGroup'); + + this.trigger('rulesChanged'); + } + + return del; +}; + +/** + * Performs actions when a group's condition changes + * @param {Group} group + * @param {object} previousCondition + * @fires QueryBuilder.afterUpdateGroupCondition + * @private + */ +QueryBuilder.prototype.updateGroupCondition = function(group, previousCondition) { + group.$el.find('>' + QueryBuilder.selectors.group_condition).each(function() { + var $this = $(this); + $this.prop('checked', $this.val() === group.condition); + $this.parent().toggleClass('active', $this.val() === group.condition); + }); + + /** + * After the group condition has been modified + * @event afterUpdateGroupCondition + * @memberof QueryBuilder + * @param {Group} group + * @param {object} previousCondition + */ + this.trigger('afterUpdateGroupCondition', group, previousCondition); + + this.trigger('rulesChanged'); +}; + +/** + * Updates the visibility of conditions based on number of rules inside each group + * @private + */ +QueryBuilder.prototype.refreshGroupsConditions = function() { + (function walk(group) { + if (!group.flags || (group.flags && !group.flags.condition_readonly)) { + group.$el.find('>' + QueryBuilder.selectors.group_condition).prop('disabled', group.rules.length <= 1) + .parent().toggleClass('disabled', group.rules.length <= 1); + } + + group.each(null, function(group) { + walk(group); + }, this); + }(this.model.root)); +}; + +/** + * Adds a new rule + * @param {Group} parent + * @param {object} [data] - rule custom data + * @param {object} [flags] - flags to apply to the rule + * @returns {Rule} + * @fires QueryBuilder.beforeAddRule + * @fires QueryBuilder.afterAddRule + * @fires QueryBuilder.changer:getDefaultFilter + */ +QueryBuilder.prototype.addRule = function(parent, data, flags) { + /** + * Just before adding a rule, can be prevented + * @event beforeAddRule + * @memberof QueryBuilder + * @param {Group} parent + */ + var e = this.trigger('beforeAddRule', parent); + if (e.isDefaultPrevented()) { + return null; + } + + var rule_id = this.nextRuleId(); + var $rule = $($.parseHTML(this.getRuleTemplate(rule_id))); + var model = parent.addRule($rule); + + model.data = data; + model.flags = $.extend({}, this.settings.default_rule_flags, flags); + + /** + * Just after adding a rule + * @event afterAddRule + * @memberof QueryBuilder + * @param {Rule} rule + */ + this.trigger('afterAddRule', model); + + this.trigger('rulesChanged'); + + this.createRuleFilters(model); + + if (this.settings.default_filter || !this.settings.display_empty_filter) { + /** + * Modifies the default filter for a rule + * @event changer:getDefaultFilter + * @memberof QueryBuilder + * @param {QueryBuilder.Filter} filter + * @param {Rule} rule + * @returns {QueryBuilder.Filter} + */ + model.filter = this.change('getDefaultFilter', + this.getFilterById(this.settings.default_filter || this.filters[0].id), + model + ); + } + + return model; +}; + +/** + * Tries to delete a rule + * @param {Rule} rule + * @returns {boolean} if the rule has been deleted + * @fires QueryBuilder.beforeDeleteRule + * @fires QueryBuilder.afterDeleteRule + */ +QueryBuilder.prototype.deleteRule = function(rule) { + if (rule.flags.no_delete) { + return false; + } + + /** + * Just before deleting a rule, can be prevented + * @event beforeDeleteRule + * @memberof QueryBuilder + * @param {Rule} rule + */ + var e = this.trigger('beforeDeleteRule', rule); + if (e.isDefaultPrevented()) { + return false; + } + + rule.drop(); + + /** + * Just after deleting a rule + * @event afterDeleteRule + * @memberof QueryBuilder + */ + this.trigger('afterDeleteRule'); + + this.trigger('rulesChanged'); + + return true; +}; + +/** + * Creates the filters for a rule + * @param {Rule} rule + * @fires QueryBuilder.changer:getRuleFilters + * @fires QueryBuilder.afterCreateRuleFilters + * @private + */ +QueryBuilder.prototype.createRuleFilters = function(rule) { + /** + * Modifies the list a filters available for a rule + * @event changer:getRuleFilters + * @memberof QueryBuilder + * @param {QueryBuilder.Filter[]} filters + * @param {Rule} rule + * @returns {QueryBuilder.Filter[]} + */ + var filters = this.change('getRuleFilters', this.filters, rule); + var $filterSelect = $($.parseHTML(this.getRuleFilterSelect(rule, filters))); + + rule.$el.find(QueryBuilder.selectors.filter_container).html($filterSelect); + + /** + * After creating the dropdown for filters + * @event afterCreateRuleFilters + * @memberof QueryBuilder + * @param {Rule} rule + */ + this.trigger('afterCreateRuleFilters', rule); + + this.applyRuleFlags(rule); +}; + +/** + * Creates the operators for a rule and init the rule operator + * @param {Rule} rule + * @fires QueryBuilder.afterCreateRuleOperators + * @private + */ +QueryBuilder.prototype.createRuleOperators = function(rule) { + var $operatorContainer = rule.$el.find(QueryBuilder.selectors.operator_container).empty(); + + if (!rule.filter) { + return; + } + + var operators = this.getOperators(rule.filter); + var $operatorSelect = $($.parseHTML(this.getRuleOperatorSelect(rule, operators))); + + $operatorContainer.html($operatorSelect); + + // set the operator without triggering update event + if (rule.filter.default_operator) { + rule.__.operator = this.getOperatorByType(rule.filter.default_operator); + } + else { + rule.__.operator = operators[0]; + } + + rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type); + + /** + * After creating the dropdown for operators + * @event afterCreateRuleOperators + * @memberof QueryBuilder + * @param {Rule} rule + * @param {QueryBuilder.Operator[]} operators - allowed operators for this rule + */ + this.trigger('afterCreateRuleOperators', rule, operators); + + this.applyRuleFlags(rule); +}; + +/** + * Creates the main input for a rule + * @param {Rule} rule + * @fires QueryBuilder.afterCreateRuleInput + * @private + */ +QueryBuilder.prototype.createRuleInput = function(rule) { + var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container).empty(); + + rule.__.value = undefined; + + if (!rule.filter || !rule.operator || rule.operator.nb_inputs === 0) { + return; + } + + var self = this; + var $inputs = $(); + var filter = rule.filter; + + for (var i = 0; i < rule.operator.nb_inputs; i++) { + var $ruleInput = $($.parseHTML($.trim(this.getRuleInput(rule, i)))); + if (i > 0) $valueContainer.append(this.settings.inputs_separator); + $valueContainer.append($ruleInput); + $inputs = $inputs.add($ruleInput); + } + + $valueContainer.css('display', ''); + + $inputs.on('change ' + (filter.input_event || ''), function() { + if (!rule._updating_input) { + rule._updating_value = true; + rule.value = self.getRuleInputValue(rule); + rule._updating_value = false; + } + }); + + if (filter.plugin) { + $inputs[filter.plugin](filter.plugin_config || {}); + } + + /** + * After creating the input for a rule and initializing optional plugin + * @event afterCreateRuleInput + * @memberof QueryBuilder + * @param {Rule} rule + */ + this.trigger('afterCreateRuleInput', rule); + + if (filter.default_value !== undefined) { + rule.value = filter.default_value; + } + else { + rule._updating_value = true; + rule.value = self.getRuleInputValue(rule); + rule._updating_value = false; + } + + this.applyRuleFlags(rule); +}; + +/** + * Performs action when a rule's filter changes + * @param {Rule} rule + * @param {object} previousFilter + * @fires QueryBuilder.afterUpdateRuleFilter + * @private + */ +QueryBuilder.prototype.updateRuleFilter = function(rule, previousFilter) { + this.createRuleOperators(rule); + this.createRuleInput(rule); + + rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1'); + + // clear rule data if the filter changed + if (previousFilter && rule.filter && previousFilter.id !== rule.filter.id) { + rule.data = undefined; + } + + /** + * After the filter has been updated and the operators and input re-created + * @event afterUpdateRuleFilter + * @memberof QueryBuilder + * @param {Rule} rule + * @param {object} previousFilter + */ + this.trigger('afterUpdateRuleFilter', rule, previousFilter); + + this.trigger('rulesChanged'); +}; + +/** + * Performs actions when a rule's operator changes + * @param {Rule} rule + * @param {object} previousOperator + * @fires QueryBuilder.afterUpdateRuleOperator + * @private + */ +QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) { + var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container); + + if (!rule.operator || rule.operator.nb_inputs === 0) { + $valueContainer.hide(); + + rule.__.value = undefined; + } + else { + $valueContainer.css('display', ''); + + if ($valueContainer.is(':empty') || !previousOperator || + rule.operator.nb_inputs !== previousOperator.nb_inputs || + rule.operator.optgroup !== previousOperator.optgroup + ) { + this.createRuleInput(rule); + } + } + + if (rule.operator) { + rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type); + + // refresh value if the format changed for this operator + rule.__.value = this.getRuleInputValue(rule); + } + + /** + * After the operator has been updated and the input optionally re-created + * @event afterUpdateRuleOperator + * @memberof QueryBuilder + * @param {Rule} rule + * @param {object} previousOperator + */ + this.trigger('afterUpdateRuleOperator', rule, previousOperator); + + this.trigger('rulesChanged'); +}; + +/** + * Performs actions when rule's value changes + * @param {Rule} rule + * @param {object} previousValue + * @fires QueryBuilder.afterUpdateRuleValue + * @private + */ +QueryBuilder.prototype.updateRuleValue = function(rule, previousValue) { + if (!rule._updating_value) { + this.setRuleInputValue(rule, rule.value); + } + + /** + * After the rule value has been modified + * @event afterUpdateRuleValue + * @memberof QueryBuilder + * @param {Rule} rule + * @param {*} previousValue + */ + this.trigger('afterUpdateRuleValue', rule, previousValue); + + this.trigger('rulesChanged'); +}; + +/** + * Changes a rule's properties depending on its flags + * @param {Rule} rule + * @fires QueryBuilder.afterApplyRuleFlags + * @private + */ +QueryBuilder.prototype.applyRuleFlags = function(rule) { + var flags = rule.flags; + var Selectors = QueryBuilder.selectors; + + rule.$el.find(Selectors.rule_filter).prop('disabled', flags.filter_readonly); + rule.$el.find(Selectors.rule_operator).prop('disabled', flags.operator_readonly); + rule.$el.find(Selectors.rule_value).prop('disabled', flags.value_readonly); + + if (flags.no_delete) { + rule.$el.find(Selectors.delete_rule).remove(); + } + + /** + * After rule's flags has been applied + * @event afterApplyRuleFlags + * @memberof QueryBuilder + * @param {Rule} rule + */ + this.trigger('afterApplyRuleFlags', rule); +}; + +/** + * Changes group's properties depending on its flags + * @param {Group} group + * @fires QueryBuilder.afterApplyGroupFlags + * @private + */ +QueryBuilder.prototype.applyGroupFlags = function(group) { + var flags = group.flags; + var Selectors = QueryBuilder.selectors; + + group.$el.find('>' + Selectors.group_condition).prop('disabled', flags.condition_readonly) + .parent().toggleClass('readonly', flags.condition_readonly); + + if (flags.no_add_rule) { + group.$el.find(Selectors.add_rule).remove(); + } + if (flags.no_add_group) { + group.$el.find(Selectors.add_group).remove(); + } + if (flags.no_delete) { + group.$el.find(Selectors.delete_group).remove(); + } + + /** + * After group's flags has been applied + * @event afterApplyGroupFlags + * @memberof QueryBuilder + * @param {Group} group + */ + this.trigger('afterApplyGroupFlags', group); +}; + +/** + * Clears all errors markers + * @param {Node} [node] default is root Group + */ +QueryBuilder.prototype.clearErrors = function(node) { + node = node || this.model.root; + + if (!node) { + return; + } + + node.error = null; + + if (node instanceof Group) { + node.each(function(rule) { + rule.error = null; + }, function(group) { + this.clearErrors(group); + }, this); + } +}; + +/** + * Adds/Removes error on a Rule or Group + * @param {Node} node + * @fires QueryBuilder.changer:displayError + * @private + */ +QueryBuilder.prototype.updateError = function(node) { + if (this.settings.display_errors) { + if (node.error === null) { + node.$el.removeClass('has-error'); + } + else { + var errorMessage = this.translate('errors', node.error[0]); + errorMessage = Utils.fmt(errorMessage, node.error.slice(1)); + + /** + * Modifies an error message before display + * @event changer:displayError + * @memberof QueryBuilder + * @param {string} errorMessage - the error message (translated and formatted) + * @param {array} error - the raw error array (error code and optional arguments) + * @param {Node} node + * @returns {string} + */ + errorMessage = this.change('displayError', errorMessage, node.error, node); + + node.$el.addClass('has-error') + .find(QueryBuilder.selectors.error_container).eq(0) + .attr('title', errorMessage); + } + } +}; + +/** + * Triggers a validation error event + * @param {Node} node + * @param {string|array} error + * @param {*} value + * @fires QueryBuilder.validationError + * @private + */ +QueryBuilder.prototype.triggerValidationError = function(node, error, value) { + if (!$.isArray(error)) { + error = [error]; + } + + /** + * Fired when a validation error occurred, can be prevented + * @event validationError + * @memberof QueryBuilder + * @param {Node} node + * @param {string} error + * @param {*} value + */ + var e = this.trigger('validationError', node, error, value); + if (!e.isDefaultPrevented()) { + node.error = error; + } +}; + + +/** + * Destroys the builder + * @fires QueryBuilder.beforeDestroy + */ +QueryBuilder.prototype.destroy = function() { + /** + * Before the {@link QueryBuilder#destroy} method + * @event beforeDestroy + * @memberof QueryBuilder + */ + this.trigger('beforeDestroy'); + + if (this.status.generated_id) { + this.$el.removeAttr('id'); + } + + this.clear(); + this.model = null; + + this.$el + .off('.queryBuilder') + .removeClass('query-builder') + .removeData('queryBuilder'); + + delete this.$el[0].queryBuilder; +}; + +/** + * Clear all rules and resets the root group + * @fires QueryBuilder.beforeReset + * @fires QueryBuilder.afterReset + */ +QueryBuilder.prototype.reset = function() { + /** + * Before the {@link QueryBuilder#reset} method, can be prevented + * @event beforeReset + * @memberof QueryBuilder + */ + var e = this.trigger('beforeReset'); + if (e.isDefaultPrevented()) { + return; + } + + this.status.group_id = 1; + this.status.rule_id = 0; + + this.model.root.empty(); + + this.model.root.data = undefined; + this.model.root.flags = $.extend({}, this.settings.default_group_flags); + this.model.root.condition = this.settings.default_condition; + + this.addRule(this.model.root); + + /** + * After the {@link QueryBuilder#reset} method + * @event afterReset + * @memberof QueryBuilder + */ + this.trigger('afterReset'); + + this.trigger('rulesChanged'); +}; + +/** + * Clears all rules and removes the root group + * @fires QueryBuilder.beforeClear + * @fires QueryBuilder.afterClear + */ +QueryBuilder.prototype.clear = function() { + /** + * Before the {@link QueryBuilder#clear} method, can be prevented + * @event beforeClear + * @memberof QueryBuilder + */ + var e = this.trigger('beforeClear'); + if (e.isDefaultPrevented()) { + return; + } + + this.status.group_id = 0; + this.status.rule_id = 0; + + if (this.model.root) { + this.model.root.drop(); + this.model.root = null; + } + + /** + * After the {@link QueryBuilder#clear} method + * @event afterClear + * @memberof QueryBuilder + */ + this.trigger('afterClear'); + + this.trigger('rulesChanged'); +}; + +/** + * Modifies the builder configuration.
+ * Only options defined in QueryBuilder.modifiable_options are modifiable + * @param {object} options + */ +QueryBuilder.prototype.setOptions = function(options) { + $.each(options, function(opt, value) { + if (QueryBuilder.modifiable_options.indexOf(opt) !== -1) { + this.settings[opt] = value; + } + }.bind(this)); +}; + +/** + * Returns the model associated to a DOM object, or the root model + * @param {jQuery} [target] + * @returns {Node} + */ +QueryBuilder.prototype.getModel = function(target) { + if (!target) { + return this.model.root; + } + else if (target instanceof Node) { + return target; + } + else { + return $(target).data('queryBuilderModel'); + } +}; + +/** + * Validates the whole builder + * @param {object} [options] + * @param {boolean} [options.skip_empty=false] - skips validating rules that have no filter selected + * @returns {boolean} + * @fires QueryBuilder.changer:validate + */ +QueryBuilder.prototype.validate = function(options) { + options = $.extend({ + skip_empty: false + }, options); + + this.clearErrors(); + + var self = this; + + var valid = (function parse(group) { + var done = 0; + var errors = 0; + + group.each(function(rule) { + if (!rule.filter && options.skip_empty) { + return; + } + + if (!rule.filter) { + self.triggerValidationError(rule, 'no_filter', null); + errors++; + return; + } + + if (!rule.operator) { + self.triggerValidationError(rule, 'no_operator', null); + errors++; + return; + } + + if (rule.operator.nb_inputs !== 0) { + var valid = self.validateValue(rule, rule.value); + + if (valid !== true) { + self.triggerValidationError(rule, valid, rule.value); + errors++; + return; + } + } + + done++; + + }, function(group) { + var res = parse(group); + if (res === true) { + done++; + } + else if (res === false) { + errors++; + } + }); + + if (errors > 0) { + return false; + } + else if (done === 0 && !group.isRoot() && options.skip_empty) { + return null; + } + else if (done === 0 && (!self.settings.allow_empty || !group.isRoot())) { + self.triggerValidationError(group, 'empty_group', null); + return false; + } + + return true; + + }(this.model.root)); + + /** + * Modifies the result of the {@link QueryBuilder#validate} method + * @event changer:validate + * @memberof QueryBuilder + * @param {boolean} valid + * @returns {boolean} + */ + return this.change('validate', valid); +}; + +/** + * Gets an object representing current rules + * @param {object} [options] + * @param {boolean|string} [options.get_flags=false] - export flags, true: only changes from default flags or 'all' + * @param {boolean} [options.allow_invalid=false] - returns rules even if they are invalid + * @param {boolean} [options.skip_empty=false] - remove rules that have no filter selected + * @returns {object} + * @fires QueryBuilder.changer:ruleToJson + * @fires QueryBuilder.changer:groupToJson + * @fires QueryBuilder.changer:getRules + */ +QueryBuilder.prototype.getRules = function(options) { + options = $.extend({ + get_flags: false, + allow_invalid: false, + skip_empty: false + }, options); + + var valid = this.validate(options); + if (!valid && !options.allow_invalid) { + return null; + } + + var self = this; + + var out = (function parse(group) { + var groupData = { + condition: group.condition, + rules: [] + }; + + if (group.data) { + groupData.data = $.extendext(true, 'replace', {}, group.data); + } + + if (options.get_flags) { + var flags = self.getGroupFlags(group.flags, options.get_flags === 'all'); + if (!$.isEmptyObject(flags)) { + groupData.flags = flags; + } + } + + group.each(function(rule) { + if (!rule.filter && options.skip_empty) { + return; + } + + var value = null; + if (!rule.operator || rule.operator.nb_inputs !== 0) { + value = rule.value; + } + + var ruleData = { + id: rule.filter ? rule.filter.id : null, + field: rule.filter ? rule.filter.field : null, + type: rule.filter ? rule.filter.type : null, + input: rule.filter ? rule.filter.input : null, + operator: rule.operator ? rule.operator.type : null, + value: value + }; + + if (rule.filter && rule.filter.data || rule.data) { + ruleData.data = $.extendext(true, 'replace', {}, rule.filter ? rule.filter.data : {}, rule.data); + } + + if (options.get_flags) { + var flags = self.getRuleFlags(rule.flags, options.get_flags === 'all'); + if (!$.isEmptyObject(flags)) { + ruleData.flags = flags; + } + } + + /** + * Modifies the JSON generated from a Rule object + * @event changer:ruleToJson + * @memberof QueryBuilder + * @param {object} json + * @param {Rule} rule + * @returns {object} + */ + groupData.rules.push(self.change('ruleToJson', ruleData, rule)); + + }, function(model) { + var data = parse(model); + if (data.rules.length !== 0 || !options.skip_empty) { + groupData.rules.push(data); + } + }, this); + + /** + * Modifies the JSON generated from a Group object + * @event changer:groupToJson + * @memberof QueryBuilder + * @param {object} json + * @param {Group} group + * @returns {object} + */ + return self.change('groupToJson', groupData, group); + + }(this.model.root)); + + out.valid = valid; + + /** + * Modifies the result of the {@link QueryBuilder#getRules} method + * @event changer:getRules + * @memberof QueryBuilder + * @param {object} json + * @returns {object} + */ + return this.change('getRules', out); +}; + +/** + * Sets rules from object + * @param {object} data + * @param {object} [options] + * @param {boolean} [options.allow_invalid=false] - silent-fail if the data are invalid + * @throws RulesError, UndefinedConditionError + * @fires QueryBuilder.changer:setRules + * @fires QueryBuilder.changer:jsonToRule + * @fires QueryBuilder.changer:jsonToGroup + * @fires QueryBuilder.afterSetRules + */ +QueryBuilder.prototype.setRules = function(data, options) { + options = $.extend({ + allow_invalid: false + }, options); + + if ($.isArray(data)) { + data = { + condition: this.settings.default_condition, + rules: data + }; + } + + if (!data || !data.rules || (data.rules.length === 0 && !this.settings.allow_empty)) { + Utils.error('RulesParse', 'Incorrect data object passed'); + } + + this.clear(); + this.setRoot(false, data.data, this.parseGroupFlags(data)); + + /** + * Modifies data before the {@link QueryBuilder#setRules} method + * @event changer:setRules + * @memberof QueryBuilder + * @param {object} json + * @param {object} options + * @returns {object} + */ + data = this.change('setRules', data, options); + + var self = this; + + (function add(data, group) { + if (group === null) { + return; + } + + if (data.condition === undefined) { + data.condition = self.settings.default_condition; + } + else if (self.settings.conditions.indexOf(data.condition) == -1) { + Utils.error(!options.allow_invalid, 'UndefinedCondition', 'Invalid condition "{0}"', data.condition); + data.condition = self.settings.default_condition; + } + + group.condition = data.condition; + + data.rules.forEach(function(item) { + var model; + + if (item.rules !== undefined) { + if (self.settings.allow_groups !== -1 && self.settings.allow_groups < group.level) { + Utils.error(!options.allow_invalid, 'RulesParse', 'No more than {0} groups are allowed', self.settings.allow_groups); + self.reset(); + } + else { + model = self.addGroup(group, false, item.data, self.parseGroupFlags(item)); + if (model === null) { + return; + } + + add(item, model); + } + } + else { + if (!item.empty) { + if (item.id === undefined) { + Utils.error(!options.allow_invalid, 'RulesParse', 'Missing rule field id'); + item.empty = true; + } + if (item.operator === undefined) { + item.operator = 'equal'; + } + } + + model = self.addRule(group, item.data, self.parseRuleFlags(item)); + if (model === null) { + return; + } + + if (!item.empty) { + model.filter = self.getFilterById(item.id, !options.allow_invalid); + } + + if (model.filter) { + model.operator = self.getOperatorByType(item.operator, !options.allow_invalid); + + if (!model.operator) { + model.operator = self.getOperators(model.filter)[0]; + } + } + + if (model.operator && model.operator.nb_inputs !== 0) { + if (item.value !== undefined) { + model.value = item.value; + } + else if (model.filter.default_value !== undefined) { + model.value = model.filter.default_value; + } + } + + /** + * Modifies the Rule object generated from the JSON + * @event changer:jsonToRule + * @memberof QueryBuilder + * @param {Rule} rule + * @param {object} json + * @returns {Rule} the same rule + */ + if (self.change('jsonToRule', model, item) != model) { + Utils.error('RulesParse', 'Plugin tried to change rule reference'); + } + } + }); + + /** + * Modifies the Group object generated from the JSON + * @event changer:jsonToGroup + * @memberof QueryBuilder + * @param {Group} group + * @param {object} json + * @returns {Group} the same group + */ + if (self.change('jsonToGroup', group, data) != group) { + Utils.error('RulesParse', 'Plugin tried to change group reference'); + } + + }(data, this.model.root)); + + /** + * After the {@link QueryBuilder#setRules} method + * @event afterSetRules + * @memberof QueryBuilder + */ + this.trigger('afterSetRules'); +}; + + +/** + * Performs value validation + * @param {Rule} rule + * @param {string|string[]} value + * @returns {array|boolean} true or error array + * @fires QueryBuilder.changer:validateValue + */ +QueryBuilder.prototype.validateValue = function(rule, value) { + var validation = rule.filter.validation || {}; + var result = true; + + if (validation.callback) { + result = validation.callback.call(this, value, rule); + } + else { + result = this._validateValue(rule, value); + } + + /** + * Modifies the result of the rule validation method + * @event changer:validateValue + * @memberof QueryBuilder + * @param {array|boolean} result - true or an error array + * @param {*} value + * @param {Rule} rule + * @returns {array|boolean} + */ + return this.change('validateValue', result, value, rule); +}; + +/** + * Default validation function + * @param {Rule} rule + * @param {string|string[]} value + * @returns {array|boolean} true or error array + * @throws ConfigError + * @private + */ +QueryBuilder.prototype._validateValue = function(rule, value) { + var filter = rule.filter; + var operator = rule.operator; + var validation = filter.validation || {}; + var result = true; + var tmp, tempValue; + + if (rule.operator.nb_inputs === 1) { + value = [value]; + } + + for (var i = 0; i < operator.nb_inputs; i++) { + if (!operator.multiple && $.isArray(value[i]) && value[i].length > 1) { + result = ['operator_not_multiple', operator.type, this.translate('operators', operator.type)]; + break; + } + + switch (filter.input) { + case 'radio': + if (value[i] === undefined || value[i].length === 0) { + if (!validation.allow_empty_value) { + result = ['radio_empty']; + } + break; + } + break; + + case 'checkbox': + if (value[i] === undefined || value[i].length === 0) { + if (!validation.allow_empty_value) { + result = ['checkbox_empty']; + } + break; + } + break; + + case 'select': + if (value[i] === undefined || value[i].length === 0 || (filter.placeholder && value[i] == filter.placeholder_value)) { + if (!validation.allow_empty_value) { + result = ['select_empty']; + } + break; + } + break; + + default: + tempValue = $.isArray(value[i]) ? value[i] : [value[i]]; + + for (var j = 0; j < tempValue.length; j++) { + switch (QueryBuilder.types[filter.type]) { + case 'string': + if (tempValue[j] === undefined || tempValue[j].length === 0) { + if (!validation.allow_empty_value) { + result = ['string_empty']; + } + break; + } + if (validation.min !== undefined) { + if (tempValue[j].length < parseInt(validation.min)) { + result = [this.getValidationMessage(validation, 'min', 'string_exceed_min_length'), validation.min]; + break; + } + } + if (validation.max !== undefined) { + if (tempValue[j].length > parseInt(validation.max)) { + result = [this.getValidationMessage(validation, 'max', 'string_exceed_max_length'), validation.max]; + break; + } + } + if (validation.format) { + if (typeof validation.format == 'string') { + validation.format = new RegExp(validation.format); + } + if (!validation.format.test(tempValue[j])) { + result = [this.getValidationMessage(validation, 'format', 'string_invalid_format'), validation.format]; + break; + } + } + break; + + case 'number': + if (tempValue[j] === undefined || tempValue[j].length === 0) { + if (!validation.allow_empty_value) { + result = ['number_nan']; + } + break; + } + if (isNaN(tempValue[j])) { + result = ['number_nan']; + break; + } + if (filter.type == 'integer') { + if (parseInt(tempValue[j]) != tempValue[j]) { + result = ['number_not_integer']; + break; + } + } + else { + if (parseFloat(tempValue[j]) != tempValue[j]) { + result = ['number_not_double']; + break; + } + } + if (validation.min !== undefined) { + if (tempValue[j] < parseFloat(validation.min)) { + result = [this.getValidationMessage(validation, 'min', 'number_exceed_min'), validation.min]; + break; + } + } + if (validation.max !== undefined) { + if (tempValue[j] > parseFloat(validation.max)) { + result = [this.getValidationMessage(validation, 'max', 'number_exceed_max'), validation.max]; + break; + } + } + if (validation.step !== undefined && validation.step !== 'any') { + var v = (tempValue[j] / validation.step).toPrecision(14); + if (parseInt(v) != v) { + result = [this.getValidationMessage(validation, 'step', 'number_wrong_step'), validation.step]; + break; + } + } + break; + + case 'datetime': + if (tempValue[j] === undefined || tempValue[j].length === 0) { + if (!validation.allow_empty_value) { + result = ['datetime_empty']; + } + break; + } + + // we need MomentJS + if (validation.format) { + if (!('moment' in window)) { + Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com'); + } + + var datetime = moment(tempValue[j], validation.format); + if (!datetime.isValid()) { + result = [this.getValidationMessage(validation, 'format', 'datetime_invalid'), validation.format]; + break; + } + else { + if (validation.min) { + if (datetime < moment(validation.min, validation.format)) { + result = [this.getValidationMessage(validation, 'min', 'datetime_exceed_min'), validation.min]; + break; + } + } + if (validation.max) { + if (datetime > moment(validation.max, validation.format)) { + result = [this.getValidationMessage(validation, 'max', 'datetime_exceed_max'), validation.max]; + break; + } + } + } + } + break; + + case 'boolean': + if (tempValue[j] === undefined || tempValue[j].length === 0) { + if (!validation.allow_empty_value) { + result = ['boolean_not_valid']; + } + break; + } + tmp = ('' + tempValue[j]).trim().toLowerCase(); + if (tmp !== 'true' && tmp !== 'false' && tmp !== '1' && tmp !== '0' && tempValue[j] !== 1 && tempValue[j] !== 0) { + result = ['boolean_not_valid']; + break; + } + } + + if (result !== true) { + break; + } + } + } + + if (result !== true) { + break; + } + } + + if ((rule.operator.type === 'between' || rule.operator.type === 'not_between') && value.length === 2) { + switch (QueryBuilder.types[filter.type]) { + case 'number': + if (value[0] > value[1]) { + result = ['number_between_invalid', value[0], value[1]]; + } + break; + + case 'datetime': + // we need MomentJS + if (validation.format) { + if (!('moment' in window)) { + Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com'); + } + + if (moment(value[0], validation.format).isAfter(moment(value[1], validation.format))) { + result = ['datetime_between_invalid', value[0], value[1]]; + } + } + break; + } + } + + return result; +}; + +/** + * Returns an incremented group ID + * @returns {string} + * @private + */ +QueryBuilder.prototype.nextGroupId = function() { + return this.status.id + '_group_' + (this.status.group_id++); +}; + +/** + * Returns an incremented rule ID + * @returns {string} + * @private + */ +QueryBuilder.prototype.nextRuleId = function() { + return this.status.id + '_rule_' + (this.status.rule_id++); +}; + +/** + * Returns the operators for a filter + * @param {string|object} filter - filter id or filter object + * @returns {object[]} + * @fires QueryBuilder.changer:getOperators + */ +QueryBuilder.prototype.getOperators = function(filter) { + if (typeof filter == 'string') { + filter = this.getFilterById(filter); + } + + var result = []; + + for (var i = 0, l = this.operators.length; i < l; i++) { + // filter operators check + if (filter.operators) { + if (filter.operators.indexOf(this.operators[i].type) == -1) { + continue; + } + } + // type check + else if (this.operators[i].apply_to.indexOf(QueryBuilder.types[filter.type]) == -1) { + continue; + } + + result.push(this.operators[i]); + } + + // keep sort order defined for the filter + if (filter.operators) { + result.sort(function(a, b) { + return filter.operators.indexOf(a.type) - filter.operators.indexOf(b.type); + }); + } + + /** + * Modifies the operators available for a filter + * @event changer:getOperators + * @memberof QueryBuilder + * @param {QueryBuilder.Operator[]} operators + * @param {QueryBuilder.Filter} filter + * @returns {QueryBuilder.Operator[]} + */ + return this.change('getOperators', result, filter); +}; + +/** + * Returns a particular filter by its id + * @param {string} id + * @param {boolean} [doThrow=true] + * @returns {object|null} + * @throws UndefinedFilterError + */ +QueryBuilder.prototype.getFilterById = function(id, doThrow) { + if (id == '-1') { + return null; + } + + for (var i = 0, l = this.filters.length; i < l; i++) { + if (this.filters[i].id == id) { + return this.filters[i]; + } + } + + Utils.error(doThrow !== false, 'UndefinedFilter', 'Undefined filter "{0}"', id); + + return null; +}; + +/** + * Returns a particular operator by its type + * @param {string} type + * @param {boolean} [doThrow=true] + * @returns {object|null} + * @throws UndefinedOperatorError + */ +QueryBuilder.prototype.getOperatorByType = function(type, doThrow) { + if (type == '-1') { + return null; + } + + for (var i = 0, l = this.operators.length; i < l; i++) { + if (this.operators[i].type == type) { + return this.operators[i]; + } + } + + Utils.error(doThrow !== false, 'UndefinedOperator', 'Undefined operator "{0}"', type); + + return null; +}; + +/** + * Returns rule's current input value + * @param {Rule} rule + * @returns {*} + * @fires QueryBuilder.changer:getRuleValue + * @private + */ +QueryBuilder.prototype.getRuleInputValue = function(rule) { + var filter = rule.filter; + var operator = rule.operator; + var value = []; + + if (filter.valueGetter) { + value = filter.valueGetter.call(this, rule); + } + else { + var $value = rule.$el.find(QueryBuilder.selectors.value_container); + + for (var i = 0; i < operator.nb_inputs; i++) { + var name = Utils.escapeElementId(rule.id + '_value_' + i); + var tmp; + + switch (filter.input) { + case 'radio': + value.push($value.find('[name=' + name + ']:checked').val()); + break; + + case 'checkbox': + tmp = []; + $value.find('[name=' + name + ']:checked').each(function() { + tmp.push($(this).val()); + }); + value.push(tmp); + break; + + case 'select': + if (filter.multiple) { + tmp = []; + $value.find('[name=' + name + '] option:selected').each(function() { + tmp.push($(this).val()); + }); + value.push(tmp); + } + else { + value.push($value.find('[name=' + name + '] option:selected').val()); + } + break; + + default: + value.push($value.find('[name=' + name + ']').val()); + } + } + + value = value.map(function(val) { + if (operator.multiple && filter.value_separator && typeof val == 'string') { + val = val.split(filter.value_separator); + } + + if ($.isArray(val)) { + return val.map(function(subval) { + return Utils.changeType(subval, filter.type); + }); + } + else { + return Utils.changeType(val, filter.type); + } + }); + + if (operator.nb_inputs === 1) { + value = value[0]; + } + + // @deprecated + if (filter.valueParser) { + value = filter.valueParser.call(this, rule, value); + } + } + + /** + * Modifies the rule's value grabbed from the DOM + * @event changer:getRuleValue + * @memberof QueryBuilder + * @param {*} value + * @param {Rule} rule + * @returns {*} + */ + return this.change('getRuleValue', value, rule); +}; + +/** + * Sets the value of a rule's input + * @param {Rule} rule + * @param {*} value + * @private + */ +QueryBuilder.prototype.setRuleInputValue = function(rule, value) { + var filter = rule.filter; + var operator = rule.operator; + + if (!filter || !operator) { + return; + } + + rule._updating_input = true; + + if (filter.valueSetter) { + filter.valueSetter.call(this, rule, value); + } + else { + var $value = rule.$el.find(QueryBuilder.selectors.value_container); + + if (operator.nb_inputs == 1) { + value = [value]; + } + + for (var i = 0; i < operator.nb_inputs; i++) { + var name = Utils.escapeElementId(rule.id + '_value_' + i); + + switch (filter.input) { + case 'radio': + $value.find('[name=' + name + '][value="' + value[i] + '"]').prop('checked', true).trigger('change'); + break; + + case 'checkbox': + if (!$.isArray(value[i])) { + value[i] = [value[i]]; + } + value[i].forEach(function(value) { + $value.find('[name=' + name + '][value="' + value + '"]').prop('checked', true).trigger('change'); + }); + break; + + default: + if (operator.multiple && filter.value_separator && $.isArray(value[i])) { + value[i] = value[i].join(filter.value_separator); + } + $value.find('[name=' + name + ']').val(value[i]).trigger('change'); + break; + } + } + } + + rule._updating_input = false; +}; + +/** + * Parses rule flags + * @param {object} rule + * @returns {object} + * @fires QueryBuilder.changer:parseRuleFlags + * @private + */ +QueryBuilder.prototype.parseRuleFlags = function(rule) { + var flags = $.extend({}, this.settings.default_rule_flags); + + if (rule.readonly) { + $.extend(flags, { + filter_readonly: true, + operator_readonly: true, + value_readonly: true, + no_delete: true + }); + } + + if (rule.flags) { + $.extend(flags, rule.flags); + } + + /** + * Modifies the consolidated rule's flags + * @event changer:parseRuleFlags + * @memberof QueryBuilder + * @param {object} flags + * @param {object} rule - not a Rule object + * @returns {object} + */ + return this.change('parseRuleFlags', flags, rule); +}; + +/** + * Gets a copy of flags of a rule + * @param {object} flags + * @param {boolean} [all=false] - return all flags or only changes from default flags + * @returns {object} + * @private + */ +QueryBuilder.prototype.getRuleFlags = function(flags, all) { + if (all) { + return $.extend({}, flags); + } + else { + var ret = {}; + $.each(this.settings.default_rule_flags, function(key, value) { + if (flags[key] !== value) { + ret[key] = flags[key]; + } + }); + return ret; + } +}; + +/** + * Parses group flags + * @param {object} group + * @returns {object} + * @fires QueryBuilder.changer:parseGroupFlags + * @private + */ +QueryBuilder.prototype.parseGroupFlags = function(group) { + var flags = $.extend({}, this.settings.default_group_flags); + + if (group.readonly) { + $.extend(flags, { + condition_readonly: true, + no_add_rule: true, + no_add_group: true, + no_delete: true + }); + } + + if (group.flags) { + $.extend(flags, group.flags); + } + + /** + * Modifies the consolidated group's flags + * @event changer:parseGroupFlags + * @memberof QueryBuilder + * @param {object} flags + * @param {object} group - not a Group object + * @returns {object} + */ + return this.change('parseGroupFlags', flags, group); +}; + +/** + * Gets a copy of flags of a group + * @param {object} flags + * @param {boolean} [all=false] - return all flags or only changes from default flags + * @returns {object} + * @private + */ +QueryBuilder.prototype.getGroupFlags = function(flags, all) { + if (all) { + return $.extend({}, flags); + } + else { + var ret = {}; + $.each(this.settings.default_group_flags, function(key, value) { + if (flags[key] !== value) { + ret[key] = flags[key]; + } + }); + return ret; + } +}; + +/** + * Translate a label either by looking in the `lang` object or in itself if it's an object where keys are language codes + * @param {string} [category] + * @param {string|object} key + * @returns {string} + * @fires QueryBuilder.changer:translate + */ +QueryBuilder.prototype.translate = function(category, key) { + if (!key) { + key = category; + category = undefined; + } + + var translation; + if (typeof key === 'object') { + translation = key[this.settings.lang_code] || key['en']; + } + else { + translation = (category ? this.lang[category] : this.lang)[key] || key; + } + + /** + * Modifies the translated label + * @event changer:translate + * @memberof QueryBuilder + * @param {string} translation + * @param {string|object} key + * @param {string} [category] + * @returns {string} + */ + return this.change('translate', translation, key, category); +}; + +/** + * Returns a validation message + * @param {object} validation + * @param {string} type + * @param {string} def + * @returns {string} + * @private + */ +QueryBuilder.prototype.getValidationMessage = function(validation, type, def) { + return validation.messages && validation.messages[type] || def; +}; + + +QueryBuilder.templates.group = ({ group_id, level, conditions, icons, settings, translate, builder }) => { + return ` +
+
+
+ + ${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 rule's HTML + * @param {string} rule_id + * @returns {string} + * @fires QueryBuilder.changer:getRuleTemplate + * @private + */ +QueryBuilder.prototype.getRuleTemplate = function (rule_id) { + var h = this.templates.rule({ + builder: this, + rule_id: rule_id, + icons: this.icons, + settings: this.settings, + translate: this.translate.bind(this) + }).trim(); + + /** + * Modifies the raw HTML of a rule + * @event changer:getRuleTemplate + * @memberof QueryBuilder + * @param {string} html + * @returns {string} + */ + return this.change('getRuleTemplate', h); +}; + +/** + * Returns rule's filter HTML + * @param {Rule} rule + * @param {object[]} filters + * @returns {string} + * @fires QueryBuilder.changer:getRuleFilterTemplate + * @private + */ +QueryBuilder.prototype.getRuleFilterSelect = function (rule, filters) { + var h = this.templates.filterSelect({ + builder: this, + rule: rule, + filters: filters, + icons: this.icons, + settings: this.settings, + translate: this.translate.bind(this) + }).trim(); + + /** + * Modifies the raw HTML of the rule's filter dropdown + * @event changer:getRuleFilterSelect + * @memberof QueryBuilder + * @param {string} html + * @param {Rule} rule + * @param {QueryBuilder.Filter[]} filters + * @returns {string} + */ + return this.change('getRuleFilterSelect', h, rule, filters); +}; + +/** + * Returns rule's operator HTML + * @param {Rule} rule + * @param {object[]} operators + * @returns {string} + * @fires QueryBuilder.changer:getRuleOperatorTemplate + * @private + */ +QueryBuilder.prototype.getRuleOperatorSelect = function (rule, operators) { + var h = this.templates.operatorSelect({ + builder: this, + rule: rule, + operators: operators, + icons: this.icons, + settings: this.settings, + translate: this.translate.bind(this) + }).trim(); + + /** + * Modifies the raw HTML of the rule's operator dropdown + * @event changer:getRuleOperatorSelect + * @memberof QueryBuilder + * @param {string} html + * @param {Rule} rule + * @param {QueryBuilder.Operator[]} operators + * @returns {string} + */ + return this.change('getRuleOperatorSelect', h, rule, operators); +}; + +/** + * Returns the rule's value select HTML + * @param {string} name + * @param {Rule} rule + * @returns {string} + * @fires QueryBuilder.changer:getRuleValueSelect + * @private + */ +QueryBuilder.prototype.getRuleValueSelect = function (name, rule) { + var h = this.templates.ruleValueSelect({ + builder: this, + name: name, + rule: rule, + icons: this.icons, + settings: this.settings, + translate: this.translate.bind(this) + }).trim(); + + /** + * Modifies the raw HTML of the rule's value dropdown (in case of a "select filter) + * @event changer:getRuleValueSelect + * @memberof QueryBuilder + * @param {string} html + * @param [string} name + * @param {Rule} rule + * @returns {string} + */ + return this.change('getRuleValueSelect', h, name, rule); +}; + +/** + * Returns the rule's value HTML + * @param {Rule} rule + * @param {int} value_id + * @returns {string} + * @fires QueryBuilder.changer:getRuleInput + * @private + */ +QueryBuilder.prototype.getRuleInput = function (rule, value_id) { + var filter = rule.filter; + var validation = rule.filter.validation || {}; + var name = rule.id + '_value_' + value_id; + var c = filter.vertical ? ' class=block' : ''; + var h = ''; + var placeholder = Array.isArray(filter.placeholder) ? filter.placeholder[value_id] : filter.placeholder; + + if (typeof filter.input == 'function') { + h = filter.input.call(this, rule, name); + } + else { + switch (filter.input) { + case 'radio': + case 'checkbox': + Utils.iterateOptions(filter.values, function (key, val) { + h += ' ' + val + ' '; + }); + break; + + case 'select': + h = this.getRuleValueSelect(name, rule); + 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); + } + } + }); + }); +}; + + +/** + * Main object storing data model and emitting model events + * @constructor + */ +function Model() { + /** + * @member {Group} + * @readonly + */ + this.root = null; + + /** + * Base for event emitting + * @member {jQuery} + * @readonly + * @private + */ + this.$ = $(this); +} + +$.extend(Model.prototype, /** @lends Model.prototype */ { + /** + * Triggers an event on the model + * @param {string} type + * @returns {$.Event} + */ + trigger: function(type) { + var event = new $.Event(type); + this.$.triggerHandler(event, Array.prototype.slice.call(arguments, 1)); + return event; + }, + + /** + * Attaches an event listener on the model + * @param {string} type + * @param {function} cb + * @returns {Model} + */ + on: function() { + this.$.on.apply(this.$, Array.prototype.slice.call(arguments)); + return this; + }, + + /** + * Removes an event listener from the model + * @param {string} type + * @param {function} [cb] + * @returns {Model} + */ + off: function() { + this.$.off.apply(this.$, Array.prototype.slice.call(arguments)); + return this; + }, + + /** + * Attaches an event listener called once on the model + * @param {string} type + * @param {function} cb + * @returns {Model} + */ + once: function() { + this.$.one.apply(this.$, Array.prototype.slice.call(arguments)); + return this; + } +}); + + +/** + * Root abstract object + * @constructor + * @param {Node} [parent] + * @param {jQuery} $el + */ +var Node = function(parent, $el) { + if (!(this instanceof Node)) { + return new Node(parent, $el); + } + + Object.defineProperty(this, '__', { value: {} }); + + $el.data('queryBuilderModel', this); + + /** + * @name level + * @member {int} + * @memberof Node + * @instance + * @readonly + */ + this.__.level = 1; + + /** + * @name error + * @member {string} + * @memberof Node + * @instance + */ + this.__.error = null; + + /** + * @name flags + * @member {object} + * @memberof Node + * @instance + * @readonly + */ + this.__.flags = {}; + + /** + * @name data + * @member {object} + * @memberof Node + * @instance + */ + this.__.data = undefined; + + /** + * @member {jQuery} + * @readonly + */ + this.$el = $el; + + /** + * @member {string} + * @readonly + */ + this.id = $el[0].id; + + /** + * @member {Model} + * @readonly + */ + this.model = null; + + /** + * @member {Group} + * @readonly + */ + this.parent = parent; +}; + +Utils.defineModelProperties(Node, ['level', 'error', 'data', 'flags']); + +Object.defineProperty(Node.prototype, 'parent', { + enumerable: true, + get: function() { + return this.__.parent; + }, + set: function(value) { + this.__.parent = value; + this.level = value === null ? 1 : value.level + 1; + this.model = value === null ? null : value.model; + } +}); + +/** + * Checks if this Node is the root + * @returns {boolean} + */ +Node.prototype.isRoot = function() { + return (this.level === 1); +}; + +/** + * Returns the node position inside its parent + * @returns {int} + */ +Node.prototype.getPos = function() { + if (this.isRoot()) { + return -1; + } + else { + return this.parent.getNodePos(this); + } +}; + +/** + * Deletes self + * @fires Model.model:drop + */ +Node.prototype.drop = function() { + var model = this.model; + + if (!!this.parent) { + this.parent.removeNode(this); + } + + this.$el.removeData('queryBuilderModel'); + + if (model !== null) { + /** + * After a node of the model has been removed + * @event model:drop + * @memberof Model + * @param {Node} node + */ + model.trigger('drop', this); + } +}; + +/** + * Moves itself after another Node + * @param {Node} target + * @fires Model.model:move + */ +Node.prototype.moveAfter = function(target) { + if (!this.isRoot()) { + this.move(target.parent, target.getPos() + 1); + } +}; + +/** + * Moves itself at the beginning of parent or another Group + * @param {Group} [target] + * @fires Model.model:move + */ +Node.prototype.moveAtBegin = function(target) { + if (!this.isRoot()) { + if (target === undefined) { + target = this.parent; + } + + this.move(target, 0); + } +}; + +/** + * Moves itself at the end of parent or another Group + * @param {Group} [target] + * @fires Model.model:move + */ +Node.prototype.moveAtEnd = function(target) { + if (!this.isRoot()) { + if (target === undefined) { + target = this.parent; + } + + this.move(target, target.length() === 0 ? 0 : target.length() - 1); + } +}; + +/** + * Moves itself at specific position of Group + * @param {Group} target + * @param {int} index + * @fires Model.model:move + */ +Node.prototype.move = function(target, index) { + if (!this.isRoot()) { + if (typeof target === 'number') { + index = target; + target = this.parent; + } + + this.parent.removeNode(this); + target.insertNode(this, index, false); + + if (this.model !== null) { + /** + * After a node of the model has been moved + * @event model:move + * @memberof Model + * @param {Node} node + * @param {Node} target + * @param {int} index + */ + this.model.trigger('move', this, target, index); + } + } +}; + + +/** + * Group object + * @constructor + * @extends Node + * @param {Group} [parent] + * @param {jQuery} $el + */ +var Group = function(parent, $el) { + if (!(this instanceof Group)) { + return new Group(parent, $el); + } + + Node.call(this, parent, $el); + + /** + * @member {object[]} + * @readonly + */ + this.rules = []; + + /** + * @name condition + * @member {string} + * @memberof Group + * @instance + */ + this.__.condition = null; +}; + +Group.prototype = Object.create(Node.prototype); +Group.prototype.constructor = Group; + +Utils.defineModelProperties(Group, ['condition']); + +/** + * Removes group's content + */ +Group.prototype.empty = function() { + this.each('reverse', function(rule) { + rule.drop(); + }, function(group) { + group.drop(); + }); +}; + +/** + * Deletes self + */ +Group.prototype.drop = function() { + this.empty(); + Node.prototype.drop.call(this); +}; + +/** + * Returns the number of children + * @returns {int} + */ +Group.prototype.length = function() { + return this.rules.length; +}; + +/** + * Adds a Node at specified index + * @param {Node} node + * @param {int} [index=end] + * @param {boolean} [trigger=false] - fire 'add' event + * @returns {Node} the inserted node + * @fires Model.model:add + */ +Group.prototype.insertNode = function(node, index, trigger) { + if (index === undefined) { + index = this.length(); + } + + this.rules.splice(index, 0, node); + node.parent = this; + + if (trigger && this.model !== null) { + /** + * After a node of the model has been added + * @event model:add + * @memberof Model + * @param {Node} parent + * @param {Node} node + * @param {int} index + */ + this.model.trigger('add', this, node, index); + } + + return node; +}; + +/** + * Adds a new Group at specified index + * @param {jQuery} $el + * @param {int} [index=end] + * @returns {Group} + * @fires Model.model:add + */ +Group.prototype.addGroup = function($el, index) { + return this.insertNode(new Group(this, $el), index, true); +}; + +/** + * Adds a new Rule at specified index + * @param {jQuery} $el + * @param {int} [index=end] + * @returns {Rule} + * @fires Model.model:add + */ +Group.prototype.addRule = function($el, index) { + return this.insertNode(new Rule(this, $el), index, true); +}; + +/** + * Deletes a specific Node + * @param {Node} node + */ +Group.prototype.removeNode = function(node) { + var index = this.getNodePos(node); + if (index !== -1) { + node.parent = null; + this.rules.splice(index, 1); + } +}; + +/** + * Returns the position of a child Node + * @param {Node} node + * @returns {int} + */ +Group.prototype.getNodePos = function(node) { + return this.rules.indexOf(node); +}; + +/** + * @callback Model#GroupIteratee + * @param {Node} node + * @returns {boolean} stop the iteration + */ + +/** + * Iterate over all Nodes + * @param {boolean} [reverse=false] - iterate in reverse order, required if you delete nodes + * @param {Model#GroupIteratee} cbRule - callback for Rules (can be `null` but not omitted) + * @param {Model#GroupIteratee} [cbGroup] - callback for Groups + * @param {object} [context] - context for callbacks + * @returns {boolean} if the iteration has been stopped by a callback + */ +Group.prototype.each = function(reverse, cbRule, cbGroup, context) { + if (typeof reverse !== 'boolean' && typeof reverse !== 'string') { + context = cbGroup; + cbGroup = cbRule; + cbRule = reverse; + reverse = false; + } + context = context === undefined ? null : context; + + var i = reverse ? this.rules.length - 1 : 0; + var l = reverse ? 0 : this.rules.length - 1; + var c = reverse ? -1 : 1; + var next = function() { + return reverse ? i >= l : i <= l; + }; + var stop = false; + + for (; next(); i += c) { + if (this.rules[i] instanceof Group) { + if (!!cbGroup) { + stop = cbGroup.call(context, this.rules[i]) === false; + } + } + else if (!!cbRule) { + stop = cbRule.call(context, this.rules[i]) === false; + } + + if (stop) { + break; + } + } + + return !stop; +}; + +/** + * Checks if the group contains a particular Node + * @param {Node} node + * @param {boolean} [recursive=false] + * @returns {boolean} + */ +Group.prototype.contains = function(node, recursive) { + if (this.getNodePos(node) !== -1) { + return true; + } + else if (!recursive) { + return false; + } + else { + // the loop will return with false as soon as the Node is found + return !this.each(function() { + return true; + }, function(group) { + return !group.contains(node, true); + }); + } +}; + + +/** + * Rule object + * @constructor + * @extends Node + * @param {Group} parent + * @param {jQuery} $el + */ +var Rule = function(parent, $el) { + if (!(this instanceof Rule)) { + return new Rule(parent, $el); + } + + Node.call(this, parent, $el); + + this._updating_value = false; + this._updating_input = false; + + /** + * @name filter + * @member {QueryBuilder.Filter} + * @memberof Rule + * @instance + */ + this.__.filter = null; + + /** + * @name operator + * @member {QueryBuilder.Operator} + * @memberof Rule + * @instance + */ + this.__.operator = null; + + /** + * @name value + * @member {*} + * @memberof Rule + * @instance + */ + this.__.value = undefined; +}; + +Rule.prototype = Object.create(Node.prototype); +Rule.prototype.constructor = Rule; + +Utils.defineModelProperties(Rule, ['filter', 'operator', 'value']); + +/** + * Checks if this Node is the root + * @returns {boolean} always false + */ +Rule.prototype.isRoot = function() { + return false; +}; + + +/** + * @member {function} + * @memberof QueryBuilder + * @see Group + */ +QueryBuilder.Group = Group; + +/** + * @member {function} + * @memberof QueryBuilder + * @see Rule + */ +QueryBuilder.Rule = Rule; + + +/** + * The {@link http://learn.jquery.com/plugins/|jQuery Plugins} namespace + * @external "jQuery.fn" + */ + +/** + * Instanciates or accesses the {@link QueryBuilder} on an element + * @function + * @memberof external:"jQuery.fn" + * @param {*} option - initial configuration or method name + * @param {...*} args - method arguments + * + * @example + * $('#builder').queryBuilder({ /** configuration object *\/ }); + * @example + * $('#builder').queryBuilder('methodName', methodParam1, methodParam2); + */ +$.fn.queryBuilder = function(option) { + if (this.length === 0) { + Utils.error('Config', 'No target defined'); + } + if (this.length > 1) { + Utils.error('Config', 'Unable to initialize on multiple target'); + } + + var data = this.data('queryBuilder'); + var options = (typeof option == 'object' && option) || {}; + + if (!data && option == 'destroy') { + return this; + } + if (!data) { + var builder = new QueryBuilder(this, options); + this.data('queryBuilder', builder); + builder.init(options.rules); + } + if (typeof option == 'string') { + return data[option].apply(data, Array.prototype.slice.call(arguments, 1)); + } + + return this; +}; + +/** + * @function + * @memberof external:"jQuery.fn" + * @see QueryBuilder + */ +$.fn.queryBuilder.constructor = QueryBuilder; + +/** + * @function + * @memberof external:"jQuery.fn" + * @see QueryBuilder.defaults + */ +$.fn.queryBuilder.defaults = QueryBuilder.defaults; + +/** + * @function + * @memberof external:"jQuery.fn" + * @see QueryBuilder.defaults + */ +$.fn.queryBuilder.extend = QueryBuilder.extend; + +/** + * @function + * @memberof external:"jQuery.fn" + * @see QueryBuilder.define + */ +$.fn.queryBuilder.define = QueryBuilder.define; + +/** + * @function + * @memberof external:"jQuery.fn" + * @see QueryBuilder.regional + */ +$.fn.queryBuilder.regional = QueryBuilder.regional; + + +/** + * @class BtCheckbox + * @memberof module:plugins + * @description Applies Awesome Bootstrap Checkbox for checkbox and radio inputs. + * @param {object} [options] + * @param {string} [options.font='bootstrap-icons'] + * @param {string} [options.color='default'] + */ +QueryBuilder.define('bt-checkbox', function(options) { + if (options.font === 'bootstrap-icons') { + this.$el.addClass('bt-checkbox-bootstrap-icons'); + } + + this.on('getRuleInput.filter', function(h, rule, name) { + var filter = rule.filter; + + if ((filter.input === 'radio' || filter.input === 'checkbox') && !filter.plugin) { + h.value = ''; + + if (!filter.colors) { + filter.colors = {}; + } + if (filter.color) { + filter.colors._def_ = filter.color; + } + + var style = filter.vertical ? ' style="display:block"' : ''; + var i = 0; + + Utils.iterateOptions(filter.values, function(key, val) { + var color = filter.colors[key] || filter.colors._def_ || options.color; + var id = name + '_' + (i++); + + h.value += `
`; + }); + } + }); +}, { + font: 'bootstrap-icons', + color: 'default' +}); + + +/** + * @class BtTooltipErrors + * @memberof module:plugins + * @description Applies Bootstrap Tooltips on validation error messages. + * @param {object} [options] + * @param {string} [options.placement='right'] + * @throws MissingLibraryError + */ +QueryBuilder.define('bt-tooltip-errors', function(options) { + if (! typeof bootstrap.Tooltip === "function") { + alert(typeof bootstrap.Tooltip ); + Utils.error('MissingLibrary', 'Bootstrap Popper is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com'); + } + + var self = this; + + // add BT Tooltip data + this.on('getRuleTemplate.filter getGroupTemplate.filter', function(h) { + var $h = $($.parseHTML(h.value)); + $h.find(QueryBuilder.selectors.error_container).attr('data-bs-toggle', 'tooltip'); + h.value = $h.prop('outerHTML'); + }); + + // init/refresh tooltip when title changes + this.model.on('update', function(e, node, field) { + if (field == 'error' && self.settings.display_errors) { + node.$el.find(QueryBuilder.selectors.error_container).eq(0) + .attr('data-bs-original-title',options).attr('data-bs-title',options).tooltip(); + } + }); +}, { + placement: 'right' +}); + + +/** + * @class ChangeFilters + * @memberof module:plugins + * @description Allows to change available filters after plugin initialization. + */ + +QueryBuilder.extend(/** @lends module:plugins.ChangeFilters.prototype */ { + /** + * Change the filters of the builder + * @param {boolean} [deleteOrphans=false] - delete rules using old filters + * @param {QueryBuilder[]} filters + * @fires module:plugins.ChangeFilters.changer:setFilters + * @fires module:plugins.ChangeFilters.afterSetFilters + * @throws ChangeFilterError + */ + setFilters: function(deleteOrphans, filters) { + var self = this; + + if (filters === undefined) { + filters = deleteOrphans; + deleteOrphans = false; + } + + filters = this.checkFilters(filters); + + /** + * Modifies the filters before {@link module:plugins.ChangeFilters.setFilters} method + * @event changer:setFilters + * @memberof module:plugins.ChangeFilters + * @param {QueryBuilder.Filter[]} filters + * @returns {QueryBuilder.Filter[]} + */ + filters = this.change('setFilters', filters); + + var filtersIds = filters.map(function(filter) { + return filter.id; + }); + + // check for orphans + if (!deleteOrphans) { + (function checkOrphans(node) { + node.each( + function(rule) { + if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) { + Utils.error('ChangeFilter', 'A rule is using filter "{0}"', rule.filter.id); + } + }, + checkOrphans + ); + }(this.model.root)); + } + + // replace filters + this.filters = filters; + + // apply on existing DOM + (function updateBuilder(node) { + node.each(true, + function(rule) { + if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) { + rule.drop(); + + self.trigger('rulesChanged'); + } + else { + self.createRuleFilters(rule); + + rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1'); + self.trigger('afterUpdateRuleFilter', rule); + } + }, + updateBuilder + ); + }(this.model.root)); + + // update plugins + if (this.settings.plugins) { + if (this.settings.plugins['unique-filter']) { + this.updateDisabledFilters(); + } + if (this.settings.plugins['bt-selectpicker']) { + this.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render'); + } + } + + // reset the default_filter if does not exist anymore + if (this.settings.default_filter) { + try { + this.getFilterById(this.settings.default_filter); + } + catch (e) { + this.settings.default_filter = null; + } + } + + /** + * After {@link module:plugins.ChangeFilters.setFilters} method + * @event afterSetFilters + * @memberof module:plugins.ChangeFilters + * @param {QueryBuilder.Filter[]} filters + */ + this.trigger('afterSetFilters', filters); + }, + + /** + * Adds a new filter to the builder + * @param {QueryBuilder.Filter|Filter[]} newFilters + * @param {int|string} [position=#end] - index or '#start' or '#end' + * @fires module:plugins.ChangeFilters.changer:setFilters + * @fires module:plugins.ChangeFilters.afterSetFilters + * @throws ChangeFilterError + */ + addFilter: function(newFilters, position) { + if (position === undefined || position == '#end') { + position = this.filters.length; + } + else if (position == '#start') { + position = 0; + } + + if (!$.isArray(newFilters)) { + newFilters = [newFilters]; + } + + var filters = $.extend(true, [], this.filters); + + // numeric position + if (parseInt(position) == position) { + Array.prototype.splice.apply(filters, [position, 0].concat(newFilters)); + } + else { + // after filter by its id + if (this.filters.some(function(filter, index) { + if (filter.id == position) { + position = index + 1; + return true; + } + }) + ) { + Array.prototype.splice.apply(filters, [position, 0].concat(newFilters)); + } + // defaults to end of list + else { + Array.prototype.push.apply(filters, newFilters); + } + } + + this.setFilters(filters); + }, + + /** + * Removes a filter from the builder + * @param {string|string[]} filterIds + * @param {boolean} [deleteOrphans=false] delete rules using old filters + * @fires module:plugins.ChangeFilters.changer:setFilters + * @fires module:plugins.ChangeFilters.afterSetFilters + * @throws ChangeFilterError + */ + removeFilter: function(filterIds, deleteOrphans) { + var filters = $.extend(true, [], this.filters); + if (typeof filterIds === 'string') { + filterIds = [filterIds]; + } + + filters = filters.filter(function(filter) { + return filterIds.indexOf(filter.id) === -1; + }); + + this.setFilters(deleteOrphans, filters); + } +}); + + +/** + * @class ChosenSelectpicker + * @memberof module:plugins + * @descriptioon Applies chosen-js Select on filters and operators combo-boxes. + * @param {object} [options] Supports all the options for chosen + * @throws MissingLibraryError + */ +QueryBuilder.define('chosen-selectpicker', function(options) { + + if (!$.fn.chosen) { + Utils.error('MissingLibrary', 'chosen is required to use "chosen-selectpicker" plugin. Get it here: https://github.com/harvesthq/chosen'); + } + + if (this.settings.plugins['bt-selectpicker']) { + Utils.error('Conflict', 'bt-selectpicker is already selected as the dropdown plugin. Please remove chosen-selectpicker from the plugin list'); + } + + var Selectors = QueryBuilder.selectors; + + // init selectpicker + this.on('afterCreateRuleFilters', function(e, rule) { + rule.$el.find(Selectors.rule_filter).removeClass('form-control').chosen(options); + }); + + this.on('afterCreateRuleOperators', function(e, rule) { + if (e.builder.getOperators(rule.filter).length > 1) { + rule.$el.find(Selectors.rule_operator).removeClass('form-control').chosen(options); + } + }); + + // update selectpicker on change + this.on('afterUpdateRuleFilter', function(e, rule) { + rule.$el.find(Selectors.rule_filter).trigger('chosen:updated'); + }); + + this.on('afterUpdateRuleOperator', function(e, rule) { + rule.$el.find(Selectors.rule_operator).trigger('chosen:updated'); + }); + + this.on('beforeDeleteRule', function(e, rule) { + rule.$el.find(Selectors.rule_filter).chosen('destroy'); + rule.$el.find(Selectors.rule_operator).chosen('destroy'); + }); +}); + + +/** + * @class FilterDescription + * @memberof module:plugins + * @description Provides three ways to display a description about a filter: inline, Bootsrap Popover or Bootbox. + * @param {object} [options] + * @param {string} [options.icon='bi-info-circle-fill'] + * @param {string} [options.mode='popover'] - inline, popover or bootbox + * @throws ConfigError + */ +QueryBuilder.define('filter-description', function(options) { + // INLINE + if (options.mode === 'inline') { + this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) { + var $p = rule.$el.find('p.filter-description'); + var description = e.builder.getFilterDescription(rule.filter, rule); + + if (!description) { + $p.hide(); + } + else { + if ($p.length === 0) { + $p = $($.parseHTML('

')); + $p.appendTo(rule.$el); + } + else { + $p.css('display', ''); + } + + $p.html(' ' + description); + } + }); + } + // POPOVER + else if (options.mode === 'popover') { + if (!$.fn.popover || !$.fn.popover.Constructor || !$.fn.popover.Constructor.prototype.fixTitle) { + Utils.error('MissingLibrary', 'Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com'); + } + + this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) { + var $b = rule.$el.find('button.filter-description'); + var description = e.builder.getFilterDescription(rule.filter, rule); + + if (!description) { + $b.hide(); + + if ($b.data('bs-popover')) { + $b.popover('hide'); + } + } + else { + if ($b.length === 0) { + $b = $($.parseHTML('')); + $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions)); + const popover = new bootstrap.Popover($b.get(0), { + placement: 'left', + container: 'body', + html: true + }) + $b.on('mouseout', function() { + popover('hide'); + }); + } + else { + $b.css('display', ''); + } + + $b.data('bs-popover').options.content = description; + + if ($b.attr('aria-describedby')) { + $b.popover('show'); + } + } + }); + } + // BOOTBOX + else if (options.mode === 'bootbox') { + if (!('bootbox' in window)) { + Utils.error('MissingLibrary', 'Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com'); + } + + this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) { + var $b = rule.$el.find('button.filter-description'); + var description = e.builder.getFilterDescription(rule.filter, rule); + + if (!description) { + $b.hide(); + } + else { + if ($b.length === 0) { + $b = $($.parseHTML('')); + $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions)); + + $b.on('click', function() { + bootbox.alert($b.data('description')); + }); + } + else { + $b.css('display', ''); + } + + $b.data('description', description); + } + }); + } +}, { + icon: 'bi-info-circle-fill', + mode: 'popover' +}); + +QueryBuilder.extend(/** @lends module:plugins.FilterDescription.prototype */ { + /** + * Returns the description of a filter for a particular rule (if present) + * @param {object} filter + * @param {Rule} [rule] + * @returns {string} + * @private + */ + getFilterDescription: function(filter, rule) { + if (!filter) { + return undefined; + } + else if (typeof filter.description == 'function') { + return filter.description.call(this, rule); + } + else { + return filter.description; + } + } +}); + + +/** + * @class Invert + * @memberof module:plugins + * @description Allows to invert a rule operator, a group condition or the entire builder. + * @param {object} [options] + * @param {string} [options.icon='bi-shuffle'] + * @param {boolean} [options.recursive=true] + * @param {boolean} [options.invert_rules=true] + * @param {boolean} [options.display_rules_button=false] + * @param {boolean} [options.silent_fail=false] + */ +QueryBuilder.define('invert', function(options) { + var self = this; + var Selectors = QueryBuilder.selectors; + + // Bind events + this.on('afterInit', function() { + self.$el.on('click.queryBuilder', '[data-invert=group]', function() { + var $group = $(this).closest(Selectors.group_container); + self.invert(self.getModel($group), options); + }); + + if (options.display_rules_button && options.invert_rules) { + self.$el.on('click.queryBuilder', '[data-invert=rule]', function() { + var $rule = $(this).closest(Selectors.rule_container); + self.invert(self.getModel($rule), options); + }); + } + }); + + // Modify templates + if (!options.disable_template) { + this.on('getGroupTemplate.filter', function(h) { + var $h = $($.parseHTML(h.value)); + $h.find(Selectors.condition_container).after( + '' + ); + h.value = $h.prop('outerHTML'); + }); + + if (options.display_rules_button && options.invert_rules) { + this.on('getRuleTemplate.filter', function(h) { + var $h = $($.parseHTML(h.value)); + $h.find(Selectors.rule_actions).prepend( + '' + ); + h.value = $h.prop('outerHTML'); + }); + } + } +}, { + icon: 'bi-shuffle', + recursive: true, + invert_rules: true, + display_rules_button: false, + silent_fail: false, + disable_template: false +}); + +QueryBuilder.defaults({ + operatorOpposites: { + 'equal': 'not_equal', + 'not_equal': 'equal', + 'in': 'not_in', + 'not_in': 'in', + 'less': 'greater_or_equal', + 'less_or_equal': 'greater', + 'greater': 'less_or_equal', + 'greater_or_equal': 'less', + 'between': 'not_between', + 'not_between': 'between', + 'begins_with': 'not_begins_with', + 'not_begins_with': 'begins_with', + 'contains': 'not_contains', + 'not_contains': 'contains', + 'ends_with': 'not_ends_with', + 'not_ends_with': 'ends_with', + 'is_empty': 'is_not_empty', + 'is_not_empty': 'is_empty', + 'is_null': 'is_not_null', + 'is_not_null': 'is_null' + }, + + conditionOpposites: { + 'AND': 'OR', + 'OR': 'AND' + } +}); + +QueryBuilder.extend(/** @lends module:plugins.Invert.prototype */ { + /** + * Invert a Group, a Rule or the whole builder + * @param {Node} [node] + * @param {object} [options] {@link module:plugins.Invert} + * @fires module:plugins.Invert.afterInvert + * @throws InvertConditionError, InvertOperatorError + */ + invert: function(node, options) { + if (!(node instanceof Node)) { + if (!this.model.root) return; + options = node; + node = this.model.root; + } + + if (typeof options != 'object') options = {}; + if (options.recursive === undefined) options.recursive = true; + if (options.invert_rules === undefined) options.invert_rules = true; + if (options.silent_fail === undefined) options.silent_fail = false; + if (options.trigger === undefined) options.trigger = true; + + if (node instanceof Group) { + // invert group condition + if (this.settings.conditionOpposites[node.condition]) { + node.condition = this.settings.conditionOpposites[node.condition]; + } + else if (!options.silent_fail) { + Utils.error('InvertCondition', 'Unknown inverse of condition "{0}"', node.condition); + } + + // recursive call + if (options.recursive) { + var tempOpts = $.extend({}, options, { trigger: false }); + node.each(function(rule) { + if (options.invert_rules) { + this.invert(rule, tempOpts); + } + }, function(group) { + this.invert(group, tempOpts); + }, this); + } + } + else if (node instanceof Rule) { + if (node.operator && !node.filter.no_invert) { + // invert rule operator + if (this.settings.operatorOpposites[node.operator.type]) { + var invert = this.settings.operatorOpposites[node.operator.type]; + // check if the invert is "authorized" + if (!node.filter.operators || node.filter.operators.indexOf(invert) != -1) { + node.operator = this.getOperatorByType(invert); + } + } + else if (!options.silent_fail) { + Utils.error('InvertOperator', 'Unknown inverse of operator "{0}"', node.operator.type); + } + } + } + + if (options.trigger) { + /** + * After {@link module:plugins.Invert.invert} method + * @event afterInvert + * @memberof module:plugins.Invert + * @param {Node} node - the main group or rule that has been modified + * @param {object} options + */ + this.trigger('afterInvert', node, options); + + this.trigger('rulesChanged'); + } + } +}); + + +/** + * @class MongoDbSupport + * @memberof module:plugins + * @description Allows to export rules as a MongoDB find object as well as populating the builder from a MongoDB object. + */ + +QueryBuilder.defaults({ + mongoOperators: { + // @formatter:off + equal: function(v) { return v[0]; }, + not_equal: function(v) { return { '$ne': v[0] }; }, + in: function(v) { return { '$in': v }; }, + not_in: function(v) { return { '$nin': v }; }, + less: function(v) { return { '$lt': v[0] }; }, + less_or_equal: function(v) { return { '$lte': v[0] }; }, + greater: function(v) { return { '$gt': v[0] }; }, + greater_or_equal: function(v) { return { '$gte': v[0] }; }, + between: function(v) { return { '$gte': v[0], '$lte': v[1] }; }, + not_between: function(v) { return { '$lt': v[0], '$gt': v[1] }; }, + begins_with: function(v) { return { '$regex': '^' + Utils.escapeRegExp(v[0]) }; }, + not_begins_with: function(v) { return { '$regex': '^(?!' + Utils.escapeRegExp(v[0]) + ')' }; }, + contains: function(v) { return { '$regex': Utils.escapeRegExp(v[0]) }; }, + not_contains: function(v) { return { '$regex': '^((?!' + Utils.escapeRegExp(v[0]) + ').)*$', '$options': 's' }; }, + ends_with: function(v) { return { '$regex': Utils.escapeRegExp(v[0]) + '$' }; }, + not_ends_with: function(v) { return { '$regex': '(? 0) { + parts.push(parse(rule)); + } + else { + var mdb = self.settings.mongoOperators[rule.operator]; + var ope = self.getOperatorByType(rule.operator); + + if (mdb === undefined) { + Utils.error('UndefinedMongoOperator', 'Unknown MongoDB operation for operator "{0}"', rule.operator); + } + + if (ope.nb_inputs !== 0) { + if (!(rule.value instanceof Array)) { + rule.value = [rule.value]; + } + } + + /** + * Modifies the MongoDB field used by a rule + * @event changer:getMongoDBField + * @memberof module:plugins.MongoDbSupport + * @param {string} field + * @param {Rule} rule + * @returns {string} + */ + var field = self.change('getMongoDBField', rule.field, rule); + + var ruleExpression = {}; + ruleExpression[field] = mdb.call(self, rule.value); + + /** + * Modifies the MongoDB expression generated for a rul + * @event changer:ruleToMongo + * @memberof module:plugins.MongoDbSupport + * @param {object} expression + * @param {Rule} rule + * @param {*} value + * @param {function} valueWrapper - function that takes the value and adds the operator + * @returns {object} + */ + parts.push(self.change('ruleToMongo', ruleExpression, rule, rule.value, mdb)); + } + }); + + var groupExpression = {}; + groupExpression['$' + group.condition.toLowerCase()] = parts; + + /** + * Modifies the MongoDB expression generated for a group + * @event changer:groupToMongo + * @memberof module:plugins.MongoDbSupport + * @param {object} expression + * @param {Group} group + * @returns {object} + */ + return self.change('groupToMongo', groupExpression, group); + }(data)); + }, + + /** + * Converts a MongoDB query to rules + * @param {object} query + * @returns {object} + * @fires module:plugins.MongoDbSupport.changer:parseMongoNode + * @fires module:plugins.MongoDbSupport.changer:getMongoDBFieldID + * @fires module:plugins.MongoDbSupport.changer:mongoToRule + * @fires module:plugins.MongoDbSupport.changer:mongoToGroup + * @throws MongoParseError, UndefinedMongoConditionError, UndefinedMongoOperatorError + */ + getRulesFromMongo: function(query) { + if (query === undefined || query === null) { + return null; + } + + var self = this; + + /** + * Custom parsing of a MongoDB expression, you can return a sub-part of the expression, or a well formed group or rule JSON + * @event changer:parseMongoNode + * @memberof module:plugins.MongoDbSupport + * @param {object} expression + * @returns {object} expression, rule or group + */ + query = self.change('parseMongoNode', query); + + // a plugin returned a group + if ('rules' in query && 'condition' in query) { + return query; + } + + // a plugin returned a rule + if ('id' in query && 'operator' in query && 'value' in query) { + return { + condition: this.settings.default_condition, + rules: [query] + }; + } + + var key = self.getMongoCondition(query); + if (!key) { + Utils.error('MongoParse', 'Invalid MongoDB query format'); + } + + return (function parse(data, topKey) { + var rules = data[topKey]; + var parts = []; + + rules.forEach(function(data) { + // allow plugins to manually parse or handle special cases + data = self.change('parseMongoNode', data); + + // a plugin returned a group + if ('rules' in data && 'condition' in data) { + parts.push(data); + return; + } + + // a plugin returned a rule + if ('id' in data && 'operator' in data && 'value' in data) { + parts.push(data); + return; + } + + var key = self.getMongoCondition(data); + if (key) { + parts.push(parse(data, key)); + } + else { + var field = Object.keys(data)[0]; + var value = data[field]; + + var operator = self.getMongoOperator(value); + if (operator === undefined) { + Utils.error('MongoParse', 'Invalid MongoDB query format'); + } + + var mdbrl = self.settings.mongoRuleOperators[operator]; + if (mdbrl === undefined) { + Utils.error('UndefinedMongoOperator', 'JSON Rule operation unknown for operator "{0}"', operator); + } + + var opVal = mdbrl.call(self, value); + + var id = self.getMongoDBFieldID(field, value); + + /** + * Modifies the rule generated from the MongoDB expression + * @event changer:mongoToRule + * @memberof module:plugins.MongoDbSupport + * @param {object} rule + * @param {object} expression + * @returns {object} + */ + var rule = self.change('mongoToRule', { + id: id, + field: field, + operator: opVal.op, + value: opVal.val + }, data); + + parts.push(rule); + } + }); + + /** + * Modifies the group generated from the MongoDB expression + * @event changer:mongoToGroup + * @memberof module:plugins.MongoDbSupport + * @param {object} group + * @param {object} expression + * @returns {object} + */ + return self.change('mongoToGroup', { + condition: topKey.replace('$', '').toUpperCase(), + rules: parts + }, data); + }(query, key)); + }, + + /** + * Sets rules a from MongoDB query + * @see module:plugins.MongoDbSupport.getRulesFromMongo + */ + setRulesFromMongo: function(query) { + this.setRules(this.getRulesFromMongo(query)); + }, + + /** + * Returns a filter identifier from the MongoDB field. + * Automatically use the only one filter with a matching field, fires a changer otherwise. + * @param {string} field + * @param {*} value + * @fires module:plugins.MongoDbSupport:changer:getMongoDBFieldID + * @returns {string} + * @private + */ + getMongoDBFieldID: function(field, value) { + var matchingFilters = this.filters.filter(function(filter) { + return filter.field === field; + }); + + var id; + if (matchingFilters.length === 1) { + id = matchingFilters[0].id; + } + else { + /** + * Returns a filter identifier from the MongoDB field + * @event changer:getMongoDBFieldID + * @memberof module:plugins.MongoDbSupport + * @param {string} field + * @param {*} value + * @returns {string} + */ + id = this.change('getMongoDBFieldID', field, value); + } + + return id; + }, + + /** + * Finds which operator is used in a MongoDB sub-object + * @param {*} data + * @returns {string|undefined} + * @private + */ + getMongoOperator: function(data) { + if (data !== null && typeof data === 'object') { + if (data.$gte !== undefined && data.$lte !== undefined) { + return 'between'; + } + if (data.$lt !== undefined && data.$gt !== undefined) { + return 'not_between'; + } + + var knownKeys = Object.keys(data).filter(function(key) { + return !!this.settings.mongoRuleOperators[key]; + }.bind(this)); + + if (knownKeys.length === 1) { + return knownKeys[0]; + } + } + else { + return '$eq'; + } + }, + + + /** + * Returns the key corresponding to "$or" or "$and" + * @param {object} data + * @returns {string|undefined} + * @private + */ + getMongoCondition: function(data) { + var keys = Object.keys(data); + + for (var i = 0, l = keys.length; i < l; i++) { + if (keys[i].toLowerCase() === '$or' || keys[i].toLowerCase() === '$and') { + return keys[i]; + } + } + } +}); + + +/** + * @class NotGroup + * @memberof module:plugins + * @description Adds a "Not" checkbox in front of group conditions. + * @param {object} [options] + * @param {string} [options.icon_checked='bi-check2-square'] + * @param {string} [options.icon_unchecked='bi-square'] + */ +QueryBuilder.define('not-group', function(options) { + var self = this; + + // Bind events + this.on('afterInit', function() { + self.$el.on('click.queryBuilder', '[data-not=group]', function() { + var $group = $(this).closest(QueryBuilder.selectors.group_container); + var group = self.getModel($group); + group.not = !group.not; + }); + + self.model.on('update', function(e, node, field) { + if (node instanceof Group && field === 'not') { + self.updateGroupNot(node); + } + }); + }); + + // Init "not" property + this.on('afterAddGroup', function(e, group) { + group.__.not = false; + }); + + // Modify templates + if (!options.disable_template) { + this.on('getGroupTemplate.filter', function(h) { + var $h = $($.parseHTML(h.value)); + $h.find(QueryBuilder.selectors.condition_container).prepend( + '' + ); + h.value = $h.prop('outerHTML'); + }); + } + + // Export "not" to JSON + this.on('groupToJson.filter', function(e, group) { + e.value.not = group.not; + }); + + // Read "not" from JSON + this.on('jsonToGroup.filter', function(e, json) { + e.value.not = !!json.not; + }); + + // Export "not" to SQL + this.on('groupToSQL.filter', function(e, group) { + if (group.not) { + e.value = 'NOT ( ' + e.value + ' )'; + } + }); + + // Parse "NOT" function from sqlparser + this.on('parseSQLNode.filter', function(e) { + if (e.value.name && e.value.name.toUpperCase() == 'NOT') { + e.value = e.value.arguments.value[0]; + + // if the there is no sub-group, create one + if (['AND', 'OR'].indexOf(e.value.operation.toUpperCase()) === -1) { + e.value = new SQLParser.nodes.Op( + self.settings.default_condition, + e.value, + null + ); + } + + e.value.not = true; + } + }); + + // Request to create sub-group if the "not" flag is set + this.on('sqlGroupsDistinct.filter', function(e, group, data, i) { + if (data.not && i > 0) { + e.value = true; + } + }); + + // Read "not" from parsed SQL + this.on('sqlToGroup.filter', function(e, data) { + e.value.not = !!data.not; + }); + + // Export "not" to Mongo + this.on('groupToMongo.filter', function(e, group) { + var key = '$' + group.condition.toLowerCase(); + if (group.not && e.value[key]) { + e.value = { '$nor': [e.value] }; + } + }); + + // Parse "$nor" operator from Mongo + this.on('parseMongoNode.filter', function(e) { + var keys = Object.keys(e.value); + + if (keys[0] == '$nor') { + e.value = e.value[keys[0]][0]; + e.value.not = true; + } + }); + + // Read "not" from parsed Mongo + this.on('mongoToGroup.filter', function(e, data) { + e.value.not = !!data.not; + }); +}, { + icon_unchecked: 'bi-square', + icon_checked: 'bi-check2-square', + disable_template: false +}); + +/** + * From {@link module:plugins.NotGroup} + * @name not + * @member {boolean} + * @memberof Group + * @instance + */ +Utils.defineModelProperties(Group, ['not']); + +QueryBuilder.selectors.group_not = QueryBuilder.selectors.group_header + ' [data-not=group]'; + +QueryBuilder.extend(/** @lends module:plugins.NotGroup.prototype */ { + /** + * Performs actions when a group's not changes + * @param {Group} group + * @fires module:plugins.NotGroup.afterUpdateGroupNot + * @private + */ + updateGroupNot: function(group) { + var options = this.plugins['not-group']; + group.$el.find('>' + QueryBuilder.selectors.group_not) + .toggleClass('active', group.not) + .find('i').attr('class', group.not ? options.icon_checked : options.icon_unchecked); + + /** + * After the group's not flag has been modified + * @event afterUpdateGroupNot + * @memberof module:plugins.NotGroup + * @param {Group} group + */ + this.trigger('afterUpdateGroupNot', group); + + this.trigger('rulesChanged'); + } +}); + + +/** + * @class Sortable + * @memberof module:plugins + * @description Enables drag & drop sort of rules. + * @param {object} [options] + * @param {boolean} [options.inherit_no_drop=true] + * @param {boolean} [options.inherit_no_sortable=true] + * @param {string} [options.icon='bi-sort-down'] + * @throws MissingLibraryError, ConfigError + */ +QueryBuilder.define('sortable', function(options) { + if (!('interact' in window)) { + Utils.error('MissingLibrary', 'interact.js is required to use "sortable" plugin. Get it here: http://interactjs.io'); + } + + if (options.default_no_sortable !== undefined) { + Utils.error(false, 'Config', 'Sortable plugin : "default_no_sortable" options is deprecated, use standard "default_rule_flags" and "default_group_flags" instead'); + this.settings.default_rule_flags.no_sortable = this.settings.default_group_flags.no_sortable = options.default_no_sortable; + } + + // recompute drop-zones during drag (when a rule is hidden) + interact.dynamicDrop(true); + + // set move threshold to 10px + interact.pointerMoveTolerance(10); + + var placeholder; + var ghost; + var src; + var moved; + + // Init drag and drop + this.on('afterAddRule afterAddGroup', function(e, node) { + if (node == placeholder) { + return; + } + + var self = e.builder; + + // Inherit flags + if (options.inherit_no_sortable && node.parent && node.parent.flags.no_sortable) { + node.flags.no_sortable = true; + } + if (options.inherit_no_drop && node.parent && node.parent.flags.no_drop) { + node.flags.no_drop = true; + } + + // Configure drag + if (!node.flags.no_sortable) { + interact(node.$el[0]) + .draggable({ + allowFrom: QueryBuilder.selectors.drag_handle, + onstart: function(event) { + moved = false; + + // get model of dragged element + src = self.getModel(event.target); + + // create ghost + ghost = src.$el.clone() + .appendTo(src.$el.parent()) + .width(src.$el.outerWidth()) + .addClass('dragging'); + + // create drop placeholder + var ph = $($.parseHTML('
 
')) + .height(src.$el.outerHeight()); + + placeholder = src.parent.addRule(ph, src.getPos()); + + // hide dragged element + src.$el.hide(); + }, + onmove: function(event) { + // make the ghost follow the cursor + ghost[0].style.top = event.clientY - 15 + 'px'; + ghost[0].style.left = event.clientX - 15 + 'px'; + }, + onend: function(event) { + // starting from Interact 1.3.3, onend is called before ondrop + if (event.dropzone) { + moveSortableToTarget(src, $(event.relatedTarget), self); + moved = true; + } + + // remove ghost + ghost.remove(); + ghost = undefined; + + // remove placeholder + placeholder.drop(); + placeholder = undefined; + + // show element + src.$el.css('display', ''); + + /** + * After a node has been moved with {@link module:plugins.Sortable} + * @event afterMove + * @memberof module:plugins.Sortable + * @param {Node} node + */ + self.trigger('afterMove', src); + + self.trigger('rulesChanged'); + } + }); + } + + if (!node.flags.no_drop) { + // Configure drop on groups and rules + interact(node.$el[0]) + .dropzone({ + accept: QueryBuilder.selectors.rule_and_group_containers, + ondragenter: function(event) { + moveSortableToTarget(placeholder, $(event.target), self); + }, + ondrop: function(event) { + if (!moved) { + moveSortableToTarget(src, $(event.target), self); + } + } + }); + + // Configure drop on group headers + if (node instanceof Group) { + interact(node.$el.find(QueryBuilder.selectors.group_header)[0]) + .dropzone({ + accept: QueryBuilder.selectors.rule_and_group_containers, + ondragenter: function(event) { + moveSortableToTarget(placeholder, $(event.target), self); + }, + ondrop: function(event) { + if (!moved) { + moveSortableToTarget(src, $(event.target), self); + } + } + }); + } + } + }); + + // Detach interactables + this.on('beforeDeleteRule beforeDeleteGroup', function(e, node) { + if (!e.isDefaultPrevented()) { + interact(node.$el[0]).unset(); + + if (node instanceof Group) { + interact(node.$el.find(QueryBuilder.selectors.group_header)[0]).unset(); + } + } + }); + + // Remove drag handle from non-sortable items + this.on('afterApplyRuleFlags afterApplyGroupFlags', function(e, node) { + if (node.flags.no_sortable) { + node.$el.find('.drag-handle').remove(); + } + }); + + // Modify templates + if (!options.disable_template) { + this.on('getGroupTemplate.filter', function(h, level) { + if (level > 1) { + var $h = $($.parseHTML(h.value)); + $h.find(QueryBuilder.selectors.condition_container).after('
'); + h.value = $h.prop('outerHTML'); + } + }); + + this.on('getRuleTemplate.filter', function(h) { + var $h = $($.parseHTML(h.value)); + $h.find(QueryBuilder.selectors.rule_header).after('
'); + h.value = $h.prop('outerHTML'); + }); + } +}, { + inherit_no_sortable: true, + inherit_no_drop: true, + icon: 'bi-sort-down', + disable_template: false +}); + +QueryBuilder.selectors.rule_and_group_containers = QueryBuilder.selectors.rule_container + ', ' + QueryBuilder.selectors.group_container; +QueryBuilder.selectors.drag_handle = '.drag-handle'; + +QueryBuilder.defaults({ + default_rule_flags: { + no_sortable: false, + no_drop: false + }, + default_group_flags: { + no_sortable: false, + no_drop: false + } +}); + +/** + * Moves an element (placeholder or actual object) depending on active target + * @memberof module:plugins.Sortable + * @param {Node} node + * @param {jQuery} target + * @param {QueryBuilder} [builder] + * @private + */ +function moveSortableToTarget(node, target, builder) { + var parent, method; + var Selectors = QueryBuilder.selectors; + + // on rule + parent = target.closest(Selectors.rule_container); + if (parent.length) { + method = 'moveAfter'; + } + + // on group header + if (!method) { + parent = target.closest(Selectors.group_header); + if (parent.length) { + parent = target.closest(Selectors.group_container); + method = 'moveAtBegin'; + } + } + + // on group + if (!method) { + parent = target.closest(Selectors.group_container); + if (parent.length) { + method = 'moveAtEnd'; + } + } + + if (method) { + node[method](builder.getModel(parent)); + + // refresh radio value + if (builder && node instanceof Rule) { + builder.setRuleInputValue(node, node.value); + } + } +} + + +/** + * @class SqlSupport + * @memberof module:plugins + * @description Allows to export rules as a SQL WHERE statement as well as populating the builder from an SQL query. + * @param {object} [options] + * @param {boolean} [options.boolean_as_integer=true] - `true` to convert boolean values to integer in the SQL output + */ +QueryBuilder.define('sql-support', function(options) { + +}, { + boolean_as_integer: true +}); + +QueryBuilder.defaults({ + // operators for internal -> SQL conversion + sqlOperators: { + equal: { op: '= ?' }, + not_equal: { op: '!= ?' }, + in: { op: 'IN(?)', sep: ', ' }, + not_in: { op: 'NOT IN(?)', sep: ', ' }, + less: { op: '< ?' }, + less_or_equal: { op: '<= ?' }, + greater: { op: '> ?' }, + greater_or_equal: { op: '>= ?' }, + between: { op: 'BETWEEN ?', sep: ' AND ' }, + not_between: { op: 'NOT BETWEEN ?', sep: ' AND ' }, + begins_with: { op: 'LIKE ?', mod: '{0}%', escape: '%_' }, + not_begins_with: { op: 'NOT LIKE ?', mod: '{0}%', escape: '%_' }, + contains: { op: 'LIKE ?', mod: '%{0}%', escape: '%_' }, + not_contains: { op: 'NOT LIKE ?', mod: '%{0}%', escape: '%_' }, + ends_with: { op: 'LIKE ?', mod: '%{0}', escape: '%_' }, + not_ends_with: { op: 'NOT LIKE ?', mod: '%{0}', escape: '%_' }, + is_empty: { op: '= \'\'' }, + is_not_empty: { op: '!= \'\'' }, + is_null: { op: 'IS NULL' }, + is_not_null: { op: 'IS NOT NULL' } + }, + + // operators for SQL -> internal conversion + sqlRuleOperator: { + '=': function(v) { + return { + val: v, + op: v === '' ? 'is_empty' : 'equal' + }; + }, + '!=': function(v) { + return { + val: v, + op: v === '' ? 'is_not_empty' : 'not_equal' + }; + }, + 'LIKE': function(v) { + if (v.slice(0, 1) == '%' && v.slice(-1) == '%') { + return { + val: v.slice(1, -1), + op: 'contains' + }; + } + else if (v.slice(0, 1) == '%') { + return { + val: v.slice(1), + op: 'ends_with' + }; + } + else if (v.slice(-1) == '%') { + return { + val: v.slice(0, -1), + op: 'begins_with' + }; + } + else { + Utils.error('SQLParse', 'Invalid value for LIKE operator "{0}"', v); + } + }, + 'NOT LIKE': function(v) { + if (v.slice(0, 1) == '%' && v.slice(-1) == '%') { + return { + val: v.slice(1, -1), + op: 'not_contains' + }; + } + else if (v.slice(0, 1) == '%') { + return { + val: v.slice(1), + op: 'not_ends_with' + }; + } + else if (v.slice(-1) == '%') { + return { + val: v.slice(0, -1), + op: 'not_begins_with' + }; + } + else { + Utils.error('SQLParse', 'Invalid value for NOT LIKE operator "{0}"', v); + } + }, + 'IN': function(v) { + return { val: v, op: 'in' }; + }, + 'NOT IN': function(v) { + return { val: v, op: 'not_in' }; + }, + '<': function(v) { + return { val: v, op: 'less' }; + }, + '<=': function(v) { + return { val: v, op: 'less_or_equal' }; + }, + '>': function(v) { + return { val: v, op: 'greater' }; + }, + '>=': function(v) { + return { val: v, op: 'greater_or_equal' }; + }, + 'BETWEEN': function(v) { + return { val: v, op: 'between' }; + }, + 'NOT BETWEEN': function(v) { + return { val: v, op: 'not_between' }; + }, + 'IS': function(v) { + if (v !== null) { + Utils.error('SQLParse', 'Invalid value for IS operator'); + } + return { val: null, op: 'is_null' }; + }, + 'IS NOT': function(v) { + if (v !== null) { + Utils.error('SQLParse', 'Invalid value for IS operator'); + } + return { val: null, op: 'is_not_null' }; + } + }, + + // statements for internal -> SQL conversion + sqlStatements: { + 'question_mark': function() { + var params = []; + return { + add: function(rule, value) { + params.push(value); + return '?'; + }, + run: function() { + return params; + } + }; + }, + + 'numbered': function(char) { + if (!char || char.length > 1) char = '$'; + var index = 0; + var params = []; + return { + add: function(rule, value) { + params.push(value); + index++; + return char + index; + }, + run: function() { + return params; + } + }; + }, + + 'named': function(char) { + if (!char || char.length > 1) char = ':'; + var indexes = {}; + var params = {}; + return { + add: function(rule, value) { + if (!indexes[rule.field]) indexes[rule.field] = 1; + var key = rule.field + '_' + (indexes[rule.field]++); + params[key] = value; + return char + key; + }, + run: function() { + return params; + } + }; + } + }, + + // statements for SQL -> internal conversion + sqlRuleStatement: { + 'question_mark': function(values) { + var index = 0; + return { + parse: function(v) { + return v == '?' ? values[index++] : v; + }, + esc: function(sql) { + return sql.replace(/\?/g, '\'?\''); + } + }; + }, + + 'numbered': function(values, char) { + if (!char || char.length > 1) char = '$'; + var regex1 = new RegExp('^\\' + char + '[0-9]+$'); + var regex2 = new RegExp('\\' + char + '([0-9]+)', 'g'); + return { + parse: function(v) { + return regex1.test(v) ? values[v.slice(1) - 1] : v; + }, + esc: function(sql) { + return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\''); + } + }; + }, + + 'named': function(values, char) { + if (!char || char.length > 1) char = ':'; + var regex1 = new RegExp('^\\' + char); + var regex2 = new RegExp('\\' + char + '(' + Object.keys(values).join('|') + ')\\b', 'g'); + return { + parse: function(v) { + return regex1.test(v) ? values[v.slice(1)] : v; + }, + esc: function(sql) { + return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\''); + } + }; + } + } +}); + +/** + * @typedef {object} SqlQuery + * @memberof module:plugins.SqlSupport + * @property {string} sql + * @property {object} params + */ + +QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ { + /** + * Returns rules as a SQL query + * @param {boolean|string} [stmt] - use prepared statements: false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)' + * @param {boolean} [nl=false] output with new lines + * @param {object} [data] - current rules by default + * @returns {module:plugins.SqlSupport.SqlQuery} + * @fires module:plugins.SqlSupport.changer:getSQLField + * @fires module:plugins.SqlSupport.changer:ruleToSQL + * @fires module:plugins.SqlSupport.changer:groupToSQL + * @throws UndefinedSQLConditionError, UndefinedSQLOperatorError + */ + getSQL: function(stmt, nl, data) { + data = (data === undefined) ? this.getRules() : data; + + if (!data) { + return null; + } + + nl = !!nl ? '\n' : ' '; + var boolean_as_integer = this.getPluginOptions('sql-support', 'boolean_as_integer'); + + if (stmt === true) { + stmt = 'question_mark'; + } + if (typeof stmt == 'string') { + var config = getStmtConfig(stmt); + stmt = this.settings.sqlStatements[config[1]](config[2]); + } + + var self = this; + + var sql = (function parse(group) { + if (!group.condition) { + group.condition = self.settings.default_condition; + } + if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) { + Utils.error('UndefinedSQLCondition', 'Unable to build SQL query with condition "{0}"', group.condition); + } + + if (!group.rules) { + return ''; + } + + var parts = []; + + group.rules.forEach(function(rule) { + if (rule.rules && rule.rules.length > 0) { + parts.push('(' + nl + parse(rule) + nl + ')' + nl); + } + else { + var sql = self.settings.sqlOperators[rule.operator]; + var ope = self.getOperatorByType(rule.operator); + var value = ''; + + if (sql === undefined) { + Utils.error('UndefinedSQLOperator', 'Unknown SQL operation for operator "{0}"', rule.operator); + } + + if (ope.nb_inputs !== 0) { + if (!(rule.value instanceof Array)) { + rule.value = [rule.value]; + } + + rule.value.forEach(function(v, i) { + if (i > 0) { + value += sql.sep; + } + + if (rule.type == 'boolean' && boolean_as_integer) { + v = v ? 1 : 0; + } + else if (!stmt && rule.type !== 'integer' && rule.type !== 'double' && rule.type !== 'boolean') { + v = Utils.escapeString(v, sql.escape); + } + + if (sql.mod) { + v = Utils.fmt(sql.mod, v); + } + + if (stmt) { + value += stmt.add(rule, v); + } + else { + if (typeof v == 'string') { + v = '\'' + v + '\''; + } + + value += v; + } + }); + } + + var sqlFn = function(v) { + return sql.op.replace('?', function() { + return v; + }); + }; + + /** + * Modifies the SQL field used by a rule + * @event changer:getSQLField + * @memberof module:plugins.SqlSupport + * @param {string} field + * @param {Rule} rule + * @returns {string} + */ + var field = self.change('getSQLField', rule.field, rule); + + var ruleExpression = field + ' ' + sqlFn(value); + + /** + * Modifies the SQL generated for a rule + * @event changer:ruleToSQL + * @memberof module:plugins.SqlSupport + * @param {string} expression + * @param {Rule} rule + * @param {*} value + * @param {function} valueWrapper - function that takes the value and adds the operator + * @returns {string} + */ + parts.push(self.change('ruleToSQL', ruleExpression, rule, value, sqlFn)); + } + }); + + var groupExpression = parts.join(' ' + group.condition + nl); + + /** + * Modifies the SQL generated for a group + * @event changer:groupToSQL + * @memberof module:plugins.SqlSupport + * @param {string} expression + * @param {Group} group + * @returns {string} + */ + return self.change('groupToSQL', groupExpression, group); + }(data)); + + if (stmt) { + return { + sql: sql, + params: stmt.run() + }; + } + else { + return { + sql: sql + }; + } + }, + + /** + * Convert a SQL query to rules + * @param {string|module:plugins.SqlSupport.SqlQuery} query + * @param {boolean|string} stmt + * @returns {object} + * @fires module:plugins.SqlSupport.changer:parseSQLNode + * @fires module:plugins.SqlSupport.changer:getSQLFieldID + * @fires module:plugins.SqlSupport.changer:sqlToRule + * @fires module:plugins.SqlSupport.changer:sqlToGroup + * @throws MissingLibraryError, SQLParseError, UndefinedSQLOperatorError + */ + getRulesFromSQL: function(query, stmt) { + if (!('SQLParser' in window)) { + Utils.error('MissingLibrary', 'SQLParser is required to parse SQL queries. Get it here https://github.com/mistic100/sql-parser'); + } + + var self = this; + + if (typeof query == 'string') { + query = { sql: query }; + } + + if (stmt === true) stmt = 'question_mark'; + if (typeof stmt == 'string') { + var config = getStmtConfig(stmt); + stmt = this.settings.sqlRuleStatement[config[1]](query.params, config[2]); + } + + if (stmt) { + query.sql = stmt.esc(query.sql); + } + + if (query.sql.toUpperCase().indexOf('SELECT') !== 0) { + query.sql = 'SELECT * FROM table WHERE ' + query.sql; + } + + var parsed = SQLParser.parse(query.sql); + + if (!parsed.where) { + Utils.error('SQLParse', 'No WHERE clause found'); + } + + /** + * Custom parsing of an AST node generated by SQLParser, you can return a sub-part of the tree, or a well formed group or rule JSON + * @event changer:parseSQLNode + * @memberof module:plugins.SqlSupport + * @param {object} AST node + * @returns {object} tree, rule or group + */ + var data = self.change('parseSQLNode', parsed.where.conditions); + + // a plugin returned a group + if ('rules' in data && 'condition' in data) { + return data; + } + + // a plugin returned a rule + if ('id' in data && 'operator' in data && 'value' in data) { + return { + condition: this.settings.default_condition, + rules: [data] + }; + } + + // create root group + var out = self.change('sqlToGroup', { + condition: this.settings.default_condition, + rules: [] + }, data); + + // keep track of current group + var curr = out; + + (function flatten(data, i) { + if (data === null) { + return; + } + + // allow plugins to manually parse or handle special cases + data = self.change('parseSQLNode', data); + + // a plugin returned a group + if ('rules' in data && 'condition' in data) { + curr.rules.push(data); + return; + } + + // a plugin returned a rule + if ('id' in data && 'operator' in data && 'value' in data) { + curr.rules.push(data); + return; + } + + // data must be a SQL parser node + if (!('left' in data) || !('right' in data) || !('operation' in data)) { + Utils.error('SQLParse', 'Unable to parse WHERE clause'); + } + + // it's a node + if (['AND', 'OR'].indexOf(data.operation.toUpperCase()) !== -1) { + // create a sub-group if the condition is not the same and it's not the first level + + /** + * Given an existing group and an AST node, determines if a sub-group must be created + * @event changer:sqlGroupsDistinct + * @memberof module:plugins.SqlSupport + * @param {boolean} create - true by default if the group condition is different + * @param {object} group + * @param {object} AST + * @param {int} current group level + * @returns {boolean} + */ + var createGroup = self.change('sqlGroupsDistinct', i > 0 && curr.condition != data.operation.toUpperCase(), curr, data, i); + + if (createGroup) { + /** + * Modifies the group generated from the SQL expression (this is called before the group is filled with rules) + * @event changer:sqlToGroup + * @memberof module:plugins.SqlSupport + * @param {object} group + * @param {object} AST + * @returns {object} + */ + var group = self.change('sqlToGroup', { + condition: self.settings.default_condition, + rules: [] + }, data); + + curr.rules.push(group); + curr = group; + } + + curr.condition = data.operation.toUpperCase(); + i++; + + // some magic ! + var next = curr; + flatten(data.left, i); + + curr = next; + flatten(data.right, i); + } + // it's a leaf + else { + if ($.isPlainObject(data.right.value)) { + Utils.error('SQLParse', 'Value format not supported for {0}.', data.left.value); + } + + // convert array + var value; + if ($.isArray(data.right.value)) { + value = data.right.value.map(function(v) { + return v.value; + }); + } + else { + value = data.right.value; + } + + // get actual values + if (stmt) { + if ($.isArray(value)) { + value = value.map(stmt.parse); + } + else { + value = stmt.parse(value); + } + } + + // convert operator + var operator = data.operation.toUpperCase(); + if (operator == '<>') { + operator = '!='; + } + + var sqlrl = self.settings.sqlRuleOperator[operator]; + if (sqlrl === undefined) { + Utils.error('UndefinedSQLOperator', 'Invalid SQL operation "{0}".', data.operation); + } + + var opVal = sqlrl.call(this, value, data.operation); + + // find field name + var field; + if ('values' in data.left) { + field = data.left.values.join('.'); + } + else if ('value' in data.left) { + field = data.left.value; + } + else { + Utils.error('SQLParse', 'Cannot find field name in {0}', JSON.stringify(data.left)); + } + + // unescape chars declared by the operator + var finalValue = opVal.val; + var sql = self.settings.sqlOperators[opVal.op]; + if (!stmt && sql && sql.escape) { + var searchChars = sql.escape.split('').map(function(c) { + return '\\\\' + c; + }).join('|'); + finalValue = finalValue + .replace(new RegExp('(' + searchChars + ')', 'g'), function(s) { + return s[1]; + }); + } + + var id = self.getSQLFieldID(field, value); + + /** + * Modifies the rule generated from the SQL expression + * @event changer:sqlToRule + * @memberof module:plugins.SqlSupport + * @param {object} rule + * @param {object} AST + * @returns {object} + */ + var rule = self.change('sqlToRule', { + id: id, + field: field, + operator: opVal.op, + value: finalValue + }, data); + + curr.rules.push(rule); + } + }(data, 0)); + + return out; + }, + + /** + * Sets the builder's rules from a SQL query + * @see module:plugins.SqlSupport.getRulesFromSQL + */ + setRulesFromSQL: function(query, stmt) { + this.setRules(this.getRulesFromSQL(query, stmt)); + }, + + /** + * Returns a filter identifier from the SQL field. + * Automatically use the only one filter with a matching field, fires a changer otherwise. + * @param {string} field + * @param {*} value + * @fires module:plugins.SqlSupport:changer:getSQLFieldID + * @returns {string} + * @private + */ + getSQLFieldID: function(field, value) { + var matchingFilters = this.filters.filter(function(filter) { + return filter.field.toLowerCase() === field.toLowerCase(); + }); + + var id; + if (matchingFilters.length === 1) { + id = matchingFilters[0].id; + } + else { + /** + * Returns a filter identifier from the SQL field + * @event changer:getSQLFieldID + * @memberof module:plugins.SqlSupport + * @param {string} field + * @param {*} value + * @returns {string} + */ + id = this.change('getSQLFieldID', field, value); + } + + return id; + } +}); + +/** + * Parses the statement configuration + * @memberof module:plugins.SqlSupport + * @param {string} stmt + * @returns {Array} null, mode, option + * @private + */ +function getStmtConfig(stmt) { + var config = stmt.match(/(question_mark|numbered|named)(?:\((.)\))?/); + if (!config) config = [null, 'question_mark', undefined]; + return config; +} + + +/** + * @class UniqueFilter + * @memberof module:plugins + * @description Allows to define some filters as "unique": ie which can be used for only one rule, globally or in the same group. + */ +QueryBuilder.define('unique-filter', function() { + this.status.used_filters = {}; + + this.on('afterUpdateRuleFilter', this.updateDisabledFilters); + this.on('afterDeleteRule', this.updateDisabledFilters); + this.on('afterCreateRuleFilters', this.applyDisabledFilters); + this.on('afterReset', this.clearDisabledFilters); + this.on('afterClear', this.clearDisabledFilters); + + // Ensure that the default filter is not already used if unique + this.on('getDefaultFilter.filter', function(e, model) { + var self = e.builder; + + self.updateDisabledFilters(); + + if (e.value.id in self.status.used_filters) { + var found = self.filters.some(function(filter) { + if (!(filter.id in self.status.used_filters) || self.status.used_filters[filter.id].length > 0 && self.status.used_filters[filter.id].indexOf(model.parent) === -1) { + e.value = filter; + return true; + } + }); + + if (!found) { + Utils.error(false, 'UniqueFilter', 'No more non-unique filters available'); + e.value = undefined; + } + } + }); +}); + +QueryBuilder.extend(/** @lends module:plugins.UniqueFilter.prototype */ { + /** + * Updates the list of used filters + * @param {$.Event} [e] + * @private + */ + updateDisabledFilters: function(e) { + var self = e ? e.builder : this; + + self.status.used_filters = {}; + + if (!self.model) { + return; + } + + // get used filters + (function walk(group) { + group.each(function(rule) { + if (rule.filter && rule.filter.unique) { + if (!self.status.used_filters[rule.filter.id]) { + self.status.used_filters[rule.filter.id] = []; + } + if (rule.filter.unique == 'group') { + self.status.used_filters[rule.filter.id].push(rule.parent); + } + } + }, function(group) { + walk(group); + }); + }(self.model.root)); + + self.applyDisabledFilters(e); + }, + + /** + * Clear the list of used filters + * @param {$.Event} [e] + * @private + */ + clearDisabledFilters: function(e) { + var self = e ? e.builder : this; + + self.status.used_filters = {}; + + self.applyDisabledFilters(e); + }, + + /** + * Disabled filters depending on the list of used ones + * @param {$.Event} [e] + * @private + */ + applyDisabledFilters: function(e) { + var self = e ? e.builder : this; + + // re-enable everything + self.$el.find(QueryBuilder.selectors.filter_container + ' option').prop('disabled', false); + + // disable some + $.each(self.status.used_filters, function(filterId, groups) { + if (groups.length === 0) { + self.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true); + } + else { + groups.forEach(function(group) { + group.each(function(rule) { + rule.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true); + }); + }); + } + }); + + // update Selectpicker + if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) { + self.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render'); + } + } +}); + + +/*! + * jQuery QueryBuilder 3.0.0 + * Locale: English (en) + * Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr + * Licensed under MIT (https://opensource.org/licenses/MIT) + */ + +QueryBuilder.regional['en'] = { + "__locale": "English (en)", + "__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr", + "add_rule": "Add rule", + "add_group": "Add group", + "delete_rule": "Delete", + "delete_group": "Delete", + "conditions": { + "AND": "AND", + "OR": "OR" + }, + "operators": { + "equal": "equal", + "not_equal": "not equal", + "in": "in", + "not_in": "not in", + "less": "less", + "less_or_equal": "less or equal", + "greater": "greater", + "greater_or_equal": "greater or equal", + "between": "between", + "not_between": "not between", + "begins_with": "begins with", + "not_begins_with": "doesn't begin with", + "contains": "contains", + "not_contains": "doesn't contain", + "ends_with": "ends with", + "not_ends_with": "doesn't end with", + "is_empty": "is empty", + "is_not_empty": "is not empty", + "is_null": "is null", + "is_not_null": "is not null" + }, + "errors": { + "no_filter": "No filter selected", + "empty_group": "The group is empty", + "radio_empty": "No value selected", + "checkbox_empty": "No value selected", + "select_empty": "No value selected", + "string_empty": "Empty value", + "string_exceed_min_length": "Must contain at least {0} characters", + "string_exceed_max_length": "Must not contain more than {0} characters", + "string_invalid_format": "Invalid format ({0})", + "number_nan": "Not a number", + "number_not_integer": "Not an integer", + "number_not_double": "Not a real number", + "number_exceed_min": "Must be greater than {0}", + "number_exceed_max": "Must be lower than {0}", + "number_wrong_step": "Must be a multiple of {0}", + "number_between_invalid": "Invalid values, {0} is greater than {1}", + "datetime_empty": "Empty value", + "datetime_invalid": "Invalid date format ({0})", + "datetime_exceed_min": "Must be after {0}", + "datetime_exceed_max": "Must be before {0}", + "datetime_between_invalid": "Invalid values, {0} is greater than {1}", + "boolean_not_valid": "Not a boolean", + "operator_not_multiple": "Operator \"{1}\" cannot accept multiple values" + }, + "invert": "Invert", + "NOT": "NOT" +}; + +QueryBuilder.defaults({ lang_code: 'en' }); +return QueryBuilder; + +})); diff --git a/dist/query-builder.css b/dist/query-builder.css deleted file mode 100644 index a194d84e..00000000 --- a/dist/query-builder.css +++ /dev/null @@ -1,131 +0,0 @@ -/*! - * jQuery QueryBuilder 1.4.1 - * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) - */ -.query-builder .rule-container, -.query-builder .rules-group-container, -.query-builder .rule-placeholder { - margin:4px 0; - border-radius:5px; - padding:5px; - border:1px solid #eee; - background:#fff; - background:rgba(255, 255, 255, 0.9); -} - -.query-builder .rules-group-container { - padding:10px 10px 5px 10px; - border:1px solid #DCC896; - background:#FCF9ED; - background:rgba(250, 240, 210, 0.5); -} - .query-builder .rules-group-header { - margin-bottom:10px; - } - .query-builder .rules-group-header input[name$=_cond] { - display:none; - } - .query-builder .rules-list { - list-style:none; - padding:0 0 0 20px; - margin:0; - } - -.query-builder .rule-container {} - .query-builder .rule-container>div:not(.rule-header) { - display:inline-block; - margin:0 5px 0 0; - vertical-align:top; - } - .query-builder .rule-value-container:not(:empty) { - border-left:1px solid #ddd; - padding-left:5px; - } - .query-builder .rule-value-container label { - margin-bottom:0; - } - .query-builder .rule-value-container label.block { - display:block; - } - .query-builder .rule-container select, - .query-builder .rule-container input[type=text], - .query-builder .rule-container input[type=number] { - padding:1px; - } - -.query-builder .has-error { - background:#fdd; - border-color:#f99; -} - -.query-builder .error-container { - display:none !important; - cursor:help; - color:red; -} - -.query-builder .has-error .error-container { - display:inline-block !important; -} - -.query-builder .rules-list>* { - position:relative; -} - .query-builder .rules-list>*:before, - .query-builder .rules-list>*:after { - content:''; - position:absolute; - left:-15px; - width:15px; - height:calc(50% + 4px); - border-color:#ccc; - border-style:solid; - } - - .query-builder .rules-list>*:before { - top:-2px; - 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 .tooltip-inner { - color:#fdd !important; -} -.query-builder p.filter-description { - margin:5px 0 0 0; - background:#D9EDF7; - border:1px solid #BCE8F1; - color:#31708F; - border-radius:4px; - padding:2px 5px; - font-size:0.8em; -} -.query-builder .drag-handle { - cursor:move; - display:inline-block; - vertical-align:middle; - margin-left:5px; -} - -.query-builder .dragged { - opacity:0.5; -} - -.query-builder .rule-placeholder { - border:1px dashed #bbb; - opacity:0.7; -} \ No newline at end of file diff --git a/dist/query-builder.js b/dist/query-builder.js deleted file mode 100644 index b3486bb6..00000000 --- a/dist/query-builder.js +++ /dev/null @@ -1,2289 +0,0 @@ -/*! - * jQuery QueryBuilder 1.4.1 - * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) - */ - -// Modules: bt-selectpicker, bt-tooltip-errors, filter-description, mongodb-support, sortable, sql-support -(function(root, factory) { - if (typeof define === 'function' && define.amd) { - define(['jquery', 'microevent', 'jQuery.extendext'], factory); - } - else { - factory(root.jQuery, root.MicroEvent); - } -}(this, function($, MicroEvent) { - "use strict"; - - var types = [ - 'string', - 'integer', - 'double', - 'date', - 'time', - 'datetime' - ], - internalTypes = [ - 'string', - 'number', - 'datetime' - ], - inputs = [ - 'text', - 'textarea', - 'radio', - 'checkbox', - 'select' - ]; - - - var QueryBuilder = function($el, options) { - this.$el = $el; - this.init(options); - }; - - MicroEvent.mixin(QueryBuilder); - - - QueryBuilder.DEFAULTS = { - filters: [], - - plugins: null, - - onValidationError: null, - onAfterAddGroup: null, - onAfterAddRule: null, - - display_errors: true, - allow_groups: -1, - allow_empty: false, - conditions: ['AND', 'OR'], - default_condition: 'AND', - - default_rule_flags: { - filter_readonly: false, - operator_readonly: false, - value_readonly: false, - no_delete: false - }, - - template: { - group: null, - rule: null - }, - - lang: { - "add_rule": 'Add rule', - "add_group": 'Add group', - "delete_rule": 'Delete', - "delete_group": 'Delete', - - "condition_and": 'AND', - "condition_or": 'OR', - - "filter_select_placeholder": '------', - - "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", - "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_invalid": "Invalid date format ({0})", - "datetime_exceed_min": "Must be after {0}", - "datetime_exceed_max": "Must be before {0}" - } - }, - - operators: [ - {type: 'equal', accept_values: 1, apply_to: ['string', 'number', 'datetime']}, - {type: 'not_equal', accept_values: 1, apply_to: ['string', 'number', 'datetime']}, - {type: 'in', accept_values: 1, apply_to: ['string', 'number', 'datetime']}, - {type: 'not_in', accept_values: 1, apply_to: ['string', 'number', 'datetime']}, - {type: 'less', accept_values: 1, apply_to: ['number', 'datetime']}, - {type: 'less_or_equal', accept_values: 1, apply_to: ['number', 'datetime']}, - {type: 'greater', accept_values: 1, apply_to: ['number', 'datetime']}, - {type: 'greater_or_equal', accept_values: 1, apply_to: ['number', 'datetime']}, - {type: 'between', accept_values: 2, apply_to: ['number', 'datetime']}, - {type: 'begins_with', accept_values: 1, apply_to: ['string']}, - {type: 'not_begins_with', accept_values: 1, apply_to: ['string']}, - {type: 'contains', accept_values: 1, apply_to: ['string']}, - {type: 'not_contains', accept_values: 1, apply_to: ['string']}, - {type: 'ends_with', accept_values: 1, apply_to: ['string']}, - {type: 'not_ends_with', accept_values: 1, apply_to: ['string']}, - {type: 'is_empty', accept_values: 0, apply_to: ['string']}, - {type: 'is_not_empty', accept_values: 0, apply_to: ['string']}, - {type: 'is_null', accept_values: 0, apply_to: ['string', 'number', 'datetime']}, - {type: 'is_not_null', accept_values: 0, apply_to: ['string', 'number', 'datetime']} - ], - - 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' - } - }; - - - QueryBuilder.plugins = {}; - - /** - * Define a new plugin - * @param {string} - * @param {function} - */ - QueryBuilder.define = function(name, fct) { - QueryBuilder.plugins[name] = fct; - }; - - /** - * Add new methods - * @param {object} - */ - QueryBuilder.extend = function(methods) { - $.extend(QueryBuilder.prototype, methods); - }; - - /** - * Init plugins for an instance - */ - QueryBuilder.prototype.initPlugins = function() { - if (!this.settings.plugins) { - return; - } - - var that = this, - queue = {}; - - if ($.isArray(this.settings.plugins)) { - $.each(this.settings.plugins, function(i, plugin) { - queue[plugin] = {}; - }); - } - else { - $.each(this.settings.plugins, function(plugin, options) { - queue[plugin] = options; - }); - } - - $.each(queue, function(plugin, options) { - if (plugin in QueryBuilder.plugins) { - QueryBuilder.plugins[plugin].call(that, options); - } - else { - $.error('Unable to find plugin "' + plugin +'"'); - } - }); - }; - - - /** - * Init the builder - */ - QueryBuilder.prototype.init = function(options) { - // PROPERTIES - this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options); - this.status = { - group_id: 0, - rule_id: 0, - generatedId: false, - has_optgroup: false - }; - - // "allow_groups" changed in 1.3.1 from boolean to 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.filters = this.settings.filters; - this.lang = this.settings.lang; - this.icons = this.settings.icons; - this.operators = this.settings.operators; - this.template = this.settings.template; - - if (this.template.group === null) { - this.template.group = this.getGroupTemplate; - } - if (this.template.rule === null) { - this.template.rule = this.getRuleTemplate; - } - - // CHECK FILTERS - if (!this.filters || this.filters.length < 1) { - $.error('Missing filters list'); - } - this.checkFilters(); - - // ensure we have a container id - if (!this.$el.attr('id')) { - this.$el.attr('id', 'qb_'+Math.floor(Math.random()*99999)); - this.status.generatedId = true; - } - this.$el_id = this.$el.attr('id'); - - this.$el.addClass('query-builder'); - - // INIT - this.bindEvents(); - - this.initPlugins(); - - this.trigger('afterInit'); - - if (options.rules) { - this.setRules(options.rules); - } - else { - this.addGroup(this.$el); - } - }; - - /** - * Destroy the plugin - */ - QueryBuilder.prototype.destroy = function() { - this.trigger('beforeDestroy'); - - if (this.status.generatedId) { - this.$el.removeAttr('id'); - } - - this.$el.empty() - .off('click.queryBuilder change.queryBuilder') - .removeClass('query-builder') - .removeData('queryBuilder'); - }; - - /** - * Reset the plugin - */ - QueryBuilder.prototype.reset = function() { - this.status.group_id = 1; - this.status.rule_id = 0; - - this.$el.find('>.rules-group-container>.rules-group-body>.rules-list').empty(); - - this.addRule(this.$el.find('>.rules-group-container')); - - this.trigger('afterReset'); - }; - - /** - * Clear the plugin - */ - QueryBuilder.prototype.clear = function() { - this.status.group_id = 0; - this.status.rule_id = 0; - - this.$el.empty(); - - this.trigger('afterClear'); - }; - - /** - * Get an object representing current rules - * @return {object} - */ - QueryBuilder.prototype.getRules = function() { - this.clearErrors(); - - var $group = this.$el.find('>.rules-group-container'), - that = this; - - var rules = (function parse($group) { - var out = {}, - $elements = $group.find('>.rules-group-body>.rules-list>*'); - - out.condition = that.getGroupCondition($group); - out.rules = []; - - for (var i=0, l=$elements.length; i 1)) { - that.triggerValidationError(['empty_group'], $group, null, null, null); - return {}; - } - - return out; - }($group)); - - return this.change('getRules', rules); - }; - - /** - * Set rules from object - * @param data {object} - */ - QueryBuilder.prototype.setRules = function(data) { - this.clear(); - - if (!data || !data.rules || (data.rules.length===0 && !this.settings.allow_empty)) { - $.error('Incorrect data object passed'); - } - - data = this.change('setRules', data); - - var $container = this.$el, - that = this; - - (function add(data, $container){ - var $group = that.addGroup($container, false); - if ($group === null) { - return; - } - - var $buttons = $group.find('>.rules-group-header [name$=_cond]'); - - if (data.condition === undefined) { - data.condition = that.settings.default_condition; - } - - for (var i=0, l=that.settings.conditions.length; i0) { - if (that.settings.allow_groups !== -1 && that.settings.allow_groups < $group.data('queryBuilder').level) { - that.reset(); - $.error(fmt('No more than {0} groups are allowed', that.settings.allow_groups)); - } - else { - add(rule, $group); - } - } - else { - if (rule.id === undefined) { - $.error('Missing rule field id'); - } - if (rule.value === undefined) { - rule.value = ''; - } - if (rule.operator === undefined) { - rule.operator = 'equal'; - } - - var $rule = that.addRule($group); - if ($rule === null) { - return; - } - - var filter = that.getFilterById(rule.id), - operator = that.getOperatorByType(rule.operator); - - $rule.find('.rule-filter-container [name$=_filter]').val(rule.id).trigger('change'); - $rule.find('.rule-operator-container [name$=_operator]').val(rule.operator).trigger('change'); - - if (operator.accept_values !== 0) { - that.setRuleValue($rule, rule.value, filter, operator); - } - - that.applyRuleFlags($rule, rule); - } - }); - - }(data, $container)); - }; - - - /** - * Checks the configuration of each filter - */ - QueryBuilder.prototype.checkFilters = function() { - var definedFilters = [], - that = this; - - $.each(this.filters, function(i, filter) { - if (!filter.id) { - $.error('Missing filter id: '+ i); - } - if (definedFilters.indexOf(filter.id) != -1) { - $.error('Filter already defined: '+ filter.id); - } - definedFilters.push(filter.id); - - if (!filter.type) { - $.error('Missing filter type: '+ filter.id); - } - if (types.indexOf(filter.type) == -1) { - $.error('Invalid type: '+ filter.type); - } - - if (!filter.input) { - filter.input = 'text'; - } - else if (typeof filter.input != 'function' && inputs.indexOf(filter.input) == -1) { - $.error('Invalid input: '+ filter.input); - } - - if (!filter.field) { - filter.field = filter.id; - } - if (!filter.label) { - filter.label = filter.field; - } - - that.status.has_optgroup|= !!filter.optgroup; - if (!filter.optgroup) { - filter.optgroup = null; - } - - switch (filter.type) { - case 'string': - filter.internalType = 'string'; - break; - case 'integer': case 'double': - filter.internalType = 'number'; - break; - case 'date': case 'time': case 'datetime': - filter.internalType = 'datetime'; - break; - } - - switch (filter.input) { - case 'radio': case 'checkbox': - if (!filter.values || filter.values.length < 1) { - $.error('Missing values for filter: '+ filter.id); - } - break; - } - }); - - // group filters with same optgroup, preserving declaration order when possible - if (this.status.has_optgroup) { - var optgroups = [], - filters = []; - - $.each(this.filters, function(i, filter) { - var idx; - - if (filter.optgroup) { - idx = optgroups.lastIndexOf(filter.optgroup); - - if (idx == -1) { - idx = optgroups.length; - } - } - else { - idx = optgroups.length; - } - - optgroups.splice(idx, 0, filter.optgroup); - filters.splice(idx, 0, filter); - }); - - this.filters = filters; - } - - this.trigger('afterCheckFilters'); - }; - - /** - * Add all events listeners - */ - QueryBuilder.prototype.bindEvents = function() { - var that = this; - - // group condition change - this.$el.on('change.queryBuilder', '.rules-group-header [name$=_cond]', function() { - var $this = $(this); - - if ($this.is(':checked')) { - $this.parent().addClass('active').siblings().removeClass('active'); - } - }); - - // rule filter change - this.$el.on('change.queryBuilder', '.rule-filter-container [name$=_filter]', function() { - var $this = $(this), - $rule = $this.closest('.rule-container'); - - that.updateRuleFilter($rule, $this.val()); - }); - - // rule operator change - this.$el.on('change.queryBuilder', '.rule-operator-container [name$=_operator]', function() { - var $this = $(this), - $rule = $this.closest('.rule-container'); - - that.updateRuleOperator($rule, $this.val()); - }); - - // add rule button - this.$el.on('click.queryBuilder', '[data-add=rule]', function() { - var $this = $(this), - $group = $this.closest('.rules-group-container'); - - that.addRule($group); - }); - - // delete rule button - this.$el.on('click.queryBuilder', '[data-delete=rule]', function() { - var $this = $(this), - $rule = $this.closest('.rule-container'); - - that.deleteRule($rule); - }); - - if (this.settings.allow_groups !== 0) { - // add group button - this.$el.on('click.queryBuilder', '[data-add=group]', function() { - var $this = $(this), - $group = $this.closest('.rules-group-container'); - - that.addGroup($group); - }); - - // delete group button - this.$el.on('click.queryBuilder', '[data-delete=group]', function() { - var $this = $(this), - $group = $this.closest('.rules-group-container'); - - that.deleteGroup($group); - }); - } - }; - - /** - * Add a new rules group - * @param $parent {jQuery} - * @param addRule {bool} (optional - add a default empty rule) - * @return $group {jQuery} - */ - QueryBuilder.prototype.addGroup = function($parent, addRule) { - var group_id = this.nextGroupId(), - level = (($parent.data('queryBuilder') || {}).level || 0) + 1, - $container = level===1 ? $parent : $parent.find('>.rules-group-body>.rules-list'), - $group = $(this.template.group.call(this, group_id, level)); - - $group.data('queryBuilder', {level:level}); - - var e = $.Event('addGroup.queryBuilder', { - group_id: group_id, - level: level, - addRule: addRule, - group: $group, - parent: $parent, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return null; - } - - $container.append($group); - - if (this.settings.onAfterAddGroup) { - this.settings.onAfterAddGroup.call(this, $group); - } - - this.trigger('afterAddGroup', $group); - - if (addRule === undefined || addRule === true) { - this.addRule($group); - } - - return $group; - }; - - /** - * Tries to delete a group. The group is not deleted if at least one rule is no_delete. - * @param $group {jQuery} - * @return {boolean} true if the group has been deleted - */ - QueryBuilder.prototype.deleteGroup = function($group) { - if ($group[0].id == this.$el_id + '_group_0') { - return; - } - - var e = $.Event('deleteGroup.queryBuilder', { - group_id: $group[0].id, - group: $group, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return false; - } - - this.trigger('beforeDeleteGroup', $group); - - var that = this, - keepGroup = false; - - $group.find('>.rules-group-body>.rules-list>*').each(function() { - var $element = $(this); - - if ($element.hasClass('rule-container')) { - if ($element.data('queryBuilder').flags.no_delete) { - keepGroup = true; - } - else { - $element.remove(); - } - } - else { - keepGroup|= !that.deleteGroup($element); - } - }); - - if (!keepGroup) { - $group.remove(); - } - - return !keepGroup; - }; - - /** - * Add a new rule - * @param $parent {jQuery} - * @return $rule {jQuery} - */ - QueryBuilder.prototype.addRule = function($parent) { - var rule_id = this.nextRuleId(), - $container = $parent.find('>.rules-group-body>.rules-list'), - $rule = $(this.template.rule.call(this, rule_id)), - $filterSelect = $(this.getRuleFilterSelect(rule_id)); - - $rule.data('queryBuilder', {flags: {}}); - - var e = $.Event('addRule.queryBuilder', { - rule_id: rule_id, - rule: $rule, - parent: $parent, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return null; - } - - $container.append($rule); - $rule.find('.rule-filter-container').append($filterSelect); - - if (this.settings.onAfterAddRule) { - this.settings.onAfterAddRule.call(this, $rule); - } - - this.trigger('afterAddRule', $rule); - - return $rule; - }; - - /** - * Delete a rule. - * @param $rule {jQuery} - * @return {boolean} true if the rule has been deleted - */ - QueryBuilder.prototype.deleteRule = function($rule) { - var e = $.Event('deleteRule.queryBuilder', { - rule_id: $rule[0].id, - rule: $rule, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return false; - } - - this.trigger('beforeDeleteRule', $rule); - - $rule.remove(); - return true; - }; - - /** - * Create operators for a rule - * @param $rule {jQuery} (
  • element) - * @param filter {object} - */ - QueryBuilder.prototype.createRuleInput = function($rule, filter) { - var $valueContainer = $rule.find('.rule-value-container').empty(); - - if (filter === null) { - return; - } - - var operator = this.getOperatorByType(this.getRuleOperator($rule)); - - if (operator.accept_values === 0) { - return; - } - - var $inputs = $(); - - for (var i=0; i 0) $valueContainer.append(' , '); - $valueContainer.append($ruleInput); - $inputs = $inputs.add($ruleInput); - } - - $valueContainer.show(); - - if (filter.onAfterCreateRuleInput) { - filter.onAfterCreateRuleInput.call(this, $rule, filter); - } - - if (filter.plugin) { - $inputs[filter.plugin](filter.plugin_config || {}); - } - - if (filter.default_value !== undefined) { - this.setRuleValue($rule, filter.default_value, filter, operator); - } - - this.trigger('afterCreateRuleInput', $rule, filter, operator); - }; - - /** - * Perform action when rule's filter is changed - * @param $rule {jQuery} (
  • element) - * @param filterId {string} - */ - QueryBuilder.prototype.updateRuleFilter = function($rule, filterId) { - var filter = filterId != '-1' ? this.getFilterById(filterId) : null; - - this.createRuleOperators($rule, filter); - this.createRuleInput($rule, filter); - - $rule.data('queryBuilder').filter = filter; - - this.trigger('afterUpdateRuleFilter', $rule, filter); - }; - - /** - * Update main visibility when rule operator changes - * @param $rule {jQuery} (
  • element) - * @param operatorType {string} - */ - QueryBuilder.prototype.updateRuleOperator = function($rule, operatorType) { - var $valueContainer = $rule.find('.rule-value-container'), - filter = this.getFilterById(this.getRuleFilter($rule)), - operator = this.getOperatorByType(operatorType); - - if (operator.accept_values === 0) { - $valueContainer.hide(); - } - else { - $valueContainer.show(); - - var previousOperator = $rule.data('queryBuilder').operator; - - if ($valueContainer.is(':empty') || operator.accept_values != previousOperator.accept_values) { - this.createRuleInput($rule, filter); - } - } - - $rule.data('queryBuilder').operator = operator; - - if (filter.onAfterChangeOperator) { - filter.onAfterChangeOperator.call(this, $rule, filter, operator); - } - - this.trigger('afterChangeOperator', $rule, filter, operator); - }; - - /** - * Check if a value is correct for a filter - * @param $rule {jQuery} (
  • element) - * @param value {string|string[]|undefined} - * @param filter {object} - * @param operator {object} - * @return {array|true} - */ - QueryBuilder.prototype.validateValue = function($rule, value, filter, operator) { - var validation = filter.validation || {}, - result = true; - - if (operator.accept_values == 1) { - value = [value]; - } - else { - value = value; - } - - if (validation.callback) { - result = validation.callback.call(this, value, filter, operator, $rule); - return this.change('validateValue', result, $rule, value, filter, operator); - } - - for (var i=0; i< validation.min) { - result = ['string_exceed_min_length', validation.min]; - break; - } - } - else if (value[i].length === 0) { - result = ['string_empty']; - break; - } - if (validation.max !== undefined) { - if (value[i].length > validation.max) { - result = ['string_exceed_max_length', validation.max]; - break; - } - } - if (validation.format) { - if (!(validation.format.test(value[i]))) { - result = ['string_invalid_format', validation.format]; - break; - } - } - break; - - case 'number': - if (isNaN(value[i])) { - result = ['number_nan']; - break; - } - if (filter.type == 'integer') { - if (parseInt(value[i]) != value[i]) { - result = ['number_not_integer']; - break; - } - } - else { - if (parseFloat(value[i]) != value[i]) { - result = ['number_not_double']; - break; - } - } - if (validation.min !== undefined) { - if (value[i] < validation.min) { - result = ['number_exceed_min', validation.min]; - break; - } - } - if (validation.max !== undefined) { - if (value[i] > validation.max) { - result = ['number_exceed_max', validation.max]; - break; - } - } - if (validation.step !== undefined) { - var v = value[i]/validation.step; - if (parseInt(v) != v) { - result = ['number_wrong_step', validation.step]; - break; - } - } - break; - - case 'datetime': - // we need MomentJS - if (window.moment && validation.format) { - var datetime = moment(value[i], validation.format); - if (!datetime.isValid()) { - result = ['datetime_invalid']; - break; - } - else { - if (validation.min) { - if (datetime < moment(validation.min, validation.format)) { - result = ['datetime_exceed_min', validation.min]; - break; - } - } - if (validation.max) { - if (datetime > moment(validation.max, validation.format)) { - result = ['datetime_exceed_max', validation.max]; - break; - } - } - } - } - break; - } - } - - if (result !== true) { - break; - } - } - - return this.change('validateValue', result, $rule, value, filter, operator); - }; - - /** - * Remove 'has-error' from everything - */ - QueryBuilder.prototype.clearErrors = function() { - this.$el.find('.has-error').removeClass('has-error'); - }; - - /** - * Trigger a validation error event with custom params - * @param error {array} - * @param $target {jQuery} - * @param value {mixed} - * @param filter {object} - * @param operator {object} - */ - QueryBuilder.prototype.triggerValidationError = function(error, $target, value, filter, operator) { - if (!$.isArray(error)) { - error = [error]; - } - - if (filter && filter.onValidationError) { - filter.onValidationError.call(this, $target, error, value, filter, operator); - } - if (this.settings.onValidationError) { - this.settings.onValidationError.call(this, $target, error, value, filter, operator); - } - - var e = $.Event('validationError.queryBuilder', { - error: error, - filter: filter, - operator: operator, - value: value, - targetRule: $target[0], - builder: this - }); - - this.$el.trigger(e); - - if (this.settings.display_errors && !e.isDefaultPrevented()) { - // translate the text without modifying event array - var errorLoc = $.extend([], error, [ - this.lang.errors[error[0]] || error[0] - ]); - - $target.addClass('has-error'); - var $error = $target.find('.error-container').eq(0); - $error.attr('title', fmt.apply(null, errorLoc)); - } - - this.trigger('validationError', $target, error); - }; - - - /** - * Returns an incremented group ID - * @return {string} - */ - QueryBuilder.prototype.nextGroupId = function() { - return this.$el_id + '_group_' + (this.status.group_id++); - }; - - /** - * Returns an incremented rule ID - * @return {string} - */ - QueryBuilder.prototype.nextRuleId = function() { - return this.$el_id + '_rule_' + (this.status.rule_id++); - }; - - /** - * Returns the operators for a filter - * @param filter {string|object} (filter id name or filter object) - * @return {object[]} - */ - QueryBuilder.prototype.getOperators = function(filter) { - if (typeof filter === 'string') { - filter = this.getFilterById(filter); - } - - var result = []; - - for (var i=0, l=this.operators.length; i
    element) - * @return {string} - */ - QueryBuilder.prototype.getGroupCondition = function($group) { - return $group.find('>.rules-group-header [name$=_cond]:checked').val(); - }; - - /** - * Returns the selected filter of a rule - * @param $rule {jQuery} (
  • element) - * @return {string} - */ - QueryBuilder.prototype.getRuleFilter = function($rule) { - return $rule.find('.rule-filter-container [name$=_filter]').val(); - }; - - /** - * Returns the selected operator of a rule - * @param $rule {jQuery} (
  • element) - * @return {string} - */ - QueryBuilder.prototype.getRuleOperator = function($rule) { - return $rule.find('.rule-operator-container [name$=_operator]').val(); - }; - - /** - * Returns rule value - * @param $rule {jQuery} (
  • element) - * @param filter {object} (optional - current rule filter) - * @param operator {object} (optional - current rule operator) - * @return {string|string[]|undefined} - */ - QueryBuilder.prototype.getRuleValue = function($rule, filter, operator) { - filter = filter || this.getFilterById(this.getRuleFilter($rule)); - operator = operator || this.getOperatorByType(this.getRuleOperator($rule)); - - var value = [], tmp, - $value = $rule.find('.rule-value-container'); - - for (var i=0; i
  • element) - * @param value {mixed} - * @param filter {object} - * @param operator {object} - */ - QueryBuilder.prototype.setRuleValue = function($rule, value, filter, operator) { - filter = filter || this.getFilterById(this.getRuleFilter($rule)); - operator = operator || this.getOperatorByType(this.getRuleOperator($rule)); - - this.trigger('beforeSetRuleValue', $rule, value, filter, operator); - - if (filter.valueSetter) { - filter.valueSetter.call(this, $rule, value, filter, operator); - } - else { - var $value = $rule.find('.rule-value-container'); - - if (operator.accept_values == 1) { - value = [value]; - } - else { - value = value; - } - - for (var i=0; i
  • element) - * @param rule {object} - */ - QueryBuilder.prototype.applyRuleFlags = function($rule, rule) { - var flags = this.getRuleFlags(rule); - $rule.data('queryBuilder').flags = flags; - - if (flags.filter_readonly) { - $rule.find('[name$=_filter]').prop('disabled', true); - } - if (flags.operator_readonly) { - $rule.find('[name$=_operator]').prop('disabled', true); - } - if (flags.value_readonly) { - $rule.find('[name*=_value_]').prop('disabled', true); - } - if (flags.no_delete) { - $rule.find('[data-delete=rule]').remove(); - } - - this.trigger('afterApplyRuleFlags', $rule, rule, flags); - }; - - - /** - * Returns group HTML - * @param group_id {string} - * @param level {int} - * @return {string} - */ - QueryBuilder.prototype.getGroupTemplate = function(group_id, level) { - var h = '\ -
    \ -
    \ -
    \ - \ - '+ (this.settings.allow_groups===-1 || this.settings.allow_groups>=level ? - '' - :'') +' \ - '+ (level>1 ? - '' - : '') +' \ -
    \ -
    \ - '+ this.getGroupConditions(group_id) +' \ -
    \ - '+ (this.settings.display_errors ? - '
    ' - :'') +'\ -
    \ -
    \ -
      \ -
      \ -
      '; - - return this.change('getGroupTemplate', h, level); - }; - - /** - * Returns group conditions HTML - * @param group_id {string} - * @return {string} - */ - QueryBuilder.prototype.getGroupConditions = function(group_id) { - var h = ''; - - for (var i=0, l=this.settings.conditions.length; i \ - '+ label +' \ - '; - } - - return this.change('getGroupConditions', h); - }; - - /** - * Returns rule HTML - * @param rule_id {string} - * @return {string} - */ - QueryBuilder.prototype.getRuleTemplate = function(rule_id) { - var h = '\ -
    • \ -
      \ -
      \ - \ -
      \ -
      \ - '+ (this.settings.display_errors ? - '
      ' - :'') +'\ -
      \ -
      \ -
      \ -
    • '; - - return this.change('getRuleTemplate', h); - }; - - /** - * Returns rule filter '; - h+= ''; - - $.each(this.filters, function(i, filter) { - if (optgroup != filter.optgroup) { - if (optgroup !== null) h+= ''; - optgroup = filter.optgroup; - if (optgroup !== null) h+= ''; - } - - h+= ''; - }); - - if (optgroup !== null) h+= ''; - h+= ''; - - return this.change('getRuleFilterSelect', h); - }; - - /** - * Returns rule operator '; - - for (var i=0, l=operators.length; i'+ label +''; - } - - h+= ''; - - return this.change('getRuleOperatorSelect', h); - }; - - /** - * Return the rule value HTML - * @param $rule {jQuery} - * @param filter {object} - * @param value_id {int} - * @return {string} - */ - QueryBuilder.prototype.getRuleInput = function($rule, filter, value_id) { - var validation = filter.validation || {}, - name = $rule[0].id +'_value_'+ value_id, - h = '', c; - - if (typeof filter.input === 'function') { - h = filter.input.call(this, $rule, filter, name); - } - else { - switch (filter.input) { - case 'radio': - c = filter.vertical ? ' class=block' : ''; - iterateOptions(filter.values, function(key, val) { - h+= ' '+ val +' '; - }); - break; - - case 'checkbox': - c = filter.vertical ? ' class=block' : ''; - iterateOptions(filter.values, function(key, val) { - h+= ' '+ val +' '; - }); - break; - - case 'select': - h+= ''; - break; - - case 'textarea': - h+= '";break;default:switch(c.internalType){case"number":h+='1&&$.error("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 j(this,c)),"string"==typeof a?b[a].apply(b,Array.prototype.slice.call(arguments,1)):this):this},$.fn.queryBuilder.defaults={set:function(a){$.extendext(!0,"replace",j.DEFAULTS,a)},get:function(a){var b=j.DEFAULTS;return a&&(b=b[a]),$.extend(!0,{},b)}},$.fn.queryBuilder.constructor=j,$.fn.queryBuilder.extend=j.extend,$.fn.queryBuilder.define=j.define,$.fn.queryBuilder.define("bt-selectpicker",function(a){$.fn.selectpicker&&$.fn.selectpicker.Constructor||$.error('Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select'),a=$.extend({container:"body",style:"btn-inverse btn-xs",width:"auto",showIcon:!1},a||{}),this.on("afterAddRule",function(b){b.find(".rule-filter-container select").selectpicker(a)}),this.on("afterCreateRuleOperators",function(b){b.find(".rule-operator-container select").selectpicker(a)})}),$.fn.queryBuilder.define("bt-tooltip-errors",function(a){$.fn.tooltip&&$.fn.tooltip.Constructor&&$.fn.tooltip.Constructor.prototype.fixTitle||$.error('Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com'),a=$.extend({placement:"right"},a||{}),this.on("getRuleTemplate",function(a){return a.replace('class="error-container"','class="error-container" data-toggle="tooltip"')}),this.on("validationError",function(b){b.find(".error-container").eq(0).tooltip(a).tooltip("hide").tooltip("fixTitle")})}),$.fn.queryBuilder.define("filter-description",function(a){a=$.extend({icon:"glyphicon glyphicon-info-sign",mode:"popover"},a||{}),"inline"===a.mode?this.on("afterUpdateRuleFilter",function(b,c){var d=b.find("p.filter-description");c&&c.description?(0===d.length?(d=$('

      '),d.appendTo(b)):d.show(),d.html(' '+c.description)):d.hide()}):"popover"===a.mode?($.fn.popover&&$.fn.popover.Constructor&&$.fn.popover.Constructor.prototype.fixTitle||$.error('Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com'),this.on("afterUpdateRuleFilter",function(b,c){var d=b.find("button.filter-description");c&&c.description?(0===d.length?(d=$(''),d.prependTo(b.find(".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.description,d.attr("aria-describedby")&&d.popover("show")):(d.hide(),d.data("bs.popover")&&d.popover("hide"))})):"bootbox"===a.mode&&(window.bootbox||$.error('Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com'),this.on("afterUpdateRuleFilter",function(b,c){var d=b.find("button.filter-description");c&&c.description?(0===d.length&&(d=$(''),d.prependTo(b.find(".rule-actions")),d.on("click",function(){bootbox.alert(d.data("description"))})),d.data("description",c.description)):d.hide()}))}),$.fn.queryBuilder.defaults.set({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]}},begins_with:function(a){return{$regex:"^"+e(a[0])}},not_begins_with:function(a){return{$regex:"^(?!"+e(a[0])+")"}},contains:function(a){return{$regex:e(a[0])}},not_contains:function(a){return{$regex:"^((?!"+e(a[0])+").)*$",$options:"s"}},ends_with:function(a){return{$regex:e(a[0])+"$"}},not_ends_with:function(a){return{$regex:"(?0)e.push(c(f));else{var g=b.settings.mongoOperators[f.operator],h=b.getOperatorByType(f.operator),i=[];void 0===g&&$.error("MongoDB operation unknown for operator "+f.operator),h.accept_values&&(f.value instanceof Array||(f.value=[f.value]),f.value.forEach(function(a){i.push(d(a,f.type))}));var j={};j[f.field]=g.call(b,i),e.push(j)}});var f={};return e.length>0&&(f["$"+a.condition.toLowerCase()]=e),f}(a)}}),$.fn.queryBuilder.define("sortable",function(a){a=$.extend({default_no_sortable:!1,icon:"glyphicon glyphicon-sort"},a||{}),this.on("afterInit",function(){$.event.props.push("dataTransfer");var a,b,c=this;this.$el.on("mouseover",".drag-handle",function(){c.$el.find(".rule-container, .rules-group-container").attr("draggable",!0)}),this.$el.on("mouseout",".drag-handle",function(){c.$el.find(".rule-container, .rules-group-container").removeAttr("draggable")}),this.$el.on("dragstart","[draggable]",function(c){c.stopPropagation(),c.dataTransfer.setData("text","drag"),b=$(c.target),a=$('
       
      '),a.css("min-height",b.height()),a.insertAfter(b),setTimeout(function(){b.hide()},0)}),this.$el.on("dragenter","[draggable]",function(b){b.preventDefault(),b.stopPropagation(),f(a,$(b.target))}),this.$el.on("dragover","[draggable]",function(a){a.preventDefault(),a.stopPropagation()}),this.$el.on("drop",function(a){a.preventDefault(),a.stopPropagation(),f(b,$(a.target))}),this.$el.on("dragend","[draggable]",function(d){d.preventDefault(),d.stopPropagation(),b.show(),a.remove(),b=a=null,c.$el.find(".rule-container, .rules-group-container").removeAttr("draggable")})}),this.on("getRuleFlags",function(b){return void 0===b.no_sortable&&(b.no_sortable=a.default_no_sortable),b}),this.on("afterApplyRuleFlags",function(a,b,c){c.no_sortable&&a.find(".drag-handle").remove()}),this.on("getGroupTemplate",function(b,c){if(c>1){var d=$(b);d.find(".group-conditions").after('
      '),b=d.prop("outerHTML")}return b}),this.on("getRuleTemplate",function(b){var c=$(b);return c.find(".rule-header").after('
      '),c.prop("outerHTML")})}),$.fn.queryBuilder.defaults.set({sqlOperators:{equal:"= ?",not_equal:"!= ?","in":{op:"IN(?)",list:!0,sep:", "},not_in:{op:"NOT IN(?)",list:!0,sep:", "},less:"< ?",less_or_equal:"<= ?",greater:"> ?",greater_or_equal:">= ?",between:{op:"BETWEEN ?",list:!0,sep:" AND "},begins_with:{op:"LIKE(?)",fn:function(a){return a+"%"}},not_begins_with:{op:"NOT LIKE(?)",fn:function(a){return a+"%"}},contains:{op:"LIKE(?)",fn:function(a){return"%"+a+"%"}},not_contains:{op:"NOT LIKE(?)",fn:function(a){return"%"+a+"%"}},ends_with:{op:"LIKE(?)",fn:function(a){return"%"+a}},not_ends_with:{op:"NOT LIKE(?)",fn:function(a){return"%"+a}},is_empty:'== ""',is_not_empty:'!= ""',is_null:"IS NULL",is_not_null:"IS NOT NULL"}}),$.fn.queryBuilder.extend({getSQL:function(a,b,c){c=void 0===c?this.getRules():c,a=a===!0||void 0===a?"question_mark":a,b=b||void 0===b?"\n":" "; -var e=this,f=1,h=[],i=function j(c){if(c.condition||(c.condition=e.settings.default_condition),-1===["AND","OR"].indexOf(c.condition.toUpperCase())&&$.error("Unable to build SQL query with "+c.condition+" condition"),!c.rules)return"";var i=[];return $.each(c.rules,function(c,k){if(k.rules&&k.rules.length>0)i.push("("+b+j(k)+b+")"+b);else{var l=e.getSqlOperator(k.operator),m=e.getOperatorByType(k.operator),n="";l===!1&&$.error("SQL operation unknown for operator "+k.operator),m.accept_values&&(k.value instanceof Array?!l.list&&k.value.length>1&&$.error("Operator "+k.operator+" cannot accept multiple values"):k.value=[k.value],k.value.forEach(function(b,c){c>0&&(n+=l.sep),"integer"==k.type||"double"==k.type?b=d(b,k.type):a||(b=g(b)),l.fn&&(b=l.fn(b)),a?(n+="question_mark"==a?"?":"$"+f,h.push(b),f++):("string"==typeof b&&(b="'"+b+"'"),n+=b)})),i.push(k.field+" "+l.op.replace(/\?/,n))}}),i.join(" "+c.condition+b)}(c);return a?{sql:i,params:h}:{sql:i}},getSqlOperator:function(a){var b=this.settings.sqlOperators[a];return void 0===b?!1:("string"==typeof b&&(b={op:b}),b.list||(b.list=!1),b.list&&!b.sep&&(b.sep=", "),b)}})}); \ No newline at end of file diff --git a/dist/query-builder.standalone.js b/dist/query-builder.standalone.js deleted file mode 100644 index 0e32f20b..00000000 --- a/dist/query-builder.standalone.js +++ /dev/null @@ -1,2623 +0,0 @@ -/*! - * MicroEvent - to make any js object an event emitter - * Copyright 2011 Jerome Etienne (http://jetienne.com) - * Copyright 2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) - */ - -(function(root, factory) { - if (typeof module !== 'undefined' && module.exports) { - module.exports = factory(); - } - else if (typeof define === 'function' && define.amd) { - define('microevent', [], factory); - } - else { - root.MicroEvent = factory(); - } -}(this, function() { - "use strict"; - - var MicroEvent = function(){}; - - MicroEvent.prototype = { - /** - * Add one or many event handlers - * - * @param {String,Object} events - * @param {Function} optional, callback - * - * obj.on('event', callback) - * obj.on('event1 event2', callback) - * obj.on({ event1: callback1, event2: callback2 }) - */ - on: function (events, fct) { - this._events = this._events || {}; - - if (typeof events === 'object') { - for (var event in events) { - if (events.hasOwnProperty(event)) { - this._events[event] = this._events[event] || []; - this._events[event].push(events[event]); - } - } - } - else { - events.split(' ').forEach(function(event) { - this._events[event] = this._events[event] || []; - this._events[event].push(fct); - }, this); - } - - return this; - }, - - /** - * Remove one or many or all event handlers - * - * @param {String,Object} optional, events - * @param {Function} optional, callback - * - * obj.off('event') - * obj.off('event', callback) - * obj.off('event1 event2') - * obj.off({ event1: callback1, event2: callback2 }) - * obj.off() - */ - off: function (events, fct) { - this._events = this._events || {}; - - if (typeof events === 'object') { - for (var event in events) { - if (events.hasOwnProperty(event) && (event in this._events)) { - var index = this._events[event].indexOf(events[event]); - if (index !== -1) this._events[event].splice(index, 1); - } - } - } - else if (!!events) { - events.split(' ').forEach(function(event) { - if (event in this._events) { - if (fct) { - var index = this._events[event].indexOf(fct); - if (index !== -1) this._events[event].splice(index, 1); - } - else { - this._events[event] = []; - } - } - }, this); - } - else { - this._events = {}; - } - - return this; - }, - - /** - * Add one or many event handlers that will be called only once - * This handlers are only applicable to "trigger", not "change" - * - * @param {String,Object} events - * @param {Function} optional, callback - * - * obj.once('event', callback) - * obj.once('event1 event2', callback) - * obj.once({ event1: callback1, event2: callback2 }) - */ - once: function (events, fct) { - this._once = this._once || {}; - - if (typeof events === 'object') { - for (var event in events) { - if (events.hasOwnProperty(event)) { - this._once[event] = this._once[event] || []; - this._once[event].push(events[event]); - } - } - } - else { - events.split(' ').forEach(function(event) { - this._once[event] = this._once[event] || []; - this._once[event].push(fct); - }, this); - } - - return this; - }, - - /** - * Trigger all handlers for an event - * - * @param {String} event name - * @param {Mixed...} optional, arguments - */ - trigger: function (event /* , args... */) { - this._events = this._events || {}; - this._once = this._once || {}; - - var args = Array.prototype.slice.call(arguments, 1), - callbacks; - - if (event in this._events) { - callbacks = this._events[event].slice(); - while (callbacks.length) { - callbacks.shift().apply(this, args); - } - } - - if (event in this._once) { - callbacks = this._once[event].slice(); - while (callbacks.length) { - callbacks.shift().apply(this, args); - } - delete this._once[event]; - } - - return this; - }, - - /** - * Trigger all modificators for an event, each handler must return a value - * - * @param {String} event name - * @param {Mixed} event value - * @param {Mixed...} optional, arguments - */ - change: function(event, value /* , args... */) { - this._events = this._events || {}; - - if (event in this._events) { - var args = Array.prototype.slice.call(arguments, 1); - - for (var i=0, l=this._events[event].length; i< length; i++ ) { - // Only deal with non-null/undefined values - if ( (options = arguments[ i ]) !== null ) { - // Special operations for arrays - if ($.isArray(options) && arrayMode !== 'default') { - clone = target && $.isArray(target) ? target : []; - - switch (arrayMode) { - case 'concat': - target = clone.concat( $.extend( deep, [], options ) ); - break; - - case 'replace': - target = $.extend( deep, [], options ); - break; - - case 'extend': - options.forEach(function(e, i) { - if (typeof e === 'object') { - var type = $.isArray(e) ? [] : {}; - clone[i] = $.extendext( deep, arrayMode, clone[i] || type, e ); - - } else if (clone.indexOf(e) === -1) { - clone.push(e); - } - }); - - target = clone; - break; - } - - } else { - // Extend the base object - for ( name in options ) { - src = target[ name ]; - copy = options[ name ]; - - // Prevent never-ending loop - if ( target === copy ) { - continue; - } - - // Recurse if we're merging plain objects or arrays - if ( deep && copy && ( $.isPlainObject(copy) || - (copyIsArray = $.isArray(copy)) ) ) { - - if ( copyIsArray ) { - copyIsArray = false; - clone = src && $.isArray(src) ? src : []; - - } else { - clone = src && $.isPlainObject(src) ? src : {}; - } - - // Never move original objects, clone them - target[ name ] = $.extendext( deep, arrayMode, clone, copy ); - - // Don't bring in undefined values - } else if ( copy !== undefined ) { - target[ name ] = copy; - } - } - } - } - } - - // Return the modified object - return target; - }; -})); - -/*! - * jQuery QueryBuilder 1.4.1 - * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) - */ - -// Modules: bt-selectpicker, bt-tooltip-errors, filter-description, mongodb-support, sortable, sql-support -(function(root, factory) { - if (typeof define === 'function' && define.amd) { - define('query-builder', ['jquery', 'microevent', 'jQuery.extendext'], factory); - } - else { - factory(root.jQuery, root.MicroEvent); - } -}(this, function($, MicroEvent) { - "use strict"; - - var types = [ - 'string', - 'integer', - 'double', - 'date', - 'time', - 'datetime' - ], - internalTypes = [ - 'string', - 'number', - 'datetime' - ], - inputs = [ - 'text', - 'textarea', - 'radio', - 'checkbox', - 'select' - ]; - - - var QueryBuilder = function($el, options) { - this.$el = $el; - this.init(options); - }; - - MicroEvent.mixin(QueryBuilder); - - - QueryBuilder.DEFAULTS = { - filters: [], - - plugins: null, - - onValidationError: null, - onAfterAddGroup: null, - onAfterAddRule: null, - - display_errors: true, - allow_groups: -1, - allow_empty: false, - conditions: ['AND', 'OR'], - default_condition: 'AND', - - default_rule_flags: { - filter_readonly: false, - operator_readonly: false, - value_readonly: false, - no_delete: false - }, - - template: { - group: null, - rule: null - }, - - lang: { - "add_rule": 'Add rule', - "add_group": 'Add group', - "delete_rule": 'Delete', - "delete_group": 'Delete', - - "condition_and": 'AND', - "condition_or": 'OR', - - "filter_select_placeholder": '------', - - "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", - "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_invalid": "Invalid date format ({0})", - "datetime_exceed_min": "Must be after {0}", - "datetime_exceed_max": "Must be before {0}" - } - }, - - operators: [ - {type: 'equal', accept_values: 1, apply_to: ['string', 'number', 'datetime']}, - {type: 'not_equal', accept_values: 1, apply_to: ['string', 'number', 'datetime']}, - {type: 'in', accept_values: 1, apply_to: ['string', 'number', 'datetime']}, - {type: 'not_in', accept_values: 1, apply_to: ['string', 'number', 'datetime']}, - {type: 'less', accept_values: 1, apply_to: ['number', 'datetime']}, - {type: 'less_or_equal', accept_values: 1, apply_to: ['number', 'datetime']}, - {type: 'greater', accept_values: 1, apply_to: ['number', 'datetime']}, - {type: 'greater_or_equal', accept_values: 1, apply_to: ['number', 'datetime']}, - {type: 'between', accept_values: 2, apply_to: ['number', 'datetime']}, - {type: 'begins_with', accept_values: 1, apply_to: ['string']}, - {type: 'not_begins_with', accept_values: 1, apply_to: ['string']}, - {type: 'contains', accept_values: 1, apply_to: ['string']}, - {type: 'not_contains', accept_values: 1, apply_to: ['string']}, - {type: 'ends_with', accept_values: 1, apply_to: ['string']}, - {type: 'not_ends_with', accept_values: 1, apply_to: ['string']}, - {type: 'is_empty', accept_values: 0, apply_to: ['string']}, - {type: 'is_not_empty', accept_values: 0, apply_to: ['string']}, - {type: 'is_null', accept_values: 0, apply_to: ['string', 'number', 'datetime']}, - {type: 'is_not_null', accept_values: 0, apply_to: ['string', 'number', 'datetime']} - ], - - 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' - } - }; - - - QueryBuilder.plugins = {}; - - /** - * Define a new plugin - * @param {string} - * @param {function} - */ - QueryBuilder.define = function(name, fct) { - QueryBuilder.plugins[name] = fct; - }; - - /** - * Add new methods - * @param {object} - */ - QueryBuilder.extend = function(methods) { - $.extend(QueryBuilder.prototype, methods); - }; - - /** - * Init plugins for an instance - */ - QueryBuilder.prototype.initPlugins = function() { - if (!this.settings.plugins) { - return; - } - - var that = this, - queue = {}; - - if ($.isArray(this.settings.plugins)) { - $.each(this.settings.plugins, function(i, plugin) { - queue[plugin] = {}; - }); - } - else { - $.each(this.settings.plugins, function(plugin, options) { - queue[plugin] = options; - }); - } - - $.each(queue, function(plugin, options) { - if (plugin in QueryBuilder.plugins) { - QueryBuilder.plugins[plugin].call(that, options); - } - else { - $.error('Unable to find plugin "' + plugin +'"'); - } - }); - }; - - - /** - * Init the builder - */ - QueryBuilder.prototype.init = function(options) { - // PROPERTIES - this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options); - this.status = { - group_id: 0, - rule_id: 0, - generatedId: false, - has_optgroup: false - }; - - // "allow_groups" changed in 1.3.1 from boolean to 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.filters = this.settings.filters; - this.lang = this.settings.lang; - this.icons = this.settings.icons; - this.operators = this.settings.operators; - this.template = this.settings.template; - - if (this.template.group === null) { - this.template.group = this.getGroupTemplate; - } - if (this.template.rule === null) { - this.template.rule = this.getRuleTemplate; - } - - // CHECK FILTERS - if (!this.filters || this.filters.length < 1) { - $.error('Missing filters list'); - } - this.checkFilters(); - - // ensure we have a container id - if (!this.$el.attr('id')) { - this.$el.attr('id', 'qb_'+Math.floor(Math.random()*99999)); - this.status.generatedId = true; - } - this.$el_id = this.$el.attr('id'); - - this.$el.addClass('query-builder'); - - // INIT - this.bindEvents(); - - this.initPlugins(); - - this.trigger('afterInit'); - - if (options.rules) { - this.setRules(options.rules); - } - else { - this.addGroup(this.$el); - } - }; - - /** - * Destroy the plugin - */ - QueryBuilder.prototype.destroy = function() { - this.trigger('beforeDestroy'); - - if (this.status.generatedId) { - this.$el.removeAttr('id'); - } - - this.$el.empty() - .off('click.queryBuilder change.queryBuilder') - .removeClass('query-builder') - .removeData('queryBuilder'); - }; - - /** - * Reset the plugin - */ - QueryBuilder.prototype.reset = function() { - this.status.group_id = 1; - this.status.rule_id = 0; - - this.$el.find('>.rules-group-container>.rules-group-body>.rules-list').empty(); - - this.addRule(this.$el.find('>.rules-group-container')); - - this.trigger('afterReset'); - }; - - /** - * Clear the plugin - */ - QueryBuilder.prototype.clear = function() { - this.status.group_id = 0; - this.status.rule_id = 0; - - this.$el.empty(); - - this.trigger('afterClear'); - }; - - /** - * Get an object representing current rules - * @return {object} - */ - QueryBuilder.prototype.getRules = function() { - this.clearErrors(); - - var $group = this.$el.find('>.rules-group-container'), - that = this; - - var rules = (function parse($group) { - var out = {}, - $elements = $group.find('>.rules-group-body>.rules-list>*'); - - out.condition = that.getGroupCondition($group); - out.rules = []; - - for (var i=0, l=$elements.length; i 1)) { - that.triggerValidationError(['empty_group'], $group, null, null, null); - return {}; - } - - return out; - }($group)); - - return this.change('getRules', rules); - }; - - /** - * Set rules from object - * @param data {object} - */ - QueryBuilder.prototype.setRules = function(data) { - this.clear(); - - if (!data || !data.rules || (data.rules.length===0 && !this.settings.allow_empty)) { - $.error('Incorrect data object passed'); - } - - data = this.change('setRules', data); - - var $container = this.$el, - that = this; - - (function add(data, $container){ - var $group = that.addGroup($container, false); - if ($group === null) { - return; - } - - var $buttons = $group.find('>.rules-group-header [name$=_cond]'); - - if (data.condition === undefined) { - data.condition = that.settings.default_condition; - } - - for (var i=0, l=that.settings.conditions.length; i0) { - if (that.settings.allow_groups !== -1 && that.settings.allow_groups < $group.data('queryBuilder').level) { - that.reset(); - $.error(fmt('No more than {0} groups are allowed', that.settings.allow_groups)); - } - else { - add(rule, $group); - } - } - else { - if (rule.id === undefined) { - $.error('Missing rule field id'); - } - if (rule.value === undefined) { - rule.value = ''; - } - if (rule.operator === undefined) { - rule.operator = 'equal'; - } - - var $rule = that.addRule($group); - if ($rule === null) { - return; - } - - var filter = that.getFilterById(rule.id), - operator = that.getOperatorByType(rule.operator); - - $rule.find('.rule-filter-container [name$=_filter]').val(rule.id).trigger('change'); - $rule.find('.rule-operator-container [name$=_operator]').val(rule.operator).trigger('change'); - - if (operator.accept_values !== 0) { - that.setRuleValue($rule, rule.value, filter, operator); - } - - that.applyRuleFlags($rule, rule); - } - }); - - }(data, $container)); - }; - - - /** - * Checks the configuration of each filter - */ - QueryBuilder.prototype.checkFilters = function() { - var definedFilters = [], - that = this; - - $.each(this.filters, function(i, filter) { - if (!filter.id) { - $.error('Missing filter id: '+ i); - } - if (definedFilters.indexOf(filter.id) != -1) { - $.error('Filter already defined: '+ filter.id); - } - definedFilters.push(filter.id); - - if (!filter.type) { - $.error('Missing filter type: '+ filter.id); - } - if (types.indexOf(filter.type) == -1) { - $.error('Invalid type: '+ filter.type); - } - - if (!filter.input) { - filter.input = 'text'; - } - else if (typeof filter.input != 'function' && inputs.indexOf(filter.input) == -1) { - $.error('Invalid input: '+ filter.input); - } - - if (!filter.field) { - filter.field = filter.id; - } - if (!filter.label) { - filter.label = filter.field; - } - - that.status.has_optgroup|= !!filter.optgroup; - if (!filter.optgroup) { - filter.optgroup = null; - } - - switch (filter.type) { - case 'string': - filter.internalType = 'string'; - break; - case 'integer': case 'double': - filter.internalType = 'number'; - break; - case 'date': case 'time': case 'datetime': - filter.internalType = 'datetime'; - break; - } - - switch (filter.input) { - case 'radio': case 'checkbox': - if (!filter.values || filter.values.length < 1) { - $.error('Missing values for filter: '+ filter.id); - } - break; - } - }); - - // group filters with same optgroup, preserving declaration order when possible - if (this.status.has_optgroup) { - var optgroups = [], - filters = []; - - $.each(this.filters, function(i, filter) { - var idx; - - if (filter.optgroup) { - idx = optgroups.lastIndexOf(filter.optgroup); - - if (idx == -1) { - idx = optgroups.length; - } - } - else { - idx = optgroups.length; - } - - optgroups.splice(idx, 0, filter.optgroup); - filters.splice(idx, 0, filter); - }); - - this.filters = filters; - } - - this.trigger('afterCheckFilters'); - }; - - /** - * Add all events listeners - */ - QueryBuilder.prototype.bindEvents = function() { - var that = this; - - // group condition change - this.$el.on('change.queryBuilder', '.rules-group-header [name$=_cond]', function() { - var $this = $(this); - - if ($this.is(':checked')) { - $this.parent().addClass('active').siblings().removeClass('active'); - } - }); - - // rule filter change - this.$el.on('change.queryBuilder', '.rule-filter-container [name$=_filter]', function() { - var $this = $(this), - $rule = $this.closest('.rule-container'); - - that.updateRuleFilter($rule, $this.val()); - }); - - // rule operator change - this.$el.on('change.queryBuilder', '.rule-operator-container [name$=_operator]', function() { - var $this = $(this), - $rule = $this.closest('.rule-container'); - - that.updateRuleOperator($rule, $this.val()); - }); - - // add rule button - this.$el.on('click.queryBuilder', '[data-add=rule]', function() { - var $this = $(this), - $group = $this.closest('.rules-group-container'); - - that.addRule($group); - }); - - // delete rule button - this.$el.on('click.queryBuilder', '[data-delete=rule]', function() { - var $this = $(this), - $rule = $this.closest('.rule-container'); - - that.deleteRule($rule); - }); - - if (this.settings.allow_groups !== 0) { - // add group button - this.$el.on('click.queryBuilder', '[data-add=group]', function() { - var $this = $(this), - $group = $this.closest('.rules-group-container'); - - that.addGroup($group); - }); - - // delete group button - this.$el.on('click.queryBuilder', '[data-delete=group]', function() { - var $this = $(this), - $group = $this.closest('.rules-group-container'); - - that.deleteGroup($group); - }); - } - }; - - /** - * Add a new rules group - * @param $parent {jQuery} - * @param addRule {bool} (optional - add a default empty rule) - * @return $group {jQuery} - */ - QueryBuilder.prototype.addGroup = function($parent, addRule) { - var group_id = this.nextGroupId(), - level = (($parent.data('queryBuilder') || {}).level || 0) + 1, - $container = level===1 ? $parent : $parent.find('>.rules-group-body>.rules-list'), - $group = $(this.template.group.call(this, group_id, level)); - - $group.data('queryBuilder', {level:level}); - - var e = $.Event('addGroup.queryBuilder', { - group_id: group_id, - level: level, - addRule: addRule, - group: $group, - parent: $parent, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return null; - } - - $container.append($group); - - if (this.settings.onAfterAddGroup) { - this.settings.onAfterAddGroup.call(this, $group); - } - - this.trigger('afterAddGroup', $group); - - if (addRule === undefined || addRule === true) { - this.addRule($group); - } - - return $group; - }; - - /** - * Tries to delete a group. The group is not deleted if at least one rule is no_delete. - * @param $group {jQuery} - * @return {boolean} true if the group has been deleted - */ - QueryBuilder.prototype.deleteGroup = function($group) { - if ($group[0].id == this.$el_id + '_group_0') { - return; - } - - var e = $.Event('deleteGroup.queryBuilder', { - group_id: $group[0].id, - group: $group, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return false; - } - - this.trigger('beforeDeleteGroup', $group); - - var that = this, - keepGroup = false; - - $group.find('>.rules-group-body>.rules-list>*').each(function() { - var $element = $(this); - - if ($element.hasClass('rule-container')) { - if ($element.data('queryBuilder').flags.no_delete) { - keepGroup = true; - } - else { - $element.remove(); - } - } - else { - keepGroup|= !that.deleteGroup($element); - } - }); - - if (!keepGroup) { - $group.remove(); - } - - return !keepGroup; - }; - - /** - * Add a new rule - * @param $parent {jQuery} - * @return $rule {jQuery} - */ - QueryBuilder.prototype.addRule = function($parent) { - var rule_id = this.nextRuleId(), - $container = $parent.find('>.rules-group-body>.rules-list'), - $rule = $(this.template.rule.call(this, rule_id)), - $filterSelect = $(this.getRuleFilterSelect(rule_id)); - - $rule.data('queryBuilder', {flags: {}}); - - var e = $.Event('addRule.queryBuilder', { - rule_id: rule_id, - rule: $rule, - parent: $parent, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return null; - } - - $container.append($rule); - $rule.find('.rule-filter-container').append($filterSelect); - - if (this.settings.onAfterAddRule) { - this.settings.onAfterAddRule.call(this, $rule); - } - - this.trigger('afterAddRule', $rule); - - return $rule; - }; - - /** - * Delete a rule. - * @param $rule {jQuery} - * @return {boolean} true if the rule has been deleted - */ - QueryBuilder.prototype.deleteRule = function($rule) { - var e = $.Event('deleteRule.queryBuilder', { - rule_id: $rule[0].id, - rule: $rule, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return false; - } - - this.trigger('beforeDeleteRule', $rule); - - $rule.remove(); - return true; - }; - - /** - * Create operators for a rule - * @param $rule {jQuery} (
    • element) - * @param filter {object} - */ - QueryBuilder.prototype.createRuleInput = function($rule, filter) { - var $valueContainer = $rule.find('.rule-value-container').empty(); - - if (filter === null) { - return; - } - - var operator = this.getOperatorByType(this.getRuleOperator($rule)); - - if (operator.accept_values === 0) { - return; - } - - var $inputs = $(); - - for (var i=0; i 0) $valueContainer.append(' , '); - $valueContainer.append($ruleInput); - $inputs = $inputs.add($ruleInput); - } - - $valueContainer.show(); - - if (filter.onAfterCreateRuleInput) { - filter.onAfterCreateRuleInput.call(this, $rule, filter); - } - - if (filter.plugin) { - $inputs[filter.plugin](filter.plugin_config || {}); - } - - if (filter.default_value !== undefined) { - this.setRuleValue($rule, filter.default_value, filter, operator); - } - - this.trigger('afterCreateRuleInput', $rule, filter, operator); - }; - - /** - * Perform action when rule's filter is changed - * @param $rule {jQuery} (
    • element) - * @param filterId {string} - */ - QueryBuilder.prototype.updateRuleFilter = function($rule, filterId) { - var filter = filterId != '-1' ? this.getFilterById(filterId) : null; - - this.createRuleOperators($rule, filter); - this.createRuleInput($rule, filter); - - $rule.data('queryBuilder').filter = filter; - - this.trigger('afterUpdateRuleFilter', $rule, filter); - }; - - /** - * Update main visibility when rule operator changes - * @param $rule {jQuery} (
    • element) - * @param operatorType {string} - */ - QueryBuilder.prototype.updateRuleOperator = function($rule, operatorType) { - var $valueContainer = $rule.find('.rule-value-container'), - filter = this.getFilterById(this.getRuleFilter($rule)), - operator = this.getOperatorByType(operatorType); - - if (operator.accept_values === 0) { - $valueContainer.hide(); - } - else { - $valueContainer.show(); - - var previousOperator = $rule.data('queryBuilder').operator; - - if ($valueContainer.is(':empty') || operator.accept_values != previousOperator.accept_values) { - this.createRuleInput($rule, filter); - } - } - - $rule.data('queryBuilder').operator = operator; - - if (filter.onAfterChangeOperator) { - filter.onAfterChangeOperator.call(this, $rule, filter, operator); - } - - this.trigger('afterChangeOperator', $rule, filter, operator); - }; - - /** - * Check if a value is correct for a filter - * @param $rule {jQuery} (
    • element) - * @param value {string|string[]|undefined} - * @param filter {object} - * @param operator {object} - * @return {array|true} - */ - QueryBuilder.prototype.validateValue = function($rule, value, filter, operator) { - var validation = filter.validation || {}, - result = true; - - if (operator.accept_values == 1) { - value = [value]; - } - else { - value = value; - } - - if (validation.callback) { - result = validation.callback.call(this, value, filter, operator, $rule); - return this.change('validateValue', result, $rule, value, filter, operator); - } - - for (var i=0; i< validation.min) { - result = ['string_exceed_min_length', validation.min]; - break; - } - } - else if (value[i].length === 0) { - result = ['string_empty']; - break; - } - if (validation.max !== undefined) { - if (value[i].length > validation.max) { - result = ['string_exceed_max_length', validation.max]; - break; - } - } - if (validation.format) { - if (!(validation.format.test(value[i]))) { - result = ['string_invalid_format', validation.format]; - break; - } - } - break; - - case 'number': - if (isNaN(value[i])) { - result = ['number_nan']; - break; - } - if (filter.type == 'integer') { - if (parseInt(value[i]) != value[i]) { - result = ['number_not_integer']; - break; - } - } - else { - if (parseFloat(value[i]) != value[i]) { - result = ['number_not_double']; - break; - } - } - if (validation.min !== undefined) { - if (value[i] < validation.min) { - result = ['number_exceed_min', validation.min]; - break; - } - } - if (validation.max !== undefined) { - if (value[i] > validation.max) { - result = ['number_exceed_max', validation.max]; - break; - } - } - if (validation.step !== undefined) { - var v = value[i]/validation.step; - if (parseInt(v) != v) { - result = ['number_wrong_step', validation.step]; - break; - } - } - break; - - case 'datetime': - // we need MomentJS - if (window.moment && validation.format) { - var datetime = moment(value[i], validation.format); - if (!datetime.isValid()) { - result = ['datetime_invalid']; - break; - } - else { - if (validation.min) { - if (datetime < moment(validation.min, validation.format)) { - result = ['datetime_exceed_min', validation.min]; - break; - } - } - if (validation.max) { - if (datetime > moment(validation.max, validation.format)) { - result = ['datetime_exceed_max', validation.max]; - break; - } - } - } - } - break; - } - } - - if (result !== true) { - break; - } - } - - return this.change('validateValue', result, $rule, value, filter, operator); - }; - - /** - * Remove 'has-error' from everything - */ - QueryBuilder.prototype.clearErrors = function() { - this.$el.find('.has-error').removeClass('has-error'); - }; - - /** - * Trigger a validation error event with custom params - * @param error {array} - * @param $target {jQuery} - * @param value {mixed} - * @param filter {object} - * @param operator {object} - */ - QueryBuilder.prototype.triggerValidationError = function(error, $target, value, filter, operator) { - if (!$.isArray(error)) { - error = [error]; - } - - if (filter && filter.onValidationError) { - filter.onValidationError.call(this, $target, error, value, filter, operator); - } - if (this.settings.onValidationError) { - this.settings.onValidationError.call(this, $target, error, value, filter, operator); - } - - var e = $.Event('validationError.queryBuilder', { - error: error, - filter: filter, - operator: operator, - value: value, - targetRule: $target[0], - builder: this - }); - - this.$el.trigger(e); - - if (this.settings.display_errors && !e.isDefaultPrevented()) { - // translate the text without modifying event array - var errorLoc = $.extend([], error, [ - this.lang.errors[error[0]] || error[0] - ]); - - $target.addClass('has-error'); - var $error = $target.find('.error-container').eq(0); - $error.attr('title', fmt.apply(null, errorLoc)); - } - - this.trigger('validationError', $target, error); - }; - - - /** - * Returns an incremented group ID - * @return {string} - */ - QueryBuilder.prototype.nextGroupId = function() { - return this.$el_id + '_group_' + (this.status.group_id++); - }; - - /** - * Returns an incremented rule ID - * @return {string} - */ - QueryBuilder.prototype.nextRuleId = function() { - return this.$el_id + '_rule_' + (this.status.rule_id++); - }; - - /** - * Returns the operators for a filter - * @param filter {string|object} (filter id name or filter object) - * @return {object[]} - */ - QueryBuilder.prototype.getOperators = function(filter) { - if (typeof filter === 'string') { - filter = this.getFilterById(filter); - } - - var result = []; - - for (var i=0, l=this.operators.length; i
      element) - * @return {string} - */ - QueryBuilder.prototype.getGroupCondition = function($group) { - return $group.find('>.rules-group-header [name$=_cond]:checked').val(); - }; - - /** - * Returns the selected filter of a rule - * @param $rule {jQuery} (
    • element) - * @return {string} - */ - QueryBuilder.prototype.getRuleFilter = function($rule) { - return $rule.find('.rule-filter-container [name$=_filter]').val(); - }; - - /** - * Returns the selected operator of a rule - * @param $rule {jQuery} (
    • element) - * @return {string} - */ - QueryBuilder.prototype.getRuleOperator = function($rule) { - return $rule.find('.rule-operator-container [name$=_operator]').val(); - }; - - /** - * Returns rule value - * @param $rule {jQuery} (
    • element) - * @param filter {object} (optional - current rule filter) - * @param operator {object} (optional - current rule operator) - * @return {string|string[]|undefined} - */ - QueryBuilder.prototype.getRuleValue = function($rule, filter, operator) { - filter = filter || this.getFilterById(this.getRuleFilter($rule)); - operator = operator || this.getOperatorByType(this.getRuleOperator($rule)); - - var value = [], tmp, - $value = $rule.find('.rule-value-container'); - - for (var i=0; i
    • element) - * @param value {mixed} - * @param filter {object} - * @param operator {object} - */ - QueryBuilder.prototype.setRuleValue = function($rule, value, filter, operator) { - filter = filter || this.getFilterById(this.getRuleFilter($rule)); - operator = operator || this.getOperatorByType(this.getRuleOperator($rule)); - - this.trigger('beforeSetRuleValue', $rule, value, filter, operator); - - if (filter.valueSetter) { - filter.valueSetter.call(this, $rule, value, filter, operator); - } - else { - var $value = $rule.find('.rule-value-container'); - - if (operator.accept_values == 1) { - value = [value]; - } - else { - value = value; - } - - for (var i=0; i
    • element) - * @param rule {object} - */ - QueryBuilder.prototype.applyRuleFlags = function($rule, rule) { - var flags = this.getRuleFlags(rule); - $rule.data('queryBuilder').flags = flags; - - if (flags.filter_readonly) { - $rule.find('[name$=_filter]').prop('disabled', true); - } - if (flags.operator_readonly) { - $rule.find('[name$=_operator]').prop('disabled', true); - } - if (flags.value_readonly) { - $rule.find('[name*=_value_]').prop('disabled', true); - } - if (flags.no_delete) { - $rule.find('[data-delete=rule]').remove(); - } - - this.trigger('afterApplyRuleFlags', $rule, rule, flags); - }; - - - /** - * Returns group HTML - * @param group_id {string} - * @param level {int} - * @return {string} - */ - QueryBuilder.prototype.getGroupTemplate = function(group_id, level) { - var h = '\ -
      \ -
      \ -
      \ - \ - '+ (this.settings.allow_groups===-1 || this.settings.allow_groups>=level ? - '' - :'') +' \ - '+ (level>1 ? - '' - : '') +' \ -
      \ -
      \ - '+ this.getGroupConditions(group_id) +' \ -
      \ - '+ (this.settings.display_errors ? - '
      ' - :'') +'\ -
      \ -
      \ -
        \ -
        \ -
        '; - - return this.change('getGroupTemplate', h, level); - }; - - /** - * Returns group conditions HTML - * @param group_id {string} - * @return {string} - */ - QueryBuilder.prototype.getGroupConditions = function(group_id) { - var h = ''; - - for (var i=0, l=this.settings.conditions.length; i \ - '+ label +' \ - '; - } - - return this.change('getGroupConditions', h); - }; - - /** - * Returns rule HTML - * @param rule_id {string} - * @return {string} - */ - QueryBuilder.prototype.getRuleTemplate = function(rule_id) { - var h = '\ -
      • \ -
        \ -
        \ - \ -
        \ -
        \ - '+ (this.settings.display_errors ? - '
        ' - :'') +'\ -
        \ -
        \ -
        \ -
      • '; - - return this.change('getRuleTemplate', h); - }; - - /** - * Returns rule filter '; - h+= ''; - - $.each(this.filters, function(i, filter) { - if (optgroup != filter.optgroup) { - if (optgroup !== null) h+= ''; - optgroup = filter.optgroup; - if (optgroup !== null) h+= ''; - } - - h+= ''; - }); - - if (optgroup !== null) h+= ''; - h+= ''; - - return this.change('getRuleFilterSelect', h); - }; - - /** - * Returns rule operator '; - - for (var i=0, l=operators.length; i'+ label +''; - } - - h+= ''; - - return this.change('getRuleOperatorSelect', h); - }; - - /** - * Return the rule value HTML - * @param $rule {jQuery} - * @param filter {object} - * @param value_id {int} - * @return {string} - */ - QueryBuilder.prototype.getRuleInput = function($rule, filter, value_id) { - var validation = filter.validation || {}, - name = $rule[0].id +'_value_'+ value_id, - h = '', c; - - if (typeof filter.input === 'function') { - h = filter.input.call(this, $rule, filter, name); - } - else { - switch (filter.input) { - case 'radio': - c = filter.vertical ? ' class=block' : ''; - iterateOptions(filter.values, function(key, val) { - h+= ' '+ val +' '; - }); - break; - - case 'checkbox': - c = filter.vertical ? ' class=block' : ''; - iterateOptions(filter.values, function(key, val) { - h+= ' '+ val +' '; - }); - break; - - case 'select': - h+= ''; - break; - - case 'textarea': - h+= '";break;default:switch(c.internalType){case"number":h+='1&&$.error("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 j(this,c)),"string"==typeof a?b[a].apply(b,Array.prototype.slice.call(arguments,1)):this):this},$.fn.queryBuilder.defaults={set:function(a){$.extendext(!0,"replace",j.DEFAULTS,a)},get:function(a){var b=j.DEFAULTS;return a&&(b=b[a]),$.extend(!0,{},b)}},$.fn.queryBuilder.constructor=j,$.fn.queryBuilder.extend=j.extend,$.fn.queryBuilder.define=j.define,$.fn.queryBuilder.define("bt-selectpicker",function(a){$.fn.selectpicker&&$.fn.selectpicker.Constructor||$.error('Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select'),a=$.extend({container:"body",style:"btn-inverse btn-xs",width:"auto",showIcon:!1},a||{}),this.on("afterAddRule",function(b){b.find(".rule-filter-container select").selectpicker(a)}),this.on("afterCreateRuleOperators",function(b){b.find(".rule-operator-container select").selectpicker(a)})}),$.fn.queryBuilder.define("bt-tooltip-errors",function(a){$.fn.tooltip&&$.fn.tooltip.Constructor&&$.fn.tooltip.Constructor.prototype.fixTitle||$.error('Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com'),a=$.extend({placement:"right"},a||{}),this.on("getRuleTemplate",function(a){return a.replace('class="error-container"','class="error-container" data-toggle="tooltip"')}),this.on("validationError",function(b){b.find(".error-container").eq(0).tooltip(a).tooltip("hide").tooltip("fixTitle")})}),$.fn.queryBuilder.define("filter-description",function(a){a=$.extend({icon:"glyphicon glyphicon-info-sign",mode:"popover"},a||{}),"inline"===a.mode?this.on("afterUpdateRuleFilter",function(b,c){var d=b.find("p.filter-description");c&&c.description?(0===d.length?(d=$('

        '),d.appendTo(b)):d.show(),d.html(' '+c.description)):d.hide()}):"popover"===a.mode?($.fn.popover&&$.fn.popover.Constructor&&$.fn.popover.Constructor.prototype.fixTitle||$.error('Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com'),this.on("afterUpdateRuleFilter",function(b,c){var d=b.find("button.filter-description");c&&c.description?(0===d.length?(d=$(''),d.prependTo(b.find(".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.description,d.attr("aria-describedby")&&d.popover("show")):(d.hide(),d.data("bs.popover")&&d.popover("hide"))})):"bootbox"===a.mode&&(window.bootbox||$.error('Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com'),this.on("afterUpdateRuleFilter",function(b,c){var d=b.find("button.filter-description");c&&c.description?(0===d.length&&(d=$(''),d.prependTo(b.find(".rule-actions")),d.on("click",function(){bootbox.alert(d.data("description"))})),d.data("description",c.description)):d.hide()}))}),$.fn.queryBuilder.defaults.set({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]}},begins_with:function(a){return{$regex:"^"+e(a[0])}},not_begins_with:function(a){return{$regex:"^(?!"+e(a[0])+")"}},contains:function(a){return{$regex:e(a[0])}},not_contains:function(a){return{$regex:"^((?!"+e(a[0])+").)*$",$options:"s"}},ends_with:function(a){return{$regex:e(a[0])+"$"}},not_ends_with:function(a){return{$regex:"(?0)e.push(c(f));else{var g=b.settings.mongoOperators[f.operator],h=b.getOperatorByType(f.operator),i=[];void 0===g&&$.error("MongoDB operation unknown for operator "+f.operator),h.accept_values&&(f.value instanceof Array||(f.value=[f.value]),f.value.forEach(function(a){i.push(d(a,f.type))}));var j={};j[f.field]=g.call(b,i),e.push(j)}});var f={};return e.length>0&&(f["$"+a.condition.toLowerCase()]=e),f}(a)}}),$.fn.queryBuilder.define("sortable",function(a){a=$.extend({default_no_sortable:!1,icon:"glyphicon glyphicon-sort"},a||{}),this.on("afterInit",function(){$.event.props.push("dataTransfer");var a,b,c=this;this.$el.on("mouseover",".drag-handle",function(){c.$el.find(".rule-container, .rules-group-container").attr("draggable",!0)}),this.$el.on("mouseout",".drag-handle",function(){c.$el.find(".rule-container, .rules-group-container").removeAttr("draggable")}),this.$el.on("dragstart","[draggable]",function(c){c.stopPropagation(),c.dataTransfer.setData("text","drag"),b=$(c.target),a=$('
         
        '),a.css("min-height",b.height()),a.insertAfter(b),setTimeout(function(){b.hide()},0)}),this.$el.on("dragenter","[draggable]",function(b){b.preventDefault(),b.stopPropagation(),f(a,$(b.target))}),this.$el.on("dragover","[draggable]",function(a){a.preventDefault(),a.stopPropagation()}),this.$el.on("drop",function(a){a.preventDefault(),a.stopPropagation(),f(b,$(a.target))}),this.$el.on("dragend","[draggable]",function(d){d.preventDefault(),d.stopPropagation(),b.show(),a.remove(),b=a=null,c.$el.find(".rule-container, .rules-group-container").removeAttr("draggable")})}),this.on("getRuleFlags",function(b){return void 0===b.no_sortable&&(b.no_sortable=a.default_no_sortable),b}),this.on("afterApplyRuleFlags",function(a,b,c){c.no_sortable&&a.find(".drag-handle").remove()}),this.on("getGroupTemplate",function(b,c){if(c>1){var d=$(b);d.find(".group-conditions").after('
        '),b=d.prop("outerHTML")}return b}),this.on("getRuleTemplate",function(b){var c=$(b);return c.find(".rule-header").after('
        '),c.prop("outerHTML")})}),$.fn.queryBuilder.defaults.set({sqlOperators:{equal:"= ?",not_equal:"!= ?","in":{op:"IN(?)",list:!0,sep:", "},not_in:{op:"NOT IN(?)",list:!0,sep:", "},less:"< ?",less_or_equal:"<= ?",greater:"> ?",greater_or_equal:">= ?",between:{op:"BETWEEN ?",list:!0,sep:" AND "},begins_with:{op:"LIKE(?)",fn:function(a){return a+"%"}},not_begins_with:{op:"NOT LIKE(?)",fn:function(a){return a+"%"}},contains:{op:"LIKE(?)",fn:function(a){return"%"+a+"%"}},not_contains:{op:"NOT LIKE(?)",fn:function(a){return"%"+a+"%"}},ends_with:{op:"LIKE(?)",fn:function(a){return"%"+a}},not_ends_with:{op:"NOT LIKE(?)",fn:function(a){return"%"+a}},is_empty:'== ""',is_not_empty:'!= ""',is_null:"IS NULL",is_not_null:"IS NOT NULL"}}),$.fn.queryBuilder.extend({getSQL:function(a,b,c){c=void 0===c?this.getRules():c,a=a===!0||void 0===a?"question_mark":a,b=b||void 0===b?"\n":" ";var e=this,f=1,h=[],i=function j(c){if(c.condition||(c.condition=e.settings.default_condition),-1===["AND","OR"].indexOf(c.condition.toUpperCase())&&$.error("Unable to build SQL query with "+c.condition+" condition"),!c.rules)return"";var i=[];return $.each(c.rules,function(c,k){if(k.rules&&k.rules.length>0)i.push("("+b+j(k)+b+")"+b);else{var l=e.getSqlOperator(k.operator),m=e.getOperatorByType(k.operator),n="";l===!1&&$.error("SQL operation unknown for operator "+k.operator),m.accept_values&&(k.value instanceof Array?!l.list&&k.value.length>1&&$.error("Operator "+k.operator+" cannot accept multiple values"):k.value=[k.value],k.value.forEach(function(b,c){c>0&&(n+=l.sep),"integer"==k.type||"double"==k.type?b=d(b,k.type):a||(b=g(b)),l.fn&&(b=l.fn(b)),a?(n+="question_mark"==a?"?":"$"+f,h.push(b),f++):("string"==typeof b&&(b="'"+b+"'"),n+=b)})),i.push(k.field+" "+l.op.replace(/\?/,n))}}),i.join(" "+c.condition+b)}(c);return a?{sql:i,params:h}:{sql:i}},getSqlOperator:function(a){var b=this.settings.sqlOperators[a];return void 0===b?!1:("string"==typeof b&&(b={op:b}),b.list||(b.list=!1),b.list&&!b.sep&&(b.sep=", "),b)}})}); \ No newline at end of file diff --git a/dist/scss/dark.scss b/dist/scss/dark.scss new file mode 100644 index 00000000..f9c5ecde --- /dev/null +++ b/dist/scss/dark.scss @@ -0,0 +1,19 @@ +/*! + * 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); +$group-border-color: #00164A; + +$rule-background-color: rgba(40, 40, 40, .9); +$rule-border-color: #111; + +$error-border-color: #800; +$error-background-color: #322; + +$ticks-color: #222; + +@import 'default'; diff --git a/dist/scss/default.scss b/dist/scss/default.scss new file mode 100644 index 00000000..8c2b9c0d --- /dev/null +++ b/dist/scss/default.scss @@ -0,0 +1,178 @@ +/*! + * 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 +$item-vertical-spacing: 4px !default; +$item-border-radius: 5px !default; + +// groups +$group-background-color: rgba(250, 240, 210, .5) !default; +$group-border-color: #DCC896 !default; +$group-border: 1px solid $group-border-color !default; +$group-padding: 10px !default; + +// rules +$rule-background-color: rgba(255, 255, 255, .9) !default; +$rule-border-color: #EEE !default; +$rule-border: 1px solid $rule-border-color !default; +$rule-padding: 5px !default; +// scss-lint:disable ColorVariable +$rule-value-separator: 1px solid #DDD !default; + +// errors +$error-icon-color: #F00 !default; +$error-border-color: #F99 !default; +$error-background-color: #FDD !default; + +// ticks +$ticks-width: 2px !default; +$ticks-color: #CCC !default; +$ticks-position: 5px, 10px !default; + + +// ABSTRACTS +%base-container { + position: relative; + margin: $item-vertical-spacing 0; + border-radius: $item-border-radius; + padding: $rule-padding; + border: $rule-border; + background: $rule-background-color; +} + +%rule-component { + display: inline-block; + margin: 0 5px 0 0; + vertical-align: middle; +} + +.query-builder { + + // GROUPS + .rules-group-container { + @extend %base-container; + + padding: $group-padding; + padding-bottom: #{$group-padding - $item-vertical-spacing}; + border: $group-border; + background: $group-background-color; + } + + .rules-group-header { + margin-bottom: $group-padding; + + .group-conditions { + .btn.readonly:not(.active), + 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 { + border-radius: 3px; + } + } + } + + .rules-list { + list-style: none; + padding: 0 0 0 #{nth($ticks-position, 1) + nth($ticks-position, 2)}; + margin: 0; + } + + // RULES + .rule-container { + @extend %base-container; + + .rule-filter-container, + .rule-operator-container, + .rule-value-container { + @extend %rule-component; + } + } + + .rule-value-container { + border-left: $rule-value-separator; + padding-left: 5px; + + label { + margin-bottom: 0; + font-weight: normal; + + &.block { + display: block; + } + } + } + + // ERRORS + .error-container { + @extend %rule-component; + display: none; + cursor: help; + color: $error-icon-color; + } + + .has-error { + background-color: $error-background-color; + border-color: $error-border-color; + + .error-container { + display: inline-block !important; + } + } + + // TICKS + .rules-list>* { + &::before, + &::after { + content: ''; + position: absolute; + left: #{-1 * nth($ticks-position, 2)}; + width: nth($ticks-position, 2); + height: calc(50% + #{$item-vertical-spacing}); + border-color: $ticks-color; + border-style: solid; + } + + &::before { + top: #{-2 * $ticks-width}; + border-width: 0 0 $ticks-width $ticks-width; + } + + &::after { + top: 50%; + border-width: 0 0 0 $ticks-width; + } + + &:first-child::before { + top: #{-$group-padding - $ticks-width}; + height: calc(50% + #{$group-padding + $item-vertical-spacing}); + } + + &:last-child::before { + border-radius: 0 0 0 #{2 * $ticks-width}; + } + + &:last-child::after { + display: none; + } + } +} + +@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/bt-tooltip-errors.scss b/dist/scss/plugins/bt-tooltip-errors.scss new file mode 100644 index 00000000..21323e5f --- /dev/null +++ b/dist/scss/plugins/bt-tooltip-errors.scss @@ -0,0 +1,9 @@ +$error-tooltip-color: #F99; + +@if $theme-name == 'dark' { + $error-tooltip-color: #F22; +} + +.query-builder .error-container + .tooltip .tooltip-inner { + color: $error-tooltip-color !important; +} diff --git a/dist/scss/plugins/filter-description.scss b/dist/scss/plugins/filter-description.scss new file mode 100644 index 00000000..41498718 --- /dev/null +++ b/dist/scss/plugins/filter-description.scss @@ -0,0 +1,21 @@ +$description-background-color: #D9EDF7; +$description-border-color: #BCE8F1; +$description-text-color: #31708F; + +@if $theme-name == 'dark' { + $description-background-color: rgba(0, 170, 255, .2); + $description-text-color: #AAD1E4; + $description-border-color: #346F7B; +} + +$description-border: 1px solid $description-border-color; + +.query-builder p.filter-description { + margin: $rule-padding 0 0 0; + background: $description-background-color; + border: $description-border; + color: $description-text-color; + border-radius: $item-border-radius; + padding: #{$rule-padding * .5} $rule-padding; + font-size: .8em; +} diff --git a/dist/scss/plugins/invert.scss b/dist/scss/plugins/invert.scss new file mode 100644 index 00000000..5eb0458b --- /dev/null +++ b/dist/scss/plugins/invert.scss @@ -0,0 +1,5 @@ +.query-builder { + .rules-group-header [data-invert] { + margin-left: 5px; + } +} diff --git a/dist/scss/plugins/sortable.scss b/dist/scss/plugins/sortable.scss new file mode 100644 index 00000000..ac902fe1 --- /dev/null +++ b/dist/scss/plugins/sortable.scss @@ -0,0 +1,28 @@ +$placeholder-border-color: #BBB; +$placeholder-border: 1px dashed $placeholder-border-color; + +.query-builder { + .drag-handle { + @extend %rule-component; + cursor: move; + vertical-align: middle; + margin-left: 5px; + } + + .dragging { + position: fixed; + opacity: .5; + z-index: 100; + + &::before, + &::after { + display: none; + } + } + + .rule-placeholder { + @extend %base-container; + border: $placeholder-border; + opacity: .7; + } +} diff --git a/examples/index.html b/examples/index.html index 5c915818..180652b8 100644 --- a/examples/index.html +++ b/examples/index.html @@ -1,35 +1,94 @@ - + - - - - - - + jQuery QueryBuilder Example + + + + + + + +
        -
        -

        Bootstrap Slider and Bootbox must be installed with Bower to run this demo.

        - +
        + + +
        + +
        + + +
        + + + +
        +
        - +
        - - + +
        - + +
        + + + + +
        +
        + - - +
        @@ -39,267 +98,558 @@

        Output

        + + + + + + + + + + - - - - - - - - - - - - + + }); + + // set filters + $('.set-filters').on('click', function() { + $(this).prop('disabled', true); + bootstrap.Tooltip.getInstance($(this)).hide(); + + // add a new filter after 'state' + $('#builder').queryBuilder('addFilter', + { + id: 'new_one', + label: 'New filter', + type: 'string' + }, + 'state' + ); + + // remove filter 'coord' + $('#builder').queryBuilder('removeFilter', + ['coord', 'state', 'bson'], + true + ); + // also available : 'setFilters' + }); + diff --git a/examples/screenshot.png b/examples/screenshot.png new file mode 100644 index 00000000..df0136c8 Binary files /dev/null and b/examples/screenshot.png differ diff --git a/package.json b/package.json index 5691b66e..ed1b750a 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,40 @@ { "name": "jQuery-QueryBuilder", - "version": "1.4.1", + "version": "3.0.0", "author": { "name": "Damien \"Mistic\" Sorel", - "homepage": "http://www.strangeplanet.fr" + "email": "contact@git.strangeplanet.fr", + "url": "https://www.strangeplanet.fr" }, "description": "jQuery plugin for user friendly query/filter creator", - "main": "dist/query-builder.js", + "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": { - "qunitjs": "~1.16.0", - "grunt": "~0.4.5", - "grunt-contrib-qunit": "~0.5.2", - "grunt-contrib-uglify": "~0.5.0", - "grunt-contrib-cssmin": "~0.9.0", - "grunt-contrib-copy": "~0.5.0", - "grunt-contrib-concat": "~0.4.0", - "grunt-contrib-jshint": "~0.10.0", - "grunt-wrap": "~0.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", @@ -25,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" @@ -34,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 ac68993a..2b326090 100644 --- a/src/.wrapper.js +++ b/src/.wrapper.js @@ -1,12 +1,18 @@ (function(root, factory) { - if (typeof define === 'function' && define.amd) { - define(['jquery', 'microevent', 'jQuery.extendext'], factory); + if (typeof define == 'function' && define.amd) { + define(['jquery', 'jquery-extendext'], factory); + } + else if (typeof module === 'object' && module.exports) { + module.exports = factory(require('jquery'), require('jquery-extendext')); } else { - factory(root.jQuery, root.MicroEvent); + factory(root.jQuery); } -}(this, function($, MicroEvent) { - "use strict"; - - @@js -})); \ No newline at end of file +}(this, function($) { +"use strict"; + +@@js + +return QueryBuilder; + +})); diff --git a/src/core.js b/src/core.js new file mode 100644 index 00000000..bcb7c912 --- /dev/null +++ b/src/core.js @@ -0,0 +1,981 @@ +/** + * Final initialisation of the builder + * @param {object} [rules] + * @fires QueryBuilder.afterInit + * @private + */ +QueryBuilder.prototype.init = function(rules) { + /** + * When the initilization is done, just before creating the root group + * @event afterInit + * @memberof QueryBuilder + */ + this.trigger('afterInit'); + + if (rules) { + this.setRules(rules); + delete this.settings.rules; + } + else { + this.setRoot(true); + } +}; + +/** + * Checks the configuration of each filter + * @param {QueryBuilder.Filter[]} filters + * @returns {QueryBuilder.Filter[]} + * @throws ConfigError + */ +QueryBuilder.prototype.checkFilters = function(filters) { + var definedFilters = []; + + if (!filters || filters.length === 0) { + Utils.error('Config', 'Missing filters list'); + } + + filters.forEach(function(filter, i) { + if (!filter.id) { + Utils.error('Config', 'Missing filter {0} id', i); + } + if (definedFilters.indexOf(filter.id) != -1) { + Utils.error('Config', 'Filter "{0}" already defined', filter.id); + } + definedFilters.push(filter.id); + + if (!filter.type) { + filter.type = 'string'; + } + else if (!QueryBuilder.types[filter.type]) { + Utils.error('Config', 'Invalid type "{0}"', filter.type); + } + + if (!filter.input) { + filter.input = QueryBuilder.types[filter.type] === 'number' ? 'number' : 'text'; + } + else if (typeof filter.input != 'function' && QueryBuilder.inputs.indexOf(filter.input) == -1) { + Utils.error('Config', 'Invalid input "{0}"', filter.input); + } + + if (filter.operators) { + filter.operators.forEach(function(operator) { + if (typeof operator != 'string') { + Utils.error('Config', 'Filter operators must be global operators types (string)'); + } + }); + } + + if (!filter.field) { + filter.field = filter.id; + } + if (!filter.label) { + filter.label = filter.field; + } + + if (!filter.optgroup) { + filter.optgroup = null; + } + else { + this.status.has_optgroup = true; + + // register optgroup if needed + if (!this.settings.optgroups[filter.optgroup]) { + this.settings.optgroups[filter.optgroup] = filter.optgroup; + } + } + + switch (filter.input) { + case 'radio': + case 'checkbox': + if (!filter.values || filter.values.length < 1) { + Utils.error('Config', 'Missing filter "{0}" values', filter.id); + } + break; + + case 'select': + var cleanValues = []; + filter.has_optgroup = false; + + Utils.iterateOptions(filter.values, function(value, label, optgroup) { + cleanValues.push({ + value: value, + label: label, + optgroup: optgroup || null + }); + + if (optgroup) { + filter.has_optgroup = true; + + // register optgroup if needed + if (!this.settings.optgroups[optgroup]) { + this.settings.optgroups[optgroup] = optgroup; + } + } + }.bind(this)); + + if (filter.has_optgroup) { + filter.values = Utils.groupSort(cleanValues, 'optgroup'); + } + else { + filter.values = cleanValues; + } + + if (filter.placeholder) { + if (filter.placeholder_value === undefined) { + filter.placeholder_value = -1; + } + + filter.values.forEach(function(entry) { + if (entry.value == filter.placeholder_value) { + Utils.error('Config', 'Placeholder of filter "{0}" overlaps with one of its values', filter.id); + } + }); + } + break; + } + }, this); + + if (this.settings.sort_filters) { + if (typeof this.settings.sort_filters == 'function') { + filters.sort(this.settings.sort_filters); + } + else { + var self = this; + filters.sort(function(a, b) { + return self.translate(a.label).localeCompare(self.translate(b.label)); + }); + } + } + + if (this.status.has_optgroup) { + filters = Utils.groupSort(filters, 'optgroup'); + } + + return filters; +}; + +/** + * Checks the configuration of each operator + * @param {QueryBuilder.Operator[]} operators + * @returns {QueryBuilder.Operator[]} + * @throws ConfigError + */ +QueryBuilder.prototype.checkOperators = function(operators) { + var definedOperators = []; + + operators.forEach(function(operator, i) { + if (typeof operator == 'string') { + if (!QueryBuilder.OPERATORS[operator]) { + Utils.error('Config', 'Unknown operator "{0}"', operator); + } + + operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator]); + } + else { + if (!operator.type) { + Utils.error('Config', 'Missing "type" for operator {0}', i); + } + + if (QueryBuilder.OPERATORS[operator.type]) { + operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator.type], operator); + } + + if (operator.nb_inputs === undefined || operator.apply_to === undefined) { + Utils.error('Config', 'Missing "nb_inputs" and/or "apply_to" for operator "{0}"', operator.type); + } + } + + if (definedOperators.indexOf(operator.type) != -1) { + Utils.error('Config', 'Operator "{0}" already defined', operator.type); + } + definedOperators.push(operator.type); + + if (!operator.optgroup) { + operator.optgroup = null; + } + else { + this.status.has_operator_optgroup = true; + + // register optgroup if needed + if (!this.settings.optgroups[operator.optgroup]) { + this.settings.optgroups[operator.optgroup] = operator.optgroup; + } + } + }, this); + + if (this.status.has_operator_optgroup) { + operators = Utils.groupSort(operators, 'optgroup'); + } + + return operators; +}; + +/** + * Adds all events listeners to the builder + * @private + */ +QueryBuilder.prototype.bindEvents = function() { + var self = this; + var Selectors = QueryBuilder.selectors; + + // group condition change + this.$el.on('change.queryBuilder', Selectors.group_condition, function() { + if ($(this).is(':checked')) { + var $group = $(this).closest(Selectors.group_container); + self.getModel($group).condition = $(this).val(); + } + }); + + // rule filter change + this.$el.on('change.queryBuilder', Selectors.rule_filter, function() { + var $rule = $(this).closest(Selectors.rule_container); + self.getModel($rule).filter = self.getFilterById($(this).val()); + }); + + // rule operator change + this.$el.on('change.queryBuilder', Selectors.rule_operator, function() { + var $rule = $(this).closest(Selectors.rule_container); + self.getModel($rule).operator = self.getOperatorByType($(this).val()); + }); + + // add rule button + this.$el.on('click.queryBuilder', Selectors.add_rule, function() { + var $group = $(this).closest(Selectors.group_container); + self.addRule(self.getModel($group)); + }); + + // delete rule button + this.$el.on('click.queryBuilder', Selectors.delete_rule, function() { + var $rule = $(this).closest(Selectors.rule_container); + self.deleteRule(self.getModel($rule)); + }); + + if (this.settings.allow_groups !== 0) { + // add group button + this.$el.on('click.queryBuilder', Selectors.add_group, function() { + var $group = $(this).closest(Selectors.group_container); + self.addGroup(self.getModel($group)); + }); + + // delete group button + this.$el.on('click.queryBuilder', Selectors.delete_group, function() { + var $group = $(this).closest(Selectors.group_container); + self.deleteGroup(self.getModel($group)); + }); + } + + // model events + this.model.on({ + 'drop': function(e, node) { + node.$el.remove(); + self.refreshGroupsConditions(); + }, + 'add': function(e, parent, node, index) { + if (index === 0) { + node.$el.prependTo(parent.$el.find('>' + QueryBuilder.selectors.rules_list)); + } + else { + node.$el.insertAfter(parent.rules[index - 1].$el); + } + self.refreshGroupsConditions(); + }, + 'move': function(e, node, group, index) { + node.$el.detach(); + + if (index === 0) { + node.$el.prependTo(group.$el.find('>' + QueryBuilder.selectors.rules_list)); + } + else { + node.$el.insertAfter(group.rules[index - 1].$el); + } + self.refreshGroupsConditions(); + }, + 'update': function(e, node, field, value, oldValue) { + if (node instanceof Rule) { + switch (field) { + case 'error': + self.updateError(node); + break; + + case 'flags': + self.applyRuleFlags(node); + break; + + case 'filter': + self.updateRuleFilter(node, oldValue); + break; + + case 'operator': + self.updateRuleOperator(node, oldValue); + break; + + case 'value': + self.updateRuleValue(node, oldValue); + break; + } + } + else { + switch (field) { + case 'error': + self.updateError(node); + break; + + case 'flags': + self.applyGroupFlags(node); + break; + + case 'condition': + self.updateGroupCondition(node, oldValue); + break; + } + } + } + }); +}; + +/** + * Creates the root group + * @param {boolean} [addRule=true] - adds a default empty rule + * @param {object} [data] - group custom data + * @param {object} [flags] - flags to apply to the group + * @returns {Group} root group + * @fires QueryBuilder.afterAddGroup + */ +QueryBuilder.prototype.setRoot = function(addRule, data, flags) { + addRule = (addRule === undefined || addRule === true); + + var group_id = this.nextGroupId(); + var $group = $($.parseHTML(this.getGroupTemplate(group_id, 1))); + + this.$el.append($group); + this.model.root = new Group(null, $group); + this.model.root.model = this.model; + + this.model.root.data = data; + this.model.root.flags = $.extend({}, this.settings.default_group_flags, flags); + this.model.root.condition = this.settings.default_condition; + + this.trigger('afterAddGroup', this.model.root); + + if (addRule) { + this.addRule(this.model.root); + } + + return this.model.root; +}; + +/** + * Adds a new group + * @param {Group} parent + * @param {boolean} [addRule=true] - adds a default empty rule + * @param {object} [data] - group custom data + * @param {object} [flags] - flags to apply to the group + * @returns {Group} + * @fires QueryBuilder.beforeAddGroup + * @fires QueryBuilder.afterAddGroup + */ +QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) { + addRule = (addRule === undefined || addRule === true); + + var level = parent.level + 1; + + /** + * Just before adding a group, can be prevented. + * @event beforeAddGroup + * @memberof QueryBuilder + * @param {Group} parent + * @param {boolean} addRule - if an empty rule will be added in the group + * @param {int} level - nesting level of the group, 1 is the root group + */ + var e = this.trigger('beforeAddGroup', parent, addRule, level); + if (e.isDefaultPrevented()) { + return null; + } + + var group_id = this.nextGroupId(); + var $group = $(this.getGroupTemplate(group_id, level)); + var model = parent.addGroup($group); + + model.data = data; + model.flags = $.extend({}, this.settings.default_group_flags, flags); + model.condition = this.settings.default_condition; + + /** + * Just after adding a group + * @event afterAddGroup + * @memberof QueryBuilder + * @param {Group} group + */ + this.trigger('afterAddGroup', model); + + /** + * After any change in the rules + * @event rulesChanged + * @memberof QueryBuilder + */ + this.trigger('rulesChanged'); + + if (addRule) { + this.addRule(model); + } + + return model; +}; + +/** + * Tries to delete a group. The group is not deleted if at least one rule is flagged `no_delete`. + * @param {Group} group + * @returns {boolean} if the group has been deleted + * @fires QueryBuilder.beforeDeleteGroup + * @fires QueryBuilder.afterDeleteGroup + */ +QueryBuilder.prototype.deleteGroup = function(group) { + if (group.isRoot()) { + return false; + } + + /** + * Just before deleting a group, can be prevented + * @event beforeDeleteGroup + * @memberof QueryBuilder + * @param {Group} parent + */ + var e = this.trigger('beforeDeleteGroup', group); + if (e.isDefaultPrevented()) { + return false; + } + + var del = true; + + group.each('reverse', function(rule) { + del &= this.deleteRule(rule); + }, function(group) { + del &= this.deleteGroup(group); + }, this); + + if (del) { + group.drop(); + + /** + * Just after deleting a group + * @event afterDeleteGroup + * @memberof QueryBuilder + */ + this.trigger('afterDeleteGroup'); + + this.trigger('rulesChanged'); + } + + return del; +}; + +/** + * Performs actions when a group's condition changes + * @param {Group} group + * @param {object} previousCondition + * @fires QueryBuilder.afterUpdateGroupCondition + * @private + */ +QueryBuilder.prototype.updateGroupCondition = function(group, previousCondition) { + group.$el.find('>' + QueryBuilder.selectors.group_condition).each(function() { + var $this = $(this); + $this.prop('checked', $this.val() === group.condition); + $this.parent().toggleClass('active', $this.val() === group.condition); + }); + + /** + * After the group condition has been modified + * @event afterUpdateGroupCondition + * @memberof QueryBuilder + * @param {Group} group + * @param {object} previousCondition + */ + this.trigger('afterUpdateGroupCondition', group, previousCondition); + + this.trigger('rulesChanged'); +}; + +/** + * Updates the visibility of conditions based on number of rules inside each group + * @private + */ +QueryBuilder.prototype.refreshGroupsConditions = function() { + (function walk(group) { + if (!group.flags || (group.flags && !group.flags.condition_readonly)) { + group.$el.find('>' + QueryBuilder.selectors.group_condition).prop('disabled', group.rules.length <= 1) + .parent().toggleClass('disabled', group.rules.length <= 1); + } + + group.each(null, function(group) { + walk(group); + }, this); + }(this.model.root)); +}; + +/** + * Adds a new rule + * @param {Group} parent + * @param {object} [data] - rule custom data + * @param {object} [flags] - flags to apply to the rule + * @returns {Rule} + * @fires QueryBuilder.beforeAddRule + * @fires QueryBuilder.afterAddRule + * @fires QueryBuilder.changer:getDefaultFilter + */ +QueryBuilder.prototype.addRule = function(parent, data, flags) { + /** + * Just before adding a rule, can be prevented + * @event beforeAddRule + * @memberof QueryBuilder + * @param {Group} parent + */ + var e = this.trigger('beforeAddRule', parent); + if (e.isDefaultPrevented()) { + return null; + } + + var rule_id = this.nextRuleId(); + var $rule = $($.parseHTML(this.getRuleTemplate(rule_id))); + var model = parent.addRule($rule); + + model.data = data; + model.flags = $.extend({}, this.settings.default_rule_flags, flags); + + /** + * Just after adding a rule + * @event afterAddRule + * @memberof QueryBuilder + * @param {Rule} rule + */ + this.trigger('afterAddRule', model); + + this.trigger('rulesChanged'); + + this.createRuleFilters(model); + + if (this.settings.default_filter || !this.settings.display_empty_filter) { + /** + * Modifies the default filter for a rule + * @event changer:getDefaultFilter + * @memberof QueryBuilder + * @param {QueryBuilder.Filter} filter + * @param {Rule} rule + * @returns {QueryBuilder.Filter} + */ + model.filter = this.change('getDefaultFilter', + this.getFilterById(this.settings.default_filter || this.filters[0].id), + model + ); + } + + return model; +}; + +/** + * Tries to delete a rule + * @param {Rule} rule + * @returns {boolean} if the rule has been deleted + * @fires QueryBuilder.beforeDeleteRule + * @fires QueryBuilder.afterDeleteRule + */ +QueryBuilder.prototype.deleteRule = function(rule) { + if (rule.flags.no_delete) { + return false; + } + + /** + * Just before deleting a rule, can be prevented + * @event beforeDeleteRule + * @memberof QueryBuilder + * @param {Rule} rule + */ + var e = this.trigger('beforeDeleteRule', rule); + if (e.isDefaultPrevented()) { + return false; + } + + rule.drop(); + + /** + * Just after deleting a rule + * @event afterDeleteRule + * @memberof QueryBuilder + */ + this.trigger('afterDeleteRule'); + + this.trigger('rulesChanged'); + + return true; +}; + +/** + * Creates the filters for a rule + * @param {Rule} rule + * @fires QueryBuilder.changer:getRuleFilters + * @fires QueryBuilder.afterCreateRuleFilters + * @private + */ +QueryBuilder.prototype.createRuleFilters = function(rule) { + /** + * Modifies the list a filters available for a rule + * @event changer:getRuleFilters + * @memberof QueryBuilder + * @param {QueryBuilder.Filter[]} filters + * @param {Rule} rule + * @returns {QueryBuilder.Filter[]} + */ + var filters = this.change('getRuleFilters', this.filters, rule); + var $filterSelect = $($.parseHTML(this.getRuleFilterSelect(rule, filters))); + + rule.$el.find(QueryBuilder.selectors.filter_container).html($filterSelect); + + /** + * After creating the dropdown for filters + * @event afterCreateRuleFilters + * @memberof QueryBuilder + * @param {Rule} rule + */ + this.trigger('afterCreateRuleFilters', rule); + + this.applyRuleFlags(rule); +}; + +/** + * Creates the operators for a rule and init the rule operator + * @param {Rule} rule + * @fires QueryBuilder.afterCreateRuleOperators + * @private + */ +QueryBuilder.prototype.createRuleOperators = function(rule) { + var $operatorContainer = rule.$el.find(QueryBuilder.selectors.operator_container).empty(); + + if (!rule.filter) { + return; + } + + var operators = this.getOperators(rule.filter); + var $operatorSelect = $($.parseHTML(this.getRuleOperatorSelect(rule, operators))); + + $operatorContainer.html($operatorSelect); + + // set the operator without triggering update event + if (rule.filter.default_operator) { + rule.__.operator = this.getOperatorByType(rule.filter.default_operator); + } + else { + rule.__.operator = operators[0]; + } + + rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type); + + /** + * After creating the dropdown for operators + * @event afterCreateRuleOperators + * @memberof QueryBuilder + * @param {Rule} rule + * @param {QueryBuilder.Operator[]} operators - allowed operators for this rule + */ + this.trigger('afterCreateRuleOperators', rule, operators); + + this.applyRuleFlags(rule); +}; + +/** + * Creates the main input for a rule + * @param {Rule} rule + * @fires QueryBuilder.afterCreateRuleInput + * @private + */ +QueryBuilder.prototype.createRuleInput = function(rule) { + var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container).empty(); + + rule.__.value = undefined; + + if (!rule.filter || !rule.operator || rule.operator.nb_inputs === 0) { + return; + } + + var self = this; + var $inputs = $(); + var filter = rule.filter; + + for (var i = 0; i < rule.operator.nb_inputs; i++) { + var $ruleInput = $($.parseHTML($.trim(this.getRuleInput(rule, i)))); + if (i > 0) $valueContainer.append(this.settings.inputs_separator); + $valueContainer.append($ruleInput); + $inputs = $inputs.add($ruleInput); + } + + $valueContainer.css('display', ''); + + $inputs.on('change ' + (filter.input_event || ''), function() { + if (!rule._updating_input) { + rule._updating_value = true; + rule.value = self.getRuleInputValue(rule); + rule._updating_value = false; + } + }); + + if (filter.plugin) { + $inputs[filter.plugin](filter.plugin_config || {}); + } + + /** + * After creating the input for a rule and initializing optional plugin + * @event afterCreateRuleInput + * @memberof QueryBuilder + * @param {Rule} rule + */ + this.trigger('afterCreateRuleInput', rule); + + if (filter.default_value !== undefined) { + rule.value = filter.default_value; + } + else { + rule._updating_value = true; + rule.value = self.getRuleInputValue(rule); + rule._updating_value = false; + } + + this.applyRuleFlags(rule); +}; + +/** + * Performs action when a rule's filter changes + * @param {Rule} rule + * @param {object} previousFilter + * @fires QueryBuilder.afterUpdateRuleFilter + * @private + */ +QueryBuilder.prototype.updateRuleFilter = function(rule, previousFilter) { + this.createRuleOperators(rule); + this.createRuleInput(rule); + + rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1'); + + // clear rule data if the filter changed + if (previousFilter && rule.filter && previousFilter.id !== rule.filter.id) { + rule.data = undefined; + } + + /** + * After the filter has been updated and the operators and input re-created + * @event afterUpdateRuleFilter + * @memberof QueryBuilder + * @param {Rule} rule + * @param {object} previousFilter + */ + this.trigger('afterUpdateRuleFilter', rule, previousFilter); + + this.trigger('rulesChanged'); +}; + +/** + * Performs actions when a rule's operator changes + * @param {Rule} rule + * @param {object} previousOperator + * @fires QueryBuilder.afterUpdateRuleOperator + * @private + */ +QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) { + var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container); + + if (!rule.operator || rule.operator.nb_inputs === 0) { + $valueContainer.hide(); + + rule.__.value = undefined; + } + else { + $valueContainer.css('display', ''); + + if ($valueContainer.is(':empty') || !previousOperator || + rule.operator.nb_inputs !== previousOperator.nb_inputs || + rule.operator.optgroup !== previousOperator.optgroup + ) { + this.createRuleInput(rule); + } + } + + if (rule.operator) { + rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type); + + // refresh value if the format changed for this operator + rule.__.value = this.getRuleInputValue(rule); + } + + /** + * After the operator has been updated and the input optionally re-created + * @event afterUpdateRuleOperator + * @memberof QueryBuilder + * @param {Rule} rule + * @param {object} previousOperator + */ + this.trigger('afterUpdateRuleOperator', rule, previousOperator); + + this.trigger('rulesChanged'); +}; + +/** + * Performs actions when rule's value changes + * @param {Rule} rule + * @param {object} previousValue + * @fires QueryBuilder.afterUpdateRuleValue + * @private + */ +QueryBuilder.prototype.updateRuleValue = function(rule, previousValue) { + if (!rule._updating_value) { + this.setRuleInputValue(rule, rule.value); + } + + /** + * After the rule value has been modified + * @event afterUpdateRuleValue + * @memberof QueryBuilder + * @param {Rule} rule + * @param {*} previousValue + */ + this.trigger('afterUpdateRuleValue', rule, previousValue); + + this.trigger('rulesChanged'); +}; + +/** + * Changes a rule's properties depending on its flags + * @param {Rule} rule + * @fires QueryBuilder.afterApplyRuleFlags + * @private + */ +QueryBuilder.prototype.applyRuleFlags = function(rule) { + var flags = rule.flags; + var Selectors = QueryBuilder.selectors; + + rule.$el.find(Selectors.rule_filter).prop('disabled', flags.filter_readonly); + rule.$el.find(Selectors.rule_operator).prop('disabled', flags.operator_readonly); + rule.$el.find(Selectors.rule_value).prop('disabled', flags.value_readonly); + + if (flags.no_delete) { + rule.$el.find(Selectors.delete_rule).remove(); + } + + /** + * After rule's flags has been applied + * @event afterApplyRuleFlags + * @memberof QueryBuilder + * @param {Rule} rule + */ + this.trigger('afterApplyRuleFlags', rule); +}; + +/** + * Changes group's properties depending on its flags + * @param {Group} group + * @fires QueryBuilder.afterApplyGroupFlags + * @private + */ +QueryBuilder.prototype.applyGroupFlags = function(group) { + var flags = group.flags; + var Selectors = QueryBuilder.selectors; + + group.$el.find('>' + Selectors.group_condition).prop('disabled', flags.condition_readonly) + .parent().toggleClass('readonly', flags.condition_readonly); + + if (flags.no_add_rule) { + group.$el.find(Selectors.add_rule).remove(); + } + if (flags.no_add_group) { + group.$el.find(Selectors.add_group).remove(); + } + if (flags.no_delete) { + group.$el.find(Selectors.delete_group).remove(); + } + + /** + * After group's flags has been applied + * @event afterApplyGroupFlags + * @memberof QueryBuilder + * @param {Group} group + */ + this.trigger('afterApplyGroupFlags', group); +}; + +/** + * Clears all errors markers + * @param {Node} [node] default is root Group + */ +QueryBuilder.prototype.clearErrors = function(node) { + node = node || this.model.root; + + if (!node) { + return; + } + + node.error = null; + + if (node instanceof Group) { + node.each(function(rule) { + rule.error = null; + }, function(group) { + this.clearErrors(group); + }, this); + } +}; + +/** + * Adds/Removes error on a Rule or Group + * @param {Node} node + * @fires QueryBuilder.changer:displayError + * @private + */ +QueryBuilder.prototype.updateError = function(node) { + if (this.settings.display_errors) { + if (node.error === null) { + node.$el.removeClass('has-error'); + } + else { + var errorMessage = this.translate('errors', node.error[0]); + errorMessage = Utils.fmt(errorMessage, node.error.slice(1)); + + /** + * Modifies an error message before display + * @event changer:displayError + * @memberof QueryBuilder + * @param {string} errorMessage - the error message (translated and formatted) + * @param {array} error - the raw error array (error code and optional arguments) + * @param {Node} node + * @returns {string} + */ + errorMessage = this.change('displayError', errorMessage, node.error, node); + + node.$el.addClass('has-error') + .find(QueryBuilder.selectors.error_container).eq(0) + .attr('title', errorMessage); + } + } +}; + +/** + * Triggers a validation error event + * @param {Node} node + * @param {string|array} error + * @param {*} value + * @fires QueryBuilder.validationError + * @private + */ +QueryBuilder.prototype.triggerValidationError = function(node, error, value) { + if (!$.isArray(error)) { + error = [error]; + } + + /** + * Fired when a validation error occurred, can be prevented + * @event validationError + * @memberof QueryBuilder + * @param {Node} node + * @param {string} error + * @param {*} value + */ + var e = this.trigger('validationError', node, error, value); + if (!e.isDefaultPrevented()) { + node.error = error; + } +}; diff --git a/src/data.js b/src/data.js new file mode 100644 index 00000000..7f466940 --- /dev/null +++ b/src/data.js @@ -0,0 +1,660 @@ +/** + * Performs value validation + * @param {Rule} rule + * @param {string|string[]} value + * @returns {array|boolean} true or error array + * @fires QueryBuilder.changer:validateValue + */ +QueryBuilder.prototype.validateValue = function(rule, value) { + var validation = rule.filter.validation || {}; + var result = true; + + if (validation.callback) { + result = validation.callback.call(this, value, rule); + } + else { + result = this._validateValue(rule, value); + } + + /** + * Modifies the result of the rule validation method + * @event changer:validateValue + * @memberof QueryBuilder + * @param {array|boolean} result - true or an error array + * @param {*} value + * @param {Rule} rule + * @returns {array|boolean} + */ + return this.change('validateValue', result, value, rule); +}; + +/** + * Default validation function + * @param {Rule} rule + * @param {string|string[]} value + * @returns {array|boolean} true or error array + * @throws ConfigError + * @private + */ +QueryBuilder.prototype._validateValue = function(rule, value) { + var filter = rule.filter; + var operator = rule.operator; + var validation = filter.validation || {}; + var result = true; + var tmp, tempValue; + + if (rule.operator.nb_inputs === 1) { + value = [value]; + } + + for (var i = 0; i < operator.nb_inputs; i++) { + if (!operator.multiple && $.isArray(value[i]) && value[i].length > 1) { + result = ['operator_not_multiple', operator.type, this.translate('operators', operator.type)]; + break; + } + + switch (filter.input) { + case 'radio': + if (value[i] === undefined || value[i].length === 0) { + if (!validation.allow_empty_value) { + result = ['radio_empty']; + } + break; + } + break; + + case 'checkbox': + if (value[i] === undefined || value[i].length === 0) { + if (!validation.allow_empty_value) { + result = ['checkbox_empty']; + } + break; + } + break; + + case 'select': + if (value[i] === undefined || value[i].length === 0 || (filter.placeholder && value[i] == filter.placeholder_value)) { + if (!validation.allow_empty_value) { + result = ['select_empty']; + } + break; + } + break; + + default: + tempValue = $.isArray(value[i]) ? value[i] : [value[i]]; + + for (var j = 0; j < tempValue.length; j++) { + switch (QueryBuilder.types[filter.type]) { + case 'string': + if (tempValue[j] === undefined || tempValue[j].length === 0) { + if (!validation.allow_empty_value) { + result = ['string_empty']; + } + break; + } + if (validation.min !== undefined) { + if (tempValue[j].length < parseInt(validation.min)) { + result = [this.getValidationMessage(validation, 'min', 'string_exceed_min_length'), validation.min]; + break; + } + } + if (validation.max !== undefined) { + if (tempValue[j].length > parseInt(validation.max)) { + result = [this.getValidationMessage(validation, 'max', 'string_exceed_max_length'), validation.max]; + break; + } + } + if (validation.format) { + if (typeof validation.format == 'string') { + validation.format = new RegExp(validation.format); + } + if (!validation.format.test(tempValue[j])) { + result = [this.getValidationMessage(validation, 'format', 'string_invalid_format'), validation.format]; + break; + } + } + break; + + case 'number': + if (tempValue[j] === undefined || tempValue[j].length === 0) { + if (!validation.allow_empty_value) { + result = ['number_nan']; + } + break; + } + if (isNaN(tempValue[j])) { + result = ['number_nan']; + break; + } + if (filter.type == 'integer') { + if (parseInt(tempValue[j]) != tempValue[j]) { + result = ['number_not_integer']; + break; + } + } + else { + if (parseFloat(tempValue[j]) != tempValue[j]) { + result = ['number_not_double']; + break; + } + } + if (validation.min !== undefined) { + if (tempValue[j] < parseFloat(validation.min)) { + result = [this.getValidationMessage(validation, 'min', 'number_exceed_min'), validation.min]; + break; + } + } + if (validation.max !== undefined) { + if (tempValue[j] > parseFloat(validation.max)) { + result = [this.getValidationMessage(validation, 'max', 'number_exceed_max'), validation.max]; + break; + } + } + if (validation.step !== undefined && validation.step !== 'any') { + var v = (tempValue[j] / validation.step).toPrecision(14); + if (parseInt(v) != v) { + result = [this.getValidationMessage(validation, 'step', 'number_wrong_step'), validation.step]; + break; + } + } + break; + + case 'datetime': + if (tempValue[j] === undefined || tempValue[j].length === 0) { + if (!validation.allow_empty_value) { + result = ['datetime_empty']; + } + break; + } + + // we need MomentJS + if (validation.format) { + if (!('moment' in window)) { + Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com'); + } + + var datetime = moment(tempValue[j], validation.format); + if (!datetime.isValid()) { + result = [this.getValidationMessage(validation, 'format', 'datetime_invalid'), validation.format]; + break; + } + else { + if (validation.min) { + if (datetime < moment(validation.min, validation.format)) { + result = [this.getValidationMessage(validation, 'min', 'datetime_exceed_min'), validation.min]; + break; + } + } + if (validation.max) { + if (datetime > moment(validation.max, validation.format)) { + result = [this.getValidationMessage(validation, 'max', 'datetime_exceed_max'), validation.max]; + break; + } + } + } + } + break; + + case 'boolean': + if (tempValue[j] === undefined || tempValue[j].length === 0) { + if (!validation.allow_empty_value) { + result = ['boolean_not_valid']; + } + break; + } + tmp = ('' + tempValue[j]).trim().toLowerCase(); + if (tmp !== 'true' && tmp !== 'false' && tmp !== '1' && tmp !== '0' && tempValue[j] !== 1 && tempValue[j] !== 0) { + result = ['boolean_not_valid']; + break; + } + } + + if (result !== true) { + break; + } + } + } + + if (result !== true) { + break; + } + } + + if ((rule.operator.type === 'between' || rule.operator.type === 'not_between') && value.length === 2) { + switch (QueryBuilder.types[filter.type]) { + case 'number': + if (value[0] > value[1]) { + result = ['number_between_invalid', value[0], value[1]]; + } + break; + + case 'datetime': + // we need MomentJS + if (validation.format) { + if (!('moment' in window)) { + Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com'); + } + + if (moment(value[0], validation.format).isAfter(moment(value[1], validation.format))) { + result = ['datetime_between_invalid', value[0], value[1]]; + } + } + break; + } + } + + return result; +}; + +/** + * Returns an incremented group ID + * @returns {string} + * @private + */ +QueryBuilder.prototype.nextGroupId = function() { + return this.status.id + '_group_' + (this.status.group_id++); +}; + +/** + * Returns an incremented rule ID + * @returns {string} + * @private + */ +QueryBuilder.prototype.nextRuleId = function() { + return this.status.id + '_rule_' + (this.status.rule_id++); +}; + +/** + * Returns the operators for a filter + * @param {string|object} filter - filter id or filter object + * @returns {object[]} + * @fires QueryBuilder.changer:getOperators + */ +QueryBuilder.prototype.getOperators = function(filter) { + if (typeof filter == 'string') { + filter = this.getFilterById(filter); + } + + var result = []; + + for (var i = 0, l = this.operators.length; i < l; i++) { + // filter operators check + if (filter.operators) { + if (filter.operators.indexOf(this.operators[i].type) == -1) { + continue; + } + } + // type check + else if (this.operators[i].apply_to.indexOf(QueryBuilder.types[filter.type]) == -1) { + continue; + } + + result.push(this.operators[i]); + } + + // keep sort order defined for the filter + if (filter.operators) { + result.sort(function(a, b) { + return filter.operators.indexOf(a.type) - filter.operators.indexOf(b.type); + }); + } + + /** + * Modifies the operators available for a filter + * @event changer:getOperators + * @memberof QueryBuilder + * @param {QueryBuilder.Operator[]} operators + * @param {QueryBuilder.Filter} filter + * @returns {QueryBuilder.Operator[]} + */ + return this.change('getOperators', result, filter); +}; + +/** + * Returns a particular filter by its id + * @param {string} id + * @param {boolean} [doThrow=true] + * @returns {object|null} + * @throws UndefinedFilterError + */ +QueryBuilder.prototype.getFilterById = function(id, doThrow) { + if (id == '-1') { + return null; + } + + for (var i = 0, l = this.filters.length; i < l; i++) { + if (this.filters[i].id == id) { + return this.filters[i]; + } + } + + Utils.error(doThrow !== false, 'UndefinedFilter', 'Undefined filter "{0}"', id); + + return null; +}; + +/** + * Returns a particular operator by its type + * @param {string} type + * @param {boolean} [doThrow=true] + * @returns {object|null} + * @throws UndefinedOperatorError + */ +QueryBuilder.prototype.getOperatorByType = function(type, doThrow) { + if (type == '-1') { + return null; + } + + for (var i = 0, l = this.operators.length; i < l; i++) { + if (this.operators[i].type == type) { + return this.operators[i]; + } + } + + Utils.error(doThrow !== false, 'UndefinedOperator', 'Undefined operator "{0}"', type); + + return null; +}; + +/** + * Returns rule's current input value + * @param {Rule} rule + * @returns {*} + * @fires QueryBuilder.changer:getRuleValue + * @private + */ +QueryBuilder.prototype.getRuleInputValue = function(rule) { + var filter = rule.filter; + var operator = rule.operator; + var value = []; + + if (filter.valueGetter) { + value = filter.valueGetter.call(this, rule); + } + else { + var $value = rule.$el.find(QueryBuilder.selectors.value_container); + + for (var i = 0; i < operator.nb_inputs; i++) { + var name = Utils.escapeElementId(rule.id + '_value_' + i); + var tmp; + + switch (filter.input) { + case 'radio': + value.push($value.find('[name=' + name + ']:checked').val()); + break; + + case 'checkbox': + tmp = []; + $value.find('[name=' + name + ']:checked').each(function() { + tmp.push($(this).val()); + }); + value.push(tmp); + break; + + case 'select': + if (filter.multiple) { + tmp = []; + $value.find('[name=' + name + '] option:selected').each(function() { + tmp.push($(this).val()); + }); + value.push(tmp); + } + else { + value.push($value.find('[name=' + name + '] option:selected').val()); + } + break; + + default: + value.push($value.find('[name=' + name + ']').val()); + } + } + + value = value.map(function(val) { + if (operator.multiple && filter.value_separator && typeof val == 'string') { + val = val.split(filter.value_separator); + } + + if ($.isArray(val)) { + return val.map(function(subval) { + return Utils.changeType(subval, filter.type); + }); + } + else { + return Utils.changeType(val, filter.type); + } + }); + + if (operator.nb_inputs === 1) { + value = value[0]; + } + + // @deprecated + if (filter.valueParser) { + value = filter.valueParser.call(this, rule, value); + } + } + + /** + * Modifies the rule's value grabbed from the DOM + * @event changer:getRuleValue + * @memberof QueryBuilder + * @param {*} value + * @param {Rule} rule + * @returns {*} + */ + return this.change('getRuleValue', value, rule); +}; + +/** + * Sets the value of a rule's input + * @param {Rule} rule + * @param {*} value + * @private + */ +QueryBuilder.prototype.setRuleInputValue = function(rule, value) { + var filter = rule.filter; + var operator = rule.operator; + + if (!filter || !operator) { + return; + } + + rule._updating_input = true; + + if (filter.valueSetter) { + filter.valueSetter.call(this, rule, value); + } + else { + var $value = rule.$el.find(QueryBuilder.selectors.value_container); + + if (operator.nb_inputs == 1) { + value = [value]; + } + + for (var i = 0; i < operator.nb_inputs; i++) { + var name = Utils.escapeElementId(rule.id + '_value_' + i); + + switch (filter.input) { + case 'radio': + $value.find('[name=' + name + '][value="' + value[i] + '"]').prop('checked', true).trigger('change'); + break; + + case 'checkbox': + if (!$.isArray(value[i])) { + value[i] = [value[i]]; + } + value[i].forEach(function(value) { + $value.find('[name=' + name + '][value="' + value + '"]').prop('checked', true).trigger('change'); + }); + break; + + default: + if (operator.multiple && filter.value_separator && $.isArray(value[i])) { + value[i] = value[i].join(filter.value_separator); + } + $value.find('[name=' + name + ']').val(value[i]).trigger('change'); + break; + } + } + } + + rule._updating_input = false; +}; + +/** + * Parses rule flags + * @param {object} rule + * @returns {object} + * @fires QueryBuilder.changer:parseRuleFlags + * @private + */ +QueryBuilder.prototype.parseRuleFlags = function(rule) { + var flags = $.extend({}, this.settings.default_rule_flags); + + if (rule.readonly) { + $.extend(flags, { + filter_readonly: true, + operator_readonly: true, + value_readonly: true, + no_delete: true + }); + } + + if (rule.flags) { + $.extend(flags, rule.flags); + } + + /** + * Modifies the consolidated rule's flags + * @event changer:parseRuleFlags + * @memberof QueryBuilder + * @param {object} flags + * @param {object} rule - not a Rule object + * @returns {object} + */ + return this.change('parseRuleFlags', flags, rule); +}; + +/** + * Gets a copy of flags of a rule + * @param {object} flags + * @param {boolean} [all=false] - return all flags or only changes from default flags + * @returns {object} + * @private + */ +QueryBuilder.prototype.getRuleFlags = function(flags, all) { + if (all) { + return $.extend({}, flags); + } + else { + var ret = {}; + $.each(this.settings.default_rule_flags, function(key, value) { + if (flags[key] !== value) { + ret[key] = flags[key]; + } + }); + return ret; + } +}; + +/** + * Parses group flags + * @param {object} group + * @returns {object} + * @fires QueryBuilder.changer:parseGroupFlags + * @private + */ +QueryBuilder.prototype.parseGroupFlags = function(group) { + var flags = $.extend({}, this.settings.default_group_flags); + + if (group.readonly) { + $.extend(flags, { + condition_readonly: true, + no_add_rule: true, + no_add_group: true, + no_delete: true + }); + } + + if (group.flags) { + $.extend(flags, group.flags); + } + + /** + * Modifies the consolidated group's flags + * @event changer:parseGroupFlags + * @memberof QueryBuilder + * @param {object} flags + * @param {object} group - not a Group object + * @returns {object} + */ + return this.change('parseGroupFlags', flags, group); +}; + +/** + * Gets a copy of flags of a group + * @param {object} flags + * @param {boolean} [all=false] - return all flags or only changes from default flags + * @returns {object} + * @private + */ +QueryBuilder.prototype.getGroupFlags = function(flags, all) { + if (all) { + return $.extend({}, flags); + } + else { + var ret = {}; + $.each(this.settings.default_group_flags, function(key, value) { + if (flags[key] !== value) { + ret[key] = flags[key]; + } + }); + return ret; + } +}; + +/** + * Translate a label either by looking in the `lang` object or in itself if it's an object where keys are language codes + * @param {string} [category] + * @param {string|object} key + * @returns {string} + * @fires QueryBuilder.changer:translate + */ +QueryBuilder.prototype.translate = function(category, key) { + if (!key) { + key = category; + category = undefined; + } + + var translation; + if (typeof key === 'object') { + translation = key[this.settings.lang_code] || key['en']; + } + else { + translation = (category ? this.lang[category] : this.lang)[key] || key; + } + + /** + * Modifies the translated label + * @event changer:translate + * @memberof QueryBuilder + * @param {string} translation + * @param {string|object} key + * @param {string} [category] + * @returns {string} + */ + return this.change('translate', translation, key, category); +}; + +/** + * Returns a validation message + * @param {object} validation + * @param {string} type + * @param {string} def + * @returns {string} + * @private + */ +QueryBuilder.prototype.getValidationMessage = function(validation, type, def) { + return validation.messages && validation.messages[type] || def; +}; diff --git a/src/defaults.js b/src/defaults.js new file mode 100644 index 00000000..b63b1251 --- /dev/null +++ b/src/defaults.js @@ -0,0 +1,196 @@ +/** + * Allowed types and their internal representation + * @type {object.} + * @readonly + * @private + */ +QueryBuilder.types = { + 'string': 'string', + 'integer': 'number', + 'double': 'number', + 'date': 'datetime', + 'time': 'datetime', + 'datetime': 'datetime', + 'boolean': 'boolean' +}; + +/** + * Allowed inputs + * @type {string[]} + * @readonly + * @private + */ +QueryBuilder.inputs = [ + 'text', + 'number', + 'textarea', + 'radio', + 'checkbox', + 'select' +]; + +/** + * Runtime modifiable options with `setOptions` method + * @type {string[]} + * @readonly + * @private + */ +QueryBuilder.modifiable_options = [ + 'display_errors', + 'allow_groups', + 'allow_empty', + 'default_condition', + 'default_filter' +]; + +/** + * CSS selectors for common components + * @type {object.} + * @readonly + */ +QueryBuilder.selectors = { + group_container: '.rules-group-container', + rule_container: '.rule-container', + filter_container: '.rule-filter-container', + operator_container: '.rule-operator-container', + value_container: '.rule-value-container', + error_container: '.error-container', + condition_container: '.rules-group-header .group-conditions', + + rule_header: '.rule-header', + group_header: '.rules-group-header', + group_actions: '.group-actions', + rule_actions: '.rule-actions', + + rules_list: '.rules-group-body>.rules-list', + + group_condition: '.rules-group-header [name$=_cond]', + rule_filter: '.rule-filter-container [name$=_filter]', + rule_operator: '.rule-operator-container [name$=_operator]', + rule_value: '.rule-value-container [name*=_value_]', + + add_rule: '[data-add=rule]', + delete_rule: '[data-delete=rule]', + add_group: '[data-add=group]', + delete_group: '[data-delete=group]' +}; + +/** + * Template strings (see template.js) + * @type {object.} + * @readonly + */ +QueryBuilder.templates = {}; + +/** + * Localized strings (see i18n/) + * @type {object.} + * @readonly + */ +QueryBuilder.regional = {}; + +/** + * Default operators + * @type {object.} + * @readonly + */ +QueryBuilder.OPERATORS = { + equal: { type: 'equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] }, + not_equal: { type: 'not_equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] }, + in: { type: 'in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime'] }, + not_in: { type: 'not_in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime'] }, + less: { type: 'less', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] }, + less_or_equal: { type: 'less_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] }, + greater: { type: 'greater', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] }, + greater_or_equal: { type: 'greater_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] }, + between: { type: 'between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] }, + not_between: { type: 'not_between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] }, + begins_with: { type: 'begins_with', nb_inputs: 1, multiple: false, apply_to: ['string'] }, + not_begins_with: { type: 'not_begins_with', nb_inputs: 1, multiple: false, apply_to: ['string'] }, + contains: { type: 'contains', nb_inputs: 1, multiple: false, apply_to: ['string'] }, + not_contains: { type: 'not_contains', nb_inputs: 1, multiple: false, apply_to: ['string'] }, + ends_with: { type: 'ends_with', nb_inputs: 1, multiple: false, apply_to: ['string'] }, + not_ends_with: { type: 'not_ends_with', nb_inputs: 1, multiple: false, apply_to: ['string'] }, + is_empty: { type: 'is_empty', nb_inputs: 0, multiple: false, apply_to: ['string'] }, + is_not_empty: { type: 'is_not_empty', nb_inputs: 0, multiple: false, apply_to: ['string'] }, + is_null: { type: 'is_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] }, + is_not_null: { type: 'is_not_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] } +}; + +/** + * Default configuration + * @type {object} + * @readonly + */ +QueryBuilder.DEFAULTS = { + filters: [], + plugins: [], + + sort_filters: false, + display_errors: true, + allow_groups: -1, + allow_empty: false, + conditions: ['AND', 'OR'], + default_condition: 'AND', + inputs_separator: ' , ', + select_placeholder: '------', + display_empty_filter: true, + default_filter: null, + optgroups: {}, + + default_rule_flags: { + filter_readonly: false, + operator_readonly: false, + value_readonly: false, + no_delete: false + }, + + default_group_flags: { + condition_readonly: false, + no_add_rule: false, + no_add_group: false, + no_delete: false + }, + + templates: { + group: null, + rule: null, + filterSelect: null, + operatorSelect: null, + ruleValueSelect: null + }, + + lang_code: 'en', + lang: {}, + + operators: [ + 'equal', + 'not_equal', + 'in', + 'not_in', + 'less', + 'less_or_equal', + 'greater', + 'greater_or_equal', + 'between', + 'not_between', + 'begins_with', + 'not_begins_with', + 'contains', + 'not_contains', + 'ends_with', + 'not_ends_with', + 'is_empty', + 'is_not_empty', + 'is_null', + 'is_not_null' + ], + + icons: { + add_group: 'bi-plus-circle-fill', + add_rule: 'bi-plus-lg', + remove_group: 'bi-x-lg', + remove_rule: 'bi-x-lg', + error: 'bi-exclamation-triangle' + } +}; diff --git a/src/i18n/.wrapper.js b/src/i18n/.wrapper.js new file mode 100644 index 00000000..9cf5498f --- /dev/null +++ b/src/i18n/.wrapper.js @@ -0,0 +1,15 @@ +(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; + +@@js + +})); \ No newline at end of file diff --git a/src/i18n/ar.json b/src/i18n/ar.json new file mode 100644 index 00000000..4590e791 --- /dev/null +++ b/src/i18n/ar.json @@ -0,0 +1,57 @@ + { + "__locale": "Arabic (ar)", + "__author": "Mohamed YOUNES, https://github.com/MedYOUNES", + "add_rule": "إضافة حُكم", + "add_group": "إضافة زُمْرَة", + "delete_rule": "حذف", + "delete_group": "حذف", + "conditions": { + "AND": "و", + "OR": "أو" + }, + "operators": { + "equal": "يساوي", + "not_equal": "غير مساوٍ", + "in": "في", + "not_in": "ليس في", + "less": "أقل من", + "less_or_equal": "أصغر أو مساو", + "greater": "أكبر", + "greater_or_equal": "أكبر أو مساو", + "between": "محصور بين", + "not_between": "غير محصور بين", + "begins_with": "يبدأ بـ", + "not_begins_with": "لا يبدأ بـ", + "contains": "يحتوي على", + "not_contains": "لا يحتوي على", + "ends_with": "ينتهي بـ", + "not_ends_with": "لا ينتهي بـ", + "is_empty": "فارغ", + "is_not_empty": "غير فارغ", + "is_null": "صفر", + "is_not_null": "ليس صفرا" + }, + "errors": { + "no_filter": "لم تحدد أي مرشح", + "empty_group": "الزمرة فارغة", + "radio_empty": "لم تحدد أي قيمة", + "checkbox_empty": "لم تحدد أي قيمة", + "select_empty": "لم تحدد أي قيمة", + "string_empty": "النص فارغ", + "string_exceed_min_length": "النص دون الأدنى المسموح به", + "string_exceed_max_length": "النص فوق الأقصى المسموح به", + "string_invalid_format": "تركيبة غير صحيحة", + "number_nan": "ليس عددا", + "number_not_integer": "ليس عددا صحيحا", + "number_not_double": "ليس عددا كسريا", + "number_exceed_min": "العدد أصغر من الأدنى المسموح به", + "number_exceed_max": "العدد أكبر من الأقصى المسموح به", + "number_wrong_step": "أخطأت في حساب مضاعفات العدد", + "datetime_empty": "لم تحدد التاريخ", + "datetime_invalid": "صيغة التاريخ غير صحيحة", + "datetime_exceed_min": "التاريخ دون الأدنى المسموح به", + "datetime_exceed_max": "التاريخ أكبر من الأقصى المسموح به", + "boolean_not_valid": "ليست قيمة منطقية ثنائية", + "operator_not_multiple": "العامل ليس متعدد القيَم" + } + } diff --git a/src/i18n/az.json b/src/i18n/az.json new file mode 100644 index 00000000..5e12334f --- /dev/null +++ b/src/i18n/az.json @@ -0,0 +1,57 @@ +{ + "__locale": "Azerbaijan (az)", + "__author": "Megaplan, mborisv ", + + "add_rule": "Əlavə etmək", + "add_group": "Qrup əlavə etmək", + "delete_rule": "Silmək", + "delete_group": "Silmək", + "conditions": { + "AND": "VƏ", + "OR": "VƏ YA" + }, + "operators": { + "equal": "bərabərdir", + "not_equal": "bərabər deyil", + "in": "qeyd edilmişlərdən", + "not_in": "qeyd olunmamışlardan", + "less": "daha az", + "less_or_equal": "daha az və ya bərabər", + "greater": "daha çox", + "greater_or_equal": "daha çox və ya bərabər", + "between": "arasında", + "begins_with": "başlayır", + "not_begins_with": "başlamır", + "contains": "ibarətdir", + "not_contains": "yoxdur", + "ends_with": "başa çatır", + "not_ends_with": "başa çatmır", + "is_empty": "boş sətir", + "is_not_empty": "boş olmayan sətir", + "is_null": "boşdur", + "is_not_null": "boş deyil" + }, + "errors": { + "no_filter": "Filterlər seçilməyib", + "empty_group": "Qrup boşdur", + "radio_empty": "Məna seçilməyib", + "checkbox_empty": "Məna seçilməyib", + "select_empty": "Məna seçilməyib", + "string_empty": "Doldurulmayıb", + "string_exceed_min_length": "{0} daha çox simvol olmalıdır", + "string_exceed_max_length": "{0} daha az simvol olmalıdır", + "string_invalid_format": "Yanlış format ({0})", + "number_nan": "Rəqəm deyil", + "number_not_integer": "Rəqəm deyil", + "number_not_double": "Rəqəm deyil", + "number_exceed_min": "{0} daha çox olmalıdır", + "number_exceed_max": "{0} daha az olmalıdır", + "number_wrong_step": "{0} bölünən olmalıdır", + "datetime_empty": "Doldurulmayıb", + "datetime_invalid": "Yanlış tarix formatı ({0})", + "datetime_exceed_min": "{0} sonra olmalıdır", + "datetime_exceed_max": "{0} əvvəl olmalıdır", + "boolean_not_valid": "Loqik olmayan", + "operator_not_multiple": "\"{1}\" operatoru çoxlu məna daşımır" + } +} \ 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 new file mode 100644 index 00000000..448cc97d --- /dev/null +++ b/src/i18n/cs.json @@ -0,0 +1,57 @@ +{ + "__locale": "Čeština (cs)", + "__author": "Megaplan, mborisv ", + + "add_rule": "Přidat", + "add_group": "Přidat skupinu", + "delete_rule": "Odstranit", + "delete_group": "Odstranit skupinu", + "conditions": { + "AND": "I", + "OR": "NEBO" + }, + "operators": { + "equal": "stejně", + "not_equal": "liší se", + "in": "z uvedených", + "not_in": "ne z uvedených", + "less": "méně", + "less_or_equal": "méně nebo stejně", + "greater": "více", + "greater_or_equal": "více nebo stejně", + "between": "mezi", + "begins_with": "začíná z", + "not_begins_with": "nezačíná z", + "contains": "obsahuje", + "not_contains": "neobsahuje", + "ends_with": "končí na", + "not_ends_with": "nekončí na", + "is_empty": "prázdný řádek", + "is_not_empty": "neprázdný řádek", + "is_null": "prázdno", + "is_not_null": "plno" + }, + "errors": { + "no_filter": "není vybraný filtr", + "empty_group": "prázdná skupina", + "radio_empty": "Není udaná hodnota", + "checkbox_empty": "Není udaná hodnota", + "select_empty": "Není udaná hodnota", + "string_empty": "Nevyplněno", + "string_exceed_min_length": "Musí obsahovat více {0} symbolů", + "string_exceed_max_length": "Musí obsahovat méně {0} symbolů", + "string_invalid_format": "Nesprávný formát ({0})", + "number_nan": "Žádné číslo", + "number_not_integer": "Žádné číslo", + "number_not_double": "Žádné číslo", + "number_exceed_min": "Musí být více {0}", + "number_exceed_max": "Musí být méně {0}", + "number_wrong_step": "Musí být násobkem {0}", + "datetime_empty": "Nevyplněno", + "datetime_invalid": "Nesprávný formát datumu ({0})", + "datetime_exceed_min": "Musí být po {0}", + "datetime_exceed_max": "Musí být do {0}", + "boolean_not_valid": "Nelogické", + "operator_not_multiple": "Operátor \"{1}\" nepodporuje mnoho hodnot" + } +} \ No newline at end of file diff --git a/dist/i18n/da.js b/src/i18n/da.json similarity index 82% rename from dist/i18n/da.js rename to src/i18n/da.json index 15630102..784c80b6 100644 --- a/dist/i18n/da.js +++ b/src/i18n/da.json @@ -1,19 +1,20 @@ -/*! - * jQuery QueryBuilder - * Oversat af Jna Borup Coyle, github@coyle.dk - */ +{ + "__locale": "Danish (da)", + "__author": "Jna Borup Coyle, github@coyle.dk", -jQuery.fn.queryBuilder.defaults.set({ lang: { "add_rule": "Tilføj regel", "add_group": "Tilføj gruppe", "delete_rule": "Slet regel", "delete_group": "Slet gruppe", + "conditions": { + "AND": "OG", + "OR": "ELLER" + }, + "condition_and": "OG", "condition_or": "ELLER", - "filter_select_placeholder": "------", - "operators": { "equal": "lig med", "not_equal": "ikke lige med", @@ -34,4 +35,4 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "is_null": "er null", "is_not_null": "er ikke null" } -}}); \ No newline at end of file +} \ No newline at end of file diff --git a/src/i18n/de.js b/src/i18n/de.js deleted file mode 100644 index 988b7826..00000000 --- a/src/i18n/de.js +++ /dev/null @@ -1,37 +0,0 @@ -/*! - * jQuery QueryBuilder - * German translation - */ - -jQuery.fn.queryBuilder.defaults.set({ lang: { - "add_rule": "neue Regel", - "add_group": "neue Gruppe", - "delete_rule": "löschen", - "delete_group": "löschen", - - "condition_and": "UND", - "condition_or": "ODER", - - "filter_select_placeholder": "------", - - "operators": { - "equal": "gleich", - "not_equal": "ungleich", - "in": "in", - "not_in": "nicht in", - "less": "kleiner", - "less_or_equal": "kleiner gleich", - "greater": "größer", - "greater_or_equal": "größer gleich", - "begins_with": "beginnt mit", - "not_begins_with": "beginnt nicht mit", - "contains": "enthält", - "not_contains": "enthält nicht", - "ends_with": "endet mit", - "not_ends_with": "endet nicht mit", - "is_empty": "ist leer", - "is_not_empty": "ist nicht leer", - "is_null": "ist null", - "is_not_null": "ist nicht null" - } -}}); \ No newline at end of file diff --git a/src/i18n/de.json b/src/i18n/de.json new file mode 100644 index 00000000..82104597 --- /dev/null +++ b/src/i18n/de.json @@ -0,0 +1,58 @@ +{ + "__locale": "German (de)", + "__author": "\"raimu\"", + + "add_rule": "neue Regel", + "add_group": "neue Gruppe", + "delete_rule": "löschen", + "delete_group": "löschen", + + "conditions": { + "AND": "UND", + "OR": "ODER" + }, + + "operators": { + "equal": "gleich", + "not_equal": "ungleich", + "in": "in", + "not_in": "nicht in", + "less": "kleiner", + "less_or_equal": "kleiner gleich", + "greater": "größer", + "greater_or_equal": "größer gleich", + "between": "zwischen", + "not_between": "nicht zwischen", + "begins_with": "beginnt mit", + "not_begins_with": "beginnt nicht mit", + "contains": "enthält", + "not_contains": "enthält nicht", + "ends_with": "endet mit", + "not_ends_with": "endet nicht mit", + "is_empty": "ist leer", + "is_not_empty": "ist nicht leer", + "is_null": "ist null", + "is_not_null": "ist nicht null" + }, + + "errors": { + "no_filter": "Kein Filter ausgewählt", + "empty_group": "Die Gruppe ist leer", + "radio_empty": "Kein Wert ausgewählt", + "checkbox_empty": "Kein Wert ausgewählt", + "select_empty": "Kein Wert ausgewählt", + "string_empty": "Leerer Wert", + "string_exceed_min_length": "Muss mindestens {0} Zeichen enthalten", + "string_exceed_max_length": "Darf nicht mehr als {0} Zeichen enthalten", + "string_invalid_format": "Ungültiges Format ({0})", + "number_nan": "Keine Zahl", + "number_not_integer": "Keine Ganzzahl", + "number_not_double": "Keine Dezimalzahl", + "number_exceed_min": "Muss größer als {0} sein", + "number_exceed_max": "Muss kleiner als {0} sein", + "number_wrong_step": "Muss ein Vielfaches von {0} sein", + "datetime_invalid": "Ungültiges Datumsformat ({0})", + "datetime_exceed_min": "Muss nach dem {0} sein", + "datetime_exceed_max": "Muss vor dem {0} sein" + } +} 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/dist/i18n/en.js b/src/i18n/en.json similarity index 74% rename from dist/i18n/en.js rename to src/i18n/en.json index 513e915a..15bbb139 100644 --- a/dist/i18n/en.js +++ b/src/i18n/en.json @@ -1,18 +1,16 @@ -/*! - * jQuery QueryBuilder - * Reference language file - */ +{ + "__locale": "English (en)", + "__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr", -jQuery.fn.queryBuilder.defaults.set({ lang: { "add_rule": "Add rule", "add_group": "Add group", "delete_rule": "Delete", "delete_group": "Delete", - "condition_and": "AND", - "condition_or": "OR", - - "filter_select_placeholder": "------", + "conditions": { + "AND": "AND", + "OR": "OR" + }, "operators": { "equal": "equal", @@ -24,6 +22,7 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "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", @@ -35,7 +34,7 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "is_null": "is null", "is_not_null": "is not null" }, - + "errors": { "no_filter": "No filter selected", "empty_group": "The group is empty", @@ -52,8 +51,13 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "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_exceed_max": "Must be before {0}", + "datetime_between_invalid": "Invalid values, {0} is greater than {1}", + "boolean_not_valid": "Not a boolean", + "operator_not_multiple": "Operator \"{1}\" cannot accept multiple values" } -}}); \ 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.js b/src/i18n/es.js deleted file mode 100644 index bc3ea3a7..00000000 --- a/src/i18n/es.js +++ /dev/null @@ -1,59 +0,0 @@ -/*! - * jQuery QueryBuilder - * Spanish translation by "pyarza" - */ - -jQuery.fn.queryBuilder.defaults.set({ lang: { - "add_rule": "Añadir regla", - "add_group": "Añadir grupo", - "delete_rule": "Borrar", - "delete_group": "Borrar", - - "condition_and": "Y", - "condition_or": "O", - - "filter_select_placeholder": "------", - - "operators": { - "equal": "igual", - "not_equal": "distinto", - "in": "en", - "not_in": "no en", - "less": "menor", - "less_or_equal": "menor o igual", - "greater": "mayor", - "greater_or_equal": "mayor o igual", - "between": "entre", - "begins_with": "empieza por", - "not_begins_with": "no empieza por", - "contains": "contiene", - "not_contains": "no contiene", - "ends_with": "acaba con", - "not_ends_with": "no acaba con", - "is_empty": "esta vacio", - "is_not_empty": "no esta vacio", - "is_null": "es nulo", - "is_not_null": "no es nulo" - }, - - "errors": { - "no_filter": "No se ha seleccionado ningun filtro", - "empty_group": "El grupo esta vacio", - "radio_empty": "Ningun valor seleccionado", - "checkbox_empty": "Ningun valor seleccionado", - "select_empty": "Ningun valor seleccionado", - "string_empty": "Cadena vacia", - "string_exceed_min_length": "Debe contener al menos {0} caracteres", - "string_exceed_max_length": "No debe contener mas de {0} caracteres", - "string_invalid_format": "Formato invalido ({0})", - "number_nan": "No es un numero", - "number_not_integer": "No es un numero entero", - "number_not_double": "No es un numero real", - "number_exceed_min": "Debe ser mayor que {0}", - "number_exceed_max": "Debe ser menot que {0}", - "number_wrong_step": "Debe ser multiplo de {0}", - "datetime_invalid": "Formato de fecha invalido ({0})", - "datetime_exceed_min": "Debe ser posterior a {0}", - "datetime_exceed_max": "Debe ser anterior a {0}" - } -}}); diff --git a/src/i18n/es.json b/src/i18n/es.json new file mode 100644 index 00000000..3374aae1 --- /dev/null +++ b/src/i18n/es.json @@ -0,0 +1,63 @@ +{ + "__locale": "Spanish (es)", + "__author": "\"pyarza\", \"kddlb\"", + + "add_rule": "Añadir regla", + "add_group": "Añadir grupo", + "delete_rule": "Borrar", + "delete_group": "Borrar", + + "conditions": { + "AND": "Y", + "OR": "O" + }, + + "operators": { + "equal": "igual", + "not_equal": "distinto", + "in": "en", + "not_in": "no en", + "less": "menor", + "less_or_equal": "menor o igual", + "greater": "mayor", + "greater_or_equal": "mayor o igual", + "between": "entre", + "not_between": "no está entre", + "begins_with": "empieza por", + "not_begins_with": "no empieza por", + "contains": "contiene", + "not_contains": "no contiene", + "ends_with": "acaba con", + "not_ends_with": "no acaba con", + "is_empty": "está vacío", + "is_not_empty": "no está vacío", + "is_null": "es nulo", + "is_not_null": "no es nulo" + }, + + "errors": { + "no_filter": "No se ha seleccionado ningún filtro", + "empty_group": "El grupo está vacío", + "radio_empty": "Ningún valor seleccionado", + "checkbox_empty": "Ningún valor seleccionado", + "select_empty": "Ningún valor seleccionado", + "string_empty": "Cadena vacía", + "string_exceed_min_length": "Debe contener al menos {0} caracteres", + "string_exceed_max_length": "No debe contener más de {0} caracteres", + "string_invalid_format": "Formato inválido ({0})", + "number_nan": "No es un número", + "number_not_integer": "No es un número entero", + "number_not_double": "No es un número real", + "number_exceed_min": "Debe ser mayor que {0}", + "number_exceed_max": "Debe ser menor que {0}", + "number_wrong_step": "Debe ser múltiplo de {0}", + "datetime_invalid": "Formato de fecha inválido ({0})", + "datetime_exceed_min": "Debe ser posterior a {0}", + "datetime_exceed_max": "Debe ser anterior a {0}", + "number_between_invalid": "Valores Inválidos, {0} es mayor que {1}", + "datetime_empty": "Campo vacio", + "datetime_between_invalid": "Valores Inválidos, {0} es mayor que {1}", + "boolean_not_valid": "No es booleano", + "operator_not_multiple": "El operador \"{1}\" no puede aceptar valores multiples" + } +} diff --git a/src/i18n/fa-IR.json b/src/i18n/fa-IR.json new file mode 100644 index 00000000..55a6e456 --- /dev/null +++ b/src/i18n/fa-IR.json @@ -0,0 +1,61 @@ +{ + "__locale": "Farsi (fa-ir)", + "__author": "Behzad Sedighzade, behzad.sedighzade@gmail.com", + + "add_rule": "افزودن قاعده", + "add_group": "افزودن گروه", + "delete_rule": "حذف قاعده", + "delete_group": "حذف گروه", + + "conditions": { + "AND": "و", + "OR": "یا" + }, + + "operators": { + "equal": "برابر با", + "not_equal": "مخالف", + "in": "شامل مجموعه شود", + "not_in": "شامل مجموعه نشود", + "less": "کمتر از", + "less_or_equal": "کمتر یا مساوی با", + "greater": "بزرگتر از", + "greater_or_equal": "بزرگتر یا مساوی با", + "between": "مابین", + "not_between": "مابین نباشد", + "begins_with": "شروع شود با", + "not_begins_with": "شروع نشود با", + "contains": "شامل شود", + "not_contains": "شامل نشود", + "ends_with": "خاتمه یابد با", + "not_ends_with": "خاتمه نیابد با", + "is_empty": "خالی باشد", + "is_not_empty": "خالی نباشد", + "is_null": "باشد ( null ) پوچ", + "is_not_null": "نباشد( null ) پوچ " + }, + + "errors": { + "no_filter": "هیچ قاعده ای انتخاب نشده است", + "empty_group": "گروه خالی است", + "radio_empty": "مقداری انتخاب نشده است", + "checkbox_empty": "مقداری انتخاب نشده است", + "select_empty": "مقداری انتخاب نشده است", + "string_empty": "مقدار متنی خالی است", + "string_exceed_min_length": "رشته حداقل باید {0} عدد حرف داشته باشد", + "string_exceed_max_length": "رشته حداکثر {0} عدد حرف می تواند قبول کند", + "string_invalid_format": "قالب رشته {0} نامعتبر ست", + "number_nan": "عدد وارد کنید", + "number_not_integer": "مقدار صحیح وارد کنید", + "number_not_double": "مقدار اعشاری وارد کنید", + "number_exceed_min": "باید از {0} بزرگتر باشد", + "number_exceed_max": "باید از {0} کمتر باشد", + "number_wrong_step": "باید مضربی از {0} باشد", + "datetime_empty": "مقدار تاریخ خالی وارد شده!", + "datetime_invalid": "قالب تاریخ ( {0} ) اشتباه است", + "datetime_exceed_min": "باید بعد از {0} باشد", + "datetime_exceed_max": "باید قبل از {0} باشد", + "boolean_not_valid": "مقدار دودویی وارد کنید", + "operator_not_multiple": "اپراتور \"{1}\" نمی تواند چند مقدار قبول کند" + } +} diff --git a/src/i18n/fr.js b/src/i18n/fr.json similarity index 60% rename from src/i18n/fr.js rename to src/i18n/fr.json index 72373000..33b27bb6 100644 --- a/src/i18n/fr.js +++ b/src/i18n/fr.json @@ -1,29 +1,28 @@ -/*! - * jQuery QueryBuilder - * French translation by Damien "Mistic" Sorel - */ +{ + "__locale": "French (fr)", + "__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr", -jQuery.fn.queryBuilder.defaults.set({ lang: { "add_rule": "Ajouter une règle", "add_group": "Ajouter un groupe", "delete_rule": "Supprimer", "delete_group": "Supprimer", - "condition_and": "ET", - "condition_or": "OU", - - "filter_select_placeholder": "------", + "conditions": { + "AND": "ET", + "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", - "between": "entre", + "equal": "est égal à", + "not_equal": "n'est pas égal à", + "in": "est compris dans", + "not_in": "n'est pas compris dans", + "less": "est inférieur à", + "less_or_equal": "est inférieur ou égal à", + "greater": "est supérieur à", + "greater_or_equal": "est supérieur ou égal à", + "between": "est entre", + "not_between": "n'est pas entre", "begins_with": "commence par", "not_begins_with": "ne commence pas par", "contains": "contient", @@ -35,7 +34,7 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "is_null": "est nul", "is_not_null": "n'est pas nul" }, - + "errors": { "no_filter": "Aucun filtre sélectionné", "empty_group": "Le groupe est vide", @@ -52,8 +51,13 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "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_exceed_max": "Doit être avant {0}", + "datetime_between_invalid": "Valeurs invalides, {0} est plus grand que {1}", + "boolean_not_valid": "N'est pas un booléen", + "operator_not_multiple": "L'opérateur \"{1}\" ne peut utiliser plusieurs valeurs" } -}}); \ 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.js b/src/i18n/it.js deleted file mode 100644 index 87a100b1..00000000 --- a/src/i18n/it.js +++ /dev/null @@ -1,37 +0,0 @@ -/*! - * jQuery QueryBuilder - * Italian translation - */ - -jQuery.fn.queryBuilder.defaults.set({ lang: { - "add_rule": "Aggiungi regola", - "add_group": "Aggiungi gruppo", - "delete_rule": "Elimina", - "delete_group": "Elimina", - - "condition_and": "E", - "condition_or": "O", - - "filter_select_placeholder": "------", - - "operators": { - "equal": "uguale", - "not_equal": "non uguale", - "in": "in", - "not_in": "non in", - "less": "minore", - "less_or_equal": "minore o uguale", - "greater": "maggiore", - "greater_or_equal": "maggiore o uguale", - "begins_with": "inizia con", - "not_begins_with": "non inizia con", - "contains": "contiene", - "not_contains": "non contiene", - "ends_with": "finisce con", - "not_ends_with": "non finisce con", - "is_empty": "è vuoto", - "is_not_empty": "non è vuoto", - "is_null": "è nullo", - "is_not_null": "non è nullo" - } -}}); \ No newline at end of file diff --git a/src/i18n/it.json b/src/i18n/it.json new file mode 100644 index 00000000..076cc172 --- /dev/null +++ b/src/i18n/it.json @@ -0,0 +1,63 @@ +{ + "__locale": "Italian (it)", + "__author": "davegraziosi, Giuseppe Lodi Rizzini", + + "add_rule": "Aggiungi regola", + "add_group": "Aggiungi gruppo", + "delete_rule": "Elimina", + "delete_group": "Elimina", + + "conditions": { + "AND": "E", + "OR": "O" + }, + + "operators": { + "equal": "uguale", + "not_equal": "non uguale", + "in": "in", + "not_in": "non in", + "less": "minore", + "less_or_equal": "minore o uguale", + "greater": "maggiore", + "greater_or_equal": "maggiore o uguale", + "between" : "compreso tra", + "not_between" : "non compreso tra", + "begins_with": "inizia con", + "not_begins_with": "non inizia con", + "contains": "contiene", + "not_contains": "non contiene", + "ends_with": "finisce con", + "not_ends_with": "non finisce con", + "is_empty": "è vuoto", + "is_not_empty": "non è vuoto", + "is_null": "è nullo", + "is_not_null": "non è nullo" + }, + + "errors": { + "no_filter": "Nessun filtro selezionato", + "empty_group": "Il gruppo è vuoto", + "radio_empty": "No value selected", + "checkbox_empty": "Nessun valore selezionato", + "select_empty": "Nessun valore selezionato", + "string_empty": "Valore vuoto", + "string_exceed_min_length": "Deve contenere almeno {0} caratteri", + "string_exceed_max_length": "Non deve contenere più di {0} caratteri", + "string_invalid_format": "Formato non valido ({0})", + "number_nan": "Non è un numero", + "number_not_integer": "Non è un intero", + "number_not_double": "Non è un numero con la virgola", + "number_exceed_min": "Deve essere maggiore di {0}", + "number_exceed_max": "Deve essere minore di {0}", + "number_wrong_step": "Deve essere multiplo di {0}", + "number_between_invalid": "Valori non validi, {0} è maggiore di {1}", + "datetime_empty": "Valore vuoto", + "datetime_invalid": "Formato data non valido ({0})", + "datetime_exceed_min": "Deve essere successivo a {0}", + "datetime_exceed_max": "Deve essere precedente a {0}", + "datetime_between_invalid": "Valori non validi, {0} è maggiore di {1}", + "boolean_not_valid": "Non è un booleano", + "operator_not_multiple": "L'Operatore {0} non può accettare valori multipli" + } +} 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/dist/i18n/nl.js b/src/i18n/nl.json similarity index 88% rename from dist/i18n/nl.js rename to src/i18n/nl.json index 3a591f1f..06199640 100644 --- a/dist/i18n/nl.js +++ b/src/i18n/nl.json @@ -1,18 +1,16 @@ -/*! - * jQuery QueryBuilder - * Dutch translation by "Roywcm" - */ +{ + "__locale": "Dutch (nl)", + "__author": "\"Roywcm\"", -jQuery.fn.queryBuilder.defaults.set({ lang: { "add_rule": "Nieuwe regel", "add_group": "Nieuwe groep", "delete_rule": "Verwijder", "delete_group": "Verwijder", - "condition_and": "EN", - "condition_or": "OF", - - "filter_select_placeholder": "------", + "conditions": { + "AND": "EN", + "OR": "OF" + }, "operators": { "equal": "gelijk", @@ -24,6 +22,7 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "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", @@ -35,7 +34,7 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "is_null": "is null", "is_not_null": "is niet null" }, - + "errors": { "no_filter": "Geen filter geselecteerd", "empty_group": "De groep is leeg", @@ -56,4 +55,4 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "datetime_exceed_min": "Dient na {0}", "datetime_exceed_max": "Dient voor {0}" } -}}); \ No newline at end of file +} diff --git a/src/i18n/no.json b/src/i18n/no.json new file mode 100644 index 00000000..6f65e25e --- /dev/null +++ b/src/i18n/no.json @@ -0,0 +1,35 @@ +{ + "__locale": "Norwegian (no)", + "__author": "Jna Borup Coyle, github@coyle.dk", + + "add_rule": "Legg til regel", + "add_group": "Legg til gruppe", + "delete_rule": "Slett regel", + "delete_group": "Slett gruppe", + + "conditions": { + "AND": "OG", + "OR": "ELLER" + }, + + "operators": { + "equal": "er lik", + "not_equal": "er ikke lik", + "in": "finnes i", + "not_in": "finnes ikke i", + "less": "er mindre enn", + "less_or_equal": "er mindre eller lik", + "greater": "er større enn", + "greater_or_equal": "er større eller lik", + "begins_with": "begynner med", + "not_begins_with": "begynner ikke med", + "contains": "inneholder", + "not_contains": "inneholder ikke", + "ends_with": "slutter med", + "not_ends_with": "slutter ikke med", + "is_empty": "er tom", + "is_not_empty": "er ikke tom", + "is_null": "er null", + "is_not_null": "er ikke null" + } +} \ No newline at end of file diff --git a/dist/i18n/pl.js b/src/i18n/pl.json similarity index 81% rename from dist/i18n/pl.js rename to src/i18n/pl.json index 475d25e2..9cddba15 100644 --- a/dist/i18n/pl.js +++ b/src/i18n/pl.json @@ -1,18 +1,16 @@ -/*! - * jQuery QueryBuilder - * Polish translation by Artur Smolarek - */ +{ + "__locale": "Polish (pl)", + "__author": "Artur Smolarek", -jQuery.fn.queryBuilder.defaults.set({ lang: { "add_rule": "Dodaj regułę", "add_group": "Dodaj grupę", "delete_rule": "Usuń", "delete_group": "Usuń", - "condition_and": "AND", - "condition_or": "OR", - - "filter_select_placeholder": "------", + "conditions": { + "AND": "ORAZ", + "OR": "LUB" + }, "operators": { "equal": "równa się", @@ -24,6 +22,7 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "greater": "większe", "greater_or_equal": "większe lub równe", "between": "pomiędzy", + "not_between": "nie jest pomiędzy", "begins_with": "rozpoczyna się od", "not_begins_with": "nie rozpoczyna się od", "contains": "zawiera", @@ -35,7 +34,7 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "is_null": "jest niezdefiniowane", "is_not_null": "nie jest niezdefiniowane" }, - + "errors": { "no_filter": "Nie wybrano żadnego filtra", "empty_group": "Grupa jest pusta", @@ -52,8 +51,11 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "number_exceed_min": "Musi być większe niż {0}", "number_exceed_max": "Musi być mniejsze niż {0}", "number_wrong_step": "Musi być wielokrotnością {0}", + "datetime_empty": "Nie wybrano wartości", "datetime_invalid": "Nieprawidłowy format daty ({0})", "datetime_exceed_min": "Musi być po {0}", - "datetime_exceed_max": "Musi być przed {0}" + "datetime_exceed_max": "Musi być przed {0}", + "boolean_not_valid": "Niepoprawna wartość logiczna", + "operator_not_multiple": "Operator \"{1}\" nie przyjmuje wielu wartości" } -}}); \ No newline at end of file +} \ No newline at end of file diff --git a/dist/i18n/pt-BR.js b/src/i18n/pt-BR.json similarity index 71% rename from dist/i18n/pt-BR.js rename to src/i18n/pt-BR.json index 1aeed08e..f822aa83 100644 --- a/dist/i18n/pt-BR.js +++ b/src/i18n/pt-BR.json @@ -1,18 +1,16 @@ -/*! - * jQuery QueryBuilder - * Portuguese Brazilian translation by Leandro Gehlen (leandrogehlen@gmail.com) - */ +{ + "__locale": "Brazilian Portuguese (pr-BR)", + "__author": "Leandro Gehlen, leandrogehlen@gmail.com; Marcos Ferretti, marcosvferretti@gmail.com", -jQuery.fn.queryBuilder.defaults.set({ lang: { "add_rule": "Nova Regra", - "add_group": "Novo Gruop", + "add_group": "Novo Grupo", "delete_rule": "Excluir", "delete_group": "Excluir", - "condition_and": "E", - "condition_or": "OU", - - "filter_select_placeholder": "------", + "conditions": { + "AND": "E", + "OR": "OU" + }, "operators": { "equal": "Igual", @@ -23,7 +21,8 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "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", @@ -35,7 +34,7 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "is_null": "É nulo", "is_not_null": "Não é nulo" }, - + "errors": { "no_filter": "Nenhum filtro selecionado", "empty_group": "O grupo está vazio", @@ -44,7 +43,7 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "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", @@ -54,6 +53,9 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "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 +} \ No newline at end of file diff --git a/src/i18n/pt-BR.js b/src/i18n/pt-PT.json similarity index 67% rename from src/i18n/pt-BR.js rename to src/i18n/pt-PT.json index 1aeed08e..34a7ca0e 100644 --- a/src/i18n/pt-BR.js +++ b/src/i18n/pt-PT.json @@ -1,31 +1,29 @@ -/*! - * jQuery QueryBuilder - * Portuguese Brazilian translation by Leandro Gehlen (leandrogehlen@gmail.com) - */ +{ + "__locale": "Portuguese (pt-PT)", + "__author": "Miguel Guerreiro, migas.csi@gmail.com", -jQuery.fn.queryBuilder.defaults.set({ lang: { "add_rule": "Nova Regra", - "add_group": "Novo Gruop", + "add_group": "Novo Grupo", "delete_rule": "Excluir", "delete_group": "Excluir", - "condition_and": "E", - "condition_or": "OU", - - "filter_select_placeholder": "------", + "conditions": { + "AND": "E", + "OR": "OU" + }, "operators": { - "equal": "Igual", - "not_equal": "Diferente", + "equal": "Igual a", + "not_equal": "Diferente de", "in": "Contido", "not_in": "Não contido", - "less": "Menor", - "less_or_equal": "Menor ou igual", - "greater": "Maior", - "greater_or_equal": "Maior ou igual", - "between": "entre", - "begins_with": "Iniciando com", - "not_begins_with": "Não iniciando com", + "less": "Menor que", + "less_or_equal": "Menor ou igual a", + "greater": "Maior que", + "greater_or_equal": "Maior ou igual que", + "between": "Entre", + "begins_with": "Começar por", + "not_begins_with": "Não a começar por", "contains": "Contém", "not_contains": "Não contém", "ends_with": "Terminando com", @@ -35,7 +33,7 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "is_null": "É nulo", "is_not_null": "Não é nulo" }, - + "errors": { "no_filter": "Nenhum filtro selecionado", "empty_group": "O grupo está vazio", @@ -44,7 +42,7 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "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", @@ -56,4 +54,4 @@ jQuery.fn.queryBuilder.defaults.set({ lang: { "datetime_exceed_min": "É necessário ser superior a {0}", "datetime_exceed_max": "É necessário ser inferior a {0}" } -}}); \ No newline at end of file +} \ No newline at end of file diff --git a/src/i18n/ro.js b/src/i18n/ro.js deleted file mode 100644 index 477d8cf6..00000000 --- a/src/i18n/ro.js +++ /dev/null @@ -1,37 +0,0 @@ -/*! - * jQuery QueryBuilder - * Romanian translation by ArianServ - */ - -jQuery.fn.queryBuilder.defaults.set({ lang: { - "add_rule": "Adaugă regulă", - "add_group": "Adaugă grup", - "delete_rule": "Şterge", - "delete_group": "Şterge", - - "condition_and": "ŞI", - "condition_or": "SAU", - - "filter_select_placeholder": "------", - - "operators": { - "equal": "egal", - "not_equal": "diferit", - "in": "în", - "not_in": "nu în", - "less": "mai puţin", - "less_or_equal": "mai puţin sau egal", - "greater": "mai mare", - "greater_or_equal": "mai mare sau egal", - "begins_with": "începe cu", - "not_begins_with": "nu începe cu", - "contains": "conţine", - "not_contains": "nu conţine", - "ends_with": "se termină cu", - "not_ends_with": "nu se termină cu", - "is_empty": "este gol", - "is_not_empty": "nu este gol", - "is_null": "e nul", - "is_not_null": "nu e nul" - } -}}); \ No newline at end of file diff --git a/src/i18n/ro.json b/src/i18n/ro.json new file mode 100644 index 00000000..b2a6eae3 --- /dev/null +++ b/src/i18n/ro.json @@ -0,0 +1,63 @@ +{ + "__locale": "Romanian (ro)", + "__author": "ArianServ, totpero", + + "add_rule": "Adaugă regulă", + "add_group": "Adaugă grup", + "delete_rule": "Şterge", + "delete_group": "Şterge", + + "conditions": { + "AND": "ŞI", + "OR": "SAU" + }, + + "operators": { + "equal": "egal", + "not_equal": "diferit", + "in": "în", + "not_in": "nu în", + "less": "mai mic", + "less_or_equal": "mai mic sau egal", + "greater": "mai mare", + "greater_or_equal": "mai mare sau egal", + "between": "între", + "not_between": "nu între", + "begins_with": "începe cu", + "not_begins_with": "nu începe cu", + "contains": "conţine", + "not_contains": "nu conţine", + "ends_with": "se termină cu", + "not_ends_with": "nu se termină cu", + "is_empty": "este gol", + "is_not_empty": "nu este gol", + "is_null": "e nul", + "is_not_null": "nu e nul" + }, + + "errors": { + "no_filter": "Nici un filtru selectat", + "empty_group": "Grupul este gol", + "radio_empty": "Nici o valoare nu este selectată", + "checkbox_empty": "Nici o valoare nu este selectată", + "select_empty": "Nici o valoare nu este selectată", + "string_empty": "Valoare goală", + "string_exceed_min_length": "Trebuie să conţină mai puţin de {0} caractere", + "string_exceed_max_length": "Trebuie să conţină mai mult de {0} caractere", + "string_invalid_format": "Format invalid ({0})", + "number_nan": "Nu este număr", + "number_not_integer": "Nu este număr întreg", + "number_not_double": "Nu este număr real", + "number_exceed_min": "Trebuie să fie mai mare decât {0}", + "number_exceed_max": "Trebuie să fie mai mic decât {0}", + "number_wrong_step": "Trebuie să fie multiplu de {0}", + "number_between_invalid": "Valori invalide, {0} este mai mare decât {1}", + "datetime_empty": "Valoare goală", + "datetime_invalid": "Format dată invalid ({0})", + "datetime_exceed_min": "Trebuie să fie după {0}", + "datetime_exceed_max": "Trebuie să fie înainte {0}", + "datetime_between_invalid": "Valori invalide, {0} este mai mare decât {1}", + "boolean_not_valid": "Nu este boolean", + "operator_not_multiple": "Operatorul \"{1}\" nu poate accepta mai multe valori" + } +} diff --git a/src/i18n/ru.json b/src/i18n/ru.json new file mode 100644 index 00000000..9ccfefcc --- /dev/null +++ b/src/i18n/ru.json @@ -0,0 +1,62 @@ +{ + "__locale": "Russian (ru)", + + "add_rule": "Добавить", + "add_group": "Добавить группу", + "delete_rule": "Удалить", + "delete_group": "Удалить", + + "conditions": { + "AND": "И", + "OR": "ИЛИ" + }, + + "operators": { + "equal": "равно", + "not_equal": "не равно", + "in": "из указанных", + "not_in": "не из указанных", + "less": "меньше", + "less_or_equal": "меньше или равно", + "greater": "больше", + "greater_or_equal": "больше или равно", + "between": "между", + "not_between": "не между", + "begins_with": "начинается с", + "not_begins_with": "не начинается с", + "contains": "содержит", + "not_contains": "не содержит", + "ends_with": "оканчивается на", + "not_ends_with": "не оканчивается на", + "is_empty": "пустая строка", + "is_not_empty": "не пустая строка", + "is_null": "пусто", + "is_not_null": "не пусто" + }, + + "errors": { + "no_filter": "Фильтр не выбран", + "empty_group": "Группа пуста", + "radio_empty": "Не выбрано значение", + "checkbox_empty": "Не выбрано значение", + "select_empty": "Не выбрано значение", + "string_empty": "Не заполнено", + "string_exceed_min_length": "Должен содержать больше {0} символов", + "string_exceed_max_length": "Должен содержать меньше {0} символов", + "string_invalid_format": "Неверный формат ({0})", + "number_nan": "Не число", + "number_not_integer": "Не число", + "number_not_double": "Не число", + "number_exceed_min": "Должно быть больше {0}", + "number_exceed_max": "Должно быть меньше, чем {0}", + "number_wrong_step": "Должно быть кратно {0}", + "number_between_invalid": "Недопустимые значения, {0} больше {1}", + "datetime_empty": "Не заполнено", + "datetime_invalid": "Неверный формат даты ({0})", + "datetime_exceed_min": "Должно быть, после {0}", + "datetime_exceed_max": "Должно быть, до {0}", + "datetime_between_invalid": "Недопустимые значения, {0} больше {1}", + "boolean_not_valid": "Не логическое", + "operator_not_multiple": "Оператор \"{1}\" не поддерживает много значений" + } +} \ 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 new file mode 100644 index 00000000..415244bd --- /dev/null +++ b/src/i18n/sq.json @@ -0,0 +1,60 @@ +{ + "__locale": "Albanian (sq)", + "__author": "Tomor Pupovci", + + "add_rule": "Shto rregull", + "add_group": "Shto grup", + "delete_rule": "Fshij", + "delete_group": "Fshij", + + "conditions": { + "AND": "DHE", + "OR": "OSE" + }, + + "operators": { + "equal": "barabartë", + "not_equal": "e ndryshme prej", + "in": "në", + "not_in": "jo në", + "less": "më e vogël", + "less_or_equal": "më e vogël ose e barabartë me", + "greater": "më e madhe", + "greater_or_equal": "më e madhe ose e barabartë", + "between": "në mes", + "begins_with": "fillon me", + "not_begins_with": "nuk fillon me", + "contains": "përmban", + "not_contains": "nuk përmban", + "ends_with": "mbaron me", + "not_ends_with": "nuk mbaron me", + "is_empty": "është e zbrazët", + "is_not_empty": "nuk është e zbrazët", + "is_null": "është null", + "is_not_null": "nuk është null" + }, + + "errors": { + "no_filter": "Nuk ka filter të zgjedhur", + "empty_group": "Grupi është i zbrazët", + "radio_empty": "Nuk ka vlerë të zgjedhur", + "checkbox_empty": "Nuk ka vlerë të zgjedhur", + "select_empty": "Nuk ka vlerë të zgjedhur", + "string_empty": "Vlerë e zbrazët", + "string_exceed_min_length": "Duhet të përmbajë së paku {0} karaktere", + "string_exceed_max_length": "Nuk duhet të përmbajë më shumë se {0} karaktere", + "string_invalid_format": "Format i pasaktë ({0})", + "number_nan": "Nuk është numër", + "number_not_integer": "Nuk është numër i plotë", + "number_not_double": "Nuk është numër me presje", + "number_exceed_min": "Duhet të jetë më i madh se {0}", + "number_exceed_max": "Duhet të jetë më i vogël se {0}", + "number_wrong_step": "Duhet të jetë shumëfish i {0}", + "datetime_empty": "Vlerë e zbrazët", + "datetime_invalid": "Format i pasaktë i datës ({0})", + "datetime_exceed_min": "Duhet të jetë pas {0}", + "datetime_exceed_max": "Duhet të jetë para {0}", + "boolean_not_valid": "Nuk është boolean", + "operator_not_multiple": "Operatori \"{1}\" nuk mund të pranojë vlera të shumëfishta" + } +} \ 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 new file mode 100644 index 00000000..25ecbf78 --- /dev/null +++ b/src/i18n/tr.json @@ -0,0 +1,63 @@ +{ + "__locale": "Turkish (tr)", + "__author": "Aykut Alpgiray Ateş", + + "add_rule": "Kural Ekle", + "add_group": "Grup Ekle", + "delete_rule": "Sil", + "delete_group": "Sil", + + "conditions": { + "AND": "Ve", + "OR": "Veya" + }, + + "operators": { + "equal": "eşit", + "not_equal": "eşit değil", + "in": "içinde", + "not_in": "içinde değil", + "less": "küçük", + "less_or_equal": "küçük veya eşit", + "greater": "büyük", + "greater_or_equal": "büyük veya eşit", + "between": "arasında", + "not_between": "arasında değil", + "begins_with": "ile başlayan", + "not_begins_with": "ile başlamayan", + "contains": "içeren", + "not_contains": "içermeyen", + "ends_with": "ile biten", + "not_ends_with": "ile bitmeyen", + "is_empty": "boş ise", + "is_not_empty": "boş değil ise", + "is_null": "var ise", + "is_not_null": "yok ise" + }, + + "errors": { + "no_filter": "Bir filtre seçili değil", + "empty_group": "Grup bir eleman içermiyor", + "radio_empty": "Seçim yapılmalı", + "checkbox_empty": "Seçim yapılmalı", + "select_empty": "Seçim yapılmalı", + "string_empty": "Bir metin girilmeli", + "string_exceed_min_length": "En az {0} karakter girilmeli", + "string_exceed_max_length": "En fazla {0} karakter girilebilir", + "string_invalid_format": "Uyumsuz format ({0})", + "number_nan": "Sayı değil", + "number_not_integer": "Tam sayı değil", + "number_not_double": "Ondalıklı sayı değil", + "number_exceed_min": "Sayı {0}'den/dan daha büyük olmalı", + "number_exceed_max": "Sayı {0}'den/dan daha küçük olmalı", + "number_wrong_step": "{0} veya katı olmalı", + "number_between_invalid": "Geçersiz değerler, {0} değeri {1} değerinden büyük", + "datetime_empty": "Tarih Seçilmemiş", + "datetime_invalid": "Uygun olmayan tarih formatı ({0})", + "datetime_exceed_min": "{0} Tarihinden daha sonrası olmalı.", + "datetime_exceed_max": "{0} Tarihinden daha öncesi olmalı.", + "datetime_between_invalid": "Geçersiz değerler, {0} değeri {1} değerinden büyük", + "boolean_not_valid": "Değer Doğru/Yanlış(bool) olmalı", + "operator_not_multiple": "Operatör \"{1}\" birden fazla değer kabul etmiyor" + } +} diff --git a/src/i18n/ua.json b/src/i18n/ua.json new file mode 100644 index 00000000..2d1c5c2d --- /dev/null +++ b/src/i18n/ua.json @@ -0,0 +1,57 @@ +{ + "__locale": "Ukrainian (ua)", + "__author": "Megaplan, mborisv ", + + "add_rule": "Додати", + "add_group": "Додати групу", + "delete_rule": "Видалити", + "delete_group": "Видалити", + "conditions": { + "AND": "І", + "OR": "АБО" + }, + "operators": { + "equal": "дорівнює", + "not_equal": "не дорівнює", + "in": "з вказаних", + "not_in": "не з вказаних", + "less": "менше", + "less_or_equal": "менше або дорівнюж", + "greater": "більше", + "greater_or_equal": "більше або дорівнює", + "between": "між", + "begins_with": "починається з", + "not_begins_with": "не починається з", + "contains": "містить", + "not_contains": "не містить", + "ends_with": "закінчується на", + "not_ends_with": "не не закінчується на", + "is_empty": "порожній рядок", + "is_not_empty": "не порожній рядок", + "is_null": "порожньо", + "is_not_null": "не порожньо" + }, + "errors": { + "no_filter": "Фільтр не вибраний", + "empty_group": "Група порожня", + "radio_empty": "Значення не вибрано", + "checkbox_empty": "Значення не вибрано", + "select_empty": "Значення не вибрано", + "string_empty": "Не заповнено", + "string_exceed_min_length": "Повинен містити більше {0} символів", + "string_exceed_max_length": "Повинен містити менше {0} символів", + "string_invalid_format": "Невірний формат ({0})", + "number_nan": "Не число", + "number_not_integer": "Не число", + "number_not_double": "Не число", + "number_exceed_min": "Повинне бути більше {0}", + "number_exceed_max": "Повинне бути менше, ніж {0}", + "number_wrong_step": "Повинне бути кратне {0}", + "datetime_empty": "Не заповнено", + "datetime_invalid": "Невірний формат дати ({0})", + "datetime_exceed_min": "Повинне бути, після {0}", + "datetime_exceed_max": "Повинне бути, до {0}", + "boolean_not_valid": "Не логічне", + "operator_not_multiple": "Оператор \"{1}\" не підтримує багато значень" + } +} \ No newline at end of file diff --git a/src/i18n/zh-CN.json b/src/i18n/zh-CN.json new file mode 100644 index 00000000..666ca685 --- /dev/null +++ b/src/i18n/zh-CN.json @@ -0,0 +1,61 @@ +{ + "__locale": "Simplified Chinese (zh_CN)", + "__author": "shadowwind, shatteredwindgo@gmail.com", + + "add_rule": "添加规则", + "add_group": "添加组", + "delete_rule": "删除", + "delete_group": "删除组", + + "conditions": { + "AND": "和", + "OR": "或" + }, + + "operators": { + "equal": "等于", + "not_equal": "不等于", + "in": "在...之內", + "not_in": "不在...之內", + "less": "小于", + "less_or_equal": "小于或等于", + "greater": "大于", + "greater_or_equal": "大于或等于", + "between": "在...之间", + "not_between": "不在...之间", + "begins_with": "以...开始", + "not_begins_with": "不以...开始", + "contains": "包含以下内容", + "not_contains": "不包含以下内容", + "ends_with": "以...结束", + "not_ends_with": "不以...结束", + "is_empty": "为空", + "is_not_empty": "不为空", + "is_null": "为 null", + "is_not_null": "不为 null" + }, + + "errors": { + "no_filter": "没有选择过滤器", + "empty_group": "该组为空", + "radio_empty": "没有选中项", + "checkbox_empty": "没有选中项", + "select_empty": "没有选中项", + "string_empty": "没有输入值", + "string_exceed_min_length": "必须至少包含{0}个字符", + "string_exceed_max_length": "必须不超过{0}个字符", + "string_invalid_format": "无效格式({0})", + "number_nan": "值不是数字", + "number_not_integer": "不是整数", + "number_not_double": "不是浮点数", + "number_exceed_min": "必须大于{0}", + "number_exceed_max": "必须小于{0}", + "number_wrong_step": "必须是{0}的倍数", + "datetime_empty": "值为空", + "datetime_invalid": "不是有效日期({0})", + "datetime_exceed_min": "必须在{0}之后", + "datetime_exceed_max": "必须在{0}之前", + "boolean_not_valid": "不是布尔值", + "operator_not_multiple": "选项\"{1}\"无法接受多个值" + } +} \ No newline at end of file diff --git a/src/jquery.js b/src/jquery.js new file mode 100644 index 00000000..941ea675 --- /dev/null +++ b/src/jquery.js @@ -0,0 +1,77 @@ +/** + * The {@link http://learn.jquery.com/plugins/|jQuery Plugins} namespace + * @external "jQuery.fn" + */ + +/** + * Instanciates or accesses the {@link QueryBuilder} on an element + * @function + * @memberof external:"jQuery.fn" + * @param {*} option - initial configuration or method name + * @param {...*} args - method arguments + * + * @example + * $('#builder').queryBuilder({ /** configuration object *\/ }); + * @example + * $('#builder').queryBuilder('methodName', methodParam1, methodParam2); + */ +$.fn.queryBuilder = function(option) { + if (this.length === 0) { + Utils.error('Config', 'No target defined'); + } + if (this.length > 1) { + Utils.error('Config', 'Unable to initialize on multiple target'); + } + + var data = this.data('queryBuilder'); + var options = (typeof option == 'object' && option) || {}; + + if (!data && option == 'destroy') { + return this; + } + if (!data) { + var builder = new QueryBuilder(this, options); + this.data('queryBuilder', builder); + builder.init(options.rules); + } + if (typeof option == 'string') { + return data[option].apply(data, Array.prototype.slice.call(arguments, 1)); + } + + return this; +}; + +/** + * @function + * @memberof external:"jQuery.fn" + * @see QueryBuilder + */ +$.fn.queryBuilder.constructor = QueryBuilder; + +/** + * @function + * @memberof external:"jQuery.fn" + * @see QueryBuilder.defaults + */ +$.fn.queryBuilder.defaults = QueryBuilder.defaults; + +/** + * @function + * @memberof external:"jQuery.fn" + * @see QueryBuilder.defaults + */ +$.fn.queryBuilder.extend = QueryBuilder.extend; + +/** + * @function + * @memberof external:"jQuery.fn" + * @see QueryBuilder.define + */ +$.fn.queryBuilder.define = QueryBuilder.define; + +/** + * @function + * @memberof external:"jQuery.fn" + * @see QueryBuilder.regional + */ +$.fn.queryBuilder.regional = QueryBuilder.regional; diff --git a/src/main.js b/src/main.js new file mode 100644 index 00000000..0659d288 --- /dev/null +++ b/src/main.js @@ -0,0 +1,223 @@ +/** + * @typedef {object} Filter + * @memberof QueryBuilder + * @description See {@link http://querybuilder.js.org/index.html#filters} + */ + +/** + * @typedef {object} Operator + * @memberof QueryBuilder + * @description See {@link http://querybuilder.js.org/index.html#operators} + */ + +/** + * @param {jQuery} $el + * @param {object} options - see {@link http://querybuilder.js.org/#options} + * @constructor + */ +var QueryBuilder = function($el, options) { + $el[0].queryBuilder = this; + + /** + * Element container + * @member {jQuery} + * @readonly + */ + this.$el = $el; + + /** + * Configuration object + * @member {object} + * @readonly + */ + this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options); + + /** + * Internal model + * @member {Model} + * @readonly + */ + this.model = new Model(); + + /** + * Internal status + * @member {object} + * @property {string} id - id of the container + * @property {boolean} generated_id - if the container id has been generated + * @property {int} group_id - current group id + * @property {int} rule_id - current rule id + * @property {boolean} has_optgroup - if filters have optgroups + * @property {boolean} has_operator_optgroup - if operators have optgroups + * @readonly + * @private + */ + this.status = { + id: null, + generated_id: false, + group_id: 0, + rule_id: 0, + has_optgroup: false, + has_operator_optgroup: false + }; + + /** + * List of filters + * @member {QueryBuilder.Filter[]} + * @readonly + */ + this.filters = this.settings.filters; + + /** + * List of icons + * @member {object.} + * @readonly + */ + this.icons = this.settings.icons; + + /** + * List of operators + * @member {QueryBuilder.Operator[]} + * @readonly + */ + this.operators = this.settings.operators; + + /** + * List of templates + * @member {object.} + * @readonly + */ + this.templates = this.settings.templates; + + /** + * Plugins configuration + * @member {object.} + * @readonly + */ + this.plugins = this.settings.plugins; + + /** + * Translations object + * @member {object} + * @readonly + */ + this.lang = null; + + // translations : english << 'lang_code' << custom + if (QueryBuilder.regional['en'] === undefined) { + Utils.error('Config', '"i18n/en.js" not loaded.'); + } + this.lang = $.extendext(true, 'replace', {}, QueryBuilder.regional['en'], QueryBuilder.regional[this.settings.lang_code], this.settings.lang); + + // "allow_groups" can be boolean or int + if (this.settings.allow_groups === false) { + this.settings.allow_groups = 0; + } + else if (this.settings.allow_groups === true) { + this.settings.allow_groups = -1; + } + + // init templates + Object.keys(this.templates).forEach(function(tpl) { + if (!this.templates[tpl]) { + this.templates[tpl] = QueryBuilder.templates[tpl]; + } + if (typeof this.templates[tpl] !== 'function') { + throw new Error(`Template ${tpl} must be a function`); + } + }, this); + + // ensure we have a container id + if (!this.$el.attr('id')) { + this.$el.attr('id', 'qb_' + Math.floor(Math.random() * 99999)); + this.status.generated_id = true; + } + this.status.id = this.$el.attr('id'); + + // INIT + this.$el.addClass('query-builder'); + + this.filters = this.checkFilters(this.filters); + this.operators = this.checkOperators(this.operators); + this.bindEvents(); + this.initPlugins(); +}; + +$.extend(QueryBuilder.prototype, /** @lends QueryBuilder.prototype */ { + /** + * Triggers an event on the builder container + * @param {string} type + * @returns {$.Event} + */ + trigger: function(type) { + var event = new $.Event(this._tojQueryEvent(type), { + builder: this + }); + + this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 1)); + + return event; + }, + + /** + * Triggers an event on the builder container and returns the modified value + * @param {string} type + * @param {*} value + * @returns {*} + */ + change: function(type, value) { + var event = new $.Event(this._tojQueryEvent(type, true), { + builder: this, + value: value + }); + + this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 2)); + + return event.value; + }, + + /** + * Attaches an event listener on the builder container + * @param {string} type + * @param {function} cb + * @returns {QueryBuilder} + */ + on: function(type, cb) { + this.$el.on(this._tojQueryEvent(type), cb); + return this; + }, + + /** + * Removes an event listener from the builder container + * @param {string} type + * @param {function} [cb] + * @returns {QueryBuilder} + */ + off: function(type, cb) { + this.$el.off(this._tojQueryEvent(type), cb); + return this; + }, + + /** + * Attaches an event listener called once on the builder container + * @param {string} type + * @param {function} cb + * @returns {QueryBuilder} + */ + once: function(type, cb) { + this.$el.one(this._tojQueryEvent(type), cb); + return this; + }, + + /** + * Appends `.queryBuilder` and optionally `.filter` to the events names + * @param {string} name + * @param {boolean} [filter=false] + * @returns {string} + * @private + */ + _tojQueryEvent: function(name, filter) { + return name.split(' ').map(function(type) { + return type + '.queryBuilder' + (filter ? '.filter' : ''); + }).join(' '); + } +}); diff --git a/src/model.js b/src/model.js new file mode 100644 index 00000000..a397c98f --- /dev/null +++ b/src/model.js @@ -0,0 +1,549 @@ +/** + * Main object storing data model and emitting model events + * @constructor + */ +function Model() { + /** + * @member {Group} + * @readonly + */ + this.root = null; + + /** + * Base for event emitting + * @member {jQuery} + * @readonly + * @private + */ + this.$ = $(this); +} + +$.extend(Model.prototype, /** @lends Model.prototype */ { + /** + * Triggers an event on the model + * @param {string} type + * @returns {$.Event} + */ + trigger: function(type) { + var event = new $.Event(type); + this.$.triggerHandler(event, Array.prototype.slice.call(arguments, 1)); + return event; + }, + + /** + * Attaches an event listener on the model + * @param {string} type + * @param {function} cb + * @returns {Model} + */ + on: function() { + this.$.on.apply(this.$, Array.prototype.slice.call(arguments)); + return this; + }, + + /** + * Removes an event listener from the model + * @param {string} type + * @param {function} [cb] + * @returns {Model} + */ + off: function() { + this.$.off.apply(this.$, Array.prototype.slice.call(arguments)); + return this; + }, + + /** + * Attaches an event listener called once on the model + * @param {string} type + * @param {function} cb + * @returns {Model} + */ + once: function() { + this.$.one.apply(this.$, Array.prototype.slice.call(arguments)); + return this; + } +}); + + +/** + * Root abstract object + * @constructor + * @param {Node} [parent] + * @param {jQuery} $el + */ +var Node = function(parent, $el) { + if (!(this instanceof Node)) { + return new Node(parent, $el); + } + + Object.defineProperty(this, '__', { value: {} }); + + $el.data('queryBuilderModel', this); + + /** + * @name level + * @member {int} + * @memberof Node + * @instance + * @readonly + */ + this.__.level = 1; + + /** + * @name error + * @member {string} + * @memberof Node + * @instance + */ + this.__.error = null; + + /** + * @name flags + * @member {object} + * @memberof Node + * @instance + * @readonly + */ + this.__.flags = {}; + + /** + * @name data + * @member {object} + * @memberof Node + * @instance + */ + this.__.data = undefined; + + /** + * @member {jQuery} + * @readonly + */ + this.$el = $el; + + /** + * @member {string} + * @readonly + */ + this.id = $el[0].id; + + /** + * @member {Model} + * @readonly + */ + this.model = null; + + /** + * @member {Group} + * @readonly + */ + this.parent = parent; +}; + +Utils.defineModelProperties(Node, ['level', 'error', 'data', 'flags']); + +Object.defineProperty(Node.prototype, 'parent', { + enumerable: true, + get: function() { + return this.__.parent; + }, + set: function(value) { + this.__.parent = value; + this.level = value === null ? 1 : value.level + 1; + this.model = value === null ? null : value.model; + } +}); + +/** + * Checks if this Node is the root + * @returns {boolean} + */ +Node.prototype.isRoot = function() { + return (this.level === 1); +}; + +/** + * Returns the node position inside its parent + * @returns {int} + */ +Node.prototype.getPos = function() { + if (this.isRoot()) { + return -1; + } + else { + return this.parent.getNodePos(this); + } +}; + +/** + * Deletes self + * @fires Model.model:drop + */ +Node.prototype.drop = function() { + var model = this.model; + + if (!!this.parent) { + this.parent.removeNode(this); + } + + this.$el.removeData('queryBuilderModel'); + + if (model !== null) { + /** + * After a node of the model has been removed + * @event model:drop + * @memberof Model + * @param {Node} node + */ + model.trigger('drop', this); + } +}; + +/** + * Moves itself after another Node + * @param {Node} target + * @fires Model.model:move + */ +Node.prototype.moveAfter = function(target) { + if (!this.isRoot()) { + this.move(target.parent, target.getPos() + 1); + } +}; + +/** + * Moves itself at the beginning of parent or another Group + * @param {Group} [target] + * @fires Model.model:move + */ +Node.prototype.moveAtBegin = function(target) { + if (!this.isRoot()) { + if (target === undefined) { + target = this.parent; + } + + this.move(target, 0); + } +}; + +/** + * Moves itself at the end of parent or another Group + * @param {Group} [target] + * @fires Model.model:move + */ +Node.prototype.moveAtEnd = function(target) { + if (!this.isRoot()) { + if (target === undefined) { + target = this.parent; + } + + this.move(target, target.length() === 0 ? 0 : target.length() - 1); + } +}; + +/** + * Moves itself at specific position of Group + * @param {Group} target + * @param {int} index + * @fires Model.model:move + */ +Node.prototype.move = function(target, index) { + if (!this.isRoot()) { + if (typeof target === 'number') { + index = target; + target = this.parent; + } + + this.parent.removeNode(this); + target.insertNode(this, index, false); + + if (this.model !== null) { + /** + * After a node of the model has been moved + * @event model:move + * @memberof Model + * @param {Node} node + * @param {Node} target + * @param {int} index + */ + this.model.trigger('move', this, target, index); + } + } +}; + + +/** + * Group object + * @constructor + * @extends Node + * @param {Group} [parent] + * @param {jQuery} $el + */ +var Group = function(parent, $el) { + if (!(this instanceof Group)) { + return new Group(parent, $el); + } + + Node.call(this, parent, $el); + + /** + * @member {object[]} + * @readonly + */ + this.rules = []; + + /** + * @name condition + * @member {string} + * @memberof Group + * @instance + */ + this.__.condition = null; +}; + +Group.prototype = Object.create(Node.prototype); +Group.prototype.constructor = Group; + +Utils.defineModelProperties(Group, ['condition']); + +/** + * Removes group's content + */ +Group.prototype.empty = function() { + this.each('reverse', function(rule) { + rule.drop(); + }, function(group) { + group.drop(); + }); +}; + +/** + * Deletes self + */ +Group.prototype.drop = function() { + this.empty(); + Node.prototype.drop.call(this); +}; + +/** + * Returns the number of children + * @returns {int} + */ +Group.prototype.length = function() { + return this.rules.length; +}; + +/** + * Adds a Node at specified index + * @param {Node} node + * @param {int} [index=end] + * @param {boolean} [trigger=false] - fire 'add' event + * @returns {Node} the inserted node + * @fires Model.model:add + */ +Group.prototype.insertNode = function(node, index, trigger) { + if (index === undefined) { + index = this.length(); + } + + this.rules.splice(index, 0, node); + node.parent = this; + + if (trigger && this.model !== null) { + /** + * After a node of the model has been added + * @event model:add + * @memberof Model + * @param {Node} parent + * @param {Node} node + * @param {int} index + */ + this.model.trigger('add', this, node, index); + } + + return node; +}; + +/** + * Adds a new Group at specified index + * @param {jQuery} $el + * @param {int} [index=end] + * @returns {Group} + * @fires Model.model:add + */ +Group.prototype.addGroup = function($el, index) { + return this.insertNode(new Group(this, $el), index, true); +}; + +/** + * Adds a new Rule at specified index + * @param {jQuery} $el + * @param {int} [index=end] + * @returns {Rule} + * @fires Model.model:add + */ +Group.prototype.addRule = function($el, index) { + return this.insertNode(new Rule(this, $el), index, true); +}; + +/** + * Deletes a specific Node + * @param {Node} node + */ +Group.prototype.removeNode = function(node) { + var index = this.getNodePos(node); + if (index !== -1) { + node.parent = null; + this.rules.splice(index, 1); + } +}; + +/** + * Returns the position of a child Node + * @param {Node} node + * @returns {int} + */ +Group.prototype.getNodePos = function(node) { + return this.rules.indexOf(node); +}; + +/** + * @callback Model#GroupIteratee + * @param {Node} node + * @returns {boolean} stop the iteration + */ + +/** + * Iterate over all Nodes + * @param {boolean} [reverse=false] - iterate in reverse order, required if you delete nodes + * @param {Model#GroupIteratee} cbRule - callback for Rules (can be `null` but not omitted) + * @param {Model#GroupIteratee} [cbGroup] - callback for Groups + * @param {object} [context] - context for callbacks + * @returns {boolean} if the iteration has been stopped by a callback + */ +Group.prototype.each = function(reverse, cbRule, cbGroup, context) { + if (typeof reverse !== 'boolean' && typeof reverse !== 'string') { + context = cbGroup; + cbGroup = cbRule; + cbRule = reverse; + reverse = false; + } + context = context === undefined ? null : context; + + var i = reverse ? this.rules.length - 1 : 0; + var l = reverse ? 0 : this.rules.length - 1; + var c = reverse ? -1 : 1; + var next = function() { + return reverse ? i >= l : i <= l; + }; + var stop = false; + + for (; next(); i += c) { + if (this.rules[i] instanceof Group) { + if (!!cbGroup) { + stop = cbGroup.call(context, this.rules[i]) === false; + } + } + else if (!!cbRule) { + stop = cbRule.call(context, this.rules[i]) === false; + } + + if (stop) { + break; + } + } + + return !stop; +}; + +/** + * Checks if the group contains a particular Node + * @param {Node} node + * @param {boolean} [recursive=false] + * @returns {boolean} + */ +Group.prototype.contains = function(node, recursive) { + if (this.getNodePos(node) !== -1) { + return true; + } + else if (!recursive) { + return false; + } + else { + // the loop will return with false as soon as the Node is found + return !this.each(function() { + return true; + }, function(group) { + return !group.contains(node, true); + }); + } +}; + + +/** + * Rule object + * @constructor + * @extends Node + * @param {Group} parent + * @param {jQuery} $el + */ +var Rule = function(parent, $el) { + if (!(this instanceof Rule)) { + return new Rule(parent, $el); + } + + Node.call(this, parent, $el); + + this._updating_value = false; + this._updating_input = false; + + /** + * @name filter + * @member {QueryBuilder.Filter} + * @memberof Rule + * @instance + */ + this.__.filter = null; + + /** + * @name operator + * @member {QueryBuilder.Operator} + * @memberof Rule + * @instance + */ + this.__.operator = null; + + /** + * @name value + * @member {*} + * @memberof Rule + * @instance + */ + this.__.value = undefined; +}; + +Rule.prototype = Object.create(Node.prototype); +Rule.prototype.constructor = Rule; + +Utils.defineModelProperties(Rule, ['filter', 'operator', 'value']); + +/** + * Checks if this Node is the root + * @returns {boolean} always false + */ +Rule.prototype.isRoot = function() { + return false; +}; + + +/** + * @member {function} + * @memberof QueryBuilder + * @see Group + */ +QueryBuilder.Group = Group; + +/** + * @member {function} + * @memberof QueryBuilder + * @see Rule + */ +QueryBuilder.Rule = Rule; 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 new file mode 100644 index 00000000..26b0998f --- /dev/null +++ b/src/plugins/bt-checkbox/plugin.js @@ -0,0 +1,41 @@ +/** + * @class BtCheckbox + * @memberof module:plugins + * @description Applies Awesome Bootstrap Checkbox for checkbox and radio inputs. + * @param {object} [options] + * @param {string} [options.font='bootstrap-icons'] + * @param {string} [options.color='default'] + */ +QueryBuilder.define('bt-checkbox', function(options) { + if (options.font === 'bootstrap-icons') { + this.$el.addClass('bt-checkbox-bootstrap-icons'); + } + + this.on('getRuleInput.filter', function(h, rule, name) { + var filter = rule.filter; + + if ((filter.input === 'radio' || filter.input === 'checkbox') && !filter.plugin) { + h.value = ''; + + if (!filter.colors) { + filter.colors = {}; + } + if (filter.color) { + filter.colors._def_ = filter.color; + } + + var style = filter.vertical ? ' style="display:block"' : ''; + var i = 0; + + Utils.iterateOptions(filter.values, function(key, val) { + var color = filter.colors[key] || filter.colors._def_ || options.color; + var id = name + '_' + (i++); + + h.value += `
        `; + }); + } + }); +}, { + font: 'bootstrap-icons', + color: 'default' +}); 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 9b18a4c7..00000000 --- a/src/plugins/bt-selectpicker/plugin.js +++ /dev/null @@ -1,33 +0,0 @@ -/*! - * jQuery QueryBuilder Bootstrap Selectpicker - * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) - */ - -(function($){ - "use strict"; - - $.fn.queryBuilder.define('bt-selectpicker', function(options) { - if (!$.fn.selectpicker || !$.fn.selectpicker.Constructor) { - $.error('Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select'); - } - - options = $.extend({ - container: 'body', - style: 'btn-inverse btn-xs', - width: 'auto', - showIcon: false - }, options || {}); - - // init selectpicker on filters - this.on('afterAddRule', function($rule) { - $rule.find('.rule-filter-container select').selectpicker(options); - }); - - // init selectpicker on operators - this.on('afterCreateRuleOperators', function($rule) { - $rule.find('.rule-operator-container select').selectpicker(options); - }); - }); - -}(jQuery)); \ No newline at end of file diff --git a/src/plugins/bt-tooltip-errors/plugin.css b/src/plugins/bt-tooltip-errors/plugin.css deleted file mode 100644 index 5a9ec785..00000000 --- a/src/plugins/bt-tooltip-errors/plugin.css +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * jQuery QueryBuilder Bootstrap Tooltip Errors - * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) - */ - -.query-builder .tooltip-inner { - color:#fdd !important; -} \ No newline at end of file diff --git a/src/plugins/bt-tooltip-errors/plugin.js b/src/plugins/bt-tooltip-errors/plugin.js index c8970e84..52f4830d 100644 --- a/src/plugins/bt-tooltip-errors/plugin.js +++ b/src/plugins/bt-tooltip-errors/plugin.js @@ -1,33 +1,33 @@ -/*! - * jQuery QueryBuilder Bootstrap Tooltip Errors - * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) +/** + * @class BtTooltipErrors + * @memberof module:plugins + * @description Applies Bootstrap Tooltips on validation error messages. + * @param {object} [options] + * @param {string} [options.placement='right'] + * @throws MissingLibraryError */ +QueryBuilder.define('bt-tooltip-errors', function(options) { + if (! typeof bootstrap.Tooltip === "function") { + alert(typeof bootstrap.Tooltip ); + Utils.error('MissingLibrary', 'Bootstrap Popper is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com'); + } -(function($){ - "use strict"; + var self = this; - $.fn.queryBuilder.define('bt-tooltip-errors', function(options) { - if (!$.fn.tooltip || !$.fn.tooltip.Constructor || !$.fn.tooltip.Constructor.prototype.fixTitle) { - $.error('Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com'); - } - - options = $.extend({ - placement: 'right' - }, options || {}); - - // add BT Tooltip data - this.on('getRuleTemplate', function(h) { - return h.replace('class="error-container"', 'class="error-container" data-toggle="tooltip"'); - }); - - // init/refresh tooltip when title changes - this.on('validationError', function($target) { - $target.find('.error-container').eq(0) - .tooltip(options) - .tooltip('hide') - .tooltip('fixTitle'); - }); + // add BT Tooltip data + this.on('getRuleTemplate.filter getGroupTemplate.filter', function(h) { + var $h = $($.parseHTML(h.value)); + $h.find(QueryBuilder.selectors.error_container).attr('data-bs-toggle', 'tooltip'); + h.value = $h.prop('outerHTML'); }); -}(jQuery)); \ No newline at end of file + // init/refresh tooltip when title changes + this.model.on('update', function(e, node, field) { + if (field == 'error' && self.settings.display_errors) { + node.$el.find(QueryBuilder.selectors.error_container).eq(0) + .attr('data-bs-original-title',options).attr('data-bs-title',options).tooltip(); + } + }); +}, { + placement: 'right' +}); diff --git a/src/plugins/bt-tooltip-errors/plugin.scss b/src/plugins/bt-tooltip-errors/plugin.scss new file mode 100644 index 00000000..21323e5f --- /dev/null +++ b/src/plugins/bt-tooltip-errors/plugin.scss @@ -0,0 +1,9 @@ +$error-tooltip-color: #F99; + +@if $theme-name == 'dark' { + $error-tooltip-color: #F22; +} + +.query-builder .error-container + .tooltip .tooltip-inner { + color: $error-tooltip-color !important; +} diff --git a/src/plugins/change-filters/plugin.js b/src/plugins/change-filters/plugin.js new file mode 100644 index 00000000..f44934ab --- /dev/null +++ b/src/plugins/change-filters/plugin.js @@ -0,0 +1,171 @@ +/** + * @class ChangeFilters + * @memberof module:plugins + * @description Allows to change available filters after plugin initialization. + */ + +QueryBuilder.extend(/** @lends module:plugins.ChangeFilters.prototype */ { + /** + * Change the filters of the builder + * @param {boolean} [deleteOrphans=false] - delete rules using old filters + * @param {QueryBuilder[]} filters + * @fires module:plugins.ChangeFilters.changer:setFilters + * @fires module:plugins.ChangeFilters.afterSetFilters + * @throws ChangeFilterError + */ + setFilters: function(deleteOrphans, filters) { + var self = this; + + if (filters === undefined) { + filters = deleteOrphans; + deleteOrphans = false; + } + + filters = this.checkFilters(filters); + + /** + * Modifies the filters before {@link module:plugins.ChangeFilters.setFilters} method + * @event changer:setFilters + * @memberof module:plugins.ChangeFilters + * @param {QueryBuilder.Filter[]} filters + * @returns {QueryBuilder.Filter[]} + */ + filters = this.change('setFilters', filters); + + var filtersIds = filters.map(function(filter) { + return filter.id; + }); + + // check for orphans + if (!deleteOrphans) { + (function checkOrphans(node) { + node.each( + function(rule) { + if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) { + Utils.error('ChangeFilter', 'A rule is using filter "{0}"', rule.filter.id); + } + }, + checkOrphans + ); + }(this.model.root)); + } + + // replace filters + this.filters = filters; + + // apply on existing DOM + (function updateBuilder(node) { + node.each(true, + function(rule) { + if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) { + rule.drop(); + + self.trigger('rulesChanged'); + } + else { + self.createRuleFilters(rule); + + rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1'); + self.trigger('afterUpdateRuleFilter', rule); + } + }, + updateBuilder + ); + }(this.model.root)); + + // update plugins + if (this.settings.plugins) { + if (this.settings.plugins['unique-filter']) { + this.updateDisabledFilters(); + } + if (this.settings.plugins['bt-selectpicker']) { + this.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render'); + } + } + + // reset the default_filter if does not exist anymore + if (this.settings.default_filter) { + try { + this.getFilterById(this.settings.default_filter); + } + catch (e) { + this.settings.default_filter = null; + } + } + + /** + * After {@link module:plugins.ChangeFilters.setFilters} method + * @event afterSetFilters + * @memberof module:plugins.ChangeFilters + * @param {QueryBuilder.Filter[]} filters + */ + this.trigger('afterSetFilters', filters); + }, + + /** + * Adds a new filter to the builder + * @param {QueryBuilder.Filter|Filter[]} newFilters + * @param {int|string} [position=#end] - index or '#start' or '#end' + * @fires module:plugins.ChangeFilters.changer:setFilters + * @fires module:plugins.ChangeFilters.afterSetFilters + * @throws ChangeFilterError + */ + addFilter: function(newFilters, position) { + if (position === undefined || position == '#end') { + position = this.filters.length; + } + else if (position == '#start') { + position = 0; + } + + if (!$.isArray(newFilters)) { + newFilters = [newFilters]; + } + + var filters = $.extend(true, [], this.filters); + + // numeric position + if (parseInt(position) == position) { + Array.prototype.splice.apply(filters, [position, 0].concat(newFilters)); + } + else { + // after filter by its id + if (this.filters.some(function(filter, index) { + if (filter.id == position) { + position = index + 1; + return true; + } + }) + ) { + Array.prototype.splice.apply(filters, [position, 0].concat(newFilters)); + } + // defaults to end of list + else { + Array.prototype.push.apply(filters, newFilters); + } + } + + this.setFilters(filters); + }, + + /** + * Removes a filter from the builder + * @param {string|string[]} filterIds + * @param {boolean} [deleteOrphans=false] delete rules using old filters + * @fires module:plugins.ChangeFilters.changer:setFilters + * @fires module:plugins.ChangeFilters.afterSetFilters + * @throws ChangeFilterError + */ + removeFilter: function(filterIds, deleteOrphans) { + var filters = $.extend(true, [], this.filters); + if (typeof filterIds === 'string') { + filterIds = [filterIds]; + } + + filters = filters.filter(function(filter) { + return filterIds.indexOf(filter.id) === -1; + }); + + this.setFilters(deleteOrphans, filters); + } +}); 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.css b/src/plugins/filter-description/plugin.css deleted file mode 100644 index 0315afa4..00000000 --- a/src/plugins/filter-description/plugin.css +++ /dev/null @@ -1,15 +0,0 @@ -/*! - * jQuery QueryBuilder Filter Description - * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) - */ - -.query-builder p.filter-description { - margin:5px 0 0 0; - background:#D9EDF7; - border:1px solid #BCE8F1; - color:#31708F; - border-radius:4px; - padding:2px 5px; - font-size:0.8em; -} \ No newline at end of file diff --git a/src/plugins/filter-description/plugin.js b/src/plugins/filter-description/plugin.js index cc9c848b..3027e761 100644 --- a/src/plugins/filter-description/plugin.js +++ b/src/plugins/filter-description/plugin.js @@ -1,114 +1,129 @@ -/*! - * jQuery QueryBuilder Filter Description - * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) +/** + * @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 */ - -(function($){ - "use strict"; - - $.fn.queryBuilder.define('filter-description', function(options) { - options = $.extend({ - icon: 'glyphicon glyphicon-info-sign', - mode: 'popover' - }, options || {}); - - /** - * INLINE - */ - if (options.mode === 'inline') { - this.on('afterUpdateRuleFilter', function($rule, filter) { - var $p = $rule.find('p.filter-description'); - - if (!filter || !filter.description) { - $p.hide(); +QueryBuilder.define('filter-description', function(options) { + // INLINE + if (options.mode === 'inline') { + this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) { + var $p = rule.$el.find('p.filter-description'); + var description = e.builder.getFilterDescription(rule.filter, rule); + + if (!description) { + $p.hide(); + } + else { + if ($p.length === 0) { + $p = $($.parseHTML('

        ')); + $p.appendTo(rule.$el); } else { - if ($p.length === 0) { - $p = $('

        '); - $p.appendTo($rule); - } - else { - $p.show(); - } - - $p.html(' ' + filter.description); + $p.css('display', ''); } - }); - } - /** - * POPOVER - */ - else if (options.mode === 'popover') { - if (!$.fn.popover || !$.fn.popover.Constructor || !$.fn.popover.Constructor.prototype.fixTitle) { - $.error('Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com'); + + $p.html(' ' + description); } + }); + } + // POPOVER + else if (options.mode === 'popover') { + if (!$.fn.popover || !$.fn.popover.Constructor || !$.fn.popover.Constructor.prototype.fixTitle) { + Utils.error('MissingLibrary', 'Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com'); + } - this.on('afterUpdateRuleFilter', function($rule, filter) { - var $b = $rule.find('button.filter-description'); + this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) { + var $b = rule.$el.find('button.filter-description'); + var description = e.builder.getFilterDescription(rule.filter, rule); - if (!filter || !filter.description) { - $b.hide(); + if (!description) { + $b.hide(); - if ($b.data('bs.popover')) { - $b.popover('hide'); - } + if ($b.data('bs-popover')) { + $b.popover('hide'); + } + } + else { + if ($b.length === 0) { + $b = $($.parseHTML('')); + $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions)); + const popover = new bootstrap.Popover($b.get(0), { + placement: 'left', + container: 'body', + html: true + }) + $b.on('mouseout', function() { + popover('hide'); + }); } else { - if ($b.length === 0) { - $b = $(''); - $b.prependTo($rule.find('.rule-actions')); - - $b.popover({ - placement: 'left', - container: 'body', - html: true - }); - - $b.on('mouseout', function() { - $b.popover('hide'); - }); - } - else { - $b.show(); - } + $b.css('display', ''); + } - $b.data('bs.popover').options.content = filter.description; + $b.data('bs-popover').options.content = description; - if ($b.attr('aria-describedby')) { - $b.popover('show'); - } + if ($b.attr('aria-describedby')) { + $b.popover('show'); } - }); - } - /** - * BOOTBOX - */ - else if (options.mode === 'bootbox') { - if (!window.bootbox) { - $.error('Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com'); } + }); + } + // 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($rule, filter) { - var $b = $rule.find('button.filter-description'); + this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) { + var $b = rule.$el.find('button.filter-description'); + var description = e.builder.getFilterDescription(rule.filter, rule); - if (!filter || !filter.description) { - $b.hide(); + if (!description) { + $b.hide(); + } + else { + if ($b.length === 0) { + $b = $($.parseHTML('')); + $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions)); + + $b.on('click', function() { + bootbox.alert($b.data('description')); + }); } else { - if ($b.length === 0) { - $b = $(''); - $b.prependTo($rule.find('.rule-actions')); - - $b.on('click', function() { - bootbox.alert($b.data('description')); - }); - } - - $b.data('description', filter.description); + $b.css('display', ''); } - }); - } - }); -}(jQuery)); \ No newline at end of file + $b.data('description', description); + } + }); + } +}, { + icon: 'bi-info-circle-fill', + mode: 'popover' +}); + +QueryBuilder.extend(/** @lends module:plugins.FilterDescription.prototype */ { + /** + * Returns the description of a filter for a particular rule (if present) + * @param {object} filter + * @param {Rule} [rule] + * @returns {string} + * @private + */ + getFilterDescription: function(filter, rule) { + if (!filter) { + return undefined; + } + else if (typeof filter.description == 'function') { + return filter.description.call(this, rule); + } + else { + return filter.description; + } + } +}); diff --git a/src/plugins/filter-description/plugin.scss b/src/plugins/filter-description/plugin.scss new file mode 100644 index 00000000..41498718 --- /dev/null +++ b/src/plugins/filter-description/plugin.scss @@ -0,0 +1,21 @@ +$description-background-color: #D9EDF7; +$description-border-color: #BCE8F1; +$description-text-color: #31708F; + +@if $theme-name == 'dark' { + $description-background-color: rgba(0, 170, 255, .2); + $description-text-color: #AAD1E4; + $description-border-color: #346F7B; +} + +$description-border: 1px solid $description-border-color; + +.query-builder p.filter-description { + margin: $rule-padding 0 0 0; + background: $description-background-color; + border: $description-border; + color: $description-text-color; + border-radius: $item-border-radius; + padding: #{$rule-padding * .5} $rule-padding; + font-size: .8em; +} diff --git a/src/plugins/invert/i18n/ar.json b/src/plugins/invert/i18n/ar.json new file mode 100644 index 00000000..a9ee1c82 --- /dev/null +++ b/src/plugins/invert/i18n/ar.json @@ -0,0 +1,3 @@ +{ + "invert": "قَلْبُ" +} diff --git a/src/plugins/invert/i18n/az.json b/src/plugins/invert/i18n/az.json new file mode 100644 index 00000000..de8c1dfa --- /dev/null +++ b/src/plugins/invert/i18n/az.json @@ -0,0 +1,3 @@ +{ + "invert": "invert" +} diff --git a/src/plugins/invert/i18n/cs.json b/src/plugins/invert/i18n/cs.json new file mode 100644 index 00000000..2ed6a0a8 --- /dev/null +++ b/src/plugins/invert/i18n/cs.json @@ -0,0 +1,3 @@ +{ + "invert": "invertní" +} 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/en.json b/src/plugins/invert/i18n/en.json new file mode 100644 index 00000000..a0db5c10 --- /dev/null +++ b/src/plugins/invert/i18n/en.json @@ -0,0 +1,3 @@ +{ + "invert": "Invert" +} 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/fr.json b/src/plugins/invert/i18n/fr.json new file mode 100644 index 00000000..57087b08 --- /dev/null +++ b/src/plugins/invert/i18n/fr.json @@ -0,0 +1,3 @@ +{ + "invert": "Inverser" +} 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/pl.json b/src/plugins/invert/i18n/pl.json new file mode 100644 index 00000000..6dacfebf --- /dev/null +++ b/src/plugins/invert/i18n/pl.json @@ -0,0 +1,3 @@ +{ + "invert": "Odwróć" +} 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/ru.json b/src/plugins/invert/i18n/ru.json new file mode 100644 index 00000000..d11f20e1 --- /dev/null +++ b/src/plugins/invert/i18n/ru.json @@ -0,0 +1,3 @@ +{ + "invert": "Инвертировать" +} 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/i18n/tr.json b/src/plugins/invert/i18n/tr.json new file mode 100644 index 00000000..4d5058cc --- /dev/null +++ b/src/plugins/invert/i18n/tr.json @@ -0,0 +1,3 @@ +{ + "invert": "Ters Çevir" +} diff --git a/src/plugins/invert/i18n/ua.json b/src/plugins/invert/i18n/ua.json new file mode 100644 index 00000000..e6000450 --- /dev/null +++ b/src/plugins/invert/i18n/ua.json @@ -0,0 +1,3 @@ +{ + "invert": "інвертувати" +} diff --git a/src/plugins/invert/i18n/zh-CN.json b/src/plugins/invert/i18n/zh-CN.json new file mode 100644 index 00000000..7f09c9e3 --- /dev/null +++ b/src/plugins/invert/i18n/zh-CN.json @@ -0,0 +1,3 @@ +{ + "invert": "倒置" +} diff --git a/src/plugins/invert/plugin.js b/src/plugins/invert/plugin.js new file mode 100644 index 00000000..8b0ab7ef --- /dev/null +++ b/src/plugins/invert/plugin.js @@ -0,0 +1,165 @@ +/** + * @class Invert + * @memberof module:plugins + * @description Allows to invert a rule operator, a group condition or the entire builder. + * @param {object} [options] + * @param {string} [options.icon='bi-shuffle'] + * @param {boolean} [options.recursive=true] + * @param {boolean} [options.invert_rules=true] + * @param {boolean} [options.display_rules_button=false] + * @param {boolean} [options.silent_fail=false] + */ +QueryBuilder.define('invert', function(options) { + var self = this; + var Selectors = QueryBuilder.selectors; + + // Bind events + this.on('afterInit', function() { + self.$el.on('click.queryBuilder', '[data-invert=group]', function() { + var $group = $(this).closest(Selectors.group_container); + self.invert(self.getModel($group), options); + }); + + if (options.display_rules_button && options.invert_rules) { + self.$el.on('click.queryBuilder', '[data-invert=rule]', function() { + var $rule = $(this).closest(Selectors.rule_container); + self.invert(self.getModel($rule), options); + }); + } + }); + + // Modify templates + if (!options.disable_template) { + this.on('getGroupTemplate.filter', function(h) { + var $h = $($.parseHTML(h.value)); + $h.find(Selectors.condition_container).after( + '' + ); + h.value = $h.prop('outerHTML'); + }); + + if (options.display_rules_button && options.invert_rules) { + this.on('getRuleTemplate.filter', function(h) { + var $h = $($.parseHTML(h.value)); + $h.find(Selectors.rule_actions).prepend( + '' + ); + h.value = $h.prop('outerHTML'); + }); + } + } +}, { + icon: 'bi-shuffle', + recursive: true, + invert_rules: true, + display_rules_button: false, + silent_fail: false, + disable_template: false +}); + +QueryBuilder.defaults({ + operatorOpposites: { + 'equal': 'not_equal', + 'not_equal': 'equal', + 'in': 'not_in', + 'not_in': 'in', + 'less': 'greater_or_equal', + 'less_or_equal': 'greater', + 'greater': 'less_or_equal', + 'greater_or_equal': 'less', + 'between': 'not_between', + 'not_between': 'between', + 'begins_with': 'not_begins_with', + 'not_begins_with': 'begins_with', + 'contains': 'not_contains', + 'not_contains': 'contains', + 'ends_with': 'not_ends_with', + 'not_ends_with': 'ends_with', + 'is_empty': 'is_not_empty', + 'is_not_empty': 'is_empty', + 'is_null': 'is_not_null', + 'is_not_null': 'is_null' + }, + + conditionOpposites: { + 'AND': 'OR', + 'OR': 'AND' + } +}); + +QueryBuilder.extend(/** @lends module:plugins.Invert.prototype */ { + /** + * Invert a Group, a Rule or the whole builder + * @param {Node} [node] + * @param {object} [options] {@link module:plugins.Invert} + * @fires module:plugins.Invert.afterInvert + * @throws InvertConditionError, InvertOperatorError + */ + invert: function(node, options) { + if (!(node instanceof Node)) { + if (!this.model.root) return; + options = node; + node = this.model.root; + } + + if (typeof options != 'object') options = {}; + if (options.recursive === undefined) options.recursive = true; + if (options.invert_rules === undefined) options.invert_rules = true; + if (options.silent_fail === undefined) options.silent_fail = false; + if (options.trigger === undefined) options.trigger = true; + + if (node instanceof Group) { + // invert group condition + if (this.settings.conditionOpposites[node.condition]) { + node.condition = this.settings.conditionOpposites[node.condition]; + } + else if (!options.silent_fail) { + Utils.error('InvertCondition', 'Unknown inverse of condition "{0}"', node.condition); + } + + // recursive call + if (options.recursive) { + var tempOpts = $.extend({}, options, { trigger: false }); + node.each(function(rule) { + if (options.invert_rules) { + this.invert(rule, tempOpts); + } + }, function(group) { + this.invert(group, tempOpts); + }, this); + } + } + else if (node instanceof Rule) { + if (node.operator && !node.filter.no_invert) { + // invert rule operator + if (this.settings.operatorOpposites[node.operator.type]) { + var invert = this.settings.operatorOpposites[node.operator.type]; + // check if the invert is "authorized" + if (!node.filter.operators || node.filter.operators.indexOf(invert) != -1) { + node.operator = this.getOperatorByType(invert); + } + } + else if (!options.silent_fail) { + Utils.error('InvertOperator', 'Unknown inverse of operator "{0}"', node.operator.type); + } + } + } + + if (options.trigger) { + /** + * After {@link module:plugins.Invert.invert} method + * @event afterInvert + * @memberof module:plugins.Invert + * @param {Node} node - the main group or rule that has been modified + * @param {object} options + */ + this.trigger('afterInvert', node, options); + + this.trigger('rulesChanged'); + } + } +}); diff --git a/src/plugins/invert/plugin.scss b/src/plugins/invert/plugin.scss new file mode 100644 index 00000000..5eb0458b --- /dev/null +++ b/src/plugins/invert/plugin.scss @@ -0,0 +1,5 @@ +.query-builder { + .rules-group-header [data-invert] { + margin-left: 5px; + } +} diff --git a/src/plugins/mongodb-support/plugin.js b/src/plugins/mongodb-support/plugin.js index f7f621d7..5df30db4 100644 --- a/src/plugins/mongodb-support/plugin.js +++ b/src/plugins/mongodb-support/plugin.js @@ -1,136 +1,393 @@ -/*! - * jQuery QueryBuilder MongoDB Support - * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) +/** + * @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. */ -(function($){ - - // DEFAULT CONFIG - // =============================== - $.fn.queryBuilder.defaults.set({ - mongoOperators: { - equal: function(v){ return v[0]; }, - not_equal: function(v){ return {'$ne': v[0]}; }, - in: function(v){ return {'$in': v}; }, - not_in: function(v){ return {'$nin': v}; }, - less: function(v){ return {'$lt': v[0]}; }, - less_or_equal: function(v){ return {'$lte': v[0]}; }, - greater: function(v){ return {'$gt': v[0]}; }, - greater_or_equal: function(v){ return {'$gte': v[0]}; }, - between: function(v){ return {'$gte': v[0], '$lte': v[1]}; }, - begins_with: function(v){ return {'$regex': '^' + escapeRegExp(v[0])}; }, - not_begins_with: function(v){ return {'$regex': '^(?!' + escapeRegExp(v[0]) + ')'}; }, - contains: function(v){ return {'$regex': escapeRegExp(v[0])}; }, - not_contains: function(v){ return {'$regex': '^((?!' + escapeRegExp(v[0]) + ').)*$', '$options': 's'}; }, - ends_with: function(v){ return {'$regex': escapeRegExp(v[0]) + '$'}; }, - not_ends_with: function(v){ return {'$regex': '(? 0) { + parts.push(parse(rule)); + } + else { + var mdb = self.settings.mongoOperators[rule.operator]; + var ope = self.getOperatorByType(rule.operator); + + if (mdb === undefined) { + Utils.error('UndefinedMongoOperator', 'Unknown MongoDB operation for operator "{0}"', rule.operator); + } + + if (ope.nb_inputs !== 0) { + if (!(rule.value instanceof Array)) { + rule.value = [rule.value]; + } + } + + /** + * Modifies the MongoDB field used by a rule + * @event changer:getMongoDBField + * @memberof module:plugins.MongoDbSupport + * @param {string} field + * @param {Rule} rule + * @returns {string} + */ + var field = self.change('getMongoDBField', rule.field, rule); + + var ruleExpression = {}; + ruleExpression[field] = mdb.call(self, rule.value); + + /** + * Modifies the MongoDB expression generated for a rul + * @event changer:ruleToMongo + * @memberof module:plugins.MongoDbSupport + * @param {object} expression + * @param {Rule} rule + * @param {*} value + * @param {function} valueWrapper - function that takes the value and adds the operator + * @returns {object} + */ + parts.push(self.change('ruleToMongo', ruleExpression, rule, rule.value, mdb)); + } + }); + + var groupExpression = {}; + groupExpression['$' + group.condition.toLowerCase()] = parts; + + /** + * Modifies the MongoDB expression generated for a group + * @event changer:groupToMongo + * @memberof module:plugins.MongoDbSupport + * @param {object} expression + * @param {Group} group + * @returns {object} + */ + return self.change('groupToMongo', groupExpression, group); + }(data)); + }, + + /** + * Converts a MongoDB query to rules + * @param {object} query + * @returns {object} + * @fires module:plugins.MongoDbSupport.changer:parseMongoNode + * @fires module:plugins.MongoDbSupport.changer:getMongoDBFieldID + * @fires module:plugins.MongoDbSupport.changer:mongoToRule + * @fires module:plugins.MongoDbSupport.changer:mongoToGroup + * @throws MongoParseError, UndefinedMongoConditionError, UndefinedMongoOperatorError + */ + getRulesFromMongo: function(query) { + if (query === undefined || query === null) { + return null; } - }); + var self = this; - // PUBLIC METHODS - // =============================== - $.fn.queryBuilder.extend({ /** - * Get rules as MongoDB query - * @param data {object} (optional) rules - * @return {object} + * 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 */ - getMongo: function(data) { - data = (data===undefined) ? this.getRules() : data; + query = self.change('parseMongoNode', query); - var that = this; + // a plugin returned a group + if ('rules' in query && 'condition' in query) { + return query; + } - return (function parse(data) { - if (!data.condition) { - data.condition = that.settings.default_condition; - } - if (['AND', 'OR'].indexOf(data.condition.toUpperCase()) === -1) { - $.error('Unable to build MongoDB query with '+ data.condition +' condition'); + // 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; } - if (!data.rules) { - return {}; + // a plugin returned a rule + if ('id' in data && 'operator' in data && 'value' in data) { + parts.push(data); + return; } - var parts = []; + var key = self.getMongoCondition(data); + if (key) { + parts.push(parse(data, key)); + } + else { + var field = Object.keys(data)[0]; + var value = data[field]; - $.each(data.rules, function(i, rule) { - if (rule.rules && rule.rules.length>0) { - parts.push(parse(rule)); + var operator = self.getMongoOperator(value); + if (operator === undefined) { + Utils.error('MongoParse', 'Invalid MongoDB query format'); } - else { - var mdb = that.settings.mongoOperators[rule.operator], - ope = that.getOperatorByType(rule.operator), - values = []; - if (mdb === undefined) { - $.error('MongoDB operation unknown for operator '+ rule.operator); - } + var mdbrl = self.settings.mongoRuleOperators[operator]; + if (mdbrl === undefined) { + Utils.error('UndefinedMongoOperator', 'JSON Rule operation unknown for operator "{0}"', operator); + } - if (ope.accept_values) { - if (!(rule.value instanceof Array)) { - rule.value = [rule.value]; - } + var opVal = mdbrl.call(self, value); - rule.value.forEach(function(v, i) { - values.push(changeType(v, rule.type, 'mongo')); - }); - } + var id = self.getMongoDBFieldID(field, value); - var part = {}; - part[rule.field] = mdb.call(that, values); - parts.push(part); - } - }); + /** + * 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); - var res = {}; - if (parts.length > 0) { - res[ '$'+data.condition.toLowerCase() ] = parts; + parts.push(rule); } - 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)); + }, + + /** + * 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; + }, - // UTILITIES - // =============================== /** - * Change type of a value to int, float or boolean - * @param value {mixed} - * @param type {string} - * @return {mixed} + * Finds which operator is used in a MongoDB sub-object + * @param {*} data + * @returns {string|undefined} + * @private */ - function changeType(value, type, db) { - 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; - if (db === 'sql') { - return bool ? 1 : 0; - } - else if (db === 'mongo') { - return bool; - } - break; - default: return value; + 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'; + } + }, + /** - * Escape value for use in regex - * @param value {string} - * @return {string} + * Returns the key corresponding to "$or" or "$and" + * @param {object} data + * @returns {string|undefined} + * @private */ - function escapeRegExp(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - } + getMongoCondition: function(data) { + var keys = Object.keys(data); -}(jQuery)); \ No newline at end of file + for (var i = 0, l = keys.length; i < l; i++) { + if (keys[i].toLowerCase() === '$or' || keys[i].toLowerCase() === '$and') { + return keys[i]; + } + } + } +}); 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.css b/src/plugins/sortable/plugin.css deleted file mode 100644 index 5d370572..00000000 --- a/src/plugins/sortable/plugin.css +++ /dev/null @@ -1,21 +0,0 @@ -/*! - * jQuery QueryBuilder Sortable - * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) - */ - -.query-builder .drag-handle { - cursor:move; - display:inline-block; - vertical-align:middle; - margin-left:5px; -} - -.query-builder .dragged { - opacity:0.5; -} - -.query-builder .rule-placeholder { - border:1px dashed #bbb; - opacity:0.7; -} \ No newline at end of file diff --git a/src/plugins/sortable/plugin.js b/src/plugins/sortable/plugin.js index 39231f5d..84b29e1f 100644 --- a/src/plugins/sortable/plugin.js +++ b/src/plugins/sortable/plugin.js @@ -1,156 +1,241 @@ -/*! - * jQuery QueryBuilder Sortable - * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) +/** + * @class Sortable + * @memberof module:plugins + * @description Enables drag & drop sort of rules. + * @param {object} [options] + * @param {boolean} [options.inherit_no_drop=true] + * @param {boolean} [options.inherit_no_sortable=true] + * @param {string} [options.icon='bi-sort-down'] + * @throws MissingLibraryError, ConfigError */ +QueryBuilder.define('sortable', function(options) { + if (!('interact' in window)) { + Utils.error('MissingLibrary', 'interact.js is required to use "sortable" plugin. Get it here: http://interactjs.io'); + } -(function($){ - "use strict"; - - $.fn.queryBuilder.define('sortable', function(options) { - options = $.extend({ - default_no_sortable: false, - icon: 'glyphicon glyphicon-sort' - }, options || {}); - - /** - * Init HTML5 drag and drop - */ - this.on('afterInit', function() { - // configure jQuery to use dataTransfer - $.event.props.push('dataTransfer'); - - var placeholder, src, - that = this; - - // only add "draggable" attribute when hovering drag handle - // preventing text select bug in Firefox - this.$el.on('mouseover', '.drag-handle', function() { - that.$el.find('.rule-container, .rules-group-container').attr('draggable', true); - }); - this.$el.on('mouseout', '.drag-handle', function() { - that.$el.find('.rule-container, .rules-group-container').removeAttr('draggable'); - }); - - // dragstart: create placeholder and hide current element - this.$el.on('dragstart', '[draggable]', function(e) { - e.stopPropagation(); - - // notify drag and drop (only dummy text) - e.dataTransfer.setData('text', 'drag'); - - src = $(e.target); - - placeholder = $('
         
        '); - placeholder.css('min-height', src.height()); - placeholder.insertAfter(src); - - // Chrome glitch (helper invisible if hidden immediately) - setTimeout(function() { - src.hide(); - }, 0); - }); - - // dragenter: move the placeholder - this.$el.on('dragenter', '[draggable]', function(e) { - e.preventDefault(); - e.stopPropagation(); - - moveSortableToTarget(placeholder, $(e.target)); - }); - - // dragover: prevent glitches - this.$el.on('dragover', '[draggable]', function(e) { - e.preventDefault(); - e.stopPropagation(); - }); - - // drop: move current element - this.$el.on('drop', function(e) { - e.preventDefault(); - e.stopPropagation(); - - moveSortableToTarget(src, $(e.target)); - }); - - // dragend: show current element and delete placeholder - this.$el.on('dragend', '[draggable]', function(e) { - e.preventDefault(); - e.stopPropagation(); - - src.show(); - placeholder.remove(); - - src = placeholder = null; - - that.$el.find('.rule-container, .rules-group-container').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; + } - /** - * Remove drag handle from non-sortable rules - */ - this.on('getRuleFlags', function(flags) { - if (flags.no_sortable === undefined) { - flags.no_sortable = options.default_no_sortable; - } + // recompute drop-zones during drag (when a rule is hidden) + interact.dynamicDrop(true); - return flags; - }); + // set move threshold to 10px + interact.pointerMoveTolerance(10); + + var placeholder; + var ghost; + var src; + var moved; + + // Init drag and drop + this.on('afterAddRule afterAddGroup', function(e, node) { + if (node == placeholder) { + return; + } + + var self = e.builder; - this.on('afterApplyRuleFlags', function($rule, rule, flags) { - if (flags.no_sortable) { - $rule.find('.drag-handle').remove(); + // Inherit flags + if (options.inherit_no_sortable && node.parent && node.parent.flags.no_sortable) { + node.flags.no_sortable = true; + } + if (options.inherit_no_drop && node.parent && node.parent.flags.no_drop) { + node.flags.no_drop = true; + } + + // Configure drag + if (!node.flags.no_sortable) { + interact(node.$el[0]) + .draggable({ + allowFrom: QueryBuilder.selectors.drag_handle, + onstart: function(event) { + moved = false; + + // get model of dragged element + src = self.getModel(event.target); + + // create ghost + ghost = src.$el.clone() + .appendTo(src.$el.parent()) + .width(src.$el.outerWidth()) + .addClass('dragging'); + + // create drop placeholder + var ph = $($.parseHTML('
         
        ')) + .height(src.$el.outerHeight()); + + placeholder = src.parent.addRule(ph, src.getPos()); + + // hide dragged element + src.$el.hide(); + }, + onmove: function(event) { + // make the ghost follow the cursor + ghost[0].style.top = event.clientY - 15 + 'px'; + ghost[0].style.left = event.clientX - 15 + 'px'; + }, + onend: function(event) { + // starting from Interact 1.3.3, onend is called before ondrop + if (event.dropzone) { + moveSortableToTarget(src, $(event.relatedTarget), self); + moved = true; + } + + // remove ghost + ghost.remove(); + ghost = undefined; + + // remove placeholder + placeholder.drop(); + placeholder = undefined; + + // show element + src.$el.css('display', ''); + + /** + * After a node has been moved with {@link module:plugins.Sortable} + * @event afterMove + * @memberof module:plugins.Sortable + * @param {Node} node + */ + self.trigger('afterMove', src); + + self.trigger('rulesChanged'); + } + }); + } + + if (!node.flags.no_drop) { + // Configure drop on groups and rules + interact(node.$el[0]) + .dropzone({ + accept: QueryBuilder.selectors.rule_and_group_containers, + ondragenter: function(event) { + moveSortableToTarget(placeholder, $(event.target), self); + }, + ondrop: function(event) { + if (!moved) { + moveSortableToTarget(src, $(event.target), self); + } + } + }); + + // Configure drop on group headers + if (node instanceof Group) { + interact(node.$el.find(QueryBuilder.selectors.group_header)[0]) + .dropzone({ + accept: QueryBuilder.selectors.rule_and_group_containers, + ondragenter: function(event) { + moveSortableToTarget(placeholder, $(event.target), self); + }, + ondrop: function(event) { + if (!moved) { + moveSortableToTarget(src, $(event.target), self); + } + } + }); } - }); + } + }); - /** - * Modify templates - */ - this.on('getGroupTemplate', function(h, level) { - if (level>1) { - var $h = $(h); - $h.find('.group-conditions').after('
        '); - h = $h.prop('outerHTML'); + // Detach interactables + this.on('beforeDeleteRule beforeDeleteGroup', function(e, node) { + if (!e.isDefaultPrevented()) { + interact(node.$el[0]).unset(); + + if (node instanceof Group) { + interact(node.$el.find(QueryBuilder.selectors.group_header)[0]).unset(); } + } + }); - return h; - }); + // 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(); + } + }); - this.on('getRuleTemplate', function(h) { - var $h = $(h); - $h.find('.rule-header').after('
        '); - return $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'); + } }); - }); - /** - * Move an element (placeholder or actual object) depending on active target - */ - function moveSortableToTarget(element, target) { - var parent; + this.on('getRuleTemplate.filter', function(h) { + var $h = $($.parseHTML(h.value)); + $h.find(QueryBuilder.selectors.rule_header).after('
        '); + h.value = $h.prop('outerHTML'); + }); + } +}, { + inherit_no_sortable: true, + inherit_no_drop: true, + icon: 'bi-sort-down', + disable_template: false +}); + +QueryBuilder.selectors.rule_and_group_containers = QueryBuilder.selectors.rule_container + ', ' + QueryBuilder.selectors.group_container; +QueryBuilder.selectors.drag_handle = '.drag-handle'; + +QueryBuilder.defaults({ + default_rule_flags: { + no_sortable: false, + no_drop: false + }, + default_group_flags: { + no_sortable: false, + no_drop: false + } +}); + +/** + * Moves an element (placeholder or actual object) depending on active target + * @memberof module:plugins.Sortable + * @param {Node} node + * @param {jQuery} target + * @param {QueryBuilder} [builder] + * @private + */ +function moveSortableToTarget(node, target, builder) { + var parent, method; + var Selectors = QueryBuilder.selectors; + + // on rule + parent = target.closest(Selectors.rule_container); + if (parent.length) { + method = 'moveAfter'; + } - // on rule - parent = target.closest('.rule-container'); + // on group header + if (!method) { + parent = target.closest(Selectors.group_header); if (parent.length) { - element.detach().insertAfter(parent); - return; + parent = target.closest(Selectors.group_container); + method = 'moveAtBegin'; } + } - // on group header - parent = target.closest('.rules-group-header'); + // on group + if (!method) { + parent = target.closest(Selectors.group_container); if (parent.length) { - parent = target.closest('.rules-group-container'); - element.detach().prependTo(parent.find('.rules-list').eq(0)); - return; + method = 'moveAtEnd'; } + } - // on group - parent = target.closest('.rules-group-container'); - if (parent.length) { - element.detach().appendTo(parent.find('.rules-list').eq(0)); - return; + if (method) { + node[method](builder.getModel(parent)); + + // refresh radio value + if (builder && node instanceof Rule) { + builder.setRuleInputValue(node, node.value); } } - -}(jQuery)); \ No newline at end of file +} diff --git a/src/plugins/sortable/plugin.scss b/src/plugins/sortable/plugin.scss new file mode 100644 index 00000000..ac902fe1 --- /dev/null +++ b/src/plugins/sortable/plugin.scss @@ -0,0 +1,28 @@ +$placeholder-border-color: #BBB; +$placeholder-border: 1px dashed $placeholder-border-color; + +.query-builder { + .drag-handle { + @extend %rule-component; + cursor: move; + vertical-align: middle; + margin-left: 5px; + } + + .dragging { + position: fixed; + opacity: .5; + z-index: 100; + + &::before, + &::after { + display: none; + } + } + + .rule-placeholder { + @extend %base-container; + border: $placeholder-border; + opacity: .7; + } +} diff --git a/src/plugins/sql-support/plugin.js b/src/plugins/sql-support/plugin.js index 736aa461..4bfb996a 100644 --- a/src/plugins/sql-support/plugin.js +++ b/src/plugins/sql-support/plugin.js @@ -1,224 +1,672 @@ -/*! - * jQuery QueryBuilder SQL Support - * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) +/** + * @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) { -(function($){ - - // DEFAULT CONFIG - // =============================== - $.fn.queryBuilder.defaults.set({ - sqlOperators: { - equal: '= ?', - not_equal: '!= ?', - in: { op: 'IN(?)', list: true, sep: ', ' }, - not_in: { op: 'NOT IN(?)', list: true, sep: ', ' }, - less: '< ?', - less_or_equal: '<= ?', - greater: '> ?', - greater_or_equal: '>= ?', - between: { op: 'BETWEEN ?', list: true, sep: ' AND ' }, - begins_with: { op: 'LIKE(?)', fn: function(v){ return v+'%'; } }, - not_begins_with: { op: 'NOT LIKE(?)', fn: function(v){ return v+'%'; } }, - contains: { op: 'LIKE(?)', fn: function(v){ return '%'+v+'%'; } }, - not_contains: { op: 'NOT LIKE(?)', fn: function(v){ return '%'+v+'%'; } }, - ends_with: { op: 'LIKE(?)', fn: function(v){ return '%'+v; } }, - not_ends_with: { op: 'NOT LIKE(?)', fn: function(v){ return '%'+v; } }, - is_empty: '== ""', - is_not_empty: '!= ""', - is_null: 'IS NULL', - is_not_null: 'IS NOT NULL' - } - }); - - - // PUBLIC METHODS - // =============================== - $.fn.queryBuilder.extend({ - /** - * Get rules as SQL query - * @param stmt {false|string} use prepared statements - false, 'question_mark' or 'numbered' - * @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; - stmt = (stmt===true || stmt===undefined) ? 'question_mark' : stmt; - nl = (nl || nl===undefined) ? '\n' : ' '; +}, { + boolean_as_integer: true +}); + +QueryBuilder.defaults({ + // operators for internal -> SQL conversion + sqlOperators: { + equal: { op: '= ?' }, + not_equal: { op: '!= ?' }, + in: { op: 'IN(?)', sep: ', ' }, + not_in: { op: 'NOT IN(?)', sep: ', ' }, + less: { op: '< ?' }, + less_or_equal: { op: '<= ?' }, + greater: { op: '> ?' }, + greater_or_equal: { op: '>= ?' }, + between: { op: 'BETWEEN ?', sep: ' AND ' }, + not_between: { op: 'NOT BETWEEN ?', sep: ' AND ' }, + begins_with: { op: 'LIKE ?', mod: '{0}%', escape: '%_' }, + not_begins_with: { op: 'NOT LIKE ?', mod: '{0}%', escape: '%_' }, + contains: { op: 'LIKE ?', mod: '%{0}%', escape: '%_' }, + not_contains: { op: 'NOT LIKE ?', mod: '%{0}%', escape: '%_' }, + ends_with: { op: 'LIKE ?', mod: '%{0}', escape: '%_' }, + not_ends_with: { op: 'NOT LIKE ?', mod: '%{0}', escape: '%_' }, + is_empty: { op: '= \'\'' }, + is_not_empty: { op: '!= \'\'' }, + is_null: { op: 'IS NULL' }, + is_not_null: { op: 'IS NOT NULL' } + }, + + // operators for SQL -> internal conversion + sqlRuleOperator: { + '=': function(v) { + return { + val: v, + op: v === '' ? 'is_empty' : 'equal' + }; + }, + '!=': function(v) { + return { + val: v, + op: v === '' ? 'is_not_empty' : 'not_equal' + }; + }, + 'LIKE': function(v) { + if (v.slice(0, 1) == '%' && v.slice(-1) == '%') { + return { + val: v.slice(1, -1), + op: 'contains' + }; + } + else if (v.slice(0, 1) == '%') { + return { + val: v.slice(1), + op: 'ends_with' + }; + } + else if (v.slice(-1) == '%') { + return { + val: v.slice(0, -1), + op: 'begins_with' + }; + } + else { + Utils.error('SQLParse', 'Invalid value for LIKE operator "{0}"', v); + } + }, + 'NOT LIKE': function(v) { + if (v.slice(0, 1) == '%' && v.slice(-1) == '%') { + return { + val: v.slice(1, -1), + op: 'not_contains' + }; + } + else if (v.slice(0, 1) == '%') { + return { + val: v.slice(1), + op: 'not_ends_with' + }; + } + else if (v.slice(-1) == '%') { + return { + val: v.slice(0, -1), + op: 'not_begins_with' + }; + } + else { + Utils.error('SQLParse', 'Invalid value for NOT LIKE operator "{0}"', v); + } + }, + 'IN': function(v) { + return { val: v, op: 'in' }; + }, + 'NOT IN': function(v) { + return { val: v, op: 'not_in' }; + }, + '<': function(v) { + return { val: v, op: 'less' }; + }, + '<=': function(v) { + return { val: v, op: 'less_or_equal' }; + }, + '>': function(v) { + return { val: v, op: 'greater' }; + }, + '>=': function(v) { + return { val: v, op: 'greater_or_equal' }; + }, + 'BETWEEN': function(v) { + return { val: v, op: 'between' }; + }, + 'NOT BETWEEN': function(v) { + return { val: v, op: 'not_between' }; + }, + 'IS': function(v) { + if (v !== null) { + Utils.error('SQLParse', 'Invalid value for IS operator'); + } + return { val: null, op: 'is_null' }; + }, + 'IS NOT': function(v) { + if (v !== null) { + Utils.error('SQLParse', 'Invalid value for IS operator'); + } + return { val: null, op: 'is_not_null' }; + } + }, - var that = this, - bind_index = 1, - bind_params = []; + // statements for internal -> SQL conversion + sqlStatements: { + 'question_mark': function() { + var params = []; + return { + add: function(rule, value) { + params.push(value); + return '?'; + }, + run: function() { + return params; + } + }; + }, - var sql = (function parse(data) { - if (!data.condition) { - data.condition = that.settings.default_condition; + 'numbered': function(char) { + if (!char || char.length > 1) char = '$'; + var index = 0; + var params = []; + return { + add: function(rule, value) { + params.push(value); + index++; + return char + index; + }, + run: function() { + return params; } - if (['AND', 'OR'].indexOf(data.condition.toUpperCase()) === -1) { - $.error('Unable to build SQL query with '+ data.condition +' condition'); + }; + }, + + 'named': function(char) { + if (!char || char.length > 1) char = ':'; + var indexes = {}; + var params = {}; + return { + add: function(rule, value) { + if (!indexes[rule.field]) indexes[rule.field] = 1; + var key = rule.field + '_' + (indexes[rule.field]++); + params[key] = value; + return char + key; + }, + run: function() { + return params; } + }; + } + }, + + // statements for SQL -> internal conversion + sqlRuleStatement: { + 'question_mark': function(values) { + var index = 0; + return { + parse: function(v) { + return v == '?' ? values[index++] : v; + }, + esc: function(sql) { + return sql.replace(/\?/g, '\'?\''); + } + }; + }, + + 'numbered': function(values, char) { + if (!char || char.length > 1) char = '$'; + var regex1 = new RegExp('^\\' + char + '[0-9]+$'); + var regex2 = new RegExp('\\' + char + '([0-9]+)', 'g'); + return { + parse: function(v) { + return regex1.test(v) ? values[v.slice(1) - 1] : v; + }, + esc: function(sql) { + return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\''); + } + }; + }, - if (!data.rules) { - return ''; + 'named': function(values, char) { + if (!char || char.length > 1) char = ':'; + var regex1 = new RegExp('^\\' + char); + var regex2 = new RegExp('\\' + char + '(' + Object.keys(values).join('|') + ')\\b', 'g'); + return { + parse: function(v) { + return regex1.test(v) ? values[v.slice(1)] : v; + }, + esc: function(sql) { + return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\''); } + }; + } + } +}); + +/** + * @typedef {object} SqlQuery + * @memberof module:plugins.SqlSupport + * @property {string} sql + * @property {object} params + */ + +QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ { + /** + * Returns rules as a SQL query + * @param {boolean|string} [stmt] - use prepared statements: false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)' + * @param {boolean} [nl=false] output with new lines + * @param {object} [data] - current rules by default + * @returns {module:plugins.SqlSupport.SqlQuery} + * @fires module:plugins.SqlSupport.changer:getSQLField + * @fires module:plugins.SqlSupport.changer:ruleToSQL + * @fires module:plugins.SqlSupport.changer:groupToSQL + * @throws UndefinedSQLConditionError, UndefinedSQLOperatorError + */ + getSQL: function(stmt, nl, data) { + data = (data === undefined) ? this.getRules() : data; + + if (!data) { + return null; + } + + nl = !!nl ? '\n' : ' '; + var boolean_as_integer = this.getPluginOptions('sql-support', 'boolean_as_integer'); - var parts = []; + if (stmt === true) { + stmt = 'question_mark'; + } + if (typeof stmt == 'string') { + var config = getStmtConfig(stmt); + stmt = this.settings.sqlStatements[config[1]](config[2]); + } + + var self = this; + + var sql = (function parse(group) { + if (!group.condition) { + group.condition = self.settings.default_condition; + } + if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) { + Utils.error('UndefinedSQLCondition', 'Unable to build SQL query with condition "{0}"', group.condition); + } + + if (!group.rules) { + return ''; + } + + var parts = []; + + group.rules.forEach(function(rule) { + if (rule.rules && rule.rules.length > 0) { + parts.push('(' + nl + parse(rule) + nl + ')' + nl); + } + else { + var sql = self.settings.sqlOperators[rule.operator]; + var ope = self.getOperatorByType(rule.operator); + var value = ''; - $.each(data.rules, function(i, rule) { - if (rule.rules && rule.rules.length>0) { - parts.push('('+ nl + parse(rule) + nl +')'+ nl); + if (sql === undefined) { + Utils.error('UndefinedSQLOperator', 'Unknown SQL operation for operator "{0}"', rule.operator); } - else { - var sql = that.getSqlOperator(rule.operator), - ope = that.getOperatorByType(rule.operator), - value = ''; - if (sql === false) { - $.error('SQL operation unknown for operator '+ rule.operator); + if (ope.nb_inputs !== 0) { + if (!(rule.value instanceof Array)) { + rule.value = [rule.value]; } - if (ope.accept_values) { - if (!(rule.value instanceof Array)) { - rule.value = [rule.value]; + rule.value.forEach(function(v, i) { + if (i > 0) { + value += sql.sep; + } + + if (rule.type == 'boolean' && boolean_as_integer) { + v = v ? 1 : 0; } - else if (!sql.list && rule.value.length>1) { - $.error('Operator '+ rule.operator +' cannot accept multiple values'); + else if (!stmt && rule.type !== 'integer' && rule.type !== 'double' && rule.type !== 'boolean') { + v = Utils.escapeString(v, sql.escape); } - rule.value.forEach(function(v, i) { - if (i>0) { - value+= sql.sep; - } + if (sql.mod) { + v = Utils.fmt(sql.mod, v); + } - if (rule.type=='integer' || rule.type=='double' || rule.type=='boolean') { - v = changeType(v, rule.type, 'sql'); - } - else if (!stmt) { - v = escapeString(v); + if (stmt) { + value += stmt.add(rule, v); + } + else { + if (typeof v == 'string') { + v = '\'' + v + '\''; } - if (sql.fn) { - v = sql.fn(v); - } + value += v; + } + }); + } - if (stmt) { - if (stmt == 'question_mark') { - value+= '?'; - } - else { - value+= '$'+bind_index; - } + var sqlFn = function(v) { + return sql.op.replace('?', function() { + return v; + }); + }; - bind_params.push(v); - bind_index++; - } - else { - if (typeof v === 'string') { - v = '\''+ 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); - value+= v; - } - }); - } + var ruleExpression = field + ' ' + sqlFn(value); - parts.push(rule.field +' '+ sql.op.replace(/\?/, 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); - }(data)); + var groupExpression = parts.join(' ' + group.condition + nl); - if (stmt) { - return { - sql: sql, - params: bind_params - }; - } - else { - return { - sql: sql - }; - } - }, + /** + * Modifies the SQL generated for a group + * @event changer:groupToSQL + * @memberof module:plugins.SqlSupport + * @param {string} expression + * @param {Group} group + * @returns {string} + */ + return self.change('groupToSQL', groupExpression, group); + }(data)); + + if (stmt) { + return { + sql: sql, + params: stmt.run() + }; + } + else { + return { + sql: sql + }; + } + }, + + /** + * Convert a SQL query to rules + * @param {string|module:plugins.SqlSupport.SqlQuery} query + * @param {boolean|string} stmt + * @returns {object} + * @fires module:plugins.SqlSupport.changer:parseSQLNode + * @fires module:plugins.SqlSupport.changer:getSQLFieldID + * @fires module:plugins.SqlSupport.changer:sqlToRule + * @fires module:plugins.SqlSupport.changer:sqlToGroup + * @throws MissingLibraryError, SQLParseError, UndefinedSQLOperatorError + */ + getRulesFromSQL: function(query, stmt) { + if (!('SQLParser' in window)) { + Utils.error('MissingLibrary', 'SQLParser is required to parse SQL queries. Get it here https://github.com/mistic100/sql-parser'); + } + + var self = this; + + if (typeof query == 'string') { + query = { sql: query }; + } + + if (stmt === true) stmt = 'question_mark'; + if (typeof stmt == 'string') { + var config = getStmtConfig(stmt); + stmt = this.settings.sqlRuleStatement[config[1]](query.params, config[2]); + } + + if (stmt) { + query.sql = stmt.esc(query.sql); + } + + if (query.sql.toUpperCase().indexOf('SELECT') !== 0) { + query.sql = 'SELECT * FROM table WHERE ' + query.sql; + } + + var parsed = SQLParser.parse(query.sql); + + if (!parsed.where) { + Utils.error('SQLParse', 'No WHERE clause found'); + } /** - * Sanitize the "sql" field of an operator - * @param sql {string|object} - * @return {object} + * 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 */ - getSqlOperator: function(type) { - var sql = this.settings.sqlOperators[type]; + var data = self.change('parseSQLNode', parsed.where.conditions); - if (sql === undefined) { - return false; + // 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; } - if (typeof sql === 'string') { - sql = { op: sql }; + // 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; } - if (!sql.list) { - sql.list = false; + + // a plugin returned a rule + if ('id' in data && 'operator' in data && 'value' in data) { + curr.rules.push(data); + return; } - if (sql.list && !sql.sep) { - sql.sep = ', '; + + // 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'); } - return sql; - } - }); + // it's a node + if (['AND', 'OR'].indexOf(data.operation.toUpperCase()) !== -1) { + // create a sub-group if the condition is not the same and it's not the first level + /** + * Given an existing group and an AST node, determines if a sub-group must be created + * @event changer:sqlGroupsDistinct + * @memberof module:plugins.SqlSupport + * @param {boolean} create - true by default if the group condition is different + * @param {object} group + * @param {object} AST + * @param {int} current group level + * @returns {boolean} + */ + var createGroup = self.change('sqlGroupsDistinct', i > 0 && curr.condition != data.operation.toUpperCase(), curr, data, i); - // UTILITIES - // =============================== - /** - * Change type of a value to int, float or boolean - * @param value {mixed} - * @param type {string} - * @return {mixed} - */ - function changeType(value, type, db) { - 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; - if (db === 'sql') { - return bool ? 1 : 0; + if (createGroup) { + /** + * Modifies the group generated from the SQL expression (this is called before the group is filled with rules) + * @event changer:sqlToGroup + * @memberof module:plugins.SqlSupport + * @param {object} group + * @param {object} AST + * @returns {object} + */ + var group = self.change('sqlToGroup', { + condition: self.settings.default_condition, + rules: [] + }, data); + + curr.rules.push(group); + curr = group; } - else if (db === 'mongo') { - return bool; + + curr.condition = data.operation.toUpperCase(); + i++; + + // some magic ! + var next = curr; + flatten(data.left, i); + + curr = next; + flatten(data.right, i); + } + // it's a leaf + else { + if ($.isPlainObject(data.right.value)) { + Utils.error('SQLParse', 'Value format not supported for {0}.', data.left.value); + } + + // convert array + var value; + if ($.isArray(data.right.value)) { + value = data.right.value.map(function(v) { + return v.value; + }); + } + else { + value = data.right.value; + } + + // get actual values + if (stmt) { + if ($.isArray(value)) { + value = value.map(stmt.parse); + } + else { + value = stmt.parse(value); + } + } + + // convert operator + var operator = data.operation.toUpperCase(); + if (operator == '<>') { + operator = '!='; + } + + var sqlrl = self.settings.sqlRuleOperator[operator]; + if (sqlrl === undefined) { + Utils.error('UndefinedSQLOperator', 'Invalid SQL operation "{0}".', data.operation); + } + + var opVal = sqlrl.call(this, value, data.operation); + + // find field name + var field; + if ('values' in data.left) { + field = data.left.values.join('.'); + } + else if ('value' in data.left) { + field = data.left.value; + } + else { + Utils.error('SQLParse', 'Cannot find field name in {0}', JSON.stringify(data.left)); } - break; - default: return 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: finalValue + }, data); + + curr.rules.push(rule); + } + }(data, 0)); + + return out; + }, + + /** + * Sets the builder's rules from a SQL query + * @see module:plugins.SqlSupport.getRulesFromSQL + */ + setRulesFromSQL: function(query, stmt) { + this.setRules(this.getRulesFromSQL(query, stmt)); + }, /** - * Escape SQL value - * @param value {string} - * @return {string} + * 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 */ - function escapeString(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'); + 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; } +}); -}(jQuery)); \ No newline at end of file +/** + * Parses the statement configuration + * @memberof module:plugins.SqlSupport + * @param {string} stmt + * @returns {Array} null, mode, option + * @private + */ +function getStmtConfig(stmt) { + var config = stmt.match(/(question_mark|numbered|named)(?:\((.)\))?/); + if (!config) config = [null, 'question_mark', undefined]; + return config; +} diff --git a/src/plugins/unique-filter/plugin.js b/src/plugins/unique-filter/plugin.js new file mode 100644 index 00000000..5b6fb802 --- /dev/null +++ b/src/plugins/unique-filter/plugin.js @@ -0,0 +1,114 @@ +/** + * @class UniqueFilter + * @memberof module:plugins + * @description Allows to define some filters as "unique": ie which can be used for only one rule, globally or in the same group. + */ +QueryBuilder.define('unique-filter', function() { + this.status.used_filters = {}; + + this.on('afterUpdateRuleFilter', this.updateDisabledFilters); + this.on('afterDeleteRule', this.updateDisabledFilters); + this.on('afterCreateRuleFilters', this.applyDisabledFilters); + this.on('afterReset', this.clearDisabledFilters); + this.on('afterClear', this.clearDisabledFilters); + + // Ensure that the default filter is not already used if unique + this.on('getDefaultFilter.filter', function(e, model) { + var self = e.builder; + + self.updateDisabledFilters(); + + if (e.value.id in self.status.used_filters) { + var found = self.filters.some(function(filter) { + if (!(filter.id in self.status.used_filters) || self.status.used_filters[filter.id].length > 0 && self.status.used_filters[filter.id].indexOf(model.parent) === -1) { + e.value = filter; + return true; + } + }); + + if (!found) { + Utils.error(false, 'UniqueFilter', 'No more non-unique filters available'); + e.value = undefined; + } + } + }); +}); + +QueryBuilder.extend(/** @lends module:plugins.UniqueFilter.prototype */ { + /** + * Updates the list of used filters + * @param {$.Event} [e] + * @private + */ + updateDisabledFilters: function(e) { + var self = e ? e.builder : this; + + self.status.used_filters = {}; + + if (!self.model) { + return; + } + + // get used filters + (function walk(group) { + group.each(function(rule) { + if (rule.filter && rule.filter.unique) { + if (!self.status.used_filters[rule.filter.id]) { + self.status.used_filters[rule.filter.id] = []; + } + if (rule.filter.unique == 'group') { + self.status.used_filters[rule.filter.id].push(rule.parent); + } + } + }, function(group) { + walk(group); + }); + }(self.model.root)); + + self.applyDisabledFilters(e); + }, + + /** + * Clear the list of used filters + * @param {$.Event} [e] + * @private + */ + clearDisabledFilters: function(e) { + var self = e ? e.builder : this; + + self.status.used_filters = {}; + + self.applyDisabledFilters(e); + }, + + /** + * Disabled filters depending on the list of used ones + * @param {$.Event} [e] + * @private + */ + applyDisabledFilters: function(e) { + var self = e ? e.builder : this; + + // re-enable everything + self.$el.find(QueryBuilder.selectors.filter_container + ' option').prop('disabled', false); + + // disable some + $.each(self.status.used_filters, function(filterId, groups) { + if (groups.length === 0) { + self.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true); + } + else { + groups.forEach(function(group) { + group.each(function(rule) { + rule.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true); + }); + }); + } + }); + + // update Selectpicker + if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) { + self.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render'); + } + } +}); diff --git a/src/public.js b/src/public.js new file mode 100644 index 00000000..8e8117af --- /dev/null +++ b/src/public.js @@ -0,0 +1,471 @@ +/** + * Destroys the builder + * @fires QueryBuilder.beforeDestroy + */ +QueryBuilder.prototype.destroy = function() { + /** + * Before the {@link QueryBuilder#destroy} method + * @event beforeDestroy + * @memberof QueryBuilder + */ + this.trigger('beforeDestroy'); + + if (this.status.generated_id) { + this.$el.removeAttr('id'); + } + + this.clear(); + this.model = null; + + this.$el + .off('.queryBuilder') + .removeClass('query-builder') + .removeData('queryBuilder'); + + delete this.$el[0].queryBuilder; +}; + +/** + * Clear all rules and resets the root group + * @fires QueryBuilder.beforeReset + * @fires QueryBuilder.afterReset + */ +QueryBuilder.prototype.reset = function() { + /** + * Before the {@link QueryBuilder#reset} method, can be prevented + * @event beforeReset + * @memberof QueryBuilder + */ + var e = this.trigger('beforeReset'); + if (e.isDefaultPrevented()) { + return; + } + + this.status.group_id = 1; + this.status.rule_id = 0; + + this.model.root.empty(); + + this.model.root.data = undefined; + this.model.root.flags = $.extend({}, this.settings.default_group_flags); + this.model.root.condition = this.settings.default_condition; + + this.addRule(this.model.root); + + /** + * After the {@link QueryBuilder#reset} method + * @event afterReset + * @memberof QueryBuilder + */ + this.trigger('afterReset'); + + this.trigger('rulesChanged'); +}; + +/** + * Clears all rules and removes the root group + * @fires QueryBuilder.beforeClear + * @fires QueryBuilder.afterClear + */ +QueryBuilder.prototype.clear = function() { + /** + * Before the {@link QueryBuilder#clear} method, can be prevented + * @event beforeClear + * @memberof QueryBuilder + */ + var e = this.trigger('beforeClear'); + if (e.isDefaultPrevented()) { + return; + } + + this.status.group_id = 0; + this.status.rule_id = 0; + + if (this.model.root) { + this.model.root.drop(); + this.model.root = null; + } + + /** + * After the {@link QueryBuilder#clear} method + * @event afterClear + * @memberof QueryBuilder + */ + this.trigger('afterClear'); + + this.trigger('rulesChanged'); +}; + +/** + * Modifies the builder configuration.
        + * Only options defined in QueryBuilder.modifiable_options are modifiable + * @param {object} options + */ +QueryBuilder.prototype.setOptions = function(options) { + $.each(options, function(opt, value) { + if (QueryBuilder.modifiable_options.indexOf(opt) !== -1) { + this.settings[opt] = value; + } + }.bind(this)); +}; + +/** + * Returns the model associated to a DOM object, or the root model + * @param {jQuery} [target] + * @returns {Node} + */ +QueryBuilder.prototype.getModel = function(target) { + if (!target) { + return this.model.root; + } + else if (target instanceof Node) { + return target; + } + else { + return $(target).data('queryBuilderModel'); + } +}; + +/** + * Validates the whole builder + * @param {object} [options] + * @param {boolean} [options.skip_empty=false] - skips validating rules that have no filter selected + * @returns {boolean} + * @fires QueryBuilder.changer:validate + */ +QueryBuilder.prototype.validate = function(options) { + options = $.extend({ + skip_empty: false + }, options); + + this.clearErrors(); + + var self = this; + + var valid = (function parse(group) { + var done = 0; + var errors = 0; + + group.each(function(rule) { + if (!rule.filter && options.skip_empty) { + return; + } + + if (!rule.filter) { + self.triggerValidationError(rule, 'no_filter', null); + errors++; + return; + } + + if (!rule.operator) { + self.triggerValidationError(rule, 'no_operator', null); + errors++; + return; + } + + if (rule.operator.nb_inputs !== 0) { + var valid = self.validateValue(rule, rule.value); + + if (valid !== true) { + self.triggerValidationError(rule, valid, rule.value); + errors++; + return; + } + } + + done++; + + }, function(group) { + var res = parse(group); + if (res === true) { + done++; + } + else if (res === false) { + errors++; + } + }); + + if (errors > 0) { + return false; + } + else if (done === 0 && !group.isRoot() && options.skip_empty) { + return null; + } + else if (done === 0 && (!self.settings.allow_empty || !group.isRoot())) { + self.triggerValidationError(group, 'empty_group', null); + return false; + } + + return true; + + }(this.model.root)); + + /** + * Modifies the result of the {@link QueryBuilder#validate} method + * @event changer:validate + * @memberof QueryBuilder + * @param {boolean} valid + * @returns {boolean} + */ + return this.change('validate', valid); +}; + +/** + * Gets an object representing current rules + * @param {object} [options] + * @param {boolean|string} [options.get_flags=false] - export flags, true: only changes from default flags or 'all' + * @param {boolean} [options.allow_invalid=false] - returns rules even if they are invalid + * @param {boolean} [options.skip_empty=false] - remove rules that have no filter selected + * @returns {object} + * @fires QueryBuilder.changer:ruleToJson + * @fires QueryBuilder.changer:groupToJson + * @fires QueryBuilder.changer:getRules + */ +QueryBuilder.prototype.getRules = function(options) { + options = $.extend({ + get_flags: false, + allow_invalid: false, + skip_empty: false + }, options); + + var valid = this.validate(options); + if (!valid && !options.allow_invalid) { + return null; + } + + var self = this; + + var out = (function parse(group) { + var groupData = { + condition: group.condition, + rules: [] + }; + + if (group.data) { + groupData.data = $.extendext(true, 'replace', {}, group.data); + } + + if (options.get_flags) { + var flags = self.getGroupFlags(group.flags, options.get_flags === 'all'); + if (!$.isEmptyObject(flags)) { + groupData.flags = flags; + } + } + + group.each(function(rule) { + if (!rule.filter && options.skip_empty) { + return; + } + + var value = null; + if (!rule.operator || rule.operator.nb_inputs !== 0) { + value = rule.value; + } + + var ruleData = { + id: rule.filter ? rule.filter.id : null, + field: rule.filter ? rule.filter.field : null, + type: rule.filter ? rule.filter.type : null, + input: rule.filter ? rule.filter.input : null, + operator: rule.operator ? rule.operator.type : null, + value: value + }; + + if (rule.filter && rule.filter.data || rule.data) { + ruleData.data = $.extendext(true, 'replace', {}, rule.filter ? rule.filter.data : {}, rule.data); + } + + if (options.get_flags) { + var flags = self.getRuleFlags(rule.flags, options.get_flags === 'all'); + if (!$.isEmptyObject(flags)) { + ruleData.flags = flags; + } + } + + /** + * Modifies the JSON generated from a Rule object + * @event changer:ruleToJson + * @memberof QueryBuilder + * @param {object} json + * @param {Rule} rule + * @returns {object} + */ + groupData.rules.push(self.change('ruleToJson', ruleData, rule)); + + }, function(model) { + var data = parse(model); + if (data.rules.length !== 0 || !options.skip_empty) { + groupData.rules.push(data); + } + }, this); + + /** + * Modifies the JSON generated from a Group object + * @event changer:groupToJson + * @memberof QueryBuilder + * @param {object} json + * @param {Group} group + * @returns {object} + */ + return self.change('groupToJson', groupData, group); + + }(this.model.root)); + + out.valid = valid; + + /** + * Modifies the result of the {@link QueryBuilder#getRules} method + * @event changer:getRules + * @memberof QueryBuilder + * @param {object} json + * @returns {object} + */ + return this.change('getRules', out); +}; + +/** + * Sets rules from object + * @param {object} data + * @param {object} [options] + * @param {boolean} [options.allow_invalid=false] - silent-fail if the data are invalid + * @throws RulesError, UndefinedConditionError + * @fires QueryBuilder.changer:setRules + * @fires QueryBuilder.changer:jsonToRule + * @fires QueryBuilder.changer:jsonToGroup + * @fires QueryBuilder.afterSetRules + */ +QueryBuilder.prototype.setRules = function(data, options) { + options = $.extend({ + allow_invalid: false + }, options); + + if ($.isArray(data)) { + data = { + condition: this.settings.default_condition, + rules: data + }; + } + + if (!data || !data.rules || (data.rules.length === 0 && !this.settings.allow_empty)) { + Utils.error('RulesParse', 'Incorrect data object passed'); + } + + this.clear(); + this.setRoot(false, data.data, this.parseGroupFlags(data)); + + /** + * Modifies data before the {@link QueryBuilder#setRules} method + * @event changer:setRules + * @memberof QueryBuilder + * @param {object} json + * @param {object} options + * @returns {object} + */ + data = this.change('setRules', data, options); + + var self = this; + + (function add(data, group) { + if (group === null) { + return; + } + + if (data.condition === undefined) { + data.condition = self.settings.default_condition; + } + else if (self.settings.conditions.indexOf(data.condition) == -1) { + Utils.error(!options.allow_invalid, 'UndefinedCondition', 'Invalid condition "{0}"', data.condition); + data.condition = self.settings.default_condition; + } + + group.condition = data.condition; + + data.rules.forEach(function(item) { + var model; + + if (item.rules !== undefined) { + if (self.settings.allow_groups !== -1 && self.settings.allow_groups < group.level) { + Utils.error(!options.allow_invalid, 'RulesParse', 'No more than {0} groups are allowed', self.settings.allow_groups); + self.reset(); + } + else { + model = self.addGroup(group, false, item.data, self.parseGroupFlags(item)); + if (model === null) { + return; + } + + add(item, model); + } + } + else { + if (!item.empty) { + if (item.id === undefined) { + Utils.error(!options.allow_invalid, 'RulesParse', 'Missing rule field id'); + item.empty = true; + } + if (item.operator === undefined) { + item.operator = 'equal'; + } + } + + model = self.addRule(group, item.data, self.parseRuleFlags(item)); + if (model === null) { + return; + } + + if (!item.empty) { + model.filter = self.getFilterById(item.id, !options.allow_invalid); + } + + if (model.filter) { + model.operator = self.getOperatorByType(item.operator, !options.allow_invalid); + + if (!model.operator) { + model.operator = self.getOperators(model.filter)[0]; + } + } + + if (model.operator && model.operator.nb_inputs !== 0) { + if (item.value !== undefined) { + model.value = item.value; + } + else if (model.filter.default_value !== undefined) { + model.value = model.filter.default_value; + } + } + + /** + * Modifies the Rule object generated from the JSON + * @event changer:jsonToRule + * @memberof QueryBuilder + * @param {Rule} rule + * @param {object} json + * @returns {Rule} the same rule + */ + if (self.change('jsonToRule', model, item) != model) { + Utils.error('RulesParse', 'Plugin tried to change rule reference'); + } + } + }); + + /** + * Modifies the Group object generated from the JSON + * @event changer:jsonToGroup + * @memberof QueryBuilder + * @param {Group} group + * @param {object} json + * @returns {Group} the same group + */ + if (self.change('jsonToGroup', group, data) != group) { + Utils.error('RulesParse', 'Plugin tried to change group reference'); + } + + }(data, this.model.root)); + + /** + * After the {@link QueryBuilder#setRules} method + * @event afterSetRules + * @memberof QueryBuilder + */ + this.trigger('afterSetRules'); +}; diff --git a/src/query-builder.css b/src/query-builder.css deleted file mode 100644 index f8726a02..00000000 --- a/src/query-builder.css +++ /dev/null @@ -1,109 +0,0 @@ -/*! - * jQuery QueryBuilder - * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) - */ - -.query-builder .rule-container, -.query-builder .rules-group-container, -.query-builder .rule-placeholder { - margin:4px 0; - border-radius:5px; - padding:5px; - border:1px solid #eee; - background:#fff; - background:rgba(255, 255, 255, 0.9); -} - -/* GROUPS */ -.query-builder .rules-group-container { - padding:10px 10px 5px 10px; - border:1px solid #DCC896; - background:#FCF9ED; - background:rgba(250, 240, 210, 0.5); -} - .query-builder .rules-group-header { - margin-bottom:10px; - } - .query-builder .rules-group-header input[name$=_cond] { - display:none; - } - .query-builder .rules-list { - list-style:none; - padding:0 0 0 20px; - margin:0; - } - -/* RULES */ -.query-builder .rule-container {} - .query-builder .rule-container>div:not(.rule-header) { - display:inline-block; - margin:0 5px 0 0; - vertical-align:top; - } - .query-builder .rule-value-container:not(:empty) { - border-left:1px solid #ddd; - padding-left:5px; - } - .query-builder .rule-value-container label { - margin-bottom:0; - } - .query-builder .rule-value-container label.block { - display:block; - } - .query-builder .rule-container select, - .query-builder .rule-container input[type=text], - .query-builder .rule-container input[type=number] { - padding:1px; - } - -/* ERRORS */ -.query-builder .has-error { - background:#fdd; - border-color:#f99; -} - -.query-builder .error-container { - display:none !important; - cursor:help; - color:red; -} - -.query-builder .has-error .error-container { - display:inline-block !important; -} - -/* TICKS */ -.query-builder .rules-list>* { - position:relative; -} - .query-builder .rules-list>*:before, - .query-builder .rules-list>*:after { - content:''; - position:absolute; - left:-15px; - width:15px; - height:calc(50% + 4px); - border-color:#ccc; - border-style:solid; - } - - .query-builder .rules-list>*:before { - top:-2px; - 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; - } \ No newline at end of file diff --git a/src/query-builder.js b/src/query-builder.js deleted file mode 100644 index 262e851c..00000000 --- a/src/query-builder.js +++ /dev/null @@ -1,1712 +0,0 @@ -/*! - * jQuery QueryBuilder - * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) - */ - -/*jshint multistr:true */ -/*jshint loopfunc:true */ - -(function($){ - "use strict"; - - // GLOBAL STATIC VARIABLES - // =============================== - var types = [ - 'string', - 'integer', - 'double', - 'date', - 'time', - 'datetime', - 'boolean' - ], - internalTypes = [ - 'string', - 'number', - 'datetime', - 'boolean' - ], - inputs = [ - 'text', - 'textarea', - 'radio', - 'checkbox', - 'select' - ]; - - - // CLASS DEFINITION - // =============================== - var QueryBuilder = function($el, options) { - this.$el = $el; - this.init(options); - }; - - MicroEvent.mixin(QueryBuilder); - - - // DEFAULT CONFIG - // =============================== - QueryBuilder.DEFAULTS = { - filters: [], - - plugins: null, - - onValidationError: null, - onAfterAddGroup: null, - onAfterAddRule: null, - - display_errors: true, - allow_groups: -1, - allow_empty: false, - conditions: ['AND', 'OR'], - default_condition: 'AND', - - default_rule_flags: { - filter_readonly: false, - operator_readonly: false, - value_readonly: false, - no_delete: false - }, - - template: { - group: null, - rule: null - }, - - lang: { - "add_rule": 'Add rule', - "add_group": 'Add group', - "delete_rule": 'Delete', - "delete_group": 'Delete', - - "condition_and": 'AND', - "condition_or": 'OR', - - "filter_select_placeholder": '------', - - "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", - "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_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" - } - }, - - operators: [ - {type: 'equal', accept_values: 1, apply_to: ['string', 'number', 'datetime', 'boolean']}, - {type: 'not_equal', accept_values: 1, apply_to: ['string', 'number', 'datetime', 'boolean']}, - {type: 'in', accept_values: 1, apply_to: ['string', 'number', 'datetime']}, - {type: 'not_in', accept_values: 1, apply_to: ['string', 'number', 'datetime']}, - {type: 'less', accept_values: 1, apply_to: ['number', 'datetime']}, - {type: 'less_or_equal', accept_values: 1, apply_to: ['number', 'datetime']}, - {type: 'greater', accept_values: 1, apply_to: ['number', 'datetime']}, - {type: 'greater_or_equal', accept_values: 1, apply_to: ['number', 'datetime']}, - {type: 'between', accept_values: 2, apply_to: ['number', 'datetime']}, - {type: 'begins_with', accept_values: 1, apply_to: ['string']}, - {type: 'not_begins_with', accept_values: 1, apply_to: ['string']}, - {type: 'contains', accept_values: 1, apply_to: ['string']}, - {type: 'not_contains', accept_values: 1, apply_to: ['string']}, - {type: 'ends_with', accept_values: 1, apply_to: ['string']}, - {type: 'not_ends_with', accept_values: 1, apply_to: ['string']}, - {type: 'is_empty', accept_values: 0, apply_to: ['string']}, - {type: 'is_not_empty', accept_values: 0, apply_to: ['string']}, - {type: 'is_null', accept_values: 0, apply_to: ['string', 'number', 'datetime', 'boolean']}, - {type: 'is_not_null', accept_values: 0, apply_to: ['string', 'number', 'datetime', 'boolean']} - ], - - 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' - } - }; - - - // PLUGINS SYSTEM - // Inspired by https://github.com/brianreavis/microplugin.js - // Very lightened and without dependencies - // =============================== - QueryBuilder.plugins = {}; - - /** - * Define a new plugin - * @param {string} - * @param {function} - */ - QueryBuilder.define = function(name, fct) { - QueryBuilder.plugins[name] = fct; - }; - - /** - * Add new methods - * @param {object} - */ - QueryBuilder.extend = function(methods) { - $.extend(QueryBuilder.prototype, methods); - }; - - /** - * Init plugins for an instance - */ - QueryBuilder.prototype.initPlugins = function() { - if (!this.settings.plugins) { - return; - } - - var that = this, - queue = {}; - - if ($.isArray(this.settings.plugins)) { - $.each(this.settings.plugins, function(i, plugin) { - queue[plugin] = {}; - }); - } - else { - $.each(this.settings.plugins, function(plugin, options) { - queue[plugin] = options; - }); - } - - $.each(queue, function(plugin, options) { - if (plugin in QueryBuilder.plugins) { - QueryBuilder.plugins[plugin].call(that, options); - } - else { - $.error('Unable to find plugin "' + plugin +'"'); - } - }); - }; - - - // PUBLIC METHODS - // =============================== - /** - * Init the builder - */ - QueryBuilder.prototype.init = function(options) { - // PROPERTIES - this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options); - this.status = { - group_id: 0, - rule_id: 0, - generatedId: false, - has_optgroup: false - }; - - // "allow_groups" changed in 1.3.1 from boolean to 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.filters = this.settings.filters; - this.lang = this.settings.lang; - this.icons = this.settings.icons; - this.operators = this.settings.operators; - this.template = this.settings.template; - - if (this.template.group === null) { - this.template.group = this.getGroupTemplate; - } - if (this.template.rule === null) { - this.template.rule = this.getRuleTemplate; - } - - // CHECK FILTERS - if (!this.filters || this.filters.length < 1) { - $.error('Missing filters list'); - } - this.checkFilters(); - - // ensure we have a container id - if (!this.$el.attr('id')) { - this.$el.attr('id', 'qb_'+Math.floor(Math.random()*99999)); - this.status.generatedId = true; - } - this.$el_id = this.$el.attr('id'); - - this.$el.addClass('query-builder'); - - // INIT - this.bindEvents(); - - this.initPlugins(); - - this.trigger('afterInit'); - - if (options.rules) { - this.setRules(options.rules); - } - else { - this.addGroup(this.$el); - } - }; - - /** - * Destroy the plugin - */ - QueryBuilder.prototype.destroy = function() { - this.trigger('beforeDestroy'); - - if (this.status.generatedId) { - this.$el.removeAttr('id'); - } - - this.$el.empty() - .off('click.queryBuilder change.queryBuilder') - .removeClass('query-builder') - .removeData('queryBuilder'); - }; - - /** - * Reset the plugin - */ - QueryBuilder.prototype.reset = function() { - this.status.group_id = 1; - this.status.rule_id = 0; - - this.$el.find('>.rules-group-container>.rules-group-body>.rules-list').empty(); - - this.addRule(this.$el.find('>.rules-group-container')); - - this.trigger('afterReset'); - }; - - /** - * Clear the plugin - */ - QueryBuilder.prototype.clear = function() { - this.status.group_id = 0; - this.status.rule_id = 0; - - this.$el.empty(); - - this.trigger('afterClear'); - }; - - /** - * Get an object representing current rules - * @return {object} - */ - QueryBuilder.prototype.getRules = function() { - this.clearErrors(); - - var $group = this.$el.find('>.rules-group-container'), - that = this; - - var rules = (function parse($group) { - var out = {}, - $elements = $group.find('>.rules-group-body>.rules-list>*'); - - out.condition = that.getGroupCondition($group); - out.rules = []; - - for (var i=0, l=$elements.length; i 1)) { - that.triggerValidationError(['empty_group'], $group, null, null, null); - return {}; - } - - return out; - }($group)); - - return this.change('getRules', rules); - }; - - /** - * Set rules from object - * @param data {object} - */ - QueryBuilder.prototype.setRules = function(data) { - this.clear(); - - if (!data || !data.rules || (data.rules.length===0 && !this.settings.allow_empty)) { - $.error('Incorrect data object passed'); - } - - data = this.change('setRules', data); - - var $container = this.$el, - that = this; - - (function add(data, $container){ - var $group = that.addGroup($container, false); - if ($group === null) { - return; - } - - var $buttons = $group.find('>.rules-group-header [name$=_cond]'); - - if (data.condition === undefined) { - data.condition = that.settings.default_condition; - } - - for (var i=0, l=that.settings.conditions.length; i0) { - if (that.settings.allow_groups !== -1 && that.settings.allow_groups < $group.data('queryBuilder').level) { - that.reset(); - $.error(fmt('No more than {0} groups are allowed', that.settings.allow_groups)); - } - else { - add(rule, $group); - } - } - else { - if (rule.id === undefined) { - $.error('Missing rule field id'); - } - if (rule.value === undefined) { - rule.value = ''; - } - if (rule.operator === undefined) { - rule.operator = 'equal'; - } - - var $rule = that.addRule($group); - if ($rule === null) { - return; - } - - var filter = that.getFilterById(rule.id), - operator = that.getOperatorByType(rule.operator); - - $rule.find('.rule-filter-container [name$=_filter]').val(rule.id).trigger('change'); - $rule.find('.rule-operator-container [name$=_operator]').val(rule.operator).trigger('change'); - - if (operator.accept_values !== 0) { - that.setRuleValue($rule, rule.value, filter, operator); - } - - that.applyRuleFlags($rule, rule); - } - }); - - }(data, $container)); - }; - - - // MAIN METHODS - // =============================== - /** - * Checks the configuration of each filter - */ - QueryBuilder.prototype.checkFilters = function() { - var definedFilters = [], - that = this; - - $.each(this.filters, function(i, filter) { - if (!filter.id) { - $.error('Missing filter id: '+ i); - } - if (definedFilters.indexOf(filter.id) != -1) { - $.error('Filter already defined: '+ filter.id); - } - definedFilters.push(filter.id); - - if (!filter.type) { - $.error('Missing filter type: '+ filter.id); - } - if (types.indexOf(filter.type) == -1) { - $.error('Invalid type: '+ filter.type); - } - - if (!filter.input) { - filter.input = 'text'; - } - else if (typeof filter.input != 'function' && inputs.indexOf(filter.input) == -1) { - $.error('Invalid input: '+ filter.input); - } - - if (!filter.field) { - filter.field = filter.id; - } - if (!filter.label) { - filter.label = filter.field; - } - - that.status.has_optgroup|= !!filter.optgroup; - if (!filter.optgroup) { - filter.optgroup = null; - } - - switch (filter.type) { - case 'string': - filter.internalType = 'string'; - break; - case 'integer': case 'double': - filter.internalType = 'number'; - break; - case 'date': case 'time': case 'datetime': - filter.internalType = 'datetime'; - break; - case 'boolean': - filter.internalType = 'boolean'; - break; - } - - switch (filter.input) { - case 'radio': case 'checkbox': - if (!filter.values || filter.values.length < 1) { - $.error('Missing values for filter: '+ filter.id); - } - break; - } - }); - - // group filters with same optgroup, preserving declaration order when possible - if (this.status.has_optgroup) { - var optgroups = [], - filters = []; - - $.each(this.filters, function(i, filter) { - var idx; - - if (filter.optgroup) { - idx = optgroups.lastIndexOf(filter.optgroup); - - if (idx == -1) { - idx = optgroups.length; - } - } - else { - idx = optgroups.length; - } - - optgroups.splice(idx, 0, filter.optgroup); - filters.splice(idx, 0, filter); - }); - - this.filters = filters; - } - - this.trigger('afterCheckFilters'); - }; - - /** - * Add all events listeners - */ - QueryBuilder.prototype.bindEvents = function() { - var that = this; - - // group condition change - this.$el.on('change.queryBuilder', '.rules-group-header [name$=_cond]', function() { - var $this = $(this); - - if ($this.is(':checked')) { - $this.parent().addClass('active').siblings().removeClass('active'); - } - }); - - // rule filter change - this.$el.on('change.queryBuilder', '.rule-filter-container [name$=_filter]', function() { - var $this = $(this), - $rule = $this.closest('.rule-container'); - - that.updateRuleFilter($rule, $this.val()); - }); - - // rule operator change - this.$el.on('change.queryBuilder', '.rule-operator-container [name$=_operator]', function() { - var $this = $(this), - $rule = $this.closest('.rule-container'); - - that.updateRuleOperator($rule, $this.val()); - }); - - // add rule button - this.$el.on('click.queryBuilder', '[data-add=rule]', function() { - var $this = $(this), - $group = $this.closest('.rules-group-container'); - - that.addRule($group); - }); - - // delete rule button - this.$el.on('click.queryBuilder', '[data-delete=rule]', function() { - var $this = $(this), - $rule = $this.closest('.rule-container'); - - that.deleteRule($rule); - }); - - if (this.settings.allow_groups !== 0) { - // add group button - this.$el.on('click.queryBuilder', '[data-add=group]', function() { - var $this = $(this), - $group = $this.closest('.rules-group-container'); - - that.addGroup($group); - }); - - // delete group button - this.$el.on('click.queryBuilder', '[data-delete=group]', function() { - var $this = $(this), - $group = $this.closest('.rules-group-container'); - - that.deleteGroup($group); - }); - } - }; - - /** - * Add a new rules group - * @param $parent {jQuery} - * @param addRule {bool} (optional - add a default empty rule) - * @return $group {jQuery} - */ - QueryBuilder.prototype.addGroup = function($parent, addRule) { - var group_id = this.nextGroupId(), - level = (($parent.data('queryBuilder') || {}).level || 0) + 1, - $container = level===1 ? $parent : $parent.find('>.rules-group-body>.rules-list'), - $group = $(this.template.group.call(this, group_id, level)); - - $group.data('queryBuilder', {level:level}); - - var e = $.Event('addGroup.queryBuilder', { - group_id: group_id, - level: level, - addRule: addRule, - group: $group, - parent: $parent, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return null; - } - - $container.append($group); - - if (this.settings.onAfterAddGroup) { - this.settings.onAfterAddGroup.call(this, $group); - } - - this.trigger('afterAddGroup', $group); - - if (addRule === undefined || addRule === true) { - this.addRule($group); - } - - return $group; - }; - - /** - * Tries to delete a group. The group is not deleted if at least one rule is no_delete. - * @param $group {jQuery} - * @return {boolean} true if the group has been deleted - */ - QueryBuilder.prototype.deleteGroup = function($group) { - if ($group[0].id == this.$el_id + '_group_0') { - return; - } - - var e = $.Event('deleteGroup.queryBuilder', { - group_id: $group[0].id, - group: $group, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return false; - } - - this.trigger('beforeDeleteGroup', $group); - - var that = this, - keepGroup = false; - - $group.find('>.rules-group-body>.rules-list>*').each(function() { - var $element = $(this); - - if ($element.hasClass('rule-container')) { - if ($element.data('queryBuilder').flags.no_delete) { - keepGroup = true; - } - else { - $element.remove(); - } - } - else { - keepGroup|= !that.deleteGroup($element); - } - }); - - if (!keepGroup) { - $group.remove(); - } - - return !keepGroup; - }; - - /** - * Add a new rule - * @param $parent {jQuery} - * @return $rule {jQuery} - */ - QueryBuilder.prototype.addRule = function($parent) { - var rule_id = this.nextRuleId(), - $container = $parent.find('>.rules-group-body>.rules-list'), - $rule = $(this.template.rule.call(this, rule_id)), - $filterSelect = $(this.getRuleFilterSelect(rule_id)); - - $rule.data('queryBuilder', {flags: {}}); - - var e = $.Event('addRule.queryBuilder', { - rule_id: rule_id, - rule: $rule, - parent: $parent, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return null; - } - - $container.append($rule); - $rule.find('.rule-filter-container').append($filterSelect); - - if (this.settings.onAfterAddRule) { - this.settings.onAfterAddRule.call(this, $rule); - } - - this.trigger('afterAddRule', $rule); - - return $rule; - }; - - /** - * Delete a rule. - * @param $rule {jQuery} - * @return {boolean} true if the rule has been deleted - */ - QueryBuilder.prototype.deleteRule = function($rule) { - var e = $.Event('deleteRule.queryBuilder', { - rule_id: $rule[0].id, - rule: $rule, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return false; - } - - this.trigger('beforeDeleteRule', $rule); - - $rule.remove(); - return true; - }; - - /** - * Create operators for a rule - * @param $rule {jQuery} (
      • element) - * @param filter {object} - */ - QueryBuilder.prototype.createRuleInput = function($rule, filter) { - var $valueContainer = $rule.find('.rule-value-container').empty(); - - if (filter === null) { - return; - } - - var operator = this.getOperatorByType(this.getRuleOperator($rule)); - - if (operator.accept_values === 0) { - return; - } - - var $inputs = $(); - - for (var i=0; i 0) $valueContainer.append(' , '); - $valueContainer.append($ruleInput); - $inputs = $inputs.add($ruleInput); - } - - $valueContainer.show(); - - if (filter.onAfterCreateRuleInput) { - filter.onAfterCreateRuleInput.call(this, $rule, filter); - } - - if (filter.plugin) { - $inputs[filter.plugin](filter.plugin_config || {}); - } - - if (filter.default_value !== undefined) { - this.setRuleValue($rule, filter.default_value, filter, operator); - } - - this.trigger('afterCreateRuleInput', $rule, filter, operator); - }; - - /** - * Perform action when rule's filter is changed - * @param $rule {jQuery} (
      • element) - * @param filterId {string} - */ - QueryBuilder.prototype.updateRuleFilter = function($rule, filterId) { - var filter = filterId != '-1' ? this.getFilterById(filterId) : null; - - this.createRuleOperators($rule, filter); - this.createRuleInput($rule, filter); - - $rule.data('queryBuilder').filter = filter; - - this.trigger('afterUpdateRuleFilter', $rule, filter); - }; - - /** - * Update main visibility when rule operator changes - * @param $rule {jQuery} (
      • element) - * @param operatorType {string} - */ - QueryBuilder.prototype.updateRuleOperator = function($rule, operatorType) { - var $valueContainer = $rule.find('.rule-value-container'), - filter = this.getFilterById(this.getRuleFilter($rule)), - operator = this.getOperatorByType(operatorType); - - if (operator.accept_values === 0) { - $valueContainer.hide(); - } - else { - $valueContainer.show(); - - var previousOperator = $rule.data('queryBuilder').operator; - - if ($valueContainer.is(':empty') || operator.accept_values != previousOperator.accept_values) { - this.createRuleInput($rule, filter); - } - } - - $rule.data('queryBuilder').operator = operator; - - if (filter.onAfterChangeOperator) { - filter.onAfterChangeOperator.call(this, $rule, filter, operator); - } - - this.trigger('afterChangeOperator', $rule, filter, operator); - }; - - /** - * Check if a value is correct for a filter - * @param $rule {jQuery} (
      • element) - * @param value {string|string[]|undefined} - * @param filter {object} - * @param operator {object} - * @return {array|true} - */ - QueryBuilder.prototype.validateValue = function($rule, value, filter, operator) { - var validation = filter.validation || {}, - result = true; - - if (operator.accept_values == 1) { - value = [value]; - } - else { - value = value; - } - - if (validation.callback) { - result = validation.callback.call(this, value, filter, operator, $rule); - return this.change('validateValue', result, $rule, value, filter, operator); - } - - for (var i=0; i< validation.min) { - result = ['string_exceed_min_length', validation.min]; - break; - } - } - else if (value[i].length === 0) { - result = ['string_empty']; - break; - } - if (validation.max !== undefined) { - if (value[i].length > validation.max) { - result = ['string_exceed_max_length', validation.max]; - break; - } - } - if (validation.format) { - if (!(validation.format.test(value[i]))) { - result = ['string_invalid_format', validation.format]; - break; - } - } - break; - - case 'number': - if (isNaN(value[i])) { - result = ['number_nan']; - break; - } - if (filter.type == 'integer') { - if (parseInt(value[i]) != value[i]) { - result = ['number_not_integer']; - break; - } - } - else { - if (parseFloat(value[i]) != value[i]) { - result = ['number_not_double']; - break; - } - } - if (validation.min !== undefined) { - if (value[i] < validation.min) { - result = ['number_exceed_min', validation.min]; - break; - } - } - if (validation.max !== undefined) { - if (value[i] > validation.max) { - result = ['number_exceed_max', validation.max]; - break; - } - } - if (validation.step !== undefined) { - var v = value[i]/validation.step; - if (parseInt(v) != v) { - result = ['number_wrong_step', validation.step]; - break; - } - } - break; - - case 'datetime': - // we need MomentJS - if (window.moment && validation.format) { - var datetime = moment(value[i], validation.format); - if (!datetime.isValid()) { - result = ['datetime_invalid']; - break; - } - else { - if (validation.min) { - if (datetime < moment(validation.min, validation.format)) { - result = ['datetime_exceed_min', validation.min]; - break; - } - } - if (validation.max) { - if (datetime > moment(validation.max, validation.format)) { - result = ['datetime_exceed_max', validation.max]; - break; - } - } - } - } - break; - - case 'boolean': - if (value[i].trim().toLowerCase() !== 'true' && value[i].trim().toLowerCase() !== 'false' && - value[i].trim() !== '1' && value[i].trim() !== '0' && value[i] !== 1 && value[i] !== 0) { - result = ['boolean_not_valid']; - break; - } - } - } - - if (result !== true) { - break; - } - } - - return this.change('validateValue', result, $rule, value, filter, operator); - }; - - /** - * Remove 'has-error' from everything - */ - QueryBuilder.prototype.clearErrors = function() { - this.$el.find('.has-error').removeClass('has-error'); - }; - - /** - * Trigger a validation error event with custom params - * @param error {array} - * @param $target {jQuery} - * @param value {mixed} - * @param filter {object} - * @param operator {object} - */ - QueryBuilder.prototype.triggerValidationError = function(error, $target, value, filter, operator) { - if (!$.isArray(error)) { - error = [error]; - } - - if (filter && filter.onValidationError) { - filter.onValidationError.call(this, $target, error, value, filter, operator); - } - if (this.settings.onValidationError) { - this.settings.onValidationError.call(this, $target, error, value, filter, operator); - } - - var e = $.Event('validationError.queryBuilder', { - error: error, - filter: filter, - operator: operator, - value: value, - targetRule: $target[0], - builder: this - }); - - this.$el.trigger(e); - - if (this.settings.display_errors && !e.isDefaultPrevented()) { - // translate the text without modifying event array - var errorLoc = $.extend([], error, [ - this.lang.errors[error[0]] || error[0] - ]); - - $target.addClass('has-error'); - var $error = $target.find('.error-container').eq(0); - $error.attr('title', fmt.apply(null, errorLoc)); - } - - this.trigger('validationError', $target, error); - }; - - - // DATA ACCESS - // =============================== - /** - * Returns an incremented group ID - * @return {string} - */ - QueryBuilder.prototype.nextGroupId = function() { - return this.$el_id + '_group_' + (this.status.group_id++); - }; - - /** - * Returns an incremented rule ID - * @return {string} - */ - QueryBuilder.prototype.nextRuleId = function() { - return this.$el_id + '_rule_' + (this.status.rule_id++); - }; - - /** - * Returns the operators for a filter - * @param filter {string|object} (filter id name or filter object) - * @return {object[]} - */ - QueryBuilder.prototype.getOperators = function(filter) { - if (typeof filter === 'string') { - filter = this.getFilterById(filter); - } - - var result = []; - - for (var i=0, l=this.operators.length; i
        element) - * @return {string} - */ - QueryBuilder.prototype.getGroupCondition = function($group) { - return $group.find('>.rules-group-header [name$=_cond]:checked').val(); - }; - - /** - * Returns the selected filter of a rule - * @param $rule {jQuery} (
      • element) - * @return {string} - */ - QueryBuilder.prototype.getRuleFilter = function($rule) { - return $rule.find('.rule-filter-container [name$=_filter]').val(); - }; - - /** - * Returns the selected operator of a rule - * @param $rule {jQuery} (
      • element) - * @return {string} - */ - QueryBuilder.prototype.getRuleOperator = function($rule) { - return $rule.find('.rule-operator-container [name$=_operator]').val(); - }; - - /** - * Returns rule value - * @param $rule {jQuery} (
      • element) - * @param filter {object} (optional - current rule filter) - * @param operator {object} (optional - current rule operator) - * @return {string|string[]|undefined} - */ - QueryBuilder.prototype.getRuleValue = function($rule, filter, operator) { - filter = filter || this.getFilterById(this.getRuleFilter($rule)); - operator = operator || this.getOperatorByType(this.getRuleOperator($rule)); - - var value = [], tmp, - $value = $rule.find('.rule-value-container'); - - for (var i=0; i
      • element) - * @param value {mixed} - * @param filter {object} - * @param operator {object} - */ - QueryBuilder.prototype.setRuleValue = function($rule, value, filter, operator) { - filter = filter || this.getFilterById(this.getRuleFilter($rule)); - operator = operator || this.getOperatorByType(this.getRuleOperator($rule)); - - this.trigger('beforeSetRuleValue', $rule, value, filter, operator); - - if (filter.valueSetter) { - filter.valueSetter.call(this, $rule, value, filter, operator); - } - else { - var $value = $rule.find('.rule-value-container'); - - if (operator.accept_values == 1) { - value = [value]; - } - else { - value = value; - } - - for (var i=0; i
      • element) - * @param rule {object} - */ - QueryBuilder.prototype.applyRuleFlags = function($rule, rule) { - var flags = this.getRuleFlags(rule); - $rule.data('queryBuilder').flags = flags; - - if (flags.filter_readonly) { - $rule.find('[name$=_filter]').prop('disabled', true); - } - if (flags.operator_readonly) { - $rule.find('[name$=_operator]').prop('disabled', true); - } - if (flags.value_readonly) { - $rule.find('[name*=_value_]').prop('disabled', true); - } - if (flags.no_delete) { - $rule.find('[data-delete=rule]').remove(); - } - - this.trigger('afterApplyRuleFlags', $rule, rule, flags); - }; - - - // TEMPLATES - // =============================== - /** - * Returns group HTML - * @param group_id {string} - * @param level {int} - * @return {string} - */ - QueryBuilder.prototype.getGroupTemplate = function(group_id, level) { - var h = '\ -
        \ -
        \ -
        \ - \ - '+ (this.settings.allow_groups===-1 || this.settings.allow_groups>=level ? - '' - :'') +' \ - '+ (level>1 ? - '' - : '') +' \ -
        \ -
        \ - '+ this.getGroupConditions(group_id) +' \ -
        \ - '+ (this.settings.display_errors ? - '
        ' - :'') +'\ -
        \ -
        \ -
          \ -
          \ -
          '; - - return this.change('getGroupTemplate', h, level); - }; - - /** - * Returns group conditions HTML - * @param group_id {string} - * @return {string} - */ - QueryBuilder.prototype.getGroupConditions = function(group_id) { - var h = ''; - - for (var i=0, l=this.settings.conditions.length; i \ - '+ label +' \ - '; - } - - return this.change('getGroupConditions', h); - }; - - /** - * Returns rule HTML - * @param rule_id {string} - * @return {string} - */ - QueryBuilder.prototype.getRuleTemplate = function(rule_id) { - var h = '\ -
        • \ -
          \ -
          \ - \ -
          \ -
          \ - '+ (this.settings.display_errors ? - '
          ' - :'') +'\ -
          \ -
          \ -
          \ -
        • '; - - return this.change('getRuleTemplate', h); - }; - - /** - * Returns rule filter '; - h+= ''; - - $.each(this.filters, function(i, filter) { - if (optgroup != filter.optgroup) { - if (optgroup !== null) h+= ''; - optgroup = filter.optgroup; - if (optgroup !== null) h+= ''; - } - - h+= ''; - }); - - if (optgroup !== null) h+= ''; - h+= ''; - - return this.change('getRuleFilterSelect', h); - }; - - /** - * Returns rule operator '; - - for (var i=0, l=operators.length; i'+ label +''; - } - - h+= ''; - - return this.change('getRuleOperatorSelect', h); - }; - - /** - * Return the rule value HTML - * @param $rule {jQuery} - * @param filter {object} - * @param value_id {int} - * @return {string} - */ - QueryBuilder.prototype.getRuleInput = function($rule, filter, value_id) { - var validation = filter.validation || {}, - name = $rule[0].id +'_value_'+ value_id, - h = '', c; - - if (typeof filter.input === 'function') { - h = filter.input.call(this, $rule, filter, name); - } - else { - switch (filter.input) { - case 'radio': - c = filter.vertical ? ' class=block' : ''; - iterateOptions(filter.values, function(key, val) { - h+= ' '+ val +' '; - }); - break; - - case 'checkbox': - c = filter.vertical ? ' class=block' : ''; - 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 new file mode 100644 index 00000000..c4473ed7 --- /dev/null +++ b/tests/common.js @@ -0,0 +1,344 @@ +/** + * Sync load of language file once QUnit and Blanket are ready + * Otherwise the language file is loaded before instrumented files + */ +QUnit.begin(function() { + $.ajax({ + async: false, + url: '../dist/i18n/query-builder.en.js', + dataType: 'script' + }); +}); + +/** + * Add GitHub link in header + */ +QUnit.begin(function(){ + $('#qunit-header').append( + '
          ' + + '' + + '' + + '' + + '
          ' + ); +}); + +/** + * Modify Blanket results display + */ +QUnit.done(function(){ + $('#blanket-main') + .css('marginTop', '10px') + .addClass('col-lg-8 col-lg-push-2') + .find('.bl-file a').each(function(){ + this.innerHTML = this.innerHTML.replace(/(.*)\/src\/(.*)$/, '$2'); + }); +}); + + +/** + * Custom assert to compare rules objects + */ +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; + } + else { + ok = QUnit.equiv(a.data, b.data); + } + } + + if (b.hasOwnProperty('flags')) { + if (!a.hasOwnProperty('flags')) { + ok = false; + } + else { + ok = QUnit.equiv(a.flags, b.flags); + } + } + + if (b.hasOwnProperty('rules')) { + if (!a.hasOwnProperty('rules')) { + ok = false; + } + else { + for (var i=0, l=a.rules.length; i.rules-group-header>.group-actions [data-add=rule]').trigger('click'); + $('[name=builder_rule_0_filter]').val('name').trigger('change'); + $('[name=builder_rule_0_operator]').val('not_equal').trigger('change'); + $('[name=builder_rule_0_value_0]').val('foo').trigger('change'); + $('[name=builder_rule_1_filter]').val('category').trigger('change'); + $('[name=builder_rule_1_operator]').val('is_null').trigger('change'); + $('#builder_group_0>.rules-group-header>.group-conditions [value=OR]').trigger('click'); + $('#builder_group_0>.rules-group-header>.group-actions [data-add=rule]').trigger('click'); + $('#builder_group_0>.rules-group-header>.group-actions [data-add=group]').trigger('click'); + $('#builder_rule_2 [data-delete=rule]').trigger('click'); + $('#builder_group_1 [data-delete=group]').trigger('click'); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + { + condition: 'OR', + rules: [{ + id: 'name', + operator: 'not_equal', + value: 'foo' + }, { + id: 'category', + operator: 'is_null', + value: null + }] + }, + 'Should return correct rules after UI events' + ); + + $b.queryBuilder('destroy'); + + $b.queryBuilder({ + filters: [{ + id: 'name', + label: 'Name', + type: 'string', + input_event: 'custom.evt' + }] + }); + + $('[name=builder_rule_0_filter]').val('name').trigger('change'); + $('[name=builder_rule_0_operator]').val('equal').trigger('change'); + $('[name=builder_rule_0_value_0]').val('bar').trigger('custom.evt'); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + { + condition: 'AND', + rules: [{ + id: 'name', + operator: 'equal', + value: 'bar' + }] + }, + 'Should return correct rules after UI events with custom change event' + ); + }); + + /** + * Test filter.operators + */ + QUnit.test('Change operators', function(assert) { + $b.queryBuilder({ + filters: [{ + id: 'name', + type: 'string' + }, { + id: 'price', + type: 'double' + }, { + id: 'release', + type: 'date', + operators: ['before', 'equal', 'after'] + }], + rules: { + condition: 'AND', + rules: [{ + id: 'name', + operator: 'equal', + value: 'foo' + }, { + id: 'price', + operator: 'less', + value: 10 + }, { + id: 'release', + operator: 'before', + value: '1995-5-1' + }] + }, + operators: [ + {type: 'equal', nb_inputs: 1, apply_to: ['string']}, + {type: 'not_equal', nb_inputs: 1, apply_to: ['string']}, + {type: 'less', nb_inputs: 1, apply_to: ['number']}, + {type: 'greater', nb_inputs: 1, apply_to: ['number']}, + {type: 'before', nb_inputs: 1, apply_to: ['datetime']}, + {type: 'after', nb_inputs: 1, apply_to: ['datetime']} + ] + }); + + assert.optionsMatch( + $('#builder_rule_0 [name$=_operator] option'), + ['equal', 'not_equal'], + '"name" filter should have "equal" & "not_equal" operators' + ); + + assert.optionsMatch( + $('#builder_rule_1 [name$=_operator] option'), + ['less', 'greater'], + '"price" filter should have "less" & "greater" operators' + ); + + assert.optionsMatch( + $('#builder_rule_2 [name$=_operator] option'), + ['before', 'equal', 'after'], + '"release" filter should have "before" & "equal" & "after" operators' + ); + }); + + /** + * Test custom conditions + */ + QUnit.test('Change conditions', function(assert) { + $b.queryBuilder({ + filters: basic_filters, + conditions: ['AND'] + }); + + assert.optionsMatch( + $b.find('[name$=_cond]'), + ['AND'], + 'Available condition should be AND' + ); + + $b.queryBuilder('destroy'); + + var rules = { + condition: 'NAND', + rules: [{ + id: 'name', + operator: 'equal', + value: 'foo' + }, { + condition: 'XOR', + rules: [{ + id: 'name', + operator: 'equal', + value: 'bar' + }] + }] + }; + + $b.queryBuilder({ + filters: basic_filters, + rules: rules, + conditions: ['NAND', 'XOR'], + default_condition: 'NAND' + }); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + rules, + 'Should return correct rules' + ); + + assert.optionsMatch( + $('#builder_group_0 > .rules-group-header [name$=_cond]'), + ['NAND', 'XOR'], + 'Available onditions should be NAND & XOR' + ); + + assert.equal( + $('#builder_group_1 [name$=_cond]:checked').val(), + 'XOR', + 'The second group should have "XOR" condition selected' + ); + }); + + /** + * Test icons + */ + QUnit.test('Change icons', function(assert) { + $b.queryBuilder({ + filters: basic_filters, + icons: { + add_group: 'fa fa-plus-circle', + add_rule: 'fa fa-plus', + remove_rule: 'fa fa-times', + remove_group: 'fa fa-times', + sort: 'fa fa-sort' + } + }); + + assert.equal( + $b.find('[data-add=rule] i').attr('class'), + 'fa fa-plus', + 'Rule add icon should have been replaced' + ); + + assert.equal( + $b.find('[data-delete=rule] i').attr('class'), + 'fa fa-times', + 'Rule delete icon should have been replaced' + ); + }); + + /** + * Test readonly + */ + QUnit.test('Readonly', function(assert) { + $b.queryBuilder({ + filters: basic_filters, + rules: { + condition: 'AND', + flags: { + condition_readonly: true + }, + rules: [{ + id: 'price', + operator: 'less', + value: 10.25, + flags: { + no_delete: true + } + }, { + condition: 'OR', + rules: [{ + id: 'id', + operator: 'not_equal', + value: '1234-azer-5678', + readonly: true + }] + }, { + condition: 'AND', + readonly: true, + rules: [{ + id: 'name', + operator: 'equal', + value: 'foo' + }] + }] + } + }); + + assert.equal( + $('#builder_group_0>.rules-group-header input:not(:disabled)').length, 0, + 'Should disable group condition radio buttons' + ); + + assert.equal( + $('#builder_rule_0 [data-delete=rule]').length, 0, + 'Should hide delete button of "no_delete" rule' + ); + + assert.equal( + $('#builder_rule_0').find('input:disabled, select:disabled').length, 0, + 'Should not disable inputs of "no_delete" rule' + ); + + assert.equal( + $('#builder_rule_1 [data-delete=rule]').length, 0, + 'Should hide delete button of "readonly" rule' + ); + + assert.equal( + $('#builder_rule_1').find('input:disabled, select:disabled').length, 3, + 'Should disable inputs of "readonly" rule' + ); + + assert.equal( + $('#builder_group_2').find('[data-delete=group], [data-add=rule], [data-add=group]').length, 0, + 'Should hide all buttons of "readonly" group' + ); + + $('#builder_group_1 [data-delete=group]').click(); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + { + condition: 'AND', + rules: [{ + id: 'price', + operator: 'less', + value: 10.25 + }, { + condition: 'OR', + rules: [{ + id: 'id', + operator: 'not_equal', + value: '1234-azer-5678' + }] + }, { + condition: 'AND', + rules: [{ + id: 'name', + operator: 'equal', + value: 'foo' + }] + }] + }, + 'Should not delete group with readonly rule' + ); + }); + + /** + * Test groups limit + */ + QUnit.test('No groups', function(assert) { + $b.queryBuilder({ + filters: basic_filters, + allow_groups: false + }); + + assert.ok( + $('#builder_group_0 [data-add=group]').length == 0, + 'Should not contain group add button' + ); + + assert.throws( + function(){ $b.queryBuilder('setRules', basic_rules); }, + /No more than 0 groups are allowed/, + 'Should throw "No more than 0 groups are allowed" error' + ); + }); + + /** + * Test optgroups + */ + QUnit.test('Optgroups', function(assert) { + $b.queryBuilder({ + filters: [{ + id: '1', + optgroup: 'A' + }, { + id: '2' + }, { + id: '3', + optgroup: 'A' + }, { + id: '4', + optgroup: 'B' + }, { + id: '5' + }, { + id: '6', + optgroup: 'A' + }, { + id: '7', + optgroup: 'C' + }], + operators: [ + {type: 'equal', optgroup: 'equality'}, + {type: 'not_equal', optgroup: 'equality'}, + {type: 'in' }, + {type: 'not_in' }, + {type: 'less', optgroup: 'numbers'}, + {type: 'less_or_equal', optgroup: 'numbers'}, + {type: 'greater', optgroup: 'numbers'}, + {type: 'greater_or_equal', optgroup: 'numbers'}, + {type: 'between', optgroup: 'numbers'}, + {type: 'not_between', optgroup: 'numbers'}, + {type: 'begins_with', optgroup: 'strings'}, + {type: 'not_begins_with', optgroup: 'strings'}, + {type: 'ends_with', optgroup: 'strings'}, + {type: 'not_ends_with', optgroup: 'strings'}, + {type: 'contains', optgroup: 'strings'}, + {type: 'not_contains', optgroup: 'strings'}, + {type: 'is_empty' }, + {type: 'is_not_empty' }, + {type: 'is_null' }, + {type: 'is_not_null' } + ], + optgroups: { + A: { + en: 'AA', + fr: 'AAA' + }, + B: 'BB', + strings: { + en: 'Strings', + fr: 'Chaines' + } + }, + lang_code: 'fr' + }); + + var options = [], groups = []; + $('[name=builder_rule_0_filter]>*').each(function() { + if (this.nodeName == 'OPTION') { + options.push($(this).val()); + } + else { + var group = []; + $(this).find('option').each(function() { + group.push($(this).val()); + }); + options.push(group); + groups.push($(this).attr('label')); + } + }); + + assert.deepEqual( + options, + ['-1', ['1', '3', '6'], '2', ['4'], '5', ['7']], + 'Filters should have been put in optgroups, solving discontinuities and keeping order' + ); + + assert.deepEqual( + groups, + ['AAA', 'BB', 'C'], + 'Optgroups should have been correctly translated and created when needed' + ); + + $b[0].queryBuilder.model.root.rules[0].filter = '1'; + + options = []; groups = []; + $('[name=builder_rule_0_operator]>*').each(function() { + if (this.nodeName == 'OPTION') { + options.push($(this).val()); + } + else { + var group = []; + $(this).find('option').each(function() { + group.push($(this).val()); + }); + options.push(group); + groups.push($(this).attr('label')); + } + }); + + assert.deepEqual( + options, + [['equal', 'not_equal'], 'in', 'not_in', ['begins_with', 'not_begins_with', 'ends_with', 'not_ends_with', 'contains', 'not_contains'], 'is_empty', 'is_not_empty', 'is_null', 'is_not_null'], + 'Operators should have been put in optgroups, solving discontinuities and keeping order' + ); + + assert.deepEqual( + groups, + ['equality', 'Chaines'], + 'Optgroups should have been correctly translated and created when needed' + ); + }); + + /** + * Test filters ordering + */ + QUnit.test('Sort filters', function(assert) { + $b.queryBuilder({ + filters: [{ + id: '3', + label: { + fr: 'ccc', + en: 'Ccc' + } + }, { + id: '1', + label: 'AAA' + }, { + id: '5', + label: 'eee' + }, { + id: '2', + label: 'bbb' + }, { + id: '4', + label: { + fr: 'ddd', + en: 'Ddd' + } + }], + sort_filters: true, + lang_code: 'fr' + }); + + var options = []; + $('[name=builder_rule_0_filter]>*').each(function() { + options.push($(this).val()); + }); + + assert.deepEqual( + options, + ['-1', '1', '2', '3', '4', '5'], + 'Filters should be sorted by alphabetical order' + ); + + $b.queryBuilder('destroy'); + + $b.queryBuilder({ + filters: [{ + id: '3', + label: 'ccc' + }, { + id: '1', + label: 'AAA' + }, { + id: '5', + label: 'eee' + }, { + id: '2', + label: 'bbb' + }, { + id: '4', + label: 'ddd' + }], + sort_filters: function(a, b) { + return parseInt(b.id) - parseInt(a.id); + } + }); + + options = []; + $('[name=builder_rule_0_filter]>*').each(function() { + options.push($(this).val()); + }); + + assert.deepEqual( + options, + ['-1', '5', '4', '3', '2', '1'], + 'Filters should be sorted by custom order' + ); + }); + + /** + * 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 + */ + QUnit.test('Access to defaults', function(assert) { + if (QueryBuilder.defaults() == QueryBuilder.DEFAULTS) { + assert.push(false, '[copy]', '[original]', 'Should return full copy of defaults'); + } + else { + assert.deepEqual( + QueryBuilder.defaults(), + QueryBuilder.DEFAULTS, + 'Should return full copy of defaults' + ); + } + + assert.equal( + QueryBuilder.defaults('allow_empty'), + QueryBuilder.DEFAULTS.allow_empty, + 'Should return a specific default primitive' + ); + + assert.deepEqual( + QueryBuilder.defaults('lang'), + QueryBuilder.DEFAULTS.lang, + 'Should return a specific default object' + ); + + var orig_flags = $.extend({}, QueryBuilder.DEFAULTS.default_rule_flags); + var flags = { + filter_readonly: true, + operator_readonly: false, + value_readonly: true, + no_delete: false, + no_sortable: true, + no_drop: false + }; + + QueryBuilder.defaults({ default_rule_flags: flags }); + + assert.deepEqual( + QueryBuilder.DEFAULTS.default_rule_flags, + flags, + 'Should have modified the default config object' + ); + + QueryBuilder.defaults({ default_rule_flags: orig_flags }); + }); + + /** + * Test language load + */ + QUnit.test('Change language', function(assert) { + assert.expect(2); + var done = assert.async(); + + $.getScript('../dist/i18n/query-builder.fr.js', function() { + $b.queryBuilder({ + filters: basic_filters + }); + + assert.equal( + $b.find('[data-delete=rule]').text().trim(), + 'Supprimer', + 'Should be in french' + ); + + $b.queryBuilder('destroy'); + + $b.queryBuilder({ + filters: basic_filters, + lang_code: 'en' + }); + + assert.equal( + $b.find('[data-delete=rule]').text().trim(), + 'Delete', + 'Should be in english' + ); + + QueryBuilder.defaults({ lang_code: 'en' }); + + done(); + }); + }); +}); diff --git a/tests/core_tests.js b/tests/core_tests.js deleted file mode 100644 index 4cf9d27c..00000000 --- a/tests/core_tests.js +++ /dev/null @@ -1,208 +0,0 @@ -$(function(){ - - QUnit.test('Empty builder + destroy', function(assert) { - $('#container1').queryBuilder({ filters: basic_filters }); - assert.ok($.isEmptyObject($('#container1').queryBuilder('getRules')), 'Should return empty object'); - - $('#container1').queryBuilder('destroy'); - assert.ok(!$('#container1').hasClass('query-builder'), 'Should not have "query-builder" class'); - assert.ok($('#container1').data('queryBuilder')===undefined, 'Should not have "queryBuilder" data'); - }); - - QUnit.test('Set/get rules', function(assert) { - $('#container2').queryBuilder({ - filters: basic_filters, - rules: basic_rules - }); - - assert.ok(rulesMatch($('#container2').queryBuilder('getRules'), basic_rules), 'Should return object with rules'); - assert.deepEqual(getOptions($('#container2_rule_2 [name$=_operator] option')), basic_filters[1].operators, 'Should respect the order of operators'); - }); - - QUnit.test('Empty value check', function(assert) { - var error_str; - $('#container3').queryBuilder({ - filters: basic_filters, - rules: invalid_rules, - onValidationError: function($rule, error, value, filter, operator) { - error_str = error[0]; - } - }); - - assert.ok(rulesMatch($('#container3').queryBuilder('getRules'), {}), 'Should return empty object'); - assert.equal(error_str, 'string_empty', 'Should throw "string_empty" error'); - assert.equal($('#container3_rule_0 .error-container:visible').attr('title'), 'Empty value', 'Should display error icon with "Empty value" title'); - }); - - QUnit.test('Language change', function(assert) { - assert.expect(2); - - var done = assert.async(), - original = $.fn.queryBuilder.defaults.get('lang'); - - $.getScript('../dist/i18n/fr.js', function() { - assert.equal($.fn.queryBuilder.defaults.get('lang').delete_rule, 'Supprimer', 'Should be in french'); - $.fn.queryBuilder.defaults.set({ lang: original }); - assert.equal($.fn.queryBuilder.defaults.get('lang').delete_rule, 'Delete', 'Should be in english'); - done(); - }); - }); - - QUnit.test('Delete/add operators', function(assert) { - $('#container4').queryBuilder({ - filters: filters_for_custom_operators, - rules: rules_for_custom_operators, - operators: custom_operators - }); - - assert.deepEqual(getOptions($('#container4_rule_0 [name$=_operator] option')), ['equal', 'not_equal'], 'String type should have equal & not_equal operators'); - assert.deepEqual(getOptions($('#container4_rule_1 [name$=_operator] option')), ['less', 'greater'], 'Number type should have less & greater operators'); - assert.deepEqual(getOptions($('#container4_rule_2 [name$=_operator] option')), ['before', 'after'], 'Datetime type should have before & after operators'); - }); - - QUnit.test('Change conditions', function(assert) { - $('#container5').queryBuilder({ - filters: basic_filters, - rules: rules_for_custom_conditions, - conditions: ['NAND', 'XOR'], - default_condition: 'NAND' - }); - - assert.ok(rulesMatch($('#container5').queryBuilder('getRules'), rules_for_custom_conditions), 'Should return correct rules'); - assert.deepEqual(getOptions($('#container5_group_0>.rules-group-header [name$=_cond]')), ['NAND', 'XOR'], 'Conditions should be NAND & XOR'); - - $('#container5').queryBuilder('destroy'); - - $('#container5').queryBuilder({ - filters: basic_filters, - conditions: ['AND'] - }); - - assert.deepEqual(getOptions($('#container5_group_0>.rules-group-header [name$=_cond]')), ['AND'], 'Condition should be AND'); - }); - - QUnit.test('No groups', function(assert) { - $('#container6').queryBuilder({ - filters: basic_filters, - allow_groups: false - }); - - assert.equal($('#container6_group_0>.rules-group-header [data-add=group]').length, 0, 'Should not contain group add button'); - assert.throws(function(){ $('#container6').queryBuilder('setRules', basic_rules); }, /No more than 0 groups are allowed/, 'Should throw "Groups are disabled" error'); - }); - - QUnit.test('Readonly', function(assert) { - $('#container7').queryBuilder({ - filters: basic_filters, - rules: readonly_rules - }); - - assert.equal($('#container7_rule_2').find('[data-delete=rule]').length, 0, 'Should hide delete button of readonly rule'); - assert.equal($('#container7_rule_2').find('input:disabled, select:disabled').length, 3, 'Should disabled select and inputs'); - $('#container7_group_1>.rules-group-header [data-delete=group]').trigger('click'); - assert.ok(rulesMatch($('#container7').queryBuilder('getRules'), readonly_rules_after), 'Should not delete group with readonly rule'); - }); - - QUnit.test('Icons', function(assert) { - $('#container8').queryBuilder({ - filters: basic_filters, - rules: basic_rules, - icons: icons - }); - - assert.equal($('#container8_group_0.rules-group-container [data-add=rule] i').attr('class'), "fa fa-plus", 'Rule add icon should have been replaced'); - assert.equal($('#container8_group_1.rules-group-container [data-delete=group] i').attr('class'), "fa fa-times", 'Group delete icon should have been replaced'); - }); - - QUnit.test('SQL/MongoDB export', function(assert) { - $('#container9').queryBuilder({ - filters: basic_filters, - rules: basic_rules - }); - - assert.deepEqual($('#container9').queryBuilder('getSQL', false, false), basic_rules_sql_raw, 'Should create SQL query'); - assert.deepEqual($('#container9').queryBuilder('getSQL', true, false), basic_rules_sql_stmt, 'Should create SQL query with statements'); - assert.deepEqual($('#container9').queryBuilder('getMongo'), basic_rules_mongodb, 'Should create MongoDB query'); - }); - - QUnit.test('Validation callback', function(assert) { - $('#container10').queryBuilder({ - filters: filters_for_validation_callback, - lang: { - errors: { - translated_error: 'Translated error! {0}' - } - } - }); - - rules_for_validation_callback.rules[0].value = 1; - $('#container10').queryBuilder('setRules', rules_for_validation_callback); - $('#container10').queryBuilder('getRules'); - assert.deepEqual($('#container10_rule_0 .error-container').attr('title'), 'invalid_name', 'Should display "invalid_name" error'); - - rules_for_validation_callback.rules[0].value = 2; - $('#container10').queryBuilder('setRules', rules_for_validation_callback); - $('#container10').queryBuilder('getRules'); - assert.deepEqual($('#container10_rule_0 .error-container').attr('title'), 'Translated error! 2', 'Should display "Translated error! 2" error'); - - rules_for_validation_callback.rules[0].value = 3; - $('#container10').queryBuilder('setRules', rules_for_validation_callback); - $('#container10').queryBuilder('getRules'); - assert.ok(!$('#container10_rule_0 .error-container').is(':visible'), 'Should hide rule error'); - }); - - QUnit.test('valueSetter & valueParser', function(assert) { - $('#container11').queryBuilder({ - filters: filters_for_value_callback, - rules: rules_for_value_callback - }); - - assert.equal($('#container11_rule_0 .rule-value-container input').val(), 'abcdefghij', 'Displayed value should be "abcdefghij"'); - - $('#container11_rule_0 .rule-value-container input').val('iefdabchg'); - assert.equal($('#container11').queryBuilder('getRules').rules[0].value, '845301276', 'Final value should be "845301276"'); - }); -}); - -function getOptions($target) { - var options = []; - - $target.each(function(){ - options.push($(this).val()); - }); - - return options; -} - -function rulesMatch(a, b) { - if (a.hasOwnProperty('rules')) { - if (!b.rules) { - return false; - } - - for (var i=0, l=a.rules.length; i< ? AND name IS NULL AND ( category IN(?, ?) OR id != ? ) ', - params: [10.25, 'mo', 'mu', '1234-azer-5678'] -}; -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_mongodb = {'$and': [ - {'price': { '$lt': 10.25 }}, - {'name': null}, - {'$or': [ - {'category': {'$in': ['mo', 'mu']}}, - {'id': {'$ne': '1234-azer-5678'}} - ]} -]}; - -var invalid_rules = { - condition: 'AND', - rules: [{ - id: 'id', - operator: 'equal', - value: '' - }] -}; - -var filters_for_validation_callback = [{ - id: 'name', - type: 'integer', - validation: { - callback: function(value, filter, operator, $rule) { - switch (value[0]) { - case '1': - return 'invalid_name'; - - case '2': - return ['translated_error', value]; - - case '3': - return true; - } - } - } -}]; - -var rules_for_validation_callback = { - condition: 'AND', - rules: [{ - id: 'name', - operator: 'equal', - value: 1 - }] -}; - -var filters_for_value_callback = [{ - id: 'name', - type: 'string', - valueSetter: function($rule, value) { - value = (''+value).split('').map(function(c) { - return String.fromCharCode(97 + parseInt(c)); - }).join(''); - - $rule.find('.rule-value-container input').val(value); - }, - valueParser: function($rule, value) { - value = value.split('').map(function(c) { - return c.charCodeAt(0) - 97; - }).join(''); - - return value; - } -}]; - -var rules_for_value_callback = { - condition: 'AND', - rules: [{ - id: 'name', - operator: 'equal', - value: '0123456789' - }] -}; - -var filters_for_custom_operators = [{ - id: 'name', - label: 'Name', - type: 'string' -}, -{ - id: 'price', - label: 'Price', - type: 'double', - validation: { - min: 0, - step: 0.01 - } -}, -{ - id: 'release', - label: 'Release date', - type: 'date' -}]; - -var rules_for_custom_operators = { - condition: 'AND', - rules: [{ - id: 'name', - operator: 'equal', - value: 'foo' - }, { - id: 'price', - operator: 'less', - value: 10 - }, { - id: 'release', - operator: 'before', - value: '1995-5-1' - }] -}; - -var custom_operators = [ - {type: 'equal', accept_values: true, apply_to: ['string']}, - {type: 'not_equal', accept_values: true, apply_to: ['string']}, - {type: 'less', accept_values: true, apply_to: ['number']}, - {type: 'greater', accept_values: true, apply_to: ['number']}, - {type: 'before', accept_values: true, apply_to: ['datetime']}, - {type: 'after', accept_values: true, apply_to: ['datetime']} -]; - -var rules_for_custom_conditions = { - condition: 'NAND', - rules: [{ - id: 'name', - operator: 'equal', - value: 'foo' - }, { - condition: 'XOR', - rules: [{ - id: 'name', - operator: 'equal', - value: 'bar' - }] - }] -}; - -var readonly_rules = { - condition: 'AND', - rules: [{ - id: 'price', - operator: 'less', - value: 10.25 - }, { - condition: 'OR', - rules: [{ - id: 'category', - operator: 'equal', - value: 'mu' - }, { - id: 'id', - operator: 'not_equal', - value: '1234-azer-5678', - readonly: true, - flags: { - no_sortable: true - } - }, { - condition: 'AND', - rules: [{ - id: 'name', - operator: 'equal', - value: 'bar' - }] - }] - }] -}; - -var readonly_rules_after = { - condition: 'AND', - rules: [{ - id: 'price', - operator: 'less', - value: 10.25 - }, { - condition: 'OR', - rules: [{ - id: 'id', - operator: 'not_equal', - value: '1234-azer-5678', - readonly: true - }] - }] -}; - -var icons = { - add_group: 'fa fa-plus-circle', - add_rule: 'fa fa-plus', - remove_rule: 'fa fa-times', - remove_group: 'fa fa-times', - sort: 'fa fa-sort' -}; diff --git a/tests/data.module.js b/tests/data.module.js new file mode 100644 index 00000000..41e7b48d --- /dev/null +++ b/tests/data.module.js @@ -0,0 +1,662 @@ +$(function() { + var $b = $('#builder'); + + QUnit.module('data', { + afterEach: function() { + $b.queryBuilder('destroy'); + } + }); + + /** + * Test filters values + */ + QUnit.test('radio/checkbox/select values', function(assert) { + $b.queryBuilder({ + filters: [{ + id: '1', + type: 'string', + input: 'radio', + values: ['one', 'two', 'three'] + }, { + id: '2', + type: 'string', + input: 'checkbox', + values: { + one: 'One', + two: 'Two', + three: 'Three' + } + }, { + id: '3', + type: 'string', + input: 'select', + values: [ + { one: 'One' }, + { two: 'Two' }, + { three: 'Three' } + ] + }], + rules: { + rules: [{ + id: '1', + value: 'one' + }, { + id: '2', + value: 'two' + }, { + id: '3', + value: 'three' + }] + } + }); + + assert.optionsMatch( + $('#builder_rule_0 .rule-value-container input'), + ['one', 'two', 'three'], + 'Should take an array of values' + ); + + assert.optionsMatch( + $('#builder_rule_1 .rule-value-container input'), + ['one', 'two', 'three'], + 'Should take an object of values' + ); + + assert.optionsMatch( + $('#builder_rule_2 .rule-value-container option'), + ['one', 'two', 'three'], + 'Should take an array of objects of values' + ); + }); + + /** + * Test data validation + */ + QUnit.test('validation', function(assert) { + $b.queryBuilder({ + filters: validation_filters + }); + + assert.validationError($b, + null, + /no_filter/ + ); + + $b.queryBuilder('clear'); + $b.queryBuilder('setRoot', false); + + assert.validationError($b, + null, + /empty_group/ + ); + + assert.validationError($b, + { id: 'radio' }, + /radio_empty/ + ); + + assert.validationError($b, + { id: 'checkbox' }, + /checkbox_empty/ + ); + + assert.validationError($b, + { id: 'checkbox', value: ['one', 'two'] }, + /operator_not_multiple/ + ); + + assert.validationError($b, + { id: 'select' }, + /select_empty/ + ); + + assert.validationError($b, + { id: 'select', value: -1 }, + /select_empty/ + ); + + assert.validationError($b, + { id: 'select_mult' }, + /select_empty/ + ); + + assert.validationError($b, + { id: 'select_mult', value: ['one', 'two'] }, + /operator_not_multiple/ + ); + + assert.validationError($b, + { id: 'string' }, + /string_empty/ + ); + + assert.validationError($b, + { id: 'string_val', value: 'aa' }, + /string_exceed_min_length/ + ); + + assert.validationError($b, + { id: 'string_val', value: 'aaaaaa' }, + /string_exceed_max_length/ + ); + + assert.validationError($b, + { id: 'string_val', value: '12345' }, + /string_invalid_format/ + ); + + assert.validationError($b, + { id: 'textarea' }, + /string_empty/ + ); + + assert.validationError($b, + { id: 'integer', value: 5.2 }, + /number_not_integer/ + ); + + assert.validationError($b, + { id: 'integer', value: -15 }, + /number_exceed_min/ + ); + + assert.validationError($b, + { id: 'integer', value: 15 }, + /number_exceed_max/ + ); + + assert.validationError($b, + { id: 'double', value: 0.05 }, + /number_wrong_step/ + ); + + assert.validationError($b, + { id: 'integer', operator: 'between', value: [5, 1] }, + /number_between_invalid/ + ); + + assert.validationError($b, + { id: 'date' }, + /datetime_empty/ + ); + + assert.validationError($b, + { id: 'date', value: '2014/13/15' }, + /datetime_invalid/ + ); + + assert.validationError($b, + { id: 'time', value: '07:00' }, + /datetime_exceed_min/ + ); + + assert.validationError($b, + { id: 'time', value: '18:00' }, + /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/ + ); + + assert.validationError($b, + { id: 'custom', value: '' }, + /you_fool/ + ); + }); + + /** + * Test custom data + */ + QUnit.test('custom data', function(assert) { + var rules = { + condition: 'AND', + data: [1, 2, 3], + rules: [{ + id: 'name', + value: 'Mistic', + data: { + foo: 'bar' + } + }] + }; + + assert.expect(2); + + $b.queryBuilder({ + filters: basic_filters + }); + + $b.on('afterAddRule.queryBuilder', function(e, rule) { + assert.ok( + JSON.stringify(rule.data) === JSON.stringify(rules.rules[0].data), + 'Custom data should be accessible in "afterAddRule" event' + ); + }); + + $b.queryBuilder('setRules', rules); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + rules, + 'Should keep custom data in "getRules"' + ); + }); + + /** + * 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 + */ + QUnit.test('get flags', function(assert) { + var rules_readonly = { + condition: 'AND', + flags: { + condition_readonly: true + }, + rules: [{ + id: 'price', + operator: 'less', + value: 10.25, + flags: { + no_delete: true + } + }, { + condition: 'OR', + readonly: true, + rules: [{ + id: 'id', + operator: 'not_equal', + value: '1234-azer-5678', + readonly: true + }] + }] + }; + + $b.queryBuilder({ + filters: basic_filters, + rules: rules_readonly + }); + + var rules_changed_flags = $.extend(true, {}, rules_readonly); + rules_changed_flags.rules[1].flags = { + condition_readonly: true, + no_add_rule: true, + no_add_group: true, + no_delete: true + }; + rules_changed_flags.rules[1].rules[0].flags = { + filter_readonly: true, + operator_readonly: true, + value_readonly: true, + no_delete: true + }; + + var rules_all_flags = $.extend(true, {}, rules_changed_flags); + rules_all_flags.flags = { + condition_readonly: true, + no_add_rule: false, + no_add_group: 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_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 }), + rules_changed_flags, + 'Should export rules with changed flags' + ); + + assert.rulesMatch( + $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', + input: 'radio', + values: ['one', 'two'] + }, { + id: 'checkbox', + input: 'checkbox', + values: ['one', 'two'] + }, { + id: 'select', + input: 'select', + values: ['one', 'two'], + placeholder: '--', + placholder_value: -1 + }, { + id: 'select_mult', + input: 'select', + multiple: true, + values: ['one', 'two'] + }, { + id: 'string' + }, { + id: 'string_val', + validation: { + min: '4', max: '5', + format: '^[a-z]?$' + } + }, { + id: 'textarea', + input: 'textarea' + }, { + id: 'integer', + type: 'integer', + validation: { + min: -10, max: 10 + } + }, { + id: 'double', + type: 'double', + validation: { + step: 0.1 + } + }, { + id: 'date', + type: 'date', + validation: { + format: 'YYYY/MM/DD' + } + }, { + id: 'time', + type: 'time', + validation: { + format: 'HH:ss', + min: '08:00', + max: '17:00' + } + }, { + id: 'boolean', + type: 'boolean' + }, { + id: 'custom', + type: 'string', + validation: { + callback: function(value, rule) { + if (value == null || !value.length) { + return 'you_fool'; + } + } + } + }]; + + 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 738a036f..e322dcfd 100644 --- a/tests/index.html +++ b/tests/index.html @@ -3,38 +3,76 @@ jQuery-QueryBuilder - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          - -
          -
          -
          -
          -
          -
          -
          -
          -
          -
          -
          + +
          diff --git a/tests/plugins-gui.module.js b/tests/plugins-gui.module.js new file mode 100644 index 00000000..a6ac0d22 --- /dev/null +++ b/tests/plugins-gui.module.js @@ -0,0 +1,238 @@ +$(function(){ + var $b = $('#builder'); + + QUnit.module('plugins-gui', { + afterEach: function() { + $b.queryBuilder('destroy'); + } + }); + + /** + * Test bt-checkbox + */ + QUnit.test('bt-checkbox', function(assert) { + $b.queryBuilder({ + plugins: ['bt-checkbox'], + filters: [{ + id: 'no-color', + type: 'integer', + input: 'checkbox', + values: { + 10: 'foo', + 20: 'bar' + } + }, { + id: 'one-color', + type: 'integer', + input: 'checkbox', + values: { + 1: 'one', + 2: 'two', + 3: 'three' + }, + color: 'primary' + }, { + id: 'multi-color', + type: 'integer', + input: 'radio', + values: { + 0: 'no', + 1: 'yes', + 2: 'perhaps' + }, + colors: { + 0: 'danger', + 1: 'success' + } + }], + rules: { + condition: 'AND', + rules: [{ + id: 'no-color', + value: 10 + }, { + id: 'one-color', + value: [1,2,3] + }, { + id: 'multi-color', + value: 2 + }] + } + }); + + assert.ok( + $('#builder_rule_0 .checkbox.checkbox-default').length == 2, + 'Should have 2 checkboxes with default color' + ); + + assert.ok( + $('#builder_rule_1 .checkbox.checkbox-primary').length == 3, + 'Should have 3 checkboxes with primary color' + ); + + assert.ok( + $('#builder_rule_2 .radio.radio-danger').length == 1 && + $('#builder_rule_2 .radio.radio-success').length == 1 && + $('#builder_rule_2 .radio.radio-default').length == 1, + 'Should have 3 radios with danger, success and default colors' + ); + }); + + /** + * Test chosen-selectpicker + */ + QUnit.test('chosen-selectpicker', function(assert) { + $b.queryBuilder({ + plugins: ['chosen-selectpicker'], + filters: basic_filters, + rules: basic_rules + }); + + assert.ok( + $b.find('.chosen-single').length == 8, + 'Should have initialized chosen on all filters and operators selectors' + ); + }); + + + /** + * Test bt-tooltip-errors + */ + QUnit.test('bt-tooltip-errors', function(assert) { + $b.queryBuilder({ + plugins: ['bt-tooltip-errors'], + filters: basic_filters, + rules: { + condition: 'AND', + rules: [{ + id: 'id', + operator: 'equal', + value: '' + }] + } + }); + + $b.queryBuilder('validate'); + + assert.equal( + $('#builder_group_0 .error-container').eq(0).data('bs-toggle'), + 'tooltip', + 'Should have added data-bs-toggle="tooltip" in the template' + ); + + assert.equal( + $('#builder_rule_0 .error-container').data('originalTitle'), + 'Empty value', + 'Error title should be "Empty value"' + ); + }); + + /** + * Test filter-description + */ + QUnit.test('filter-description', function(assert) { + var filters = [{ + 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 = { + condition: 'AND', + rules: [{ + id: 'name', + value: 'Mistic' + }, { + id: 'age', + value: 25 + }] + }; + + $b.queryBuilder({ + plugins: { + 'filter-description': { mode: 'inline' } + }, + filters: filters, + rules: rules + }); + + assert.match( + $('#builder_rule_0 p.filter-description').html(), + 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({ + plugins: { + 'filter-description': { mode: 'popover' } + }, + filters: filters, + rules: rules + }); + + assert.ok( + $('#builder_rule_0 button.filter-description').data('bs-toggle') == 'popover', + 'Rule should contain a new button enabled with Popover' + ); + + $b.queryBuilder('destroy'); + + $b.queryBuilder({ + plugins: { + 'filter-description': { mode: 'bootbox' } + }, + filters: filters, + rules: rules + }); + + assert.ok( + $('#builder_rule_0 button.filter-description').data('bs-toggle') == 'bootbox', + 'Rule should contain a new button enabled with Bootbox' + ); + }); + + /** + * Test sortable + */ + QUnit.test('sortable', function(assert) { + $b.queryBuilder({ + filters: basic_filters, + rules: basic_rules, + plugins: ['sortable'] + }); + + assert.ok( + $b.find('.drag-handle').length > 0, + 'Should add the drag handles' + ); + + $b.queryBuilder('destroy'); + + $b.queryBuilder({ + plugins: { + 'sortable': {disable_template: true} + }, + filters: basic_filters, + rules: basic_rules + }); + + assert.ok( + $b.find('.drag-handle').length === 0, + 'Should not have added the handles with disable_template=true' + ); + }); +}); diff --git a/tests/plugins.module.js b/tests/plugins.module.js new file mode 100644 index 00000000..c0b4fbf6 --- /dev/null +++ b/tests/plugins.module.js @@ -0,0 +1,228 @@ +$(function(){ + var $b = $('#builder'); + + QUnit.module('plugins', { + afterEach: function() { + $b.queryBuilder('destroy'); + } + }); + + /** + * Test plugins loading + */ + QUnit.test('Plugins loading', function(assert) { + assert.ok(QueryBuilder.prototype.getSQL !== undefined, 'Should load SQL plugin automatically'); + + $b.queryBuilder({ + filters: basic_filters, + plugins: ['bt-tooltip-errors', 'filter-description'] + }); + + assert.deepEqual( + $b[0].queryBuilder.plugins['bt-tooltip-errors'], + QueryBuilder.plugins['bt-tooltip-errors'].def, + 'Should load "bt-tooltip-errors" with default config' + ); + + assert.deepEqual( + $b[0].queryBuilder.plugins['filter-description'], + QueryBuilder.plugins['filter-description'].def, + 'Should load "filter-description" with default config' + ); + + $b.queryBuilder('destroy'); + + $b.queryBuilder({ + filters: basic_filters, + plugins: { + 'bt-tooltip-errors': null, + 'filter-description': { icon: 'fa fa-info' } + } + }); + + assert.deepEqual( + $b[0].queryBuilder.plugins['bt-tooltip-errors'], + QueryBuilder.plugins['bt-tooltip-errors'].def, + 'Should load "bt-tooltip-errors" with default config' + ); + + assert.deepEqual( + $b[0].queryBuilder.plugins['filter-description'], + { icon: 'fa fa-info', mode: 'popover' }, + 'Should load "filter-description" with custom config' + ); + + $b.queryBuilder('destroy'); + + assert.throws( + function(){ + $b.queryBuilder({ + filters: basic_filters, + plugins: ['__unknown__'] + }); + }, + /Unable to find plugin "__unknown__"/, + 'Should throw error on unknown plugin' + ); + }); + + /** + * Test unique-filter + */ + QUnit.test('unique-filter', function(assert) { + var unique_filters = $.extend(true, [], basic_filters); + unique_filters[3].unique = 'group'; + unique_filters[4].unique = true; + + $b.queryBuilder({ + plugins: ['unique-filter'], + filters: unique_filters, + rules: basic_rules + }); + + assert.ok( + $('select[name=builder_rule_0_filter] option[value=id]').is(':disabled') && + $('select[name=builder_rule_1_filter] option[value=id]').is(':disabled') && + $('select[name=builder_rule_2_filter] option[value=id]').is(':disabled'), + '"Identifier" filter should be disabled everywhere' + ); + + 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' + ); + }); + + /** + * Test inversion + */ + QUnit.test('invert', function(assert) { + $b.queryBuilder({ + plugins: ['invert'], + filters: basic_filters, + rules: basic_rules + }); + + $b.queryBuilder('invert'); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + { + condition: 'OR', + rules: [{ + id: 'price', + field: 'price', + operator: 'greater_or_equal', + value: 10.25 + }, { + id: 'name', + field: 'name', + operator: 'is_not_null', + value: null + }, { + condition: 'AND', + rules: [{ + id: 'category', + field: 'category', + operator: 'not_in', + value: ['mo', 'mu'] + }, { + id: 'id', + field: 'id', + operator: 'equal', + value: '1234-azer-5678' + }] + }] + }, + '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' + ); + }); + + /** + * Test change filters + */ + QUnit.test('change-filters', function(assert) { + var filter_a = { + id: 'a', + type: 'string' + }; + + var filter_b = { + id: 'b', + type: 'string' + }; + + var filter_c = { + id: 'c', + type: 'string' + }; + + var rule_a = { + id: 'a', + field: 'a', + operator: 'equal', + value: 'foo' + }; + + var rule_b = { + id: 'b', + field: 'b', + operator: 'equal', + value: 'bar' + }; + + $b.queryBuilder({ + filters: [filter_a, filter_b], + rules: [rule_a, rule_b] + }); + + assert.throws( + function(){ + $b.queryBuilder('removeFilter', 'a'); + }, + /A rule is using filter "a"/, + 'Should throw error when deleting filter "a" w/o force' + ); + + $b.queryBuilder('removeFilter', 'a', true); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + {condition:'AND', rules: [rule_b]}, + 'Should have deleted rule using filter "a"' + ); + + $b.queryBuilder('addFilter', filter_c, 0); + + assert.optionsMatch( + $('#builder_rule_1 [name$=_filter] option'), + ['-1', filter_c.id, filter_b.id], + 'Should have added filter "c" at begining' + ); + + $b.queryBuilder('addFilter', filter_a, 'c'); + + assert.optionsMatch( + $('#builder_rule_1 [name$=_filter] option'), + ['-1', filter_c.id, filter_a.id, filter_b.id], + 'Should have added filter "a" after "c"' + ); + }); +}); 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.sql-support.module.js b/tests/plugins.sql-support.module.js new file mode 100644 index 00000000..ee347e57 --- /dev/null +++ b/tests/plugins.sql-support.module.js @@ -0,0 +1,665 @@ +$(function() { + var $b = $('#builder'); + + QUnit.module('plugins.sql-support', { + afterEach: function() { + $b.queryBuilder('destroy'); + } + }); + + QUnit.test('Raw SQL', function(assert) { + $b.queryBuilder({ + filters: basic_filters, + rules: basic_rules + }); + + assert.deepEqual( + $b.queryBuilder('getSQL', false), + basic_rules_sql_raw, + '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, + 'Should create SQL query with statements ($ numbered)' + ); + + assert.deepEqual( + $b.queryBuilder('getSQL', 'numbered(@)'), + basic_rules_sql_stmt_num_at, + 'Should create SQL query with statements (@ numbered)' + ); + + $b.queryBuilder('reset'); + + $b.queryBuilder('setRulesFromSQL', basic_rules_sql_stmt_num, 'numbered'); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + basic_rules, + 'Should parse SQL query with statements ($ numbered)' + ); + + $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 (@ 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)' + ); + + 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({ + 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 + }); + + assert.deepEqual( + $b.queryBuilder('getSQL', 'question_mark'), + all_operators_rules_sql, + '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({ + filters: [ + { 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'), + { + condition: 'AND', + rules: [{ + id: 'a', + operator: 'equal', + value: 5 + }] + }, + 'Should parse SQL with one rule' + ); + }); + + 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: [ + { + id: 'name', + type: 'string' + }, + { + id: 'last_days', + field: 'display_date', + type: 'integer', + operators: ['greater'] + } + ] + }); + + $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' + ); + + $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({ + filters: [ + { + id: 'name', + field: 'username', + type: 'string' + }, + { + id: 'last_days', + field: 'display_date', + type: 'integer' + } + ] + }); + + $b.queryBuilder('setRulesFromSQL', sql); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + 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: [{ + 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_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', + params: [ + 'foo', + 'foo', + 'bk', 'mo', + 'bk', 'mo', + 5, + 5, + 4, + 4, + 4, 5, + 4, 5, + 'foo%', + 'foo%', + '%foo%', + '%foo%', + '%foo', + '%foo' + ] + }; + + var nested_rules = { + condition: 'OR', + rules: [ + { + id: 'a', + operator: 'equal', + value: 5 + }, + { + condition: 'AND', + rules: [ + { + id: 'b', + operator: 'equal', + value: 4 + }, + { + id: 'c', + operator: 'equal', + value: 7 + }, + { + condition: 'OR', + rules: [ + { + id: 'd', + operator: 'equal', + value: 1 + }, + { + condition: 'AND', + rules: [ + { + id: 'a', + operator: 'equal', + value: 7 + }, + { + id: 'a', + operator: 'equal', + value: 1 + } + ] + } + ] + }, + { + id: 'c', + operator: 'equal', + value: 3 + }, + { + condition: 'OR', + rules: [ + { + condition: 'AND', + rules: [ + { + id: 'b', + operator: 'equal', + value: 4 + }, + { + id: 'c', + operator: 'equal', + value: 9 + } + ] + }, + { + id: 'a', + operator: 'equal', + value: 8 + }, + { + id: 'a', + operator: 'equal', + value: 10 + } + ] + } + ] + }, + { + id: 'a', + operator: 'equal', + value: 0 + }, + { + condition: 'AND', + rules: [ + { + id: 'b', + operator: 'equal', + value: 4 + }, + { + id: 'a', + operator: 'equal', + value: 4 + }, + { + condition: 'OR', + rules: [ + { + id: 'a', + operator: 'equal', + value: 4 + }, + { + id: 'c', + operator: 'equal', + value: 8 + } + ] + } + ] + } + ] + }; + + var nested_rules_sql = 'a=5 or (b=4 and c=7 and (d=1 or (a=7 and a=1)) and c=3 and ((b=4 and c=9) or a=8 or a=10)) or a=0 or (b=4 and a=4 and (a=4 or c=8))'; +}); diff --git a/tests/utils.module.js b/tests/utils.module.js new file mode 100644 index 00000000..f15f1d58 --- /dev/null +++ b/tests/utils.module.js @@ -0,0 +1,166 @@ +$(function () { + + QUnit.module('utils'); + + /** + * Test iterateOptions + */ + QUnit.test('iterateOptions', function (assert) { + var output; + function callback(a, b, c) { + output.push(a, b, c); + } + + output = []; + Utils.iterateOptions(['one', 'foo', 'bar'], callback); + assert.deepEqual( + output, + ['one', 'one', undefined, 'foo', 'foo', undefined, 'bar', 'bar', undefined], + 'Should iterate simple array' + ); + + output = []; + Utils.iterateOptions({1: 'one', 2: 'foo', 3: 'bar'}, callback); + assert.deepEqual( + output, + ['1', 'one', undefined, '2', 'foo', undefined, '3', 'bar', undefined], + 'Should iterate simple hash-map' + ); + + output = []; + Utils.iterateOptions([{1: 'one'}, {2: 'foo'}, {3: 'bar'}], callback); + assert.deepEqual( + output, + ['1', 'one', undefined, '2', 'foo', undefined, '3', 'bar', undefined], + 'Should iterate array of one element hash-maps' + ); + + output = []; + Utils.iterateOptions([{value: 1, label: 'one', optgroup: 'group'}, {value: 2, label: 'foo'}, {value: 3, label: 'bar', optgroup: 'group'}], callback); + assert.deepEqual( + output, + [1, 'one', 'group', 2, 'foo', undefined, 3, 'bar', 'group'], + 'Should iterate array of hash-maps' + ); + }); + + /** + * Test groupSort + */ + QUnit.test('groupSort', function (assert) { + var input = [ + {id: '1'}, + {id: '1.1', group: '1'}, + {id: '2'}, + {id: '2.1', group: '2'}, + {id: '1.2', group: '1'}, + {id: '2.2', group: '2'}, + {id: '3'}, + {id: '1.3', group: '1'} + ]; + + var output = [ + {id: '1'}, + {id: '1.1', group: '1'}, + {id: '1.2', group: '1'}, + {id: '1.3', group: '1'}, + {id: '2'}, + {id: '2.1', group: '2'}, + {id: '2.2', group: '2'}, + {id: '3'} + ]; + + assert.deepEqual( + Utils.groupSort(input, 'group'), + output, + 'Should sort items by group' + ); + }); + + /** + * Test fmt + */ + QUnit.test('fmt', function (assert) { + assert.equal( + Utils.fmt('{0} is equal to {1}', 1, 'one'), + '1 is equal to one', + 'Should replace arguments' + ); + + assert.equal( + Utils.fmt('{0} is equal to {0}', 1), + '1 is equal to 1', + 'Should replace one argument multiple times' + ); + }); + + /** + * Test changeType + */ + QUnit.test('changeType', function (assert) { + assert.ok( + Utils.changeType('10', 'integer') === 10, + '"10" should be parsed as integer' + ); + + assert.ok( + Utils.changeType('10.5', 'double') === 10.5, + '"10.5" should be parsed as double' + ); + + assert.ok( + Utils.changeType('true', 'boolean') === true, + '"true" should be parsed as boolean' + ); + }); + + /** + * Test escapeElementId + */ + QUnit.test('escapeElementId', function (assert) { + assert.equal( + Utils.escapeElementId('abc123'), + 'abc123', + 'Should not alter id' + ); + + var chars = ':.[],'; + for (var i = 0; i < chars.length; ++i) { + assert.equal( + Utils.escapeElementId('abc' + chars[i] + '123'), + 'abc\\' + chars[i] + '123', + 'Should escape \'' + chars[i] + '\' in id' + ); + + assert.equal( + Utils.escapeElementId('abc\\' + chars[i] + '123'), + 'abc\\' + chars[i] + '123', + 'Should not escape \'\\' + chars[i] + '\' in id' + ); + + assert.equal( + Utils.escapeElementId(chars[i] + 'abc123'), + '\\' + chars[i] + 'abc123', + 'Should escape \'' + chars[i] + '\' prefixing id' + ); + + assert.equal( + Utils.escapeElementId('\\' + chars[i] + 'abc123'), + '\\' + chars[i] + 'abc123', + 'Should not escape \'\\' + chars[i] + '\' prefixing id' + ); + + assert.equal( + Utils.escapeElementId('abc123' + chars[i]), + 'abc123\\' + chars[i], + 'Should escape \'' + chars[i] + '\' trailing in id' + ); + + assert.equal( + Utils.escapeElementId('abc123\\' + chars[i]), + 'abc123\\' + chars[i], + 'Should not escape \'\\' + chars[i] + '\' trailing in id' + ); + } + }); +}); diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000..c6f5594d --- /dev/null +++ b/yarn.lock @@ -0,0 +1,2434 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/parser@^7.9.4": + version "7.23.6" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz" + integrity sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ== + +"@babel/runtime@^7.21.0": + version "7.23.8" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz" + integrity sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw== + dependencies: + regenerator-runtime "^0.14.0" + +"@interactjs/types@1.10.26": + version "1.10.26" + resolved "https://registry.npmjs.org/@interactjs/types/-/types-1.10.26.tgz" + integrity sha512-DekYpdkMV3XJVd/0k3f4pJluZAsCiG86yEtVXvGLK0lS/Fj0+OzYEv7HoMpcBZSkQ8s7//yaeEBgnxy2tV81lA== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@popperjs/core@^2.0.0", "@popperjs/core@^2.11.8": + version "2.11.8" + resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + +"@selectize/selectize@^0.15.2": + version "0.15.2" + resolved "https://registry.npmjs.org/@selectize/selectize/-/selectize-0.15.2.tgz" + integrity sha512-gY+yzYfrVTc+1ekCAaEtDvN59+upbibFzhkePyyk6PwOXT6kEb05azGA91/w3C/71lUOHPyd3nzLnfyfuRi+pA== + optionalDependencies: + jquery-ui "^1.13.2" + +"@types/linkify-it@*": + version "3.0.5" + resolved "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz" + integrity sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw== + +"@types/markdown-it@*", "@types/markdown-it@^12.2.3": + version "12.2.3" + resolved "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz" + integrity sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ== + dependencies: + "@types/linkify-it" "*" + "@types/mdurl" "*" + +"@types/mdurl@*": + version "1.0.5" + resolved "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz" + integrity sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA== + +abbrev@1: + version "1.1.1" + resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +accepts@~1.3.4: + version "1.3.8" + resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +alive-server@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/alive-server/-/alive-server-1.3.0.tgz" + integrity sha512-zvE9vFYA6wfDTQpLw7W90s3Mj0a7+HxY7VkTAiOWvYgZZbL9qsSWgBmdL8ndWUnSYdNqaicyPj62HRsGNrC5RQ== + dependencies: + chokidar "^3.5.3" + colors "1.4.0" + connect "^3.7.0" + cors "^2.8.5" + event-stream "4.0.1" + faye-websocket "0.11.4" + http-auth "4.2.0" + http-auth-connect "^1.0.6" + morgan "^1.10.0" + object-assign "^4.1.1" + open "^8.4.2" + proxy-middleware "^0.15.0" + send "^0.18.0" + serve-index "^1.9.1" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" + integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz" + integrity sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +apache-crypt@^1.1.2: + version "1.2.6" + resolved "https://registry.npmjs.org/apache-crypt/-/apache-crypt-1.2.6.tgz" + integrity sha512-072WetlM4blL8PREJVeY+WHiUh1R5VNt2HfceGS8aKqttPHcmqE5pkKuXPz/ULmJOFkc8Hw3kfKl6vy7Qka6DA== + dependencies: + unix-crypt-td-js "^1.1.4" + +apache-md5@^1.0.6: + version "1.1.8" + resolved "https://registry.npmjs.org/apache-md5/-/apache-md5-1.1.8.tgz" + integrity sha512-FCAJojipPn0bXjuEpjOOOMN8FZDkxfWWp4JGN9mifU2IhxvKyXZYqpzPHdnTSUpmPDy+tsslB6Z1g+Vg6nVbYA== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-each@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz" + integrity sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA== + +array-slice@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz" + integrity sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w== + +async@^1.5.2: + version "1.5.2" + resolved "https://registry.npmjs.org/async/-/async-1.5.2.tgz" + integrity sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w== + +async@~3.2.0: + version "3.2.5" + resolved "https://registry.npmjs.org/async/-/async-3.2.5.tgz" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== + +awesome-bootstrap-checkbox@^0.3.7: + version "0.3.7" + resolved "https://registry.npmjs.org/awesome-bootstrap-checkbox/-/awesome-bootstrap-checkbox-0.3.7.tgz" + integrity sha512-W67P0YIPPxN5vg6uuGRrz/rVGqaATFiE3O8/YepssFBXVyMoO8AkfhNMOoHEVJk7m/O5OQ/9n64uXlTZjXiiFw== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +basic-auth@~2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + +bcryptjs@^2.4.3: + version "2.4.3" + resolved "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz" + integrity sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + +bootbox@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/bootbox/-/bootbox-6.0.0.tgz" + integrity sha512-+Calbj1v5UvxAXXDAHfoBlsx63Hcz1JqHaZdJ5EjIcOlkyAbZLCreVScx0Em6ZUvsMCqynuz/3nGDyd9FtFrNQ== + +bootstrap-icons@^1.11.3: + version "1.11.3" + resolved "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz" + integrity sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww== + +bootstrap-slider@^10.0.0: + version "10.6.2" + resolved "https://registry.npmjs.org/bootstrap-slider/-/bootstrap-slider-10.6.2.tgz" + integrity sha512-8JTPZB9QVOdrGzYF3YgC3YW6ssfPeBvBwZnXffiZ7YH/zz1D0EKlZvmQsm/w3N0XjVNYQEoQ0ax+jHrErV4K1Q== + +"bootstrap@^4.4.0 || ^5.0.0", bootstrap@^5.3.0: + version "5.3.2" + resolved "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.2.tgz" + integrity sha512-D32nmNWiQHo94BKHLmOrdjlL05q1c8oxbtBphQFb9Z5to6eGRDCm0QgeaZ4zFBHzfg2++rqa2JkqCcxDy0sH0g== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +catharsis@^0.9.0: + version "0.9.0" + resolved "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz" + integrity sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A== + dependencies: + lodash "^4.17.15" + +chalk@^1.0.0: + version "1.1.3" + resolved "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" + integrity sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A== + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^1.1.1: + version "1.1.3" + resolved "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" + integrity sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A== + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^4.1.2, chalk@~4.1.0: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +cheerio@^0.22.0: + version "0.22.0" + resolved "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz" + integrity sha512-8/MzidM6G/TgRelkzDG13y3Y9LxBjCb+8yOEZ9+wwq5gVF2w2pV0wmHvjfT0RvuxGyR7UEuK36r+yYMbT4uKgA== + dependencies: + css-select "~1.2.0" + dom-serializer "~0.1.0" + entities "~1.1.1" + htmlparser2 "^3.9.1" + lodash.assignin "^4.0.9" + lodash.bind "^4.1.4" + lodash.defaults "^4.0.1" + lodash.filter "^4.4.0" + lodash.flatten "^4.2.0" + lodash.foreach "^4.3.0" + lodash.map "^4.4.0" + lodash.merge "^4.4.0" + lodash.pick "^4.2.1" + lodash.reduce "^4.4.0" + lodash.reject "^4.4.0" + lodash.some "^4.4.0" + +chokidar@^3.5.2, chokidar@^3.5.3, "chokidar@>=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"