diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index fe882f7e..be3320fd 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -3,6 +3,8 @@ ## Work on `dev` Any merge request should be created from and issued to the `dev` branch. +Do not add the `dist` files to your pull request. The directory is ignored for a reason: it is generated and pushed only when doing a release on `master`. + ## Core vs Plugins I want to keep the core clean of extra (and certainly awesome) functionalities. That includes, but is not limited to, export/import plugins, visual aids, etc. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 25399e11..1131297f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,6 +2,6 @@ - [ ] I read the [guidelines for contributing](https://github.com/mistic100/jQuery-QueryBuilder/blob/master/.github/CONTRIBUTING.md) - [ ] I created my branch from `dev` and I am issuing the PR to `dev` -- [ ] Unit tests are OK +- [ ] I didn't pushed the `dist` directory - [ ] If it's a new feature, I added the necessary unit tests - [ ] If it's a new language, I filled the `__locale` and `__author` fields diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..0648b398 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,21 @@ +name: CI + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: '16' + cache: 'yarn' + + - name: build + run: | + yarn install + yarn build diff --git a/.gitignore b/.gitignore index 2b7141a4..a50c5eea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,7 @@ -bower_components node_modules dist doc .sass-cache -.coverage-results .idea *.iml +package-lock.json diff --git a/.jscsrc b/.jscsrc deleted file mode 100644 index ce1e5220..00000000 --- a/.jscsrc +++ /dev/null @@ -1,37 +0,0 @@ -{ - "preset": "idiomatic", - "validateIndentation": 4, - "requireCamelCaseOrUpperCaseIdentifiers": false, - "disallowKeywordsOnNewLine": [], - "requireKeywordsOnNewLine": [ - "else" - ], - "requirePaddingNewLinesAfterBlocks": false, - "safeContextKeyword": "self", - "disallowMultipleLineStrings": false, - "requirePaddingNewLinesBeforeLineComments": false, - "requireSpaceBeforeBinaryOperators": [ - "=", "+", "-", "*", "/", "%", - "<<", ">>", ">>>", "&", "|", "^", - "&&", "||", - "===", "==", ">=", "<=", "<", ">", "!=", "!==" - ], - "requireDotNotation": false, - "requireSpacesInsideBrackets": false, - "requireSpacesInsideParentheses": false, - "maximumLineLength": null, - "maximumNumberOfLines": null, - "validateQuoteMarks": { - "mark": "'", - "escape": true - }, - "requireCurlyBraces": [ - "for", - "while", - "do", - "try", - "catch" - ], - "requireEarlyReturn": false, - "validateCommentPosition": false -} \ No newline at end of file diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 518111a8..00000000 --- a/.jshintrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "-W069": true, // accesses to "regional" in language files - "multistr": true -} \ No newline at end of file diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 4dec3eec..00000000 --- a/.npmignore +++ /dev/null @@ -1,5 +0,0 @@ -.* -build -composer.json -Gruntfile.js -bower_components diff --git a/.sass-lint.yml b/.sass-lint.yml deleted file mode 100644 index 2d387551..00000000 --- a/.sass-lint.yml +++ /dev/null @@ -1,9 +0,0 @@ -rules: - property-sort-order: [0] - force-pseudo-nesting: [0] - force-element-nesting: [0] - force-attribute-nesting: [0] - no-important: [0] - no-qualifying-elements: [0] - shorthand-values: [1, {allowed-shorthands: [1, 2, 4]}] - hex-notation: [1, {style: uppercase}] diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 689c955f..00000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: node_js -node_js: - - "5" -before_install: - - npm install -g grunt-cli - - npm install -g bower -before_script: - - bower install -after_success: grunt coveralls diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index 218413d0..00000000 --- a/Gruntfile.js +++ /dev/null @@ -1,435 +0,0 @@ -var initConfig = require('./build/initConfig'); -var processLang = require('./build/processLang'); -var removeJshint = require('./build/removeJshint'); -var cleanLn = require('./build/cleanLn'); - -module.exports = function(grunt) { - require('time-grunt')(grunt); - require('jit-grunt')(grunt, { - sasslint: 'grunt-sass-lint', - sass_injection: 'grunt-sass-injection', - usebanner: 'grunt-banner' - }); - - grunt.util.linefeed = '\n'; - - var config = initConfig(grunt, { - js_core_files: [ - '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' - ], - js_files_for_standalone: [ - 'bower_components/jquery-extendext/jQuery.extendext.js', - 'bower_components/doT/doT.js', - 'dist/js/query-builder.js' - ] - }); - - grunt.initConfig({ - pkg: grunt.file.readJSON('package.json'), - - banner: '/*!\n' + - ' * jQuery QueryBuilder <%= pkg.version %>\n' + - ' * Copyright 2014-<%= grunt.template.today("yyyy") %> Damien "Mistic" Sorel (http://www.strangeplanet.fr)\n' + - ' * Licensed under MIT (http://opensource.org/licenses/MIT)\n' + - ' */', - - langBanner: '/*!\n' + - ' * jQuery QueryBuilder <%= pkg.version %>\n' + - ' * Locale: <%= lang_locale %>\n' + - '<% if (lang_author) { %> * Author: <%= lang_author %>\n<% } %>' + - ' * Licensed under MIT (http://opensource.org/licenses/MIT)\n' + - ' */', - - // serve folder content - connect: { - dev: { - options: { - host: '0.0.0.0', - port: 9000, - livereload: true - } - } - }, - - // watchers - watch: { - options: { - livereload: true - }, - js: { - files: ['src/*.js', 'src/plugins/**/plugin.js'], - tasks: ['injector:example'] - }, - css: { - files: ['src/scss/*.scss', 'src/plugins/**/plugin.scss'], - tasks: ['build_css'] - }, - lang: { - files: ['src/i18n/*.json', 'src/plugins/**/i18n/*.json'], - tasks: ['build_lang'] - }, - example: { - files: ['examples/**'], - tasks: [] - } - }, - - // open example - open: { - dev: { - path: 'http://localhost:<%= connect.dev.options.port%>/examples/index.html' - } - }, - - // copy SASS files - copy: { - sass_core: { - files: [{ - expand: true, - flatten: true, - src: ['src/scss/*.scss'], - dest: 'dist/scss' - }] - }, - sass_plugins: { - files: config.loaded_plugins.map(function(name) { - return { - src: 'src/plugins/' + name + '/plugin.scss', - dest: 'dist/scss/plugins/_' + name + '.scss' - }; - }) - }, - doc_script: { - src: 'build/jsdoc.js', - dest: 'doc/js/custom.js' - } - }, - - concat: { - // concat all JS - js: { - src: config.js_files_to_load, - dest: 'dist/js/query-builder.js', - options: { - stripBanners: false, - separator: '\n\n', - process: function(src) { - return cleanLn(removeJshint(src)); - } - } - }, - // create standalone version - js_standalone: { - src: config.js_files_for_standalone, - dest: 'dist/js/query-builder.standalone.js', - options: { - stripBanners: false, - separator: '\n\n', - process: function(src, file) { - var name = file.match(/([^\/]+?).js$/)[1]; - - return cleanLn(removeJshint(src)) - .replace(/define\((.*?)\);/, 'define(\'' + name + '\', $1);'); - } - } - }, - // compile language files with AMD wrapper - lang: { - files: Object.keys(config.all_langs).map(function(name) { - return { - src: 'src/i18n/' + name + '.json', - dest: 'dist/i18n/query-builder.' + name + '.js' - }; - }), - options: { - process: function(src, file) { - var wrapper = cleanLn(grunt.file.read('src/i18n/.wrapper.js')).split(/@@js\n/); - return processLang(grunt, config.loaded_plugins)(file, src, wrapper); - } - } - }, - // compile language files without AMD wrapper - lang_temp: { - files: Object.keys(config.all_langs).map(function(name) { - return { - src: 'src/i18n/' + name + '.json', - dest: '.temp/i18n/' + name + '.js' - }; - }), - options: { - process: function(src, file) { - return processLang(grunt, config.loaded_plugins)(file, src); - } - } - } - }, - - // add AMD wrapper - wrap: { - js: { - src: ['dist/js/query-builder.js'], - dest: '', - options: { - separator: '', - wrapper: function() { - return cleanLn(grunt.file.read('src/.wrapper.js')).split(/@@js\n/); - } - } - } - }, - - // add banners - usebanner: { - options: { - banner: '<%= banner %>' - }, - js: { - src: ['dist/js/*.js'] - }, - css: { - src: ['dist/css/*.css', 'dist/scss/*.scss'] - } - }, - - // add plugins SASS imports - sass_injection: { - dist: { - options: { - replacePath: { - pattern: 'dist/scss/', - replace: '' - } - }, - src: ['dist/scss/plugins/*.scss'], - target: 'dist/scss/default.scss' - } - }, - - // parse scss - sass: { - options: { - sourceMap: false, - outputStyle: 'expanded' - }, - dist: { - files: [{ - expand: true, - flatten: true, - src: ['dist/scss/*.scss'], - dest: 'dist/css', - ext: '.css', - rename: function(dest, src) { - return dest + '/query-builder.' + src; - } - }] - } - }, - - // compress js - uglify: { - options: { - banner: '<%= banner %>\n', - mangle: { except: ['$'] } - }, - dist: { - files: [{ - expand: true, - flatten: true, - src: ['dist/js/*.js', '!dist/js/*.min.js'], - dest: 'dist/js', - ext: '.min.js', - extDot: 'last' - }] - } - }, - - // compress css - cssmin: { - dist: { - files: [{ - expand: true, - flatten: true, - src: ['dist/css/*.css', '!dist/css/*.min.css'], - dest: 'dist/css', - ext: '.min.css', - extDot: 'last' - }] - } - }, - - // clean build dir - clean: { - temp: ['.temp'], - doc: ['doc'] - }, - - // jshint tests - jshint: { - lib: { - options: { - jshintrc: '.jshintrc' - }, - src: ['src/**/*.js', '!src/**/.wrapper.js'] - } - }, - - // jscs tests - jscs: { - lib: { - options: { - config: '.jscsrc' - }, - src: ['src/**/*.js', '!src/**/.wrapper.js'] - } - }, - - // scss tests - sasslint: { - lib: { - options: { - configFile: '.sass-lint.yml' - }, - src: ['src/**/*.scss'] - } - }, - - // jsDoc generation - jsdoc: { - lib: { - src: ['src/**/*.js', '!src/**/.wrapper.js'], - options: { - destination: 'doc', - config: '.jsdoc.json' - } - } - }, - - // inject sources files and tests modules in demo and test - injector: { - options: { - relative: true, - addRootSlash: false - }, - example: { - src: config.all_js_files.concat(['dist/i18n/query-builder.en.js']), - dest: 'examples/index.html' - }, - testSrc: { - options: { - starttag: '', - transform: function(filepath) { - return ''; - } - }, - src: config.all_js_files, - dest: 'tests/index.html' - }, - testModules: { - options: { - starttag: '' - }, - src: ['tests/*.module.js'], - dest: 'tests/index.html' - } - }, - - // qunit test suite - qunit: { - all: { - options: { - urls: ['tests/index.html?coverage=true'], - noGlobals: true - } - } - }, - - // save LCOV files - qunit_blanket_lcov: { - all: { - files: [{ - expand: true, - src: ['src/*.js', 'src/plugins/**/plugin.js'] - }], - options: { - dest: '.coverage-results/all.lcov' - } - } - }, - - // coveralls data - coveralls: { - options: { - force: true - }, - all: { - src: '.coverage-results/all.lcov' - } - } - }); - - - grunt.registerTask('build_js', [ - 'concat:lang_temp', - 'concat:js', - 'wrap:js', - 'usebanner:js', - 'concat:js_standalone', - 'uglify', - 'clean:temp' - ]); - - grunt.registerTask('build_css', [ - 'copy:sass_core', - 'copy:sass_plugins', - 'sass_injection', - 'sass', - 'cssmin', - 'usebanner:css' - ]); - - grunt.registerTask('build_lang', [ - 'concat:lang' - ]); - - grunt.registerTask('default', [ - 'build_lang', - 'build_js', - 'build_css' - ]); - - grunt.registerTask('test', [ - 'jshint', - 'jscs', - 'sasslint', - 'build_lang', - 'build_css', - 'injector:testSrc', - 'injector:testModules', - 'qunit_blanket_lcov', - 'qunit' - ]); - - grunt.registerTask('serve', [ - 'build_lang', - 'build_css', - 'injector:example', - 'open', - 'connect', - 'watch' - ]); - - grunt.registerTask('doc', [ - 'clean:doc', - 'jsdoc', - 'copy:doc_script' - ]); -}; diff --git a/LICENSE b/LICENSE index 50b5d4bd..2558fa6a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014-2017 Damien Sorel +Copyright (c) 2014-2018 Damien Sorel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index f62228aa..f0fd8ab7 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,18 @@ # jQuery QueryBuilder -[![Bower version](https://img.shields.io/bower/v/jQuery-QueryBuilder.svg?style=flat-square)](http://querybuilder.js.org) [![npm version](https://img.shields.io/npm/v/jQuery-QueryBuilder.svg?style=flat-square)](https://www.npmjs.com/package/jQuery-QueryBuilder) -[![CDN](https://img.shields.io/badge/cdn-jsdelivr-%23EB4C36.svg?style=flat-square)](https://cdn.jsdelivr.net/npm/jQuery-QueryBuilder/dist/) -[![Build Status](https://img.shields.io/travis/mistic100/jQuery-QueryBuilder.svg?style=flat-square)](https://travis-ci.org/mistic100/jQuery-QueryBuilder) -[![Coverage Status](https://img.shields.io/coveralls/mistic100/jQuery-QueryBuilder/master.svg?style=flat-square)](https://coveralls.io/r/mistic100/jQuery-QueryBuilder) +[![jsDelivr CDN](https://data.jsdelivr.com/v1/package/npm/jQuery-QueryBuilder/badge)](https://www.jsdelivr.com/package/npm/jQuery-QueryBuilder) +[![Build Status](https://github.com/mistic100/jQuery-QueryBuilder/workflows/CI/badge.svg)](https://github.com/mistic100/jQuery-QueryBuilder/actions) +[![gitlocalized](https://gitlocalize.com/repo/5259/whole_project/badge.svg)](https://gitlocalize.com/repo/5259/whole_project?utm_source=badge) jQuery plugin offering an simple interface to create complex queries. -[![screenshot](https://raw.githubusercontent.com/mistic100/jQuery-QueryBuilder/master/examples/screenshot.png)](http://querybuilder.js.org) +[![screenshot](https://raw.githubusercontent.com/mistic100/jQuery-QueryBuilder/master/examples/screenshot.png)](https://querybuilder.js.org) ## Documentation -[http://querybuilder.js.org](http://querybuilder.js.org) +[querybuilder.js.org](https://querybuilder.js.org) @@ -23,12 +22,6 @@ jQuery plugin offering an simple interface to create complex queries. [Download the latest release](https://github.com/mistic100/jQuery-QueryBuilder/releases) -#### With Bower - -```bash -$ bower install jQuery-QueryBuilder -``` - #### With npm ```bash @@ -37,65 +30,32 @@ $ npm install jQuery-QueryBuilder #### Via CDN -jQuery-QueryBuilder is available on [jsDelivr](https://cdn.jsdelivr.net/npm/jQuery-QueryBuilder/dist/) and [unpkg](https://unpkg.com/jQuery-QueryBuilder/dist/)) - +jQuery-QueryBuilder is available on [jsDelivr](https://www.jsdelivr.com/package/npm/jQuery-QueryBuilder). ### Dependencies - * jQuery >= 1.10 - * Bootstrap >= 3.1 (CSS only) + * [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) - * [doT.js >= 1.0.3](http://olado.github.io/doT) - * [MomentJS](http://momentjs.com) (optional, for Date/Time validation) + * [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 and doT.js are directly included in the [standalone](https://github.com/mistic100/jQuery-QueryBuilder/blob/master/dist/js/query-builder.standalone.js) file) +($.extendext is directly included in the [standalone](https://github.com/mistic100/jQuery-QueryBuilder/blob/master/dist/js/query-builder.standalone.js) file) -### Browser support - * Internet Explorer >= 9 - * All other recent browsers +## Developement -## Build +Install Node dependencies with `npm install`. -#### Prerequisites +#### Build - * NodeJS + NPM: `apt-get install nodejs-legacy npm` - * Grunt CLI: `npm install -g grunt-cli` - * Bower: `npm install -g bower` +Run `npm run build` in the root directory to generate production files inside `dist`. -#### Run - -Install Node and Bower dependencies `npm install & bower install` then run `grunt` in the root directory to generate production files inside `dist`. - -#### Options - -You can choose which plugins to include with `--plugins` : -```bash -# include "sql-support" and "mongodb-support" plugins -grunt --plugins=sql-support,mongodb-support - -# disable all plugins -grunt --plugins=false -``` -All plugins are included by default. - -You can also include language files with `--languages` : -```bash -# include French & Italian translation -grunt --languages=fr,it -``` - -#### Other commands - - * `grunt test` to run jshint/jscs/scsslint and the QUnit test suite. - * `grunt serve` to open the example page with automatic build and livereload. - * `grunt doc` to generate the documentation. +#### Serve +Run `npm run serve` to open the example page with automatic build and livereload. ## License This library is available under the MIT license. - -#### Inspirations - * [Knockout Query Builder](http://kindohm.github.io/knockout-query-builder/) - * [jui_filter_rules](http://www.pontikis.net/labs/jui_filter_rules/) diff --git a/bower.json b/bower.json deleted file mode 100644 index 8c6ce3a3..00000000 --- a/bower.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "jQuery-QueryBuilder", - "authors": [ - { - "name": "Damien \"Mistic\" Sorel", - "email": "contact@git.strangeplanet.fr", - "homepage": "http://www.strangeplanet.fr" - } - ], - "description": "jQuery plugin for user friendly query/filter creator", - "main": [ - "dist/js/query-builder.js", - "dist/css/query-builder.default.css" - ], - "dependencies": { - "jquery": ">=1.9.0", - "bootstrap": ">=3.1.0 <4", - "moment": ">=2.6.0", - "jquery-extendext": ">=0.1.2", - "doT": ">=1.0.3" - }, - "devDependencies": { - "blanket": "^1.1.0", - "qunit": "^1.23.0", - "bootstrap-select": "^1.10.0", - "bootbox": "^4.4.0", - "awesome-bootstrap-checkbox": "^0.3.0", - "sql-parser": "^1.1.0", - "bind-polyfill": "~1.0.0", - "interact": "^1.2.6" - }, - "keywords": [ - "jquery", - "query", - "builder", - "filter" - ], - "license": "MIT", - "homepage": "https://github.com/mistic100/jQuery-QueryBuilder", - "repository": { - "type": "git", - "url": "git://github.com/mistic100/jQuery-QueryBuilder.git" - }, - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "build", - "src", - "tests", - "composer.json", - "package.json", - "Gruntfile.js", - "CONTRIBUTING.md" - ] -} diff --git a/build/cleanLn.js b/build/cleanLn.js deleted file mode 100644 index f93432e8..00000000 --- a/build/cleanLn.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function(src) { - return src.replace(/\r\n/g, '\n'); -}; 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/initConfig.js b/build/initConfig.js deleted file mode 100644 index 5bb333ee..00000000 --- a/build/initConfig.js +++ /dev/null @@ -1,68 +0,0 @@ -module.exports = function(grunt, config) { - config.all_plugins = {}; - config.all_langs = {}; - config.loaded_plugins = []; - config.loaded_langs = []; - config.js_files_to_load = config.js_core_files.slice(); - config.all_js_files = config.js_core_files.slice(); - - // list available plugins and languages - grunt.file.expand('src/plugins/**/plugin.js') - .forEach(function(f) { - var n = f.split('/')[2]; - config.all_plugins[n] = f; - }); - - grunt.file.expand('src/i18n/*.json') - .forEach(function(f) { - var n = f.split(/[\/\.]/)[2]; - config.all_langs[n] = f; - }); - - // fill all js files - for (var p in config.all_plugins) { - config.all_js_files.push(config.all_plugins[p]); - } - - // parse 'plugins' parameter - var arg_plugins = grunt.option('plugins'); - if (typeof arg_plugins === 'string') { - arg_plugins.replace(/ /g, '').split(',').forEach(function(p) { - if (config.all_plugins[p]) { - config.js_files_to_load.push(config.all_plugins[p]); - config.loaded_plugins.push(p); - } - else { - grunt.fail.warn('Plugin ' + p + ' unknown'); - } - }); - } - else if (arg_plugins === undefined) { - for (var p in config.all_plugins) { - config.js_files_to_load.push(config.all_plugins[p]); - config.loaded_plugins.push(p); - } - } - - // default language - config.js_files_to_load.push('.temp/i18n/en.js'); - config.loaded_langs.push('en'); - - // parse 'lang' parameter - var arg_langs = grunt.option('languages'); - if (typeof arg_langs === 'string') { - arg_langs.replace(/ /g, '').split(',').forEach(function(l) { - if (config.all_langs[l]) { - if (l !== 'en') { - config.js_files_to_load.push(config.all_langs[l].replace(/^src/, '.temp').replace(/json$/, 'js')); - config.loaded_langs.push(l); - } - } - else { - grunt.fail.warn('Language ' + l + ' unknown'); - } - }); - } - - return config; -}; diff --git a/build/jsdoc.md b/build/jsdoc.md index 029fecb1..e329af92 100644 --- a/build/jsdoc.md +++ b/build/jsdoc.md @@ -1,4 +1,4 @@ -# [Main documentation](..) +# [Main documentation](..) # Entry point: [$.fn.QueryBuilder](external-_jQuery.fn_.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/build/processLang.js b/build/processLang.js deleted file mode 100644 index 33c6b2c7..00000000 --- a/build/processLang.js +++ /dev/null @@ -1,30 +0,0 @@ -var deepmerge = require('deepmerge'); - -module.exports = function(grunt, loaded_plugins) { - return function(file, src, wrapper) { - var lang = file.split(/[\/\.]/)[2]; - var content = JSON.parse(src); - wrapper = wrapper || ['', '']; - - grunt.config.set('lang_locale', content.__locale || lang); - grunt.config.set('lang_author', content.__author); - var header = grunt.template.process('<%= langBanner %>'); - - loaded_plugins.forEach(function(p) { - var plugin_file = 'src/plugins/' + p + '/i18n/' + lang + '.json'; - - if (grunt.file.exists(plugin_file)) { - content = deepmerge(content, grunt.file.readJSON(plugin_file)); - } - }); - - return header - + '\n\n' - + wrapper[0] - + 'QueryBuilder.regional[\'' + lang + '\'] = ' - + JSON.stringify(content, null, 2) - + ';\n\n' - + 'QueryBuilder.defaults({ lang_code: \'' + lang + '\' });' - + wrapper[1]; - }; -}; diff --git a/build/removeJshint.js b/build/removeJshint.js deleted file mode 100644 index 4ff583ac..00000000 --- a/build/removeJshint.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = function(src) { - return src - .replace(/\/\*jshint [a-z:]+ \*\/\r?\n\r?\n?/g, '') - .replace(/\/\*jshint -[EWI]{1}[0-9]{3} \*\/\r?\n\r?\n?/g, ''); -}; diff --git a/composer.json b/composer.json deleted file mode 100644 index 6c74835b..00000000 --- a/composer.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "mistic100/jquery-querybuilder", - "version": "2.4.0", - "authors": [{ - "name": "Damien \"Mistic\" Sorel", - "email": "contact@git.strangeplanet.fr", - "homepage": "http://www.strangeplanet.fr" - }], - "description": "jQuery plugin for user friendly query/filter creator", - "require": { - "robloach/component-installer": "*", - "components/jquery": ">=1.9.0", - "moment/moment": ">=2.6.0", - "components/bootstrap": ">=3.1.0 <4" - }, - "keywords": [ - "jquery", - "query", - "builder", - "filter" - ], - "license": "MIT", - "homepage": "https://github.com/mistic100/jQuery-QueryBuilder", - "support": { - "issues": "https://github.com/mistic100/jQuery-QueryBuilder/issues" - }, - "extra": { - "component": { - "styles": [ - "dist/css/query-builder.default.css" - ], - "scripts": [ - "dist/js/query-builder.standalone.js" - ], - "files": [ - "dist/css/query-builder.default.min.css", - "dist/css/query-builder.dark.css", - "dist/css/query-builder.dark.min.css", - "dist/js/query-builder.js", - "dist/js/query-builder.min.js", - "dist/js/query-builder.standalone.min.js", - "dist/i18n/*.js" - ] - } - } -} diff --git a/examples/bower.json b/examples/bower.json deleted file mode 100644 index 85233d7e..00000000 --- a/examples/bower.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "jQuery-QueryBuilder-example", - "dependencies" : { - "seiyria-bootstrap-slider": "latest", - "bootswatch-dist": "#slate", - "selectize": "latest" - } -} diff --git a/examples/index.html b/examples/index.html index 369c9eda..180652b8 100644 --- a/examples/index.html +++ b/examples/index.html @@ -1,84 +1,70 @@ - + jQuery QueryBuilder Example - - - - - - - - - - + + + + + + +
-
+
- -
- -
- + + + + + + + + + + + + + + + + + + + + + + + +
@@ -86,7 +72,7 @@

jQuery QueryBuilder
-
@@ -112,46 +98,23 @@

Output

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - diff --git a/package.json b/package.json index 5209cf4e..4111f3a0 100644 --- a/package.json +++ b/package.json @@ -4,43 +4,37 @@ "author": { "name": "Damien \"Mistic\" Sorel", "email": "contact@git.strangeplanet.fr", - "url": "http://www.strangeplanet.fr" + "url": "https://www.strangeplanet.fr" }, "description": "jQuery plugin for user friendly query/filter creator", "main": "dist/js/query-builder.js", + "files": [ + "dist/", + "src/" + ], "dependencies": { - "jquery": ">=1.9.0", - "bootstrap": ">=3.1.0 <4", - "moment": ">=2.6.0", - "jquery-extendext": ">=0.1.2", - "dot": ">=1.0.3" + "bootstrap": "^5.3.0", + "@popperjs/core": "^2.11.8", + "bootstrap-icons": "^1.11.3", + "jquery": "^3.5.1", + "jquery-extendext": "^1.0.0", + "moment": "^2.29.1", + "sql-parser-mistic": "^1.2.3" }, "devDependencies": { - "deepmerge": "^0.2.0", - "foodoc": "^0.0.8", - "grunt": "^1.0.0", - "grunt-banner": "^0.6.0", - "grunt-contrib-clean": "^1.0.0", - "grunt-contrib-concat": "^1.0.0", - "grunt-contrib-connect": "^1.0.0", - "grunt-contrib-copy": "^1.0.0", - "grunt-contrib-cssmin": "^1.0.0", - "grunt-contrib-jshint": "^1.0.0", - "grunt-contrib-qunit": "^0.7.0", - "grunt-contrib-uglify": "^1.0.0", - "grunt-contrib-watch": "^1.0.0", - "grunt-coveralls": "^1.0.0", - "grunt-injector": "^1.1.0", - "grunt-jscs": "^2.8.0", - "grunt-jsdoc": "^2.1.0", - "grunt-open": "^0.2.3", - "grunt-qunit-blanket-lcov": "^0.3.0", - "grunt-sass": "^2.0.0", - "grunt-sass-injection": "^1.0.3", - "grunt-sass-lint": "^0.2.2", - "grunt-wrap": "^0.3.0", - "jit-grunt": "^0.10.0", - "time-grunt": "^1.3.0" + "alive-server": "^1.3.0", + "awesome-bootstrap-checkbox": "^0.3.7", + "bootbox": "^6.0.0", + "bootstrap-slider": "^10.0.0", + "chosenjs": "^1.4.3", + "concurrently": "^8.2.0", + "deepmerge": "^2.1.0", + "foodoc": "^0.0.9", + "glob": "^10.3.1", + "interactjs": "^1.3.3", + "nodemon": "^2.0.22", + "sass": "^1.63.6", + "@selectize/selectize": "^0.15.2" }, "keywords": [ "jquery", @@ -49,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" @@ -58,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 732ae480..2b326090 100644 --- a/src/.wrapper.js +++ b/src/.wrapper.js @@ -1,14 +1,14 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'dot/doT', 'jquery-extendext'], factory); + define(['jquery', 'jquery-extendext'], factory); } else if (typeof module === 'object' && module.exports) { - module.exports = factory(require('jquery'), require('dot/doT'), require('jquery-extendext')); + module.exports = factory(require('jquery'), require('jquery-extendext')); } else { - factory(root.jQuery, root.doT); + factory(root.jQuery); } -}(this, function($, doT) { +}(this, function($) { "use strict"; @@js diff --git a/src/core.js b/src/core.js index 764c8c6b..bcb7c912 100644 --- a/src/core.js +++ b/src/core.js @@ -93,12 +93,40 @@ QueryBuilder.prototype.checkFilters = function(filters) { break; case 'select': + var cleanValues = []; + filter.has_optgroup = false; + + Utils.iterateOptions(filter.values, function(value, label, optgroup) { + cleanValues.push({ + value: value, + label: label, + optgroup: optgroup || null + }); + + if (optgroup) { + filter.has_optgroup = true; + + // register optgroup if needed + if (!this.settings.optgroups[optgroup]) { + this.settings.optgroups[optgroup] = optgroup; + } + } + }.bind(this)); + + if (filter.has_optgroup) { + filter.values = Utils.groupSort(cleanValues, 'optgroup'); + } + else { + filter.values = cleanValues; + } + if (filter.placeholder) { if (filter.placeholder_value === undefined) { filter.placeholder_value = -1; } - Utils.iterateOptions(filter.values, function(key) { - if (key == filter.placeholder_value) { + + filter.values.forEach(function(entry) { + if (entry.value == filter.placeholder_value) { Utils.error('Config', 'Placeholder of filter "{0}" overlaps with one of its values', filter.id); } }); @@ -282,7 +310,7 @@ QueryBuilder.prototype.bindEvents = function() { break; case 'value': - self.updateRuleValue(node); + self.updateRuleValue(node, oldValue); break; } } @@ -297,7 +325,7 @@ QueryBuilder.prototype.bindEvents = function() { break; case 'condition': - self.updateGroupCondition(node); + self.updateGroupCondition(node, oldValue); break; } } @@ -317,19 +345,18 @@ QueryBuilder.prototype.setRoot = function(addRule, data, flags) { addRule = (addRule === undefined || addRule === true); var group_id = this.nextGroupId(); - var $group = $(this.getGroupTemplate(group_id, 1)); + var $group = $($.parseHTML(this.getGroupTemplate(group_id, 1))); this.$el.append($group); this.model.root = new Group(null, $group); this.model.root.model = this.model; this.model.root.data = data; - this.model.root.__.flags = $.extend({}, this.settings.default_group_flags, flags); + this.model.root.flags = $.extend({}, this.settings.default_group_flags, flags); + this.model.root.condition = this.settings.default_condition; this.trigger('afterAddGroup', this.model.root); - this.model.root.condition = this.settings.default_condition; - if (addRule) { this.addRule(this.model.root); } @@ -370,7 +397,8 @@ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) { var model = parent.addGroup($group); model.data = data; - model.__.flags = $.extend({}, this.settings.default_group_flags, flags); + model.flags = $.extend({}, this.settings.default_group_flags, flags); + model.condition = this.settings.default_condition; /** * Just after adding a group @@ -380,7 +408,12 @@ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) { */ this.trigger('afterAddGroup', model); - model.condition = this.settings.default_condition; + /** + * After any change in the rules + * @event rulesChanged + * @memberof QueryBuilder + */ + this.trigger('rulesChanged'); if (addRule) { this.addRule(model); @@ -429,6 +462,8 @@ QueryBuilder.prototype.deleteGroup = function(group) { * @memberof QueryBuilder */ this.trigger('afterDeleteGroup'); + + this.trigger('rulesChanged'); } return del; @@ -437,10 +472,11 @@ QueryBuilder.prototype.deleteGroup = function(group) { /** * Performs actions when a group's condition changes * @param {Group} group + * @param {object} previousCondition * @fires QueryBuilder.afterUpdateGroupCondition * @private */ -QueryBuilder.prototype.updateGroupCondition = function(group) { +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); @@ -452,8 +488,11 @@ QueryBuilder.prototype.updateGroupCondition = function(group) { * @event afterUpdateGroupCondition * @memberof QueryBuilder * @param {Group} group + * @param {object} previousCondition */ - this.trigger('afterUpdateGroupCondition', group); + this.trigger('afterUpdateGroupCondition', group, previousCondition); + + this.trigger('rulesChanged'); }; /** @@ -496,14 +535,11 @@ QueryBuilder.prototype.addRule = function(parent, data, flags) { } var rule_id = this.nextRuleId(); - var $rule = $(this.getRuleTemplate(rule_id)); + var $rule = $($.parseHTML(this.getRuleTemplate(rule_id))); var model = parent.addRule($rule); - if (data !== undefined) { - model.data = data; - } - - model.__.flags = $.extend({}, this.settings.default_rule_flags, flags); + model.data = data; + model.flags = $.extend({}, this.settings.default_rule_flags, flags); /** * Just after adding a rule @@ -513,6 +549,8 @@ QueryBuilder.prototype.addRule = function(parent, data, flags) { */ this.trigger('afterAddRule', model); + this.trigger('rulesChanged'); + this.createRuleFilters(model); if (this.settings.default_filter || !this.settings.display_empty_filter) { @@ -565,6 +603,8 @@ QueryBuilder.prototype.deleteRule = function(rule) { */ this.trigger('afterDeleteRule'); + this.trigger('rulesChanged'); + return true; }; @@ -585,7 +625,7 @@ QueryBuilder.prototype.createRuleFilters = function(rule) { * @returns {QueryBuilder.Filter[]} */ var filters = this.change('getRuleFilters', this.filters, rule); - var $filterSelect = $(this.getRuleFilterSelect(rule, filters)); + var $filterSelect = $($.parseHTML(this.getRuleFilterSelect(rule, filters))); rule.$el.find(QueryBuilder.selectors.filter_container).html($filterSelect); @@ -596,6 +636,8 @@ QueryBuilder.prototype.createRuleFilters = function(rule) { * @param {Rule} rule */ this.trigger('afterCreateRuleFilters', rule); + + this.applyRuleFlags(rule); }; /** @@ -612,12 +654,19 @@ QueryBuilder.prototype.createRuleOperators = function(rule) { } var operators = this.getOperators(rule.filter); - var $operatorSelect = $(this.getRuleOperatorSelect(rule, operators)); + var $operatorSelect = $($.parseHTML(this.getRuleOperatorSelect(rule, operators))); $operatorContainer.html($operatorSelect); // set the operator without triggering update event - rule.__.operator = operators[0]; + if (rule.filter.default_operator) { + rule.__.operator = this.getOperatorByType(rule.filter.default_operator); + } + else { + rule.__.operator = operators[0]; + } + + rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type); /** * After creating the dropdown for operators @@ -627,6 +676,8 @@ QueryBuilder.prototype.createRuleOperators = function(rule) { * @param {QueryBuilder.Operator[]} operators - allowed operators for this rule */ this.trigger('afterCreateRuleOperators', rule, operators); + + this.applyRuleFlags(rule); }; /** @@ -649,16 +700,16 @@ QueryBuilder.prototype.createRuleInput = function(rule) { var filter = rule.filter; for (var i = 0; i < rule.operator.nb_inputs; i++) { - var $ruleInput = $(this.getRuleInput(rule, i)); + var $ruleInput = $($.parseHTML($.trim(this.getRuleInput(rule, i)))); if (i > 0) $valueContainer.append(this.settings.inputs_separator); $valueContainer.append($ruleInput); $inputs = $inputs.add($ruleInput); } - $valueContainer.show(); + $valueContainer.css('display', ''); $inputs.on('change ' + (filter.input_event || ''), function() { - if (!this._updating_input) { + if (!rule._updating_input) { rule._updating_value = true; rule.value = self.getRuleInputValue(rule); rule._updating_value = false; @@ -685,6 +736,8 @@ QueryBuilder.prototype.createRuleInput = function(rule) { rule.value = self.getRuleInputValue(rule); rule._updating_value = false; } + + this.applyRuleFlags(rule); }; /** @@ -710,8 +763,11 @@ QueryBuilder.prototype.updateRuleFilter = function(rule, previousFilter) { * @event afterUpdateRuleFilter * @memberof QueryBuilder * @param {Rule} rule + * @param {object} previousFilter */ - this.trigger('afterUpdateRuleFilter', rule); + this.trigger('afterUpdateRuleFilter', rule, previousFilter); + + this.trigger('rulesChanged'); }; /** @@ -730,7 +786,7 @@ QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) { rule.__.value = undefined; } else { - $valueContainer.show(); + $valueContainer.css('display', ''); if ($valueContainer.is(':empty') || !previousOperator || rule.operator.nb_inputs !== previousOperator.nb_inputs || @@ -742,6 +798,9 @@ QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) { 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); } /** @@ -749,19 +808,21 @@ QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) { * @event afterUpdateRuleOperator * @memberof QueryBuilder * @param {Rule} rule + * @param {object} previousOperator */ - this.trigger('afterUpdateRuleOperator', rule); + this.trigger('afterUpdateRuleOperator', rule, previousOperator); - this.updateRuleValue(rule); + 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) { +QueryBuilder.prototype.updateRuleValue = function(rule, previousValue) { if (!rule._updating_value) { this.setRuleInputValue(rule, rule.value); } @@ -771,8 +832,11 @@ QueryBuilder.prototype.updateRuleValue = function(rule) { * @event afterUpdateRuleValue * @memberof QueryBuilder * @param {Rule} rule + * @param {*} previousValue */ - this.trigger('afterUpdateRuleValue', rule); + this.trigger('afterUpdateRuleValue', rule, previousValue); + + this.trigger('rulesChanged'); }; /** @@ -785,15 +849,10 @@ QueryBuilder.prototype.applyRuleFlags = function(rule) { var flags = rule.flags; var Selectors = QueryBuilder.selectors; - if (flags.filter_readonly) { - rule.$el.find(Selectors.rule_filter).prop('disabled', true); - } - if (flags.operator_readonly) { - rule.$el.find(Selectors.rule_operator).prop('disabled', true); - } - if (flags.value_readonly) { - rule.$el.find(Selectors.rule_value).prop('disabled', true); - } + 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(); } @@ -817,10 +876,9 @@ QueryBuilder.prototype.applyGroupFlags = function(group) { var flags = group.flags; var Selectors = QueryBuilder.selectors; - if (flags.condition_readonly) { - group.$el.find('>' + Selectors.group_condition).prop('disabled', true) - .parent().addClass('readonly'); - } + 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(); } diff --git a/src/data.js b/src/data.js index 1594f334..7f466940 100644 --- a/src/data.js +++ b/src/data.js @@ -221,6 +221,29 @@ QueryBuilder.prototype._validateValue = function(rule, value) { } } + if ((rule.operator.type === 'between' || rule.operator.type === 'not_between') && value.length === 2) { + switch (QueryBuilder.types[filter.type]) { + case 'number': + if (value[0] > value[1]) { + result = ['number_between_invalid', value[0], value[1]]; + } + break; + + case 'datetime': + // we need MomentJS + if (validation.format) { + if (!('moment' in window)) { + Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com'); + } + + if (moment(value[0], validation.format).isAfter(moment(value[1], validation.format))) { + result = ['datetime_between_invalid', value[0], value[1]]; + } + } + break; + } + } + return result; }; @@ -247,7 +270,6 @@ QueryBuilder.prototype.nextRuleId = function() { * @param {string|object} filter - filter id or filter object * @returns {object[]} * @fires QueryBuilder.changer:getOperators - * @private */ QueryBuilder.prototype.getOperators = function(filter) { if (typeof filter == 'string') { @@ -295,7 +317,6 @@ QueryBuilder.prototype.getOperators = function(filter) { * @param {boolean} [doThrow=true] * @returns {object|null} * @throws UndefinedFilterError - * @private */ QueryBuilder.prototype.getFilterById = function(id, doThrow) { if (id == '-1') { @@ -319,7 +340,6 @@ QueryBuilder.prototype.getFilterById = function(id, doThrow) { * @param {boolean} [doThrow=true] * @returns {object|null} * @throws UndefinedOperatorError - * @private */ QueryBuilder.prototype.getOperatorByType = function(type, doThrow) { if (type == '-1') { @@ -366,22 +386,18 @@ QueryBuilder.prototype.getRuleInputValue = function(rule) { case 'checkbox': tmp = []; - // jshint loopfunc:true $value.find('[name=' + name + ']:checked').each(function() { tmp.push($(this).val()); }); - // jshint loopfunc:false value.push(tmp); break; case 'select': if (filter.multiple) { tmp = []; - // jshint loopfunc:true $value.find('[name=' + name + '] option:selected').each(function() { tmp.push($(this).val()); }); - // jshint loopfunc:false value.push(tmp); } else { @@ -394,11 +410,20 @@ QueryBuilder.prototype.getRuleInputValue = function(rule) { } } - if (operator.multiple && filter.value_separator) { - value = value.map(function(val) { - return val.split(filter.value_separator); - }); - } + 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]; @@ -435,7 +460,7 @@ QueryBuilder.prototype.setRuleInputValue = function(rule, value) { return; } - this._updating_input = true; + rule._updating_input = true; if (filter.valueSetter) { filter.valueSetter.call(this, rule, value); @@ -459,11 +484,9 @@ QueryBuilder.prototype.setRuleInputValue = function(rule, value) { if (!$.isArray(value[i])) { value[i] = [value[i]]; } - // jshint loopfunc:true value[i].forEach(function(value) { $value.find('[name=' + name + '][value="' + value + '"]').prop('checked', true).trigger('change'); }); - // jshint loopfunc:false break; default: @@ -476,7 +499,7 @@ QueryBuilder.prototype.setRuleInputValue = function(rule, value) { } } - this._updating_input = false; + rule._updating_input = false; }; /** diff --git a/src/defaults.js b/src/defaults.js index 56f1201d..b63b1251 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -156,7 +156,8 @@ QueryBuilder.DEFAULTS = { group: null, rule: null, filterSelect: null, - operatorSelect: null + operatorSelect: null, + ruleValueSelect: null }, lang_code: 'en', @@ -186,10 +187,10 @@ QueryBuilder.DEFAULTS = { ], icons: { - add_group: 'glyphicon glyphicon-plus-sign', - add_rule: 'glyphicon glyphicon-plus', - remove_group: 'glyphicon glyphicon-remove', - remove_rule: 'glyphicon glyphicon-remove', - error: 'glyphicon glyphicon-warning-sign' + add_group: 'bi-plus-circle-fill', + add_rule: 'bi-plus-lg', + remove_group: 'bi-x-lg', + remove_rule: 'bi-x-lg', + error: 'bi-exclamation-triangle' } }; diff --git a/src/i18n/en.json b/src/i18n/en.json index a0e9fd12..15bbb139 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -51,11 +51,13 @@ "number_exceed_min": "Must be greater than {0}", "number_exceed_max": "Must be lower than {0}", "number_wrong_step": "Must be a multiple of {0}", + "number_between_invalid": "Invalid values, {0} is greater than {1}", "datetime_empty": "Empty value", "datetime_invalid": "Invalid date format ({0})", "datetime_exceed_min": "Must be after {0}", "datetime_exceed_max": "Must be before {0}", + "datetime_between_invalid": "Invalid values, {0} is greater than {1}", "boolean_not_valid": "Not a boolean", "operator_not_multiple": "Operator \"{1}\" cannot accept multiple values" } -} \ No newline at end of file +} diff --git a/src/i18n/eo.json b/src/i18n/eo.json new file mode 100644 index 00000000..b3c9166d --- /dev/null +++ b/src/i18n/eo.json @@ -0,0 +1,63 @@ +{ + "__locale": "Esperanto (eo)", + "__author": "Robin van der Vliet, https://robinvandervliet.com/", + + "add_rule": "Aldoni regulon", + "add_group": "Aldoni grupon", + "delete_rule": "Forigi", + "delete_group": "Forigi", + + "conditions": { + "AND": "KAJ", + "OR": "AŬ" + }, + + "operators": { + "equal": "estas egala al", + "not_equal": "ne estas egala al", + "in": "estas en", + "not_in": "ne estas en", + "less": "estas malpli ol", + "less_or_equal": "estas malpli ol aŭ egala al", + "greater": "estas pli ol", + "greater_or_equal": "estas pli ol aŭ egala al", + "between": "estas inter", + "not_between": "ne estas inter", + "begins_with": "komenciĝas per", + "not_begins_with": "ne komenciĝas per", + "contains": "enhavas", + "not_contains": "ne enhavas", + "ends_with": "finiĝas per", + "not_ends_with": "ne finiĝas per", + "is_empty": "estas malplena", + "is_not_empty": "ne estas malplena", + "is_null": "estas senvalora", + "is_not_null": "ne estas senvalora" + }, + + "errors": { + "no_filter": "Neniu filtrilo elektita", + "empty_group": "La grupo estas malplena", + "radio_empty": "Neniu valoro elektita", + "checkbox_empty": "Neniu valoro elektita", + "select_empty": "Neniu valoro elektita", + "string_empty": "Malplena valoro", + "string_exceed_min_length": "Devas enhavi pli ol {0} signojn", + "string_exceed_max_length": "Devas ne enhavi pli ol {0} signojn", + "string_invalid_format": "Nevalida strukturo ({0})", + "number_nan": "Ne estas nombro", + "number_not_integer": "Ne estas entjera nombro", + "number_not_double": "Ne estas reela nombro", + "number_exceed_min": "Devas esti pli ol {0}", + "number_exceed_max": "Devas esti malpli ol {0}", + "number_wrong_step": "Devas esti oblo de {0}", + "number_between_invalid": "Nevalidaj valoroj, {0} estas pli ol {1}", + "datetime_empty": "Malplena valoro", + "datetime_invalid": "Nevalida dato ({0})", + "datetime_exceed_min": "Devas esti post {0}", + "datetime_exceed_max": "Devas esti antaŭ {0}", + "datetime_between_invalid": "Nevalidaj valoroj, {0} estas post {1}", + "boolean_not_valid": "Ne estas bulea valoro", + "operator_not_multiple": "La operacio \"{1}\" ne akceptas plurajn valorojn" + } +} diff --git a/src/i18n/es.json b/src/i18n/es.json index 3db3e098..3374aae1 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -22,6 +22,7 @@ "greater": "mayor", "greater_or_equal": "mayor o igual", "between": "entre", + "not_between": "no está entre", "begins_with": "empieza por", "not_begins_with": "no empieza por", "contains": "contiene", @@ -52,6 +53,11 @@ "number_wrong_step": "Debe ser múltiplo de {0}", "datetime_invalid": "Formato de fecha inválido ({0})", "datetime_exceed_min": "Debe ser posterior a {0}", - "datetime_exceed_max": "Debe ser anterior a {0}" + "datetime_exceed_max": "Debe ser anterior a {0}", + "number_between_invalid": "Valores Inválidos, {0} es mayor que {1}", + "datetime_empty": "Campo vacio", + "datetime_between_invalid": "Valores Inválidos, {0} es mayor que {1}", + "boolean_not_valid": "No es booleano", + "operator_not_multiple": "El operador \"{1}\" no puede aceptar valores multiples" } } diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 56cbad52..33b27bb6 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -13,14 +13,14 @@ }, "operators": { - "equal": "égal", - "not_equal": "non égal", - "in": "dans", - "not_in": "pas dans", - "less": "inférieur", - "less_or_equal": "inférieur ou égal", - "greater": "supérieur", - "greater_or_equal": "supérieur ou égal", + "equal": "est égal à", + "not_equal": "n'est pas égal à", + "in": "est compris dans", + "not_in": "n'est pas compris dans", + "less": "est inférieur à", + "less_or_equal": "est inférieur ou égal à", + "greater": "est supérieur à", + "greater_or_equal": "est supérieur ou égal à", "between": "est entre", "not_between": "n'est pas entre", "begins_with": "commence par", @@ -51,11 +51,13 @@ "number_exceed_min": "Doit être plus grand que {0}", "number_exceed_max": "Doit être plus petit que {0}", "number_wrong_step": "Doit être un multiple de {0}", + "number_between_invalid": "Valeurs invalides, {0} est plus grand que {1}", "datetime_empty": "Valeur vide", "datetime_invalid": "Fomat de date invalide ({0})", "datetime_exceed_min": "Doit être après {0}", "datetime_exceed_max": "Doit être avant {0}", + "datetime_between_invalid": "Valeurs invalides, {0} est plus grand que {1}", "boolean_not_valid": "N'est pas un booléen", "operator_not_multiple": "L'opérateur \"{1}\" ne peut utiliser plusieurs valeurs" } -} \ No newline at end of file +} diff --git a/src/i18n/hu.json b/src/i18n/hu.json new file mode 100644 index 00000000..22fa5e4c --- /dev/null +++ b/src/i18n/hu.json @@ -0,0 +1,63 @@ +{ + "__locale": "Hungarian - Magyar (hu)", + "__author": "Szabó Attila \"Tailor993\", https://www.tailor993.hu", + + "add_rule": "Feltétel hozzáadása", + "add_group": "Csoport hozzáadása", + "delete_rule": "Feltétel törlése", + "delete_group": "Csoport törlése", + + "conditions": { + "AND": "ÉS", + "OR": "VAGY" + }, + + "operators": { + "equal": "egyenlő", + "not_equal": "nem egyenlő", + "in": "bennevan", + "not_in": "nincs benne", + "less": "kisebb", + "less_or_equal": "kisebb vagy egyenlő", + "greater": "nagyobb", + "greater_or_equal": "nagyobb vagy egyenlő", + "between": "közötte", + "not_between": "nincs közötte", + "begins_with": "ezzel kezdődik", + "not_begins_with": "ezzel nem kezdődik", + "contains": "tartalmazza", + "not_contains": "nem tartalmazza", + "ends_with": "erre végződik", + "not_ends_with": "errre nem végződik", + "is_empty": "üres", + "is_not_empty": "nem üres", + "is_null": "null", + "is_not_null": "nem null" + }, + + "errors": { + "no_filter": "Nincs kiválasztott feltétel", + "empty_group": "A csoport üres", + "radio_empty": "Nincs kiválasztott érték", + "checkbox_empty": "Nincs kiválasztott érték", + "select_empty": "Nincs kiválasztott érték", + "string_empty": "Üres érték", + "string_exceed_min_length": "A megadott szöveg rövidebb a várt {0} karakternél", + "string_exceed_max_length": "A megadott szöveg nem tartalmazhat többet, mint {0} karaktert", + "string_invalid_format": "Nem megfelelő formátum ({0})", + "number_nan": "Nem szám", + "number_not_integer": "Nem egész szám (integer)", + "number_not_double": "Nem valós szám", + "number_exceed_min": "Nagyobbnak kell lennie, mint {0}", + "number_exceed_max": "Kisebbnek kell lennie, mint {0}", + "number_wrong_step": "{0} többszörösének kell lennie.", + "number_between_invalid": "INem megfelelő érték, {0} nagyobb, mint {1}", + "datetime_empty": "Üres érték", + "datetime_invalid": "nem megfelelő dátum formátum ({0})", + "datetime_exceed_min": "A dátumnak későbbinek kell lennie, mint{0}", + "datetime_exceed_max": "A dátumnak korábbinak kell lennie, mint {0}", + "datetime_between_invalid": "Nem megfelelő értékek, {0} nagyobb, mint {1}", + "boolean_not_valid": "Nem igaz/hamis (boolean)", + "operator_not_multiple": "Ez a művelet: \"{1}\" nem fogadhat el több értéket" + } +} diff --git a/src/i18n/it.json b/src/i18n/it.json index d16fb50c..076cc172 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -1,6 +1,6 @@ { "__locale": "Italian (it)", - "__author": "davegraziosi", + "__author": "davegraziosi, Giuseppe Lodi Rizzini", "add_rule": "Aggiungi regola", "add_group": "Aggiungi gruppo", @@ -51,11 +51,13 @@ "number_exceed_min": "Deve essere maggiore di {0}", "number_exceed_max": "Deve essere minore di {0}", "number_wrong_step": "Deve essere multiplo di {0}", + "number_between_invalid": "Valori non validi, {0} è maggiore di {1}", "datetime_empty": "Valore vuoto", "datetime_invalid": "Formato data non valido ({0})", "datetime_exceed_min": "Deve essere successivo a {0}", "datetime_exceed_max": "Deve essere precedente a {0}", + "datetime_between_invalid": "Valori non validi, {0} è maggiore di {1}", "boolean_not_valid": "Non è un booleano", "operator_not_multiple": "L'Operatore {0} non può accettare valori multipli" } -} \ No newline at end of file +} diff --git a/src/i18n/lt.json b/src/i18n/lt.json new file mode 100644 index 00000000..c95c4f08 --- /dev/null +++ b/src/i18n/lt.json @@ -0,0 +1,63 @@ +{ + "__locale": "Lithuanian (lt)", + "__author": "Dalius Guzauskas (aka Tichij), https://lt.linkedin.com/in/daliusg", + + "add_rule": "Pridėti taisyklę", + "add_group": "Pridėti grupę", + "delete_rule": "Ištrinti", + "delete_group": "Ištrinti", + + "conditions": { + "AND": "IR", + "OR": "ARBA" + }, + + "operators": { + "equal": "lygu", + "not_equal": "nėra lygu", + "in": "iš nurodytų", + "not_in": "ne iš nurodytų", + "less": "mažiau", + "less_or_equal": "mažiau arba lygu", + "greater": "daugiau", + "greater_or_equal": "daugiau arba lygu", + "between": "tarp", + "not_between": "nėra tarp", + "begins_with": "prasideda", + "not_begins_with": "neprasideda", + "contains": "turi", + "not_contains": "neturi", + "ends_with": "baigiasi", + "not_ends_with": "nesibaigia", + "is_empty": "tuščia", + "is_not_empty": "ne tuščia", + "is_null": "neapibrėžta", + "is_not_null": "nėra neapibrėžta" + }, + + "errors": { + "no_filter": "Nepasirinktas filtras", + "empty_group": "Grupė tuščia", + "radio_empty": "Nepasirinkta reikšmė", + "checkbox_empty": "Nepasirinkta reikšmė", + "select_empty": "Nepasirinkta reikšmė", + "string_empty": "Tuščia reikšmė", + "string_exceed_min_length": "Turi būti bent {0} simbolių", + "string_exceed_max_length": "Turi būti ne daugiau kaip {0} simbolių", + "string_invalid_format": "Klaidingas formatas ({0})", + "number_nan": "Nėra skaičius", + "number_not_integer": "Ne sveikasis skaičius", + "number_not_double": "Ne realusis skaičius", + "number_exceed_min": "Turi būti daugiau už {0}", + "number_exceed_max": "Turi būti mažiau už {0}", + "number_wrong_step": "Turi būti {0} kartotinis", + "number_between_invalid": "Klaidingos reikšmės, {0} yra daugiau už {1}", + "datetime_empty": "Tuščia reikšmė", + "datetime_invalid": "Klaidingas datos formatas ({0})", + "datetime_exceed_min": "Turi būti po {0}", + "datetime_exceed_max": "Turi būti prieš {0}", + "datetime_between_invalid": "Klaidingos reikšmės, {0} yra daugiau už {1}", + "boolean_not_valid": "Nėra loginis tipas", + "operator_not_multiple": "Operatorius \"{1}\" negali priimti kelių reikšmių" + } +} diff --git a/src/i18n/pl.json b/src/i18n/pl.json index c27069f4..9cddba15 100644 --- a/src/i18n/pl.json +++ b/src/i18n/pl.json @@ -8,8 +8,8 @@ "delete_group": "Usuń", "conditions": { - "AND": "AND", - "OR": "OR" + "AND": "ORAZ", + "OR": "LUB" }, "operators": { diff --git a/src/i18n/ro.json b/src/i18n/ro.json index a0e1fc06..b2a6eae3 100644 --- a/src/i18n/ro.json +++ b/src/i18n/ro.json @@ -1,6 +1,6 @@ { "__locale": "Romanian (ro)", - "__author": "ArianServ", + "__author": "ArianServ, totpero", "add_rule": "Adaugă regulă", "add_group": "Adaugă grup", @@ -17,10 +17,12 @@ "not_equal": "diferit", "in": "în", "not_in": "nu în", - "less": "mai puţin", - "less_or_equal": "mai puţin sau egal", + "less": "mai mic", + "less_or_equal": "mai mic sau egal", "greater": "mai mare", "greater_or_equal": "mai mare sau egal", + "between": "între", + "not_between": "nu între", "begins_with": "începe cu", "not_begins_with": "nu începe cu", "contains": "conţine", @@ -31,5 +33,31 @@ "is_not_empty": "nu este gol", "is_null": "e nul", "is_not_null": "nu e nul" + }, + + "errors": { + "no_filter": "Nici un filtru selectat", + "empty_group": "Grupul este gol", + "radio_empty": "Nici o valoare nu este selectată", + "checkbox_empty": "Nici o valoare nu este selectată", + "select_empty": "Nici o valoare nu este selectată", + "string_empty": "Valoare goală", + "string_exceed_min_length": "Trebuie să conţină mai puţin de {0} caractere", + "string_exceed_max_length": "Trebuie să conţină mai mult de {0} caractere", + "string_invalid_format": "Format invalid ({0})", + "number_nan": "Nu este număr", + "number_not_integer": "Nu este număr întreg", + "number_not_double": "Nu este număr real", + "number_exceed_min": "Trebuie să fie mai mare decât {0}", + "number_exceed_max": "Trebuie să fie mai mic decât {0}", + "number_wrong_step": "Trebuie să fie multiplu de {0}", + "number_between_invalid": "Valori invalide, {0} este mai mare decât {1}", + "datetime_empty": "Valoare goală", + "datetime_invalid": "Format dată invalid ({0})", + "datetime_exceed_min": "Trebuie să fie după {0}", + "datetime_exceed_max": "Trebuie să fie înainte {0}", + "datetime_between_invalid": "Valori invalide, {0} este mai mare decât {1}", + "boolean_not_valid": "Nu este boolean", + "operator_not_multiple": "Operatorul \"{1}\" nu poate accepta mai multe valori" } -} \ No newline at end of file +} diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 33a343e8..9ccfefcc 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -21,6 +21,7 @@ "greater": "больше", "greater_or_equal": "больше или равно", "between": "между", + "not_between": "не между", "begins_with": "начинается с", "not_begins_with": "не начинается с", "contains": "содержит", @@ -36,10 +37,10 @@ "errors": { "no_filter": "Фильтр не выбран", "empty_group": "Группа пуста", - "radio_empty": "Не выбранно значение", - "checkbox_empty": "Не выбранно значение", - "select_empty": "Не выбранно значение", - "string_empty": "Не заполненно", + "radio_empty": "Не выбрано значение", + "checkbox_empty": "Не выбрано значение", + "select_empty": "Не выбрано значение", + "string_empty": "Не заполнено", "string_exceed_min_length": "Должен содержать больше {0} символов", "string_exceed_max_length": "Должен содержать меньше {0} символов", "string_invalid_format": "Неверный формат ({0})", @@ -49,10 +50,12 @@ "number_exceed_min": "Должно быть больше {0}", "number_exceed_max": "Должно быть меньше, чем {0}", "number_wrong_step": "Должно быть кратно {0}", - "datetime_empty": "Не заполненно", + "number_between_invalid": "Недопустимые значения, {0} больше {1}", + "datetime_empty": "Не заполнено", "datetime_invalid": "Неверный формат даты ({0})", "datetime_exceed_min": "Должно быть, после {0}", "datetime_exceed_max": "Должно быть, до {0}", + "datetime_between_invalid": "Недопустимые значения, {0} больше {1}", "boolean_not_valid": "Не логическое", "operator_not_multiple": "Оператор \"{1}\" не поддерживает много значений" } 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/sv.json b/src/i18n/sv.json new file mode 100644 index 00000000..a3b06e84 --- /dev/null +++ b/src/i18n/sv.json @@ -0,0 +1,63 @@ +{ + "__locale": "Svenska (sv)", + "__author": "hekin1", + + "add_rule": "Lägg till regel", + "add_group": "Lägg till grupp", + "delete_rule": "Ta bort", + "delete_group": "Ta bort", + + "conditions": { + "AND": "OCH", + "OR": "ELLER" + }, + + "operators": { + "equal": "lika med", + "not_equal": "ej lika med", + "in": "en av", + "not_in": "ej en av", + "less": "mindre", + "less_or_equal": "mindre eller lika med", + "greater": "större", + "greater_or_equal": "större eller lika med", + "between": "mellan", + "not_between": "ej mellan", + "begins_with": "börjar med", + "not_begins_with": "börjar inte med", + "contains": "innehåller", + "not_contains": "innehåller inte", + "ends_with": "slutar med", + "not_ends_with": "slutar inte med", + "is_empty": "är tom", + "is_not_empty": "är inte tom", + "is_null": "är null", + "is_not_null": "är inte null" + }, + + "errors": { + "no_filter": "Inget filter valt", + "empty_group": "Gruppen är tom", + "radio_empty": "Inget värde valt", + "checkbox_empty": "Inget värde valt", + "select_empty": "Inget värde valt", + "string_empty": "Tomt värde", + "string_exceed_min_length": "Måste innehålla minst {0} tecken", + "string_exceed_max_length": "Får ej innehålla fler än {0} tecken", + "string_invalid_format": "Felaktigt format ({0})", + "number_nan": "Inte numeriskt", + "number_not_integer": "Inte en siffra", + "number_not_double": "Inte ett decimaltal", + "number_exceed_min": "Måste vara större än {0}", + "number_exceed_max": "Måste vara lägre än {0}", + "number_wrong_step": "Måste vara en mutipel av {0}", + "number_between_invalid": "Felaktiga värden, {0} är större än {1}", + "datetime_empty": "Tomt värde", + "datetime_invalid": "Felaktigt datumformat ({0})", + "datetime_exceed_min": "Måste vara efter {0}", + "datetime_exceed_max": "Måste vara före {0}", + "datetime_between_invalid": "Felaktiga värden, {0} är större än {1}", + "boolean_not_valid": "Inte en boolean", + "operator_not_multiple": "Operatorn \"{1}\" accepterar inte flera värden" + } +} diff --git a/src/i18n/sw.json b/src/i18n/sw.json new file mode 100644 index 00000000..e215a84f --- /dev/null +++ b/src/i18n/sw.json @@ -0,0 +1,63 @@ +{ + "__locale": "Swahili (sw)", + "__author": "Timothy Anyona", + + "add_rule": "Ongeza kanuni", + "add_group": "Ongeza kikundi", + "delete_rule": "Futa", + "delete_group": "Futa", + + "conditions": { + "AND": "NA", + "OR": "AU" + }, + + "operators": { + "equal": "ni", + "not_equal": "sio", + "in": "mojawapo ya", + "not_in": "sio mojawapo ya", + "less": "isiyozidi", + "less_or_equal": "isiyozidi au ni sawa na", + "greater": "inayozidi", + "greater_or_equal": "inayozidi au ni sawa na", + "between": "kati ya", + "not_between": "isiyo kati ya", + "begins_with": "inaanza na", + "not_begins_with": "isiyoanza na", + "contains": "ina", + "not_contains": "haina", + "ends_with": "inaisha na", + "not_ends_with": "isiyoisha na", + "is_empty": "ni tupu", + "is_not_empty": "sio tupu", + "is_null": "ni batili", + "is_not_null": "sio batili" + }, + + "errors": { + "no_filter": "Chujio halijachaguliwa", + "empty_group": "Kikundi ki tupu", + "radio_empty": "Thamani haijachaguliwa", + "checkbox_empty": "Thamani haijachaguliwa", + "select_empty": "Thamani haijachaguliwa", + "string_empty": "Thamani tupu", + "string_exceed_min_length": "Lazima iwe na vibambo visiopungua {0}", + "string_exceed_max_length": "Haifai kuwa na vibambo zaidi ya {0}", + "string_invalid_format": "Fomati batili ({0})", + "number_nan": "Sio nambari", + "number_not_integer": "Sio namba kamili", + "number_not_double": "Sio namba desimali", + "number_exceed_min": "Lazima iwe zaidi ya {0}", + "number_exceed_max": "Lazima iwe chini ya {0}", + "number_wrong_step": "Lazima iwe kigawe cha {0}", + "number_between_invalid": "Thamani batili, {0} ni kubwa kuliko {1}", + "datetime_empty": "Thamani tupu", + "datetime_invalid": "Fomati tarehe batili ({0})", + "datetime_exceed_min": "Lazima iwe baada ya {0}", + "datetime_exceed_max": "Lazima iwe kabla ya {0}", + "datetime_between_invalid": "Thamani batili, {0} ni baada ya {1}", + "boolean_not_valid": "Sio buleani", + "operator_not_multiple": "Opereta \"{1}\" haikubali thamani nyingi" + } +} diff --git a/src/i18n/tr.json b/src/i18n/tr.json index 2b2d0295..25ecbf78 100644 --- a/src/i18n/tr.json +++ b/src/i18n/tr.json @@ -38,23 +38,25 @@ "errors": { "no_filter": "Bir filtre seçili değil", "empty_group": "Grup bir eleman içermiyor", - "radio_empty": "seçim yapılmalı", - "checkbox_empty": "seçim yapılmalı", - "select_empty": "seçim yapılmalı", + "radio_empty": "Seçim yapılmalı", + "checkbox_empty": "Seçim yapılmalı", + "select_empty": "Seçim yapılmalı", "string_empty": "Bir metin girilmeli", "string_exceed_min_length": "En az {0} karakter girilmeli", "string_exceed_max_length": "En fazla {0} karakter girilebilir", "string_invalid_format": "Uyumsuz format ({0})", "number_nan": "Sayı değil", - "number_not_integer": "Tam sayı değilr", + "number_not_integer": "Tam sayı değil", "number_not_double": "Ondalıklı sayı değil", "number_exceed_min": "Sayı {0}'den/dan daha büyük olmalı", "number_exceed_max": "Sayı {0}'den/dan daha küçük olmalı", "number_wrong_step": "{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/main.js b/src/main.js index f799af6f..0659d288 100644 --- a/src/main.js +++ b/src/main.js @@ -121,8 +121,8 @@ var QueryBuilder = function($el, options) { if (!this.templates[tpl]) { this.templates[tpl] = QueryBuilder.templates[tpl]; } - if (typeof this.templates[tpl] == 'string') { - this.templates[tpl] = doT.template(this.templates[tpl]); + if (typeof this.templates[tpl] !== 'function') { + throw new Error(`Template ${tpl} must be a function`); } }, this); @@ -134,7 +134,7 @@ var QueryBuilder = function($el, options) { this.status.id = this.$el.attr('id'); // INIT - this.$el.addClass('query-builder form-inline'); + this.$el.addClass('query-builder'); this.filters = this.checkFilters(this.filters); this.operators = this.checkOperators(this.operators); diff --git a/src/plugins/bt-checkbox/plugin.js b/src/plugins/bt-checkbox/plugin.js index 7a7311c2..26b0998f 100644 --- a/src/plugins/bt-checkbox/plugin.js +++ b/src/plugins/bt-checkbox/plugin.js @@ -3,12 +3,12 @@ * @memberof module:plugins * @description Applies Awesome Bootstrap Checkbox for checkbox and radio inputs. * @param {object} [options] - * @param {string} [options.font='glyphicons'] + * @param {string} [options.font='bootstrap-icons'] * @param {string} [options.color='default'] */ QueryBuilder.define('bt-checkbox', function(options) { - if (options.font == 'glyphicons') { - this.$el.addClass('bt-checkbox-glyphicons'); + if (options.font === 'bootstrap-icons') { + this.$el.addClass('bt-checkbox-bootstrap-icons'); } this.on('getRuleInput.filter', function(h, rule, name) { @@ -31,15 +31,11 @@ QueryBuilder.define('bt-checkbox', function(options) { var color = filter.colors[key] || filter.colors._def_ || options.color; var id = name + '_' + (i++); - h.value+= '\ - \ - \ - \ -'; + h.value += `
`; }); } }); }, { - font: 'glyphicons', + font: 'bootstrap-icons', color: 'default' }); diff --git a/src/plugins/bt-checkbox/plugin.scss b/src/plugins/bt-checkbox/plugin.scss index 324f610f..22e21eed 100644 --- a/src/plugins/bt-checkbox/plugin.scss +++ b/src/plugins/bt-checkbox/plugin.scss @@ -1,12 +1,10 @@ -.query-builder.bt-checkbox-glyphicons { - .checkbox input[type='checkbox']:checked + label::after { - font-family: 'Glyphicons Halflings'; - content: '\e013'; +.query-builder.bt-checkbox-bootstrap-icons { + .checkbox input[type='checkbox'] + label::before { + outline: 0; } - .checkbox label::after { - padding-left: 4px; - padding-top: 2px; - font-size: 9px; + .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 d9ef2812..00000000 --- a/src/plugins/bt-selectpicker/plugin.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @class BtSelectpicker - * @memberof module:plugins - * @descriptioon Applies Bootstrap Select on filters and operators combo-boxes. - * @param {object} [options] - * @param {string} [options.container='body'] - * @param {string} [options.style='btn-inverse btn-xs'] - * @param {int|string} [options.width='auto'] - * @param {boolean} [options.showIcon=false] - * @throws MissingLibraryError - */ -QueryBuilder.define('bt-selectpicker', function(options) { - if (!$.fn.selectpicker || !$.fn.selectpicker.Constructor) { - Utils.error('MissingLibrary', 'Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select'); - } - - var Selectors = QueryBuilder.selectors; - - // init selectpicker - this.on('afterCreateRuleFilters', function(e, rule) { - rule.$el.find(Selectors.rule_filter).removeClass('form-control').selectpicker(options); - }); - - this.on('afterCreateRuleOperators', function(e, rule) { - rule.$el.find(Selectors.rule_operator).removeClass('form-control').selectpicker(options); - }); - - // update selectpicker on change - this.on('afterUpdateRuleFilter', function(e, rule) { - rule.$el.find(Selectors.rule_filter).selectpicker('render'); - }); - - this.on('afterUpdateRuleOperator', function(e, rule) { - rule.$el.find(Selectors.rule_operator).selectpicker('render'); - }); - - this.on('beforeDeleteRule', function(e, rule) { - rule.$el.find(Selectors.rule_filter).selectpicker('destroy'); - rule.$el.find(Selectors.rule_operator).selectpicker('destroy'); - }); -}, { - container: 'body', - style: 'btn-inverse btn-xs', - width: 'auto', - showIcon: false -}); diff --git a/src/plugins/bt-tooltip-errors/plugin.js b/src/plugins/bt-tooltip-errors/plugin.js index e9bc935c..52f4830d 100644 --- a/src/plugins/bt-tooltip-errors/plugin.js +++ b/src/plugins/bt-tooltip-errors/plugin.js @@ -7,16 +7,17 @@ * @throws MissingLibraryError */ QueryBuilder.define('bt-tooltip-errors', function(options) { - if (!$.fn.tooltip || !$.fn.tooltip.Constructor || !$.fn.tooltip.Constructor.prototype.fixTitle) { - Utils.error('MissingLibrary', 'Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com'); + if (! typeof bootstrap.Tooltip === "function") { + alert(typeof bootstrap.Tooltip ); + Utils.error('MissingLibrary', 'Bootstrap Popper is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com'); } var self = this; // add BT Tooltip data this.on('getRuleTemplate.filter getGroupTemplate.filter', function(h) { - var $h = $(h.value); - $h.find(QueryBuilder.selectors.error_container).attr('data-toggle', 'tooltip'); + var $h = $($.parseHTML(h.value)); + $h.find(QueryBuilder.selectors.error_container).attr('data-bs-toggle', 'tooltip'); h.value = $h.prop('outerHTML'); }); @@ -24,9 +25,7 @@ QueryBuilder.define('bt-tooltip-errors', function(options) { this.model.on('update', function(e, node, field) { if (field == 'error' && self.settings.display_errors) { node.$el.find(QueryBuilder.selectors.error_container).eq(0) - .tooltip(options) - .tooltip('hide') - .tooltip('fixTitle'); + .attr('data-bs-original-title',options).attr('data-bs-title',options).tooltip(); } }); }, { diff --git a/src/plugins/change-filters/plugin.js b/src/plugins/change-filters/plugin.js index 823edac1..f44934ab 100644 --- a/src/plugins/change-filters/plugin.js +++ b/src/plugins/change-filters/plugin.js @@ -59,6 +59,8 @@ QueryBuilder.extend(/** @lends module:plugins.ChangeFilters.prototype */ { function(rule) { if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) { rule.drop(); + + self.trigger('rulesChanged'); } else { self.createRuleFilters(rule); diff --git a/src/plugins/chosen-selectpicker/plugin.js b/src/plugins/chosen-selectpicker/plugin.js new file mode 100644 index 00000000..e18c8573 --- /dev/null +++ b/src/plugins/chosen-selectpicker/plugin.js @@ -0,0 +1,44 @@ +/** + * @class ChosenSelectpicker + * @memberof module:plugins + * @descriptioon Applies chosen-js Select on filters and operators combo-boxes. + * @param {object} [options] Supports all the options for chosen + * @throws MissingLibraryError + */ +QueryBuilder.define('chosen-selectpicker', function(options) { + + if (!$.fn.chosen) { + Utils.error('MissingLibrary', 'chosen is required to use "chosen-selectpicker" plugin. Get it here: https://github.com/harvesthq/chosen'); + } + + if (this.settings.plugins['bt-selectpicker']) { + Utils.error('Conflict', 'bt-selectpicker is already selected as the dropdown plugin. Please remove chosen-selectpicker from the plugin list'); + } + + var Selectors = QueryBuilder.selectors; + + // init selectpicker + this.on('afterCreateRuleFilters', function(e, rule) { + rule.$el.find(Selectors.rule_filter).removeClass('form-control').chosen(options); + }); + + this.on('afterCreateRuleOperators', function(e, rule) { + if (e.builder.getOperators(rule.filter).length > 1) { + rule.$el.find(Selectors.rule_operator).removeClass('form-control').chosen(options); + } + }); + + // update selectpicker on change + this.on('afterUpdateRuleFilter', function(e, rule) { + rule.$el.find(Selectors.rule_filter).trigger('chosen:updated'); + }); + + this.on('afterUpdateRuleOperator', function(e, rule) { + rule.$el.find(Selectors.rule_operator).trigger('chosen:updated'); + }); + + this.on('beforeDeleteRule', function(e, rule) { + rule.$el.find(Selectors.rule_filter).chosen('destroy'); + rule.$el.find(Selectors.rule_operator).chosen('destroy'); + }); +}); diff --git a/src/plugins/filter-description/plugin.js b/src/plugins/filter-description/plugin.js index 4525cbe0..3027e761 100644 --- a/src/plugins/filter-description/plugin.js +++ b/src/plugins/filter-description/plugin.js @@ -3,7 +3,7 @@ * @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='glyphicon glyphicon-info-sign'] + * @param {string} [options.icon='bi-info-circle-fill'] * @param {string} [options.mode='popover'] - inline, popover or bootbox * @throws ConfigError */ @@ -19,11 +19,11 @@ QueryBuilder.define('filter-description', function(options) { } else { if ($p.length === 0) { - $p = $('

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

')); $p.appendTo(rule.$el); } else { - $p.show(); + $p.css('display', ''); } $p.html(' ' + description); @@ -43,30 +43,28 @@ QueryBuilder.define('filter-description', function(options) { if (!description) { $b.hide(); - if ($b.data('bs.popover')) { + if ($b.data('bs-popover')) { $b.popover('hide'); } } else { if ($b.length === 0) { - $b = $(''); + $b = $($.parseHTML('')); $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions)); - - $b.popover({ + const popover = new bootstrap.Popover($b.get(0), { placement: 'left', container: 'body', html: true - }); - + }) $b.on('mouseout', function() { - $b.popover('hide'); + popover('hide'); }); } else { - $b.show(); + $b.css('display', ''); } - $b.data('bs.popover').options.content = description; + $b.data('bs-popover').options.content = description; if ($b.attr('aria-describedby')) { $b.popover('show'); @@ -89,7 +87,7 @@ QueryBuilder.define('filter-description', function(options) { } else { if ($b.length === 0) { - $b = $(''); + $b = $($.parseHTML('')); $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions)); $b.on('click', function() { @@ -97,7 +95,7 @@ QueryBuilder.define('filter-description', function(options) { }); } else { - $b.show(); + $b.css('display', ''); } $b.data('description', description); @@ -105,7 +103,7 @@ QueryBuilder.define('filter-description', function(options) { }); } }, { - icon: 'glyphicon glyphicon-info-sign', + icon: 'bi-info-circle-fill', mode: 'popover' }); diff --git a/src/plugins/filter-description/plugin.scss b/src/plugins/filter-description/plugin.scss index 80a2af71..41498718 100644 --- a/src/plugins/filter-description/plugin.scss +++ b/src/plugins/filter-description/plugin.scss @@ -16,6 +16,6 @@ $description-border: 1px solid $description-border-color; border: $description-border; color: $description-text-color; border-radius: $item-border-radius; - padding: #{$rule-padding / 2} $rule-padding; + padding: #{$rule-padding * .5} $rule-padding; font-size: .8em; } diff --git a/src/plugins/invert/i18n/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/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/sk.json b/src/plugins/invert/i18n/sk.json new file mode 100644 index 00000000..b9d3274b --- /dev/null +++ b/src/plugins/invert/i18n/sk.json @@ -0,0 +1,3 @@ +{ + "invert": "Invertný" +} diff --git a/src/plugins/invert/i18n/sv.json b/src/plugins/invert/i18n/sv.json new file mode 100644 index 00000000..aa932882 --- /dev/null +++ b/src/plugins/invert/i18n/sv.json @@ -0,0 +1,3 @@ +{ + "invert": "Invertera" +} diff --git a/src/plugins/invert/i18n/sw.json b/src/plugins/invert/i18n/sw.json new file mode 100644 index 00000000..7ee2e3fd --- /dev/null +++ b/src/plugins/invert/i18n/sw.json @@ -0,0 +1,3 @@ +{ + "invert": "Pindua" +} diff --git a/src/plugins/invert/plugin.js b/src/plugins/invert/plugin.js index 8928d4be..8b0ab7ef 100644 --- a/src/plugins/invert/plugin.js +++ b/src/plugins/invert/plugin.js @@ -3,7 +3,7 @@ * @memberof module:plugins * @description Allows to invert a rule operator, a group condition or the entire builder. * @param {object} [options] - * @param {string} [options.icon='glyphicon glyphicon-random'] + * @param {string} [options.icon='bi-shuffle'] * @param {boolean} [options.recursive=true] * @param {boolean} [options.invert_rules=true] * @param {boolean} [options.display_rules_button=false] @@ -29,25 +29,36 @@ QueryBuilder.define('invert', function(options) { }); // Modify templates - this.on('getGroupTemplate.filter', function(h, level) { - var $h = $(h.value); - $h.find(Selectors.condition_container).after(''); - h.value = $h.prop('outerHTML'); - }); - - if (options.display_rules_button && options.invert_rules) { - this.on('getRuleTemplate.filter', function(h) { - var $h = $(h.value); - $h.find(Selectors.rule_actions).prepend(''); + 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: 'glyphicon glyphicon-random', + icon: 'bi-shuffle', recursive: true, invert_rules: true, display_rules_button: false, - silent_fail: false + silent_fail: false, + disable_template: false }); QueryBuilder.defaults({ @@ -147,6 +158,8 @@ QueryBuilder.extend(/** @lends module:plugins.Invert.prototype */ { * @param {object} options */ this.trigger('afterInvert', node, options); + + this.trigger('rulesChanged'); } } }); diff --git a/src/plugins/mongodb-support/plugin.js b/src/plugins/mongodb-support/plugin.js index 93a17348..5df30db4 100644 --- a/src/plugins/mongodb-support/plugin.js +++ b/src/plugins/mongodb-support/plugin.js @@ -31,17 +31,17 @@ QueryBuilder.defaults({ }, mongoRuleOperators: { - $ne: function(v) { - v = v.$ne; + $eq: function(v) { return { 'val': v, - 'op': v === null ? 'is_not_null' : (v === '' ? 'is_not_empty' : 'not_equal') + 'op': v === null ? 'is_null' : (v === '' ? 'is_empty' : 'equal') }; }, - eq: function(v) { + $ne: function(v) { + v = v.$ne; return { 'val': v, - 'op': v === null ? 'is_null' : (v === '' ? 'is_empty' : 'equal') + 'op': v === null ? 'is_not_null' : (v === '' ? 'is_not_empty' : 'not_equal') }; }, $regex: function(v) { @@ -105,6 +105,10 @@ QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ { getMongo: function(data) { data = (data === undefined) ? this.getRules() : data; + if (!data) { + return null; + } + var self = this; return (function parse(group) { @@ -128,7 +132,6 @@ QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ { else { var mdb = self.settings.mongoOperators[rule.operator]; var ope = self.getOperatorByType(rule.operator); - var values = []; if (mdb === undefined) { Utils.error('UndefinedMongoOperator', 'Unknown MongoDB operation for operator "{0}"', rule.operator); @@ -138,10 +141,6 @@ QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ { if (!(rule.value instanceof Array)) { rule.value = [rule.value]; } - - rule.value.forEach(function(v) { - values.push(Utils.changeType(v, rule.type, false)); - }); } /** @@ -155,7 +154,7 @@ QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ { var field = self.change('getMongoDBField', rule.field, rule); var ruleExpression = {}; - ruleExpression[field] = mdb.call(self, values); + ruleExpression[field] = mdb.call(self, rule.value); /** * Modifies the MongoDB expression generated for a rul @@ -167,7 +166,7 @@ QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ { * @param {function} valueWrapper - function that takes the value and adds the operator * @returns {object} */ - parts.push(self.change('ruleToMongo', ruleExpression, rule, values, mdb)); + parts.push(self.change('ruleToMongo', ruleExpression, rule, rule.value, mdb)); } }); @@ -225,7 +224,7 @@ QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ { }; } - var key = andOr(query); + var key = self.getMongoCondition(query); if (!key) { Utils.error('MongoParse', 'Invalid MongoDB query format'); } @@ -250,7 +249,7 @@ QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ { return; } - var key = andOr(data); + var key = self.getMongoCondition(data); if (key) { parts.push(parse(data, key)); } @@ -258,7 +257,7 @@ QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ { var field = Object.keys(data)[0]; var value = data[field]; - var operator = determineMongoOperator(value, field); + var operator = self.getMongoOperator(value); if (operator === undefined) { Utils.error('MongoParse', 'Invalid MongoDB query format'); } @@ -345,58 +344,50 @@ QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ { } return id; - } -}); - -/** - * Finds which operator is used in a MongoDB sub-object - * @memberof module:plugins.MongoDbSupport - * @param {*} value - * @returns {string|undefined} - * @private - */ -function determineMongoOperator(value) { - if (value !== null && typeof value == 'object') { - var subkeys = Object.keys(value); + }, - if (subkeys.length === 1) { - return subkeys[0]; - } - else { - if (value.$gte !== undefined && value.$lte !== undefined) { + /** + * Finds which operator is used in a MongoDB sub-object + * @param {*} data + * @returns {string|undefined} + * @private + */ + getMongoOperator: function(data) { + if (data !== null && typeof data === 'object') { + if (data.$gte !== undefined && data.$lte !== undefined) { return 'between'; } - if (value.$lt !== undefined && value.$gt !== undefined) { + if (data.$lt !== undefined && data.$gt !== undefined) { return 'not_between'; } - else if (value.$regex !== undefined) { // optional $options - return '$regex'; - } - else { - return; + + 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'; - } -} + else { + return '$eq'; + } + }, -/** - * Returns the key corresponding to "$or" or "$and" - * @memberof module:plugins.MongoDbSupport - * @param {object} data - * @returns {string} - * @private - */ -function andOr(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]; + /** + * 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]; + } } } - - return undefined; -} +}); 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/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 index da6f98b0..be98bb00 100644 --- a/src/plugins/not-group/plugin.js +++ b/src/plugins/not-group/plugin.js @@ -3,8 +3,8 @@ * @memberof module:plugins * @description Adds a "Not" checkbox in front of group conditions. * @param {object} [options] - * @param {string} [options.icon_checked='glyphicon glyphicon-checked'] - * @param {string} [options.icon_unchecked='glyphicon glyphicon-unchecked'] + * @param {string} [options.icon_checked='bi-check2-square'] + * @param {string} [options.icon_unchecked='bi-square'] */ QueryBuilder.define('not-group', function(options) { var self = this; @@ -30,15 +30,17 @@ QueryBuilder.define('not-group', function(options) { }); // Modify templates - this.on('getGroupTemplate.filter', function(h, level) { - var $h = $(h.value); - $h.find(QueryBuilder.selectors.condition_container).prepend( - '' - ); - h.value = $h.prop('outerHTML'); - }); + 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) { @@ -61,10 +63,27 @@ QueryBuilder.define('not-group', function(options) { 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; @@ -93,8 +112,9 @@ QueryBuilder.define('not-group', function(options) { e.value.not = !!data.not; }); }, { - icon_unchecked: 'glyphicon glyphicon-unchecked', - icon_checked: 'glyphicon glyphicon-check' + icon_unchecked: 'bi-square', + icon_checked: 'bi-check2-square', + disable_template: false }); /** @@ -128,5 +148,7 @@ QueryBuilder.extend(/** @lends module:plugins.NotGroup.prototype */ { * @param {Group} group */ this.trigger('afterUpdateGroupNot', group); + + this.trigger('rulesChanged'); } }); diff --git a/src/plugins/sortable/plugin.js b/src/plugins/sortable/plugin.js index 83f0b4df..84b29e1f 100644 --- a/src/plugins/sortable/plugin.js +++ b/src/plugins/sortable/plugin.js @@ -5,7 +5,7 @@ * @param {object} [options] * @param {boolean} [options.inherit_no_drop=true] * @param {boolean} [options.inherit_no_sortable=true] - * @param {string} [options.icon='glyphicon glyphicon-sort'] + * @param {string} [options.icon='bi-sort-down'] * @throws MissingLibraryError, ConfigError */ QueryBuilder.define('sortable', function(options) { @@ -27,6 +27,7 @@ QueryBuilder.define('sortable', function(options) { var placeholder; var ghost; var src; + var moved; // Init drag and drop this.on('afterAddRule afterAddGroup', function(e, node) { @@ -47,9 +48,11 @@ QueryBuilder.define('sortable', function(options) { // Configure drag if (!node.flags.no_sortable) { interact(node.$el[0]) - .allowFrom(QueryBuilder.selectors.drag_handle) .draggable({ + allowFrom: QueryBuilder.selectors.drag_handle, onstart: function(event) { + moved = false; + // get model of dragged element src = self.getModel(event.target); @@ -60,7 +63,7 @@ QueryBuilder.define('sortable', function(options) { .addClass('dragging'); // create drop placeholder - var ph = $('
 
') + var ph = $($.parseHTML('
 
')) .height(src.$el.outerHeight()); placeholder = src.parent.addRule(ph, src.getPos()); @@ -73,7 +76,13 @@ QueryBuilder.define('sortable', function(options) { ghost[0].style.top = event.clientY - 15 + 'px'; ghost[0].style.left = event.clientX - 15 + 'px'; }, - onend: function() { + 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; @@ -83,7 +92,7 @@ QueryBuilder.define('sortable', function(options) { placeholder = undefined; // show element - src.$el.show(); + src.$el.css('display', ''); /** * After a node has been moved with {@link module:plugins.Sortable} @@ -92,6 +101,8 @@ QueryBuilder.define('sortable', function(options) { * @param {Node} node */ self.trigger('afterMove', src); + + self.trigger('rulesChanged'); } }); } @@ -105,7 +116,9 @@ QueryBuilder.define('sortable', function(options) { moveSortableToTarget(placeholder, $(event.target), self); }, ondrop: function(event) { - moveSortableToTarget(src, $(event.target), self); + if (!moved) { + moveSortableToTarget(src, $(event.target), self); + } } }); @@ -118,7 +131,9 @@ QueryBuilder.define('sortable', function(options) { moveSortableToTarget(placeholder, $(event.target), self); }, ondrop: function(event) { - moveSortableToTarget(src, $(event.target), self); + if (!moved) { + moveSortableToTarget(src, $(event.target), self); + } } }); } @@ -144,23 +159,26 @@ QueryBuilder.define('sortable', function(options) { }); // Modify templates - this.on('getGroupTemplate.filter', function(h, level) { - if (level > 1) { - var $h = $(h.value); - $h.find(QueryBuilder.selectors.condition_container).after('
'); - h.value = $h.prop('outerHTML'); - } - }); + if (!options.disable_template) { + this.on('getGroupTemplate.filter', function(h, level) { + if (level > 1) { + var $h = $($.parseHTML(h.value)); + $h.find(QueryBuilder.selectors.condition_container).after('
'); + h.value = $h.prop('outerHTML'); + } + }); - this.on('getRuleTemplate.filter', function(h) { - var $h = $(h.value); - $h.find(QueryBuilder.selectors.rule_header).after('
'); - h.value = $h.prop('outerHTML'); - }); + this.on('getRuleTemplate.filter', function(h) { + var $h = $($.parseHTML(h.value)); + $h.find(QueryBuilder.selectors.rule_header).after('
'); + h.value = $h.prop('outerHTML'); + }); + } }, { inherit_no_sortable: true, inherit_no_drop: true, - icon: 'glyphicon glyphicon-sort' + icon: 'bi-sort-down', + disable_template: false }); QueryBuilder.selectors.rule_and_group_containers = QueryBuilder.selectors.rule_container + ', ' + QueryBuilder.selectors.group_container; diff --git a/src/plugins/sql-support/plugin.js b/src/plugins/sql-support/plugin.js index 6ef3bc56..4bfb996a 100644 --- a/src/plugins/sql-support/plugin.js +++ b/src/plugins/sql-support/plugin.js @@ -24,12 +24,12 @@ QueryBuilder.defaults({ greater_or_equal: { op: '>= ?' }, between: { op: 'BETWEEN ?', sep: ' AND ' }, not_between: { op: 'NOT BETWEEN ?', sep: ' AND ' }, - begins_with: { op: 'LIKE(?)', mod: '{0}%' }, - not_begins_with: { op: 'NOT LIKE(?)', mod: '{0}%' }, - contains: { op: 'LIKE(?)', mod: '%{0}%' }, - not_contains: { op: 'NOT LIKE(?)', mod: '%{0}%' }, - ends_with: { op: 'LIKE(?)', mod: '%{0}' }, - not_ends_with: { op: 'NOT LIKE(?)', mod: '%{0}' }, + 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' }, @@ -214,7 +214,7 @@ QueryBuilder.defaults({ 'named': function(values, char) { if (!char || char.length > 1) char = ':'; var regex1 = new RegExp('^\\' + char); - var regex2 = new RegExp('\\' + char + '(' + Object.keys(values).join('|') + ')', 'g'); + var regex2 = new RegExp('\\' + char + '(' + Object.keys(values).join('|') + ')\\b', 'g'); return { parse: function(v) { return regex1.test(v) ? values[v.slice(1)] : v; @@ -248,10 +248,17 @@ QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ { */ 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 (stmt === true) { + stmt = 'question_mark'; + } if (typeof stmt == 'string') { var config = getStmtConfig(stmt); stmt = this.settings.sqlStatements[config[1]](config[2]); @@ -296,11 +303,11 @@ QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ { value += sql.sep; } - if (rule.type == 'integer' || rule.type == 'double' || rule.type == 'boolean') { - v = Utils.changeType(v, rule.type, boolean_as_integer); + if (rule.type == 'boolean' && boolean_as_integer) { + v = v ? 1 : 0; } - else if (!stmt) { - v = Utils.escapeString(v); + else if (!stmt && rule.type !== 'integer' && rule.type !== 'double' && rule.type !== 'boolean') { + v = Utils.escapeString(v, sql.escape); } if (sql.mod) { @@ -321,7 +328,9 @@ QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ { } var sqlFn = function(v) { - return sql.op.replace(/\?/, v); + return sql.op.replace('?', function() { + return v; + }); }; /** @@ -450,6 +459,10 @@ QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ { 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); @@ -473,7 +486,20 @@ QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ { // it's a node if (['AND', 'OR'].indexOf(data.operation.toUpperCase()) !== -1) { // create a sub-group if the condition is not the same and it's not the first level - if (i > 0 && curr.condition != data.operation.toUpperCase()) { + + /** + * 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 @@ -553,6 +579,19 @@ QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ { 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); /** @@ -567,7 +606,7 @@ QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ { id: id, field: field, operator: opVal.op, - value: opVal.val + value: finalValue }, data); curr.rules.push(rule); diff --git a/src/public.js b/src/public.js index 610a4adf..8e8117af 100644 --- a/src/public.js +++ b/src/public.js @@ -46,6 +46,10 @@ QueryBuilder.prototype.reset = function() { 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); /** @@ -54,6 +58,8 @@ QueryBuilder.prototype.reset = function() { * @memberof QueryBuilder */ this.trigger('afterReset'); + + this.trigger('rulesChanged'); }; /** @@ -86,6 +92,8 @@ QueryBuilder.prototype.clear = function() { * @memberof QueryBuilder */ this.trigger('afterClear'); + + this.trigger('rulesChanged'); }; /** @@ -264,7 +272,7 @@ QueryBuilder.prototype.getRules = function(options) { }; if (rule.filter && rule.filter.data || rule.data) { - ruleData.data = $.extendext(true, 'replace', {}, rule.filter.data, rule.data); + ruleData.data = $.extendext(true, 'replace', {}, rule.filter ? rule.filter.data : {}, rule.data); } if (options.get_flags) { @@ -344,7 +352,6 @@ QueryBuilder.prototype.setRules = function(data, options) { this.clear(); this.setRoot(false, data.data, this.parseGroupFlags(data)); - this.applyGroupFlags(this.model.root); /** * Modifies data before the {@link QueryBuilder#setRules} method @@ -387,8 +394,6 @@ QueryBuilder.prototype.setRules = function(data, options) { return; } - self.applyGroupFlags(model); - add(item, model); } } @@ -410,21 +415,24 @@ QueryBuilder.prototype.setRules = function(data, options) { 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.filter) { + model.operator = self.getOperatorByType(item.operator, !options.allow_invalid); - if (model.operator && model.operator.nb_inputs !== 0 && item.value !== undefined) { - model.value = item.value; - } + if (!model.operator) { + model.operator = self.getOperators(model.filter)[0]; } } - self.applyRuleFlags(model); + 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 diff --git a/src/scss/default.scss b/src/scss/default.scss index b8445fb7..9091634c 100644 --- a/src/scss/default.scss +++ b/src/scss/default.scss @@ -109,12 +109,6 @@ $ticks-position: 5px, 10px !default; display: block; } } - - select, - input[type='text'], - input[type='number'] { - padding: 1px; - } } // ERRORS @@ -171,6 +165,3 @@ $ticks-position: 5px, 10px !default; } } } - -// import -// endimport diff --git a/src/template.js b/src/template.js index 40b52ce1..219aa06e 100644 --- a/src/template.js +++ b/src/template.js @@ -1,91 +1,119 @@ -QueryBuilder.templates.group = '\ -
\ -
\ -
\ - \ - {{? it.settings.allow_groups===-1 || it.settings.allow_groups>=it.level }} \ - \ - {{?}} \ - {{? it.level>1 }} \ - \ - {{?}} \ -
\ -
\ - {{~ it.conditions: condition }} \ - \ - {{~}} \ -
\ - {{? it.settings.display_errors }} \ -
\ - {{?}} \ -
\ -
\ -
    \ -
    \ -
    '; +QueryBuilder.templates.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.rule = '\ -
  • \ -
    \ -
    \ - \ -
    \ -
    \ - {{? it.settings.display_errors }} \ -
    \ - {{?}} \ -
    \ -
    \ -
    \ -
  • '; +QueryBuilder.templates.filterSelect = ({ rule, filters, icons, settings, translate, builder }) => { + let optgroup = null; + return ` +`; +}; -QueryBuilder.templates.filterSelect = '\ -{{ var optgroup = null; }} \ -'; +QueryBuilder.templates.operatorSelect = ({ rule, operators, icons, settings, translate, builder }) => { + let optgroup = null; + return ` +${operators.length === 1 ? ` + +${translate("operators", operators[0].type)} + +` : ''} +`; +}; -QueryBuilder.templates.operatorSelect = '\ -{{? it.operators.length === 1 }} \ - \ -{{= it.translate("operators", it.operators[0].type) }} \ - \ -{{?}} \ -{{ var optgroup = null; }} \ -'; +QueryBuilder.templates.ruleValueSelect = ({ name, rule, icons, settings, translate, builder }) => { + let optgroup = null; + return ` +`; +}; /** * Returns group's HTML @@ -95,26 +123,26 @@ QueryBuilder.templates.operatorSelect = '\ * @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) - }); +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); + /** + * 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); }; /** @@ -124,23 +152,23 @@ QueryBuilder.prototype.getGroupTemplate = function(group_id, level) { * @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) - }); +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); + /** + * Modifies the raw HTML of a rule + * @event changer:getRuleTemplate + * @memberof QueryBuilder + * @param {string} html + * @returns {string} + */ + return this.change('getRuleTemplate', h); }; /** @@ -151,26 +179,26 @@ QueryBuilder.prototype.getRuleTemplate = function(rule_id) { * @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) - }); +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); + /** + * 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); }; /** @@ -181,26 +209,56 @@ QueryBuilder.prototype.getRuleFilterSelect = function(rule, filters) { * @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) - }); +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); + /** + * 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); }; /** @@ -211,74 +269,68 @@ QueryBuilder.prototype.getRuleOperatorSelect = function(rule, operators) { * @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 = ''; +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; + if (typeof filter.input == 'function') { + h = filter.input.call(this, rule, name); + } + else { + switch (filter.input) { + case 'radio': + case 'checkbox': + Utils.iterateOptions(filter.values, function (key, val) { + h += ' ' + val + ' '; + }); + break; - case 'select': - h += ''; - break; + case 'select': + h = this.getRuleValueSelect(name, rule); + break; - case 'textarea': - h += '