From 49cc55a5195741c60b0e0294363ba9f9a5a4c948 Mon Sep 17 00:00:00 2001 From: mistic100 Date: Sat, 21 Jan 2017 14:57:41 +0100 Subject: [PATCH 01/16] Revamp grunt config --- Gruntfile.js | 401 ++++---------- build/cleanLn.js | 3 + build/initConfig.js | 68 +++ build/processLang.js | 30 + build/removeJshint.js | 5 + build/tasks/describeErrors.js | 39 ++ build/tasks/describeTriggers.js | 36 ++ build/tasks/listModules.js | 23 + examples/index.html | 936 +++++++++++++++++--------------- package.json | 4 +- src/scss/default.scss | 3 + tests/index.html | 65 ++- 12 files changed, 828 insertions(+), 785 deletions(-) create mode 100644 build/cleanLn.js create mode 100644 build/initConfig.js create mode 100644 build/processLang.js create mode 100644 build/removeJshint.js create mode 100644 build/tasks/describeErrors.js create mode 100644 build/tasks/describeTriggers.js create mode 100644 build/tasks/listModules.js diff --git a/Gruntfile.js b/Gruntfile.js index 36ab0d62..3725a9be 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,52 +1,23 @@ -var deepmerge = require('deepmerge'); +var initConfig = require('./build/initConfig'); +var processLang = require('./build/processLang'); +var removeJshint = require('./build/removeJshint'); +var cleanLn = require('./build/cleanLn'); +var taskDescribeTriggers = require('./build/tasks/describeTriggers'); +var taskDescribeErrors = require('./build/tasks/describeErrors'); +var taskListModules = require('./build/tasks/listModules'); module.exports = function(grunt) { require('time-grunt')(grunt); require('jit-grunt')(grunt, { - scsslint: 'grunt-scss-lint' + scsslint: 'grunt-scss-lint', + sass_injection: 'grunt-sass-injection', + usebanner: 'grunt-banner' }); grunt.util.linefeed = '\n'; - function removeJshint(src) { - return src - .replace(/\/\*jshint [a-z:]+ \*\/\r?\n\r?\n?/g, '') - .replace(/\/\*jshint -[EWI]{1}[0-9]{3} \*\/\r?\n\r?\n?/g, ''); - } - - function process_lang(file, src, wrapper) { - var lang = file.split(/[\/\.]/)[2]; - var content = JSON.parse(src); - wrapper = wrapper || ['', '']; - - grunt.config.set('lang_locale', content.__locale || lang); - grunt.config.set('lang_author', content.__author); - var header = grunt.template.process('<%= langBanner %>'); - - loaded_plugins.forEach(function(p) { - var plugin_file = 'src/plugins/' + p + '/i18n/' + lang + '.json'; - - if (grunt.file.exists(plugin_file)) { - content = deepmerge(content, grunt.file.readJSON(plugin_file)); - } - }); - - return header - + '\n\n' - + wrapper[0] - + 'QueryBuilder.regional[\'' + lang + '\'] = ' - + JSON.stringify(content, null, 2) - + ';\n\n' - + 'QueryBuilder.defaults({ lang_code: \'' + lang + '\' });' - + wrapper[1]; - } - - - var all_plugins = {}, - all_langs = {}, - loaded_plugins = [], - loaded_langs = [], - js_core_files = [ + var config = initConfig(grunt, { + js_core_files: [ 'src/main.js', 'src/defaults.js', 'src/core.js', @@ -57,88 +28,23 @@ module.exports = function(grunt) { 'src/utils.js', 'src/jquery.js' ], - js_files_to_load = js_core_files.slice(), - all_js_files = js_core_files.slice(), - js_files_for_standalone = [ + js_files_for_standalone: [ 'bower_components/jquery-extendext/jQuery.extendext.js', 'bower_components/doT/doT.js', 'dist/js/query-builder.js' - ]; - - - (function() { - // list available plugins and languages - grunt.file.expand('src/plugins/**/plugin.js') - .forEach(function(f) { - var n = f.split('/')[2]; - all_plugins[n] = f; - }); - - grunt.file.expand('src/i18n/*.json') - .forEach(function(f) { - var n = f.split(/[\/\.]/)[2]; - all_langs[n] = f; - }); - - // fill all js files - for (var p in all_plugins) { - all_js_files.push(all_plugins[p]); - } - - // parse 'plugins' parameter - var arg_plugins = grunt.option('plugins'); - if (typeof arg_plugins === 'string') { - arg_plugins.replace(/ /g, '').split(',').forEach(function(p) { - if (all_plugins[p]) { - js_files_to_load.push(all_plugins[p]); - loaded_plugins.push(p); - } - else { - grunt.fail.warn('Plugin ' + p + ' unknown'); - } - }); - } - else if (arg_plugins === undefined) { - for (var p in all_plugins) { - js_files_to_load.push(all_plugins[p]); - loaded_plugins.push(p); - } - } - - // default language - js_files_to_load.push('.temp/i18n/en.js'); - loaded_langs.push('en'); - - // parse 'lang' parameter - var arg_langs = grunt.option('languages'); - if (typeof arg_langs === 'string') { - arg_langs.replace(/ /g, '').split(',').forEach(function(l) { - if (all_langs[l]) { - if (l !== 'en') { - js_files_to_load.push(all_langs[l].replace(/^src/, '.temp').replace(/json$/, 'js')); - loaded_langs.push(l); - } - } - else { - grunt.fail.warn('Language ' + l + ' unknown'); - } - }); - } - }()); - + ] + }); grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), - banner: - '/*!\n' + + 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' + + langBanner: '/*!\n' + ' * jQuery QueryBuilder <%= pkg.version %>\n' + ' * Locale: <%= lang_locale %>\n' + '<% if (lang_author) { %> * Author: <%= lang_author %>\n<% } %>' + @@ -163,7 +69,7 @@ module.exports = function(grunt) { }, js: { files: ['src/*.js', 'src/plugins/**/plugin.js'], - tasks: ['build_js'] + tasks: ['injector:example'] }, css: { files: ['src/scss/*.scss', 'src/plugins/**/plugin.scss'], @@ -197,10 +103,10 @@ module.exports = function(grunt) { }] }, sass_plugins: { - files: loaded_plugins.map(function(name) { + files: config.loaded_plugins.map(function(name) { return { src: 'src/plugins/' + name + '/plugin.scss', - dest: 'dist/scss/plugins/' + name + '.scss' + dest: 'dist/scss/plugins/_' + name + '.scss' }; }) } @@ -209,19 +115,19 @@ module.exports = function(grunt) { concat: { // concat all JS js: { - src: js_files_to_load, + src: config.js_files_to_load, dest: 'dist/js/query-builder.js', options: { stripBanners: false, separator: '\n\n', process: function(src) { - return removeJshint(src).replace(/\r\n/g, '\n'); + return cleanLn(removeJshint(src)); } } }, // create standalone version js_standalone: { - src: js_files_for_standalone, + src: config.js_files_for_standalone, dest: 'dist/js/query-builder.standalone.js', options: { stripBanners: false, @@ -229,15 +135,14 @@ module.exports = function(grunt) { process: function(src, file) { var name = file.match(/([^\/]+?).js$/)[1]; - return removeJshint(src) - .replace(/\r\n/g, '\n') + return cleanLn(removeJshint(src)) .replace(/define\((.*?)\);/, 'define(\'' + name + '\', $1);'); } } }, // compile language files with AMD wrapper lang: { - files: Object.keys(all_langs).map(function(name) { + files: Object.keys(config.all_langs).map(function(name) { return { src: 'src/i18n/' + name + '.json', dest: 'dist/i18n/query-builder.' + name + '.js' @@ -245,14 +150,14 @@ module.exports = function(grunt) { }), options: { process: function(src, file) { - var wrapper = grunt.file.read('src/i18n/.wrapper.js').replace(/\r\n/g, '\n').split(/@@js\n/); - return process_lang(file, src, wrapper); + 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 wrapper + // compile language files without AMD wrapper lang_temp: { - files: Object.keys(all_langs).map(function(name) { + files: Object.keys(config.all_langs).map(function(name) { return { src: 'src/i18n/' + name + '.json', dest: '.temp/i18n/' + name + '.js' @@ -260,60 +165,50 @@ module.exports = function(grunt) { }), options: { process: function(src, file) { - return process_lang(file, src); + return processLang(grunt, config.loaded_plugins)(file, src); } } - }, - // add banner to CSS files - css: { - options: { - banner: '<%= banner %>\n\n', - }, - files: [{ - expand: true, - src: ['dist/css/*.css', 'dist/scss/*.scss'], - dest: '' - }] } }, + // add AMD wrapper wrap: { - // add AMD wrapper and banner js: { src: ['dist/js/query-builder.js'], dest: '', options: { separator: '', wrapper: function() { - var wrapper = grunt.file.read('src/.wrapper.js').replace(/\r\n/g, '\n').split(/@@js\n/); - - if (loaded_plugins.length) { - wrapper[0] = '// Plugins: ' + loaded_plugins.join(', ') + '\n' + wrapper[0]; - } - if (loaded_langs.length) { - wrapper[0] = '// Languages: ' + loaded_langs.join(', ') + '\n' + wrapper[0]; - } - wrapper[0] = grunt.template.process('<%= banner %>\n\n') + wrapper[0]; - - return wrapper; + return cleanLn(grunt.file.read('src/.wrapper.js')).split(/@@js\n/); } } + } + }, + + // add banners + usebanner: { + options: { + banner: '<%= banner %>' }, - // add plugins SASS imports - sass: { - src: ['dist/scss/default.scss'], - dest: '', + js: { + src: ['dist/js/*.js'] + }, + css: { + src: ['dist/css/*.css', 'dist/css/*.scss'] + } + }, + + // add plugins SASS imports + sass_injection: { + dist: { options: { - separator: '', - wrapper: function() { - return ['', loaded_plugins.reduce(function(wrapper, name) { - if (grunt.file.exists('dist/scss/plugins/' + name + '.scss')) { - wrapper += '\n@import \'plugins/' + name + '\';'; - } - return wrapper; - }, '\n')]; + replacePath: { + pattern: 'dist/scss/', + replace: '' } - } + }, + src: ['dist/scss/plugins/*.scss'], + target: 'dist/scss/default.scss' } }, @@ -340,7 +235,7 @@ module.exports = function(grunt) { // compress js uglify: { options: { - banner: '<%= banner %>\n\n', + banner: '<%= banner %>\n', mangle: { except: ['$'] } }, dist: { @@ -380,7 +275,7 @@ module.exports = function(grunt) { options: { jshintrc: '.jshintrc' }, - src: js_files_to_load + src: ['src/**/*.js', '!src/**/.wrapper.js'] } }, @@ -390,7 +285,7 @@ module.exports = function(grunt) { options: { config: '.jscsrc' }, - src: js_files_to_load + src: ['src/**/*.js', '!src/**/.wrapper.js'] } }, @@ -405,42 +300,32 @@ module.exports = function(grunt) { } }, - // inject all source files and test modules in the test file - 'string-replace': { - test: { - src: 'tests/index.html', - dest: 'tests/index.html', + // 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: { - replacements: [{ - pattern: /()(?:[\s\S]*)()/m, - replacement: function(match, m1, m2) { - var scripts = '\n'; - - js_core_files.forEach(function(file) { - scripts += '\n'; - }); - - scripts += '\n'; - - for (var p in all_plugins) { - scripts += '\n'; - } - - return m1 + scripts + m2; - } - }, { - pattern: /()(?:[\s\S]*)()/m, - replacement: function(match, m1, m2) { - var scripts = '\n'; - - grunt.file.expand('tests/*.module.js').forEach(function(file) { - scripts += '\n'; - }); - - return m1 + scripts + m2; - } - }] - } + 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' } }, @@ -473,115 +358,23 @@ module.exports = function(grunt) { force: true }, all: { - src: '.coverage-results/all.lcov', + src: '.coverage-results/all.lcov' } } }); - // list the triggers and changes - grunt.registerTask('describe_triggers', 'List QueryBuilder triggers.', function() { - var triggers = {}; - var total = 0; - - for (var f in all_js_files) { - grunt.file.read(all_js_files[f]).split(/\r?\n/).forEach(function(line, i) { - var matches = /(e = )?(?:this|that)\.(trigger|change)\('(\w+)'([^)]*)\);/.exec(line); - if (matches !== null) { - triggers[matches[3]] = { - name: matches[3], - type: matches[2], - file: all_js_files[f], - line: i, - args: matches[4].slice(2), - prevent: !!matches[1] - }; - - total++; - } - }); - } - - grunt.log.write('\n'); - - for (var t in triggers) { - grunt.log.write(t['cyan'] + ' ' + triggers[t].type['magenta']); - if (triggers[t].prevent) grunt.log.write(' (*)'['yellow']); - grunt.log.write('\n'); - grunt.log.writeln(' ' + (triggers[t].file + ':' + triggers[t].line)['red'] + ' ' + triggers[t].args); - grunt.log.write('\n'); - } - - grunt.log.writeln((total + ' Triggers in QueryBuilder.')['cyan']['bold']); - }); - - // list all possible thrown errors - grunt.registerTask('describe_errors', 'List QueryBuilder errors.', function() { - var errors = {}; - var total = 0; - - for (var f in all_js_files) { - grunt.file.read(all_js_files[f]).split(/\r?\n/).forEach(function(line, i) { - var matches = /Utils\.error\('(\w+)', '([^)]+)'([^)]*)\);/.exec(line); - if (matches !== null) { - (errors[matches[1]] = errors[matches[1]] || []).push({ - type: matches[1], - message: matches[2], - file: all_js_files[f], - line: i, - args: matches[3].slice(2).split(', ') - }); - - total++; - } - }); - } - - grunt.log.write('\n'); - - for (var e in errors) { - grunt.log.writeln((e + 'Error')['cyan']); - errors[e].forEach(function(error) { - var message = error.message.replace(/{([0-9]+)}/g, function(m, i) { - return error.args[parseInt(i)]['yellow']; - }); - grunt.log.writeln(' ' + (error.file + ':' + error.line)['red']); - grunt.log.writeln(' ' + message); - }); - grunt.log.write('\n'); - } - - grunt.log.writeln((total + ' Errors in QueryBuilder.')['cyan']['bold']); - }); - - // display available modules - grunt.registerTask('list_modules', 'List QueryBuilder plugins and languages.', function() { - grunt.log.writeln('\nAvailable QueryBuilder plugins:\n'); - - for (var p in all_plugins) { - grunt.log.write(p['cyan']); - - if (grunt.file.exists(all_plugins[p].replace(/js$/, 'scss'))) { - grunt.log.write(' + CSS'); - } - - grunt.log.write('\n'); - } - - grunt.log.writeln('\nAvailable QueryBuilder languages:\n'); - - for (var l in all_langs) { - if (l !== 'en') { - grunt.log.writeln(l['cyan']); - } - } - }); + // custom tasks + taskDescribeTriggers(grunt, config); + taskDescribeErrors(grunt, config); + taskListModules(grunt, config); grunt.registerTask('build_js', [ 'concat:lang_temp', 'concat:js', 'wrap:js', + 'usebanner:js', 'concat:js_standalone', 'uglify', 'clean:temp' @@ -590,10 +383,10 @@ module.exports = function(grunt) { grunt.registerTask('build_css', [ 'copy:sass_core', 'copy:sass_plugins', - 'wrap:sass', + 'sass_injection', 'sass', 'cssmin', - 'concat:css' + 'usebanner:css' ]); grunt.registerTask('build_lang', [ @@ -610,14 +403,18 @@ module.exports = function(grunt) { 'jshint', 'jscs', 'scsslint', - 'default', - 'string-replace:test', + 'build_lang', + 'build_css', + 'injector:testSrc', + 'injector:testModules', 'qunit_blanket_lcov', 'qunit' ]); grunt.registerTask('serve', [ - 'default', + 'build_lang', + 'build_css', + 'injector:example', 'open', 'connect', 'watch' diff --git a/build/cleanLn.js b/build/cleanLn.js new file mode 100644 index 00000000..f93432e8 --- /dev/null +++ b/build/cleanLn.js @@ -0,0 +1,3 @@ +module.exports = function(src) { + return src.replace(/\r\n/g, '\n'); +}; diff --git a/build/initConfig.js b/build/initConfig.js new file mode 100644 index 00000000..5bb333ee --- /dev/null +++ b/build/initConfig.js @@ -0,0 +1,68 @@ +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/processLang.js b/build/processLang.js new file mode 100644 index 00000000..33c6b2c7 --- /dev/null +++ b/build/processLang.js @@ -0,0 +1,30 @@ +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 new file mode 100644 index 00000000..4ff583ac --- /dev/null +++ b/build/removeJshint.js @@ -0,0 +1,5 @@ +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/build/tasks/describeErrors.js b/build/tasks/describeErrors.js new file mode 100644 index 00000000..c058e6e5 --- /dev/null +++ b/build/tasks/describeErrors.js @@ -0,0 +1,39 @@ +module.exports = function(grunt, config) { + grunt.registerTask('describe_errors', 'List QueryBuilder errors.', function() { + var errors = {}; + var total = 0; + + for (var f in config.all_js_files) { + grunt.file.read(config.all_js_files[f]).split(/\r?\n/).forEach(function(line, i) { + var matches = /Utils\.error\((?:[^)]+, )?'(\w+)', '([^)]+)'([^)]*)\);/.exec(line); + if (matches !== null) { + (errors[matches[1]] = errors[matches[1]] || []).push({ + type: matches[1], + message: matches[2], + file: config.all_js_files[f], + line: i, + args: matches[3].slice(2).split(', ') + }); + + total++; + } + }); + } + + grunt.log.write('\n'); + + for (var e in errors) { + grunt.log.writeln((e + 'Error')['cyan']); + errors[e].forEach(function(error) { + var message = error.message.replace(/{([0-9]+)}/g, function(m, i) { + return error.args[parseInt(i)]['yellow']; + }); + grunt.log.writeln(' ' + (error.file + ':' + error.line)['red']); + grunt.log.writeln(' ' + message); + }); + grunt.log.write('\n'); + } + + grunt.log.writeln((total + ' Errors in QueryBuilder.')['cyan']['bold']); + }); +}; diff --git a/build/tasks/describeTriggers.js b/build/tasks/describeTriggers.js new file mode 100644 index 00000000..68b36ec9 --- /dev/null +++ b/build/tasks/describeTriggers.js @@ -0,0 +1,36 @@ +module.exports = function(grunt, config) { + grunt.registerTask('describe_triggers', 'List QueryBuilder triggers.', function() { + var triggers = {}; + var total = 0; + + for (var f in config.all_js_files) { + grunt.file.read(config.all_js_files[f]).split(/\r?\n/).forEach(function(line, i) { + var matches = /(e = )?(?:this|that)\.(trigger|change)\('(\w+)'([^)]*)\);/.exec(line); + if (matches !== null) { + triggers[matches[3]] = { + name: matches[3], + type: matches[2], + file: config.all_js_files[f], + line: i, + args: matches[4].slice(2), + prevent: !!matches[1] + }; + + total++; + } + }); + } + + grunt.log.write('\n'); + + for (var t in triggers) { + grunt.log.write(t['cyan'] + ' ' + triggers[t].type['magenta']); + if (triggers[t].prevent) grunt.log.write(' (*)'['yellow']); + grunt.log.write('\n'); + grunt.log.writeln(' ' + (triggers[t].file + ':' + triggers[t].line)['red'] + ' ' + triggers[t].args); + grunt.log.write('\n'); + } + + grunt.log.writeln((total + ' Triggers in QueryBuilder.')['cyan']['bold']); + }); +}; diff --git a/build/tasks/listModules.js b/build/tasks/listModules.js new file mode 100644 index 00000000..95d02fbf --- /dev/null +++ b/build/tasks/listModules.js @@ -0,0 +1,23 @@ +module.exports = function(grunt, config) { + grunt.registerTask('list_modules', 'List QueryBuilder plugins and languages.', function() { + grunt.log.writeln('\nAvailable QueryBuilder plugins:\n'); + + for (var p in config.all_plugins) { + grunt.log.write(p['cyan']); + + if (grunt.file.exists(config.all_plugins[p].replace(/js$/, 'scss'))) { + grunt.log.write(' + CSS'); + } + + grunt.log.write('\n'); + } + + grunt.log.writeln('\nAvailable QueryBuilder languages:\n'); + + for (var l in config.all_langs) { + if (l !== 'en') { + grunt.log.writeln(l['cyan']); + } + } + }); +}; diff --git a/examples/index.html b/examples/index.html index 2f232ee6..054b83ec 100644 --- a/examples/index.html +++ b/examples/index.html @@ -16,7 +16,9 @@ @@ -28,47 +30,55 @@ -

jQuery QueryBuilder Example

+

jQuery QueryBuilder + Example +

- - + +
@@ -76,7 +86,9 @@

jQuery QueryBuilder Example

- +
@@ -112,479 +124,505 @@

Output

- + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package.json b/package.json index c546cfad..f4c96d12 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "devDependencies": { "deepmerge": "^0.2.0", "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", @@ -28,11 +29,12 @@ "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-open": "^0.2.3", "grunt-qunit-blanket-lcov": "^0.3.0", + "grunt-sass-injection": "^1.0.3", "grunt-scss-lint": "^0.3.8", - "grunt-string-replace": "^1.2.0", "grunt-wrap": "^0.3.0", "jit-grunt": "^0.10.0", "time-grunt": "^1.3.0" diff --git a/src/scss/default.scss b/src/scss/default.scss index ceebadd7..ace2f2fb 100644 --- a/src/scss/default.scss +++ b/src/scss/default.scss @@ -169,3 +169,6 @@ $ticks-position: 5px, 10px !default; } } } + +// import +// endimport diff --git a/tests/index.html b/tests/index.html index 4c395d25..875f9320 100644 --- a/tests/index.html +++ b/tests/index.html @@ -30,40 +30,39 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + From 4d6c44e83a98ce70ca2d3157934c2e2d8c707276 Mon Sep 17 00:00:00 2001 From: mistic100 Date: Sun, 22 Jan 2017 11:09:49 +0100 Subject: [PATCH 02/16] Fix #418 validation fails on integer with separator --- .npmignore | 5 + bower.json | 1 + examples/index.html | 18 +++- src/core.js | 2 +- src/data.js | 219 ++++++++++++++++++++++--------------------- src/defaults.js | 1 + src/template.js | 35 ++++--- tests/common.js | 6 ++ tests/data.module.js | 11 ++- 9 files changed, 169 insertions(+), 129 deletions(-) create mode 100644 .npmignore diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..4dec3eec --- /dev/null +++ b/.npmignore @@ -0,0 +1,5 @@ +.* +build +composer.json +Gruntfile.js +bower_components diff --git a/bower.json b/bower.json index bf6b6519..9f597a38 100644 --- a/bower.json +++ b/bower.json @@ -45,6 +45,7 @@ "**/.*", "node_modules", "bower_components", + "build", "src", "tests", "composer.json", diff --git a/examples/index.html b/examples/index.html index 054b83ec..15da3790 100644 --- a/examples/index.html +++ b/examples/index.html @@ -204,7 +204,7 @@

Output

filters: [ /* - * basic + * string with separator */ { id: 'name', @@ -222,6 +222,18 @@

Output

}, unique: true }, + /* + * integer with separator for 'in' and 'not_in' + */ + { + id: 'age', + label: 'Age', + type: 'integer', + input: 'text', + value_separator: '|', + optgroup: 'core', + description: 'Use a pipe (|) to separate multiple values with "in" and "not in" operators' + }, /* * textarea */ @@ -533,6 +545,10 @@

Output

id: 'name', operator: 'in', value: ['Mistic', 'Damien'] + }, { + id: 'age', + operator: 'in', + value: [20,21,22] }, { empty: true }] diff --git a/src/core.js b/src/core.js index d829bd67..3ffe3763 100644 --- a/src/core.js +++ b/src/core.js @@ -102,7 +102,7 @@ QueryBuilder.prototype.checkFilters = function(filters) { } if (!filter.input) { - filter.input = 'text'; + filter.input = QueryBuilder.types[filter.type] === 'number' ? 'number' : 'text'; } else if (typeof filter.input != 'function' && QueryBuilder.inputs.indexOf(filter.input) == -1) { Utils.error('Config', 'Invalid input "{0}"', filter.input); diff --git a/src/data.js b/src/data.js index a1eddfa0..3aab4578 100644 --- a/src/data.js +++ b/src/data.js @@ -32,13 +32,18 @@ QueryBuilder.prototype.validateValueInternal = function(rule, value) { var operator = rule.operator; var validation = filter.validation || {}; var result = true; - var tmp; + var tmp, tempValue; if (rule.operator.nb_inputs === 1) { value = [value]; } for (var i = 0; i < operator.nb_inputs; i++) { + if (!operator.multiple && $.isArray(value[i]) && value[i].length > 1) { + result = ['operator_not_multiple', operator.type]; + break; + } + switch (filter.input) { case 'radio': if (value[i] === undefined || value[i].length === 0) { @@ -56,10 +61,6 @@ QueryBuilder.prototype.validateValueInternal = function(rule, value) { } break; } - else if (!operator.multiple && value[i].length > 1) { - result = ['operator_not_multiple', operator.type]; - break; - } break; case 'select': @@ -69,136 +70,140 @@ QueryBuilder.prototype.validateValueInternal = function(rule, value) { } break; } - if (filter.multiple && !operator.multiple && value[i].length > 1) { - result = ['operator_not_multiple', operator.type]; - break; - } break; default: - switch (QueryBuilder.types[filter.type]) { - case 'string': - if (value[i] === undefined || value[i].length === 0) { - if (!validation.allow_empty_value) { - result = ['string_empty']; - } - break; - } - if (validation.min !== undefined) { - if (value[i].length < parseInt(validation.min)) { - result = [this.getValidationMessage(validation, 'min', 'string_exceed_min_length'), validation.min]; - break; - } - } - if (validation.max !== undefined) { - if (value[i].length > parseInt(validation.max)) { - result = [this.getValidationMessage(validation, 'max', 'string_exceed_max_length'), validation.max]; + tempValue = $.isArray(value[i]) ? value[i] : [value[i]]; + + for (var j = 0; j < tempValue.length; j++) { + switch (QueryBuilder.types[filter.type]) { + case 'string': + if (tempValue[j] === undefined || tempValue[j].length === 0) { + if (!validation.allow_empty_value) { + result = ['string_empty']; + } break; } - } - if (validation.format) { - if (typeof validation.format == 'string') { - validation.format = new RegExp(validation.format); + if (validation.min !== undefined) { + if (tempValue[j].length < parseInt(validation.min)) { + result = [this.getValidationMessage(validation, 'min', 'string_exceed_min_length'), validation.min]; + break; + } } - if (!validation.format.test(value[i])) { - result = [this.getValidationMessage(validation, 'format', 'string_invalid_format'), validation.format]; - break; + if (validation.max !== undefined) { + if (tempValue[j].length > parseInt(validation.max)) { + result = [this.getValidationMessage(validation, 'max', 'string_exceed_max_length'), validation.max]; + break; + } } - } - break; - - case 'number': - if (value[i] === undefined || value[i].length === 0) { - if (!validation.allow_empty_value) { - result = ['number_nan']; + if (validation.format) { + if (typeof validation.format == 'string') { + validation.format = new RegExp(validation.format); + } + if (!validation.format.test(tempValue[j])) { + result = [this.getValidationMessage(validation, 'format', 'string_invalid_format'), validation.format]; + break; + } } break; - } - if (isNaN(value[i])) { - result = ['number_nan']; - break; - } - if (filter.type == 'integer') { - if (parseInt(value[i]) != value[i]) { - result = ['number_not_integer']; + + case 'number': + if (tempValue[j] === undefined || tempValue[j].length === 0) { + if (!validation.allow_empty_value) { + result = ['number_nan']; + } break; } - } - else { - if (parseFloat(value[i]) != value[i]) { - result = ['number_not_double']; + if (isNaN(tempValue[j])) { + result = ['number_nan']; break; } - } - if (validation.min !== undefined) { - if (value[i] < parseFloat(validation.min)) { - result = [this.getValidationMessage(validation, 'min', 'number_exceed_min'), validation.min]; - break; + if (filter.type == 'integer') { + if (parseInt(tempValue[j]) != tempValue[j]) { + result = ['number_not_integer']; + break; + } } - } - if (validation.max !== undefined) { - if (value[i] > parseFloat(validation.max)) { - result = [this.getValidationMessage(validation, 'max', 'number_exceed_max'), validation.max]; - break; + else { + if (parseFloat(tempValue[j]) != tempValue[j]) { + result = ['number_not_double']; + break; + } } - } - if (validation.step !== undefined && validation.step !== 'any') { - var v = (value[i] / validation.step).toPrecision(14); - if (parseInt(v) != v) { - result = [this.getValidationMessage(validation, 'step', 'number_wrong_step'), validation.step]; - break; + if (validation.min !== undefined) { + if (tempValue[j] < parseFloat(validation.min)) { + result = [this.getValidationMessage(validation, 'min', 'number_exceed_min'), validation.min]; + break; + } } - } - break; - - case 'datetime': - if (value[i] === undefined || value[i].length === 0) { - if (!validation.allow_empty_value) { - result = ['datetime_empty']; + if (validation.max !== undefined) { + if (tempValue[j] > parseFloat(validation.max)) { + result = [this.getValidationMessage(validation, 'max', 'number_exceed_max'), validation.max]; + break; + } } - break; - } - - // we need MomentJS - if (validation.format) { - if (!('moment' in window)) { - Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com'); + if (validation.step !== undefined && validation.step !== 'any') { + var v = (tempValue[j] / validation.step).toPrecision(14); + if (parseInt(v) != v) { + result = [this.getValidationMessage(validation, 'step', 'number_wrong_step'), validation.step]; + break; + } } + break; - var datetime = moment(value[i], validation.format); - if (!datetime.isValid()) { - result = [this.getValidationMessage(validation, 'format', 'datetime_invalid'), validation.format]; + case 'datetime': + if (tempValue[j] === undefined || tempValue[j].length === 0) { + if (!validation.allow_empty_value) { + result = ['datetime_empty']; + } break; } - else { - if (validation.min) { - if (datetime < moment(validation.min, validation.format)) { - result = [this.getValidationMessage(validation, 'min', 'datetime_exceed_min'), validation.min]; - break; - } + + // we need MomentJS + if (validation.format) { + if (!('moment' in window)) { + Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com'); + } + + var datetime = moment(tempValue[j], validation.format); + if (!datetime.isValid()) { + result = [this.getValidationMessage(validation, 'format', 'datetime_invalid'), validation.format]; + break; } - if (validation.max) { - if (datetime > moment(validation.max, validation.format)) { - result = [this.getValidationMessage(validation, 'max', 'datetime_exceed_max'), validation.max]; - break; + else { + if (validation.min) { + if (datetime < moment(validation.min, validation.format)) { + result = [this.getValidationMessage(validation, 'min', 'datetime_exceed_min'), validation.min]; + break; + } + } + if (validation.max) { + if (datetime > moment(validation.max, validation.format)) { + result = [this.getValidationMessage(validation, 'max', 'datetime_exceed_max'), validation.max]; + break; + } } } } - } - break; + break; - case 'boolean': - if (value[i] === undefined || value[i].length === 0) { - if (!validation.allow_empty_value) { + case 'boolean': + if (tempValue[j] === undefined || tempValue[j].length === 0) { + if (!validation.allow_empty_value) { + result = ['boolean_not_valid']; + } + break; + } + tmp = ('' + tempValue[j]).trim().toLowerCase(); + if (tmp !== 'true' && tmp !== 'false' && tmp !== '1' && tmp !== '0' && tempValue[j] !== 1 && tempValue[j] !== 0) { result = ['boolean_not_valid']; + break; } - break; - } - tmp = ('' + value[i]).trim().toLowerCase(); - if (tmp !== 'true' && tmp !== 'false' && tmp !== '1' && tmp !== '0' && value[i] !== 1 && value[i] !== 0) { - result = ['boolean_not_valid']; - break; - } + } + + if (result !== true) { + break; + } } } diff --git a/src/defaults.js b/src/defaults.js index fffde6f5..1fd031a1 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -16,6 +16,7 @@ QueryBuilder.types = { */ QueryBuilder.inputs = [ 'text', + 'number', 'textarea', 'radio', 'checkbox', diff --git a/src/template.js b/src/template.js index 3a2756e4..c5869c1a 100644 --- a/src/template.js +++ b/src/template.js @@ -210,26 +210,23 @@ QueryBuilder.prototype.getRuleInput = function(rule, value_id) { h+= '>'; break; + case 'number': + h+= ' Date: Sun, 22 Jan 2017 11:26:06 +0100 Subject: [PATCH 03/16] filter-description: description can be a function --- examples/index.html | 6 +++- src/plugins/filter-description/plugin.js | 41 ++++++++++++++++++------ tests/plugins-gui.module.js | 17 +++++++++- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/examples/index.html b/examples/index.html index 15da3790..58cab1fe 100644 --- a/examples/index.html +++ b/examples/index.html @@ -232,7 +232,11 @@

Output

input: 'text', value_separator: '|', optgroup: 'core', - description: 'Use a pipe (|) to separate multiple values with "in" and "not in" operators' + description: function(rule) { + if (rule.operator && ['in', 'not_in'].indexOf(rule.operator.type) !== -1) { + return 'Use a pipe (|) to separate multiple values with "in" and "not in" operators'; + } + } }, /* * textarea diff --git a/src/plugins/filter-description/plugin.js b/src/plugins/filter-description/plugin.js index 868ac9db..c87beb97 100644 --- a/src/plugins/filter-description/plugin.js +++ b/src/plugins/filter-description/plugin.js @@ -11,10 +11,11 @@ QueryBuilder.define('filter-description', function(options) { * INLINE */ if (options.mode === 'inline') { - this.on('afterUpdateRuleFilter', function(e, rule) { + this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) { var $p = rule.$el.find('p.filter-description'); + var description = e.builder.getFilterDescription(rule.filter, rule); - if (!rule.filter || !rule.filter.description) { + if (!description) { $p.hide(); } else { @@ -26,7 +27,7 @@ QueryBuilder.define('filter-description', function(options) { $p.show(); } - $p.html(' ' + rule.filter.description); + $p.html(' ' + description); } }); } @@ -38,10 +39,11 @@ QueryBuilder.define('filter-description', function(options) { Utils.error('MissingLibrary', 'Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com'); } - this.on('afterUpdateRuleFilter', function(e, rule) { + this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) { var $b = rule.$el.find('button.filter-description'); + var description = e.builder.getFilterDescription(rule.filter, rule); - if (!rule.filter || !rule.filter.description) { + if (!description) { $b.hide(); if ($b.data('bs.popover')) { @@ -67,7 +69,7 @@ QueryBuilder.define('filter-description', function(options) { $b.show(); } - $b.data('bs.popover').options.content = rule.filter.description; + $b.data('bs.popover').options.content = description; if ($b.attr('aria-describedby')) { $b.popover('show'); @@ -83,10 +85,11 @@ QueryBuilder.define('filter-description', function(options) { Utils.error('MissingLibrary', 'Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com'); } - this.on('afterUpdateRuleFilter', function(e, rule) { + this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) { var $b = rule.$el.find('button.filter-description'); + var description = e.builder.getFilterDescription(rule.filter, rule); - if (!rule.filter || !rule.filter.description) { + if (!description) { $b.hide(); } else { @@ -99,7 +102,7 @@ QueryBuilder.define('filter-description', function(options) { }); } - $b.data('description', rule.filter.description); + $b.data('description', description); } }); } @@ -107,3 +110,23 @@ QueryBuilder.define('filter-description', function(options) { icon: 'glyphicon glyphicon-info-sign', mode: 'popover' }); + +QueryBuilder.extend({ + /** + * Returns the description of a filter for a particular rule (if present) + * @param {object} filter + * @param {Rule} [rule] + * @returns {string} + */ + getFilterDescription: function(filter, rule) { + if (!filter) { + return undefined; + } + else if (typeof filter.description == 'function') { + return filter.description.call(this, rule); + } + else { + return filter.description; + } + } +}); diff --git a/tests/plugins-gui.module.js b/tests/plugins-gui.module.js index e344bab2..aefd2743 100644 --- a/tests/plugins-gui.module.js +++ b/tests/plugins-gui.module.js @@ -134,6 +134,12 @@ $(function(){ id: 'name', type: 'string', description: 'Lorem Ipsum sit amet.' + }, { + id: 'age', + type: 'integer', + description: function(rule) { + return 'Description of operator ' + rule.operator.type; + } }]; var rules = { @@ -141,6 +147,9 @@ $(function(){ rules: [{ id: 'name', value: 'Mistic' + }, { + id: 'age', + value: 25 }] }; @@ -154,10 +163,16 @@ $(function(){ assert.match( $('#builder_rule_0 p.filter-description').html(), - new RegExp(filters[0].description), + new RegExp('Lorem Ipsum sit amet.'), 'Paragraph should contain filter description' ); + assert.match( + $('#builder_rule_1 p.filter-description').html(), + new RegExp('Description of operator equal'), + 'Paragraph should contain filter description after function execution' + ); + $b.queryBuilder('destroy'); $b.queryBuilder({ From 62f6f446419926488178977963cc79eefa1e4275 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 1 Feb 2017 16:34:56 +0100 Subject: [PATCH 04/16] Merge pull request #428 Fix #426 add "skip_empty" flag to "getRules" --- examples/index.html | 5 ++++- src/public.js | 35 ++++++++++++++++++++++++------ tests/data.module.js | 51 +++++++++++++++++++++++++++++++++++++------- 3 files changed, 76 insertions(+), 15 deletions(-) diff --git a/examples/index.html b/examples/index.html index 58cab1fe..6704b236 100644 --- a/examples/index.html +++ b/examples/index.html @@ -595,7 +595,10 @@

Output

$('.parse-json').on('click', function() { $('#result').removeClass('hide') .find('pre').html(JSON.stringify( - $('#builder').queryBuilder('getRules', { get_flags: true }), + $('#builder').queryBuilder('getRules', { + get_flags: true, + skip_empty: true + }), undefined, 2 )); }); diff --git a/src/public.js b/src/public.js index 70db106d..7d85e666 100644 --- a/src/public.js +++ b/src/public.js @@ -72,9 +72,15 @@ QueryBuilder.prototype.getModel = function(target) { /** * Validate the whole builder + * @param {object} options + * - skip_empty: false[default] | true(skips validating rules that have no filter selected) * @return {boolean} */ -QueryBuilder.prototype.validate = function() { +QueryBuilder.prototype.validate = function(options) { + options = $.extend({ + skip_empty: false + }, options); + this.clearErrors(); var self = this; @@ -84,6 +90,10 @@ QueryBuilder.prototype.validate = function() { var errors = 0; group.each(function(rule) { + if (!rule.filter && options.skip_empty) { + return; + } + if (!rule.filter) { self.triggerValidationError(rule, 'no_filter', null); errors++; @@ -109,10 +119,11 @@ QueryBuilder.prototype.validate = function() { done++; }, function(group) { - if (parse(group)) { + var res = parse(group); + if (res === true) { done++; } - else { + else if (res === false) { errors++; } }); @@ -120,6 +131,9 @@ QueryBuilder.prototype.validate = function() { if (errors > 0) { return false; } + else if (done === 0 && !group.isRoot() && options.skip_empty) { + return null; + } else if (done === 0 && (!self.settings.allow_empty || !group.isRoot())) { self.triggerValidationError(group, 'empty_group', null); return false; @@ -137,15 +151,17 @@ QueryBuilder.prototype.validate = function() { * @param {object} options * - get_flags: false[default] | true(only changes from default flags) | 'all' * - allow_invalid: false[default] | true(returns rules even if they are invalid) + * - skip_empty: false[default] | true(remove rules that have no filter selected) * @return {object} */ QueryBuilder.prototype.getRules = function(options) { options = $.extend({ get_flags: false, - allow_invalid: false + allow_invalid: false, + skip_empty: false }, options); - var valid = this.validate(); + var valid = this.validate(options); if (!valid && !options.allow_invalid) { return null; } @@ -170,6 +186,10 @@ QueryBuilder.prototype.getRules = function(options) { } group.each(function(rule) { + if (!rule.filter && options.skip_empty) { + return; + } + var value = null; if (!rule.operator || rule.operator.nb_inputs !== 0) { value = rule.value; @@ -198,7 +218,10 @@ QueryBuilder.prototype.getRules = function(options) { groupData.rules.push(self.change('ruleToJson', ruleData, rule)); }, function(model) { - groupData.rules.push(parse(model)); + var data = parse(model); + if (data.rules.length !== 0 || !options.skip_empty) { + groupData.rules.push(data); + } }, this); return self.change('groupToJson', groupData, group); diff --git a/tests/data.module.js b/tests/data.module.js index a83c48cd..358df511 100644 --- a/tests/data.module.js +++ b/tests/data.module.js @@ -1,4 +1,4 @@ -$(function(){ +$(function() { var $b = $('#builder'); QUnit.module('data', { @@ -31,9 +31,9 @@ $(function(){ type: 'string', input: 'select', values: [ - {one: 'One'}, - {two: 'Two'}, - {three: 'Three'} + { one: 'One' }, + { two: 'Two' }, + { three: 'Three' } ] }], rules: { @@ -207,7 +207,7 @@ $(function(){ QUnit.test('custom data', function(assert) { var rules = { condition: 'AND', - data: [1,2,3], + data: [1, 2, 3], rules: [{ id: 'name', value: 'Mistic', @@ -361,13 +361,13 @@ $(function(){ }; assert.rulesMatch( - $b.queryBuilder('getRules', {get_flags: true}), + $b.queryBuilder('getRules', { get_flags: true }), rules_changed_flags, 'Should export rules with changed flags' ); assert.rulesMatch( - $b.queryBuilder('getRules', {get_flags: 'all'}), + $b.queryBuilder('getRules', { get_flags: 'all' }), rules_all_flags, 'Should export rules with all flags' ); @@ -455,13 +455,48 @@ $(function(){ ); }); + /** + * Test skip_empty option + */ + QUnit.test('skip empty', function(assert) { + $b.queryBuilder({ + filters: basic_filters + }); + + $b.queryBuilder('setRules', { + condition: 'AND', + rules: [{ + id: 'name', + operator: 'equal', + value: 'Mistic' + }, { + empty: true + }] + }); + + assert.rulesMatch( + $b.queryBuilder('getRules', { + skip_empty: true + }), + { + condition: 'AND', + rules: [{ + id: 'name', + operator: 'equal', + value: 'Mistic' + }] + }, + 'Should skip empty rules for getRules' + ); + }); + /** * Test allow_empty_value option */ QUnit.test('allow empty value', function(assert) { var filters = $.extend(true, [], basic_filters); filters.forEach(function(filter) { - filter.validation = $.extend({allow_empty_value: true}, filter.validation); + filter.validation = $.extend({ allow_empty_value: true }, filter.validation); }); $b.queryBuilder({ From 2aa23bc47328d61c5b7cfd80324bda0111eca672 Mon Sep 17 00:00:00 2001 From: mreishus Date: Thu, 9 Feb 2017 09:22:35 -0600 Subject: [PATCH 05/16] Add beforeReset / beforeClear events --- src/public.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/public.js b/src/public.js index 7d85e666..a2c1325f 100644 --- a/src/public.js +++ b/src/public.js @@ -23,6 +23,8 @@ QueryBuilder.prototype.destroy = function() { * Reset the plugin */ QueryBuilder.prototype.reset = function() { + this.trigger('beforeReset'); + this.status.group_id = 1; this.status.rule_id = 0; @@ -37,6 +39,8 @@ QueryBuilder.prototype.reset = function() { * Clear the plugin */ QueryBuilder.prototype.clear = function() { + this.trigger('beforeClear'); + this.status.group_id = 0; this.status.rule_id = 0; From 1f73bd8193e770b9dcc12c5ca466c7b6f90b72f0 Mon Sep 17 00:00:00 2001 From: mistic100 Date: Sun, 12 Feb 2017 13:10:30 +0100 Subject: [PATCH 06/16] Fix #431 re-create input when operators are from different optgroups --- src/core.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core.js b/src/core.js index 3ffe3763..22149887 100644 --- a/src/core.js +++ b/src/core.js @@ -658,7 +658,10 @@ QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) { else { $valueContainer.show(); - if ($valueContainer.is(':empty') || !previousOperator || rule.operator.nb_inputs !== previousOperator.nb_inputs) { + if ($valueContainer.is(':empty') || !previousOperator || + rule.operator.nb_inputs !== previousOperator.nb_inputs || + rule.operator.optgroup !== previousOperator.optgroup + ) { this.createRuleInput(rule); } } From cba0b70669aae07d0d2fb223d8928e2055f84509 Mon Sep 17 00:00:00 2001 From: mistic100 Date: Sun, 12 Feb 2017 18:52:10 +0100 Subject: [PATCH 07/16] a LOT of jsDoc --- .gitignore | 1 + Gruntfile.js | 13 + README.md | 2 +- package.json | 2 + src/core.js | 332 +++++++++++++----- src/data.js | 144 +++++--- src/defaults.js | 24 +- src/main.js | 138 ++++---- src/model.js | 421 +++++++++++++++-------- src/plugins/bt-checkbox/plugin.js | 23 +- src/plugins/bt-checkbox/plugins.scss | 12 + src/plugins/bt-selectpicker/plugin.js | 16 +- src/plugins/bt-tooltip-errors/plugin.js | 15 +- src/plugins/change-filters/plugin.js | 101 +++--- src/plugins/filter-description/plugin.js | 16 +- src/plugins/invert/plugin.js | 27 +- src/plugins/mongodb-support/plugin.js | 159 ++++++--- src/plugins/not-group/plugin.js | 38 +- src/plugins/sortable/plugin.js | 40 ++- src/plugins/sql-support/plugin.js | 225 ++++++++---- src/plugins/unique-filter/plugin.js | 28 +- src/public.js | 136 ++++++-- src/template.js | 136 +++++--- src/utils.js | 76 ++-- 24 files changed, 1450 insertions(+), 675 deletions(-) create mode 100644 src/plugins/bt-checkbox/plugins.scss diff --git a/.gitignore b/.gitignore index f4b28947..2b7141a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ bower_components node_modules dist +doc .sass-cache .coverage-results .idea diff --git a/Gruntfile.js b/Gruntfile.js index 3725a9be..208f6f2c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -300,6 +300,19 @@ module.exports = function(grunt) { } }, + // jsDoc generation + jsdoc: { + lib: { + src: ['src/**/*.js', '!src/**/.wrapper.js'], + dest: 'doc', + options: { + private: false, + template: 'node_modules/docdash', + readme: 'README.md' + } + } + }, + // inject sources files and tests modules in demo and test injector: { options: { diff --git a/README.md b/README.md index 4aa83f81..708b26c4 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ 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) ## Documentation -http://querybuilder.js.org +[querybuilder.js.org](http://querybuilder.js.org) ### Dependencies * jQuery >= 1.10 diff --git a/package.json b/package.json index f4c96d12..2e57f8bf 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ }, "devDependencies": { "deepmerge": "^0.2.0", + "docdash": "^0.4.0", "grunt": "^1.0.0", "grunt-banner": "^0.6.0", "grunt-contrib-clean": "^1.0.0", @@ -31,6 +32,7 @@ "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-injection": "^1.0.3", diff --git a/src/core.js b/src/core.js index 22149887..7e82498a 100644 --- a/src/core.js +++ b/src/core.js @@ -1,5 +1,9 @@ /** - * Init the builder + * Inits the builder + * @param {jQuery} $el + * @param {object} options + * @fires QueryBuilder#afterInit + * @private */ QueryBuilder.prototype.init = function($el, options) { $el[0].queryBuilder = this; @@ -63,6 +67,9 @@ QueryBuilder.prototype.init = function($el, options) { this.bindEvents(); this.initPlugins(); + /** + * @event QueryBuilder#afterInit + */ this.trigger('afterInit'); if (options.rules) { @@ -74,8 +81,43 @@ QueryBuilder.prototype.init = function($el, options) { } }; +/** + * Initializes plugins for an instance + * @throws ConfigError + * @private + */ +QueryBuilder.prototype.initPlugins = function() { + if (!this.plugins) { + return; + } + + if ($.isArray(this.plugins)) { + var tmp = {}; + this.plugins.forEach(function(plugin) { + tmp[plugin] = null; + }); + this.plugins = tmp; + } + + Object.keys(this.plugins).forEach(function(plugin) { + if (plugin in QueryBuilder.plugins) { + this.plugins[plugin] = $.extend(true, {}, + QueryBuilder.plugins[plugin].def, + this.plugins[plugin] || {} + ); + + QueryBuilder.plugins[plugin].fct.call(this, this.plugins[plugin]); + } + else { + Utils.error('Config', 'Unable to find plugin "{0}"', plugin); + } + }, this); +}; + /** * Checks the configuration of each filter + * @param {object[]} filters + * @returns {object[]} * @throws ConfigError */ QueryBuilder.prototype.checkFilters = function(filters) { @@ -136,7 +178,8 @@ QueryBuilder.prototype.checkFilters = function(filters) { } switch (filter.input) { - case 'radio': case 'checkbox': + case 'radio': + case 'checkbox': if (!filter.values || filter.values.length < 1) { Utils.error('Config', 'Missing filter "{0}" values', filter.id); } @@ -164,7 +207,7 @@ QueryBuilder.prototype.checkFilters = function(filters) { else { var self = this; filters.sort(function(a, b) { - return self.translateLabel(a.label).localeCompare(self.translateLabel(b.label)); + return self.getTranslatedLabel(a.label).localeCompare(self.getTranslatedLabel(b.label)); }); } } @@ -178,6 +221,8 @@ QueryBuilder.prototype.checkFilters = function(filters) { /** * Checks the configuration of each operator + * @param {object[]} operators + * @returns {object[]} * @throws ConfigError */ QueryBuilder.prototype.checkOperators = function(operators) { @@ -231,54 +276,56 @@ QueryBuilder.prototype.checkOperators = function(operators) { }; /** - * Add all events listeners + * Adds all events listeners to the builder + * @private */ QueryBuilder.prototype.bindEvents = function() { var self = this; + var Selectors = QueryBuilder.selectors; // group condition change this.$el.on('change.queryBuilder', Selectors.group_condition, function() { if ($(this).is(':checked')) { var $group = $(this).closest(Selectors.group_container); - Model($group).condition = $(this).val(); + self.getModel($group).condition = $(this).val(); } }); // rule filter change this.$el.on('change.queryBuilder', Selectors.rule_filter, function() { var $rule = $(this).closest(Selectors.rule_container); - Model($rule).filter = self.getFilterById($(this).val()); + self.getModel($rule).filter = self.getFilterById($(this).val()); }); // rule operator change this.$el.on('change.queryBuilder', Selectors.rule_operator, function() { var $rule = $(this).closest(Selectors.rule_container); - Model($rule).operator = self.getOperatorByType($(this).val()); + self.getModel($rule).operator = self.getOperatorByType($(this).val()); }); // add rule button this.$el.on('click.queryBuilder', Selectors.add_rule, function() { var $group = $(this).closest(Selectors.group_container); - self.addRule(Model($group)); + self.addRule(self.getModel($group)); }); // delete rule button this.$el.on('click.queryBuilder', Selectors.delete_rule, function() { var $rule = $(this).closest(Selectors.rule_container); - self.deleteRule(Model($rule)); + self.deleteRule(self.getModel($rule)); }); if (this.settings.allow_groups !== 0) { // add group button this.$el.on('click.queryBuilder', Selectors.add_group, function() { var $group = $(this).closest(Selectors.group_container); - self.addGroup(Model($group)); + self.addGroup(self.getModel($group)); }); // delete group button this.$el.on('click.queryBuilder', Selectors.delete_group, function() { var $group = $(this).closest(Selectors.group_container); - self.deleteGroup(Model($group)); + self.deleteGroup(self.getModel($group)); }); } @@ -288,12 +335,12 @@ QueryBuilder.prototype.bindEvents = function() { node.$el.remove(); self.refreshGroupsConditions(); }, - 'add': function(e, node, index) { + 'add': function(e, parent, node, index) { if (index === 0) { - node.$el.prependTo(node.parent.$el.find('>' + Selectors.rules_list)); + node.$el.prependTo(parent.$el.find('>' + QueryBuilder.selectors.rules_list)); } else { - node.$el.insertAfter(node.parent.rules[index - 1].$el); + node.$el.insertAfter(parent.rules[index - 1].$el); } self.refreshGroupsConditions(); }, @@ -301,7 +348,7 @@ QueryBuilder.prototype.bindEvents = function() { node.$el.detach(); if (index === 0) { - node.$el.prependTo(group.$el.find('>' + Selectors.rules_list)); + node.$el.prependTo(group.$el.find('>' + QueryBuilder.selectors.rules_list)); } else { node.$el.insertAfter(group.rules[index - 1].$el); @@ -312,7 +359,7 @@ QueryBuilder.prototype.bindEvents = function() { if (node instanceof Rule) { switch (field) { case 'error': - self.displayError(node); + self.updateError(node); break; case 'flags': @@ -335,7 +382,7 @@ QueryBuilder.prototype.bindEvents = function() { else { switch (field) { case 'error': - self.displayError(node); + self.updateError(node); break; case 'flags': @@ -352,11 +399,12 @@ QueryBuilder.prototype.bindEvents = function() { }; /** - * Create the root group - * @param addRule {bool,optional} add a default empty rule - * @param data {mixed,optional} group custom data - * @param flags {object,optional} flags to apply to the group - * @return group {Root} + * Creates the root group + * @param {boolean} [addRule=true] - adds a default empty rule + * @param {object} [data] - group custom data + * @param {object} [flags] - flags to apply to the group + * @returns {Group} root group + * @fires QueryBuilder#afterAddGroup */ QueryBuilder.prototype.setRoot = function(addRule, data, flags) { addRule = (addRule === undefined || addRule === true); @@ -383,18 +431,27 @@ QueryBuilder.prototype.setRoot = function(addRule, data, flags) { }; /** - * Add a new group - * @param parent {Group} - * @param addRule {bool,optional} add a default empty rule - * @param data {mixed,optional} group custom data - * @param flags {object,optional} flags to apply to the group - * @return group {Group} + * Adds a new group + * @param {Group} parent + * @param {boolean} [addRule=true] - adds a default empty rule + * @param {object} [data] - group custom data + * @param {object} [flags] - flags to apply to the group + * @returns {Group} + * @fires QueryBuilder#beforeAddGroup + * @fires QueryBuilder#afterAddGroup */ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) { addRule = (addRule === undefined || addRule === true); var level = parent.level + 1; + /** + * Preventable + * @event QueryBuilder#beforeAddGroup + * @param {Group} parent + * @param {boolean} addRule + * @param {int} level + */ var e = this.trigger('beforeAddGroup', parent, addRule, level); if (e.isDefaultPrevented()) { return null; @@ -407,6 +464,10 @@ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) { model.data = data; model.__.flags = $.extend({}, this.settings.default_group_flags, flags); + /** + * @event QueryBuilder#afterAddGroup + * @param {Group} group + */ this.trigger('afterAddGroup', model); model.condition = this.settings.default_condition; @@ -419,15 +480,22 @@ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) { }; /** - * Tries to delete a group. The group is not deleted if at least one rule is no_delete. - * @param group {Group} - * @return {boolean} true if the group has been deleted + * Tries to delete a group. The group is not deleted if at least one rule is flagged `no_delete`. + * @param {Group} group + * @returns {boolean} if the group has been deleted + * @fires QueryBuilder#beforeDeleteGroup + * @fires QueryBuilder#afterDeleteGroup */ QueryBuilder.prototype.deleteGroup = function(group) { if (group.isRoot()) { return false; } + /** + * Preventable + * @event QueryBuilder#beforeDeleteGroup + * @param {Group} parent + */ var e = this.trigger('beforeDeleteGroup', group); if (e.isDefaultPrevented()) { return false; @@ -436,13 +504,17 @@ QueryBuilder.prototype.deleteGroup = function(group) { var del = true; group.each('reverse', function(rule) { - del&= this.deleteRule(rule); + del &= this.deleteRule(rule); }, function(group) { - del&= this.deleteGroup(group); + del &= this.deleteGroup(group); }, this); if (del) { group.drop(); + + /** + * @event QueryBuilder#afterDeleteGroup + */ this.trigger('afterDeleteGroup'); } @@ -450,43 +522,57 @@ QueryBuilder.prototype.deleteGroup = function(group) { }; /** - * Changes the condition of a group - * @param group {Group} + * Performs actions when a group's condition changes + * @param {Group} group + * @fires QueryBuilder#afterUpdateGroupCondition + * @private */ QueryBuilder.prototype.updateGroupCondition = function(group) { - group.$el.find('>' + Selectors.group_condition).each(function() { + group.$el.find('>' + QueryBuilder.selectors.group_condition).each(function() { var $this = $(this); $this.prop('checked', $this.val() === group.condition); $this.parent().toggleClass('active', $this.val() === group.condition); }); + /** + * @event QueryBuilder#afterUpdateGroupCondition + * @param {Group} group + */ this.trigger('afterUpdateGroupCondition', group); }; /** - * Update visibility of conditions based on number of rules inside each group + * Updates the visibility of conditions based on number of rules inside each group + * @private */ QueryBuilder.prototype.refreshGroupsConditions = function() { (function walk(group) { if (!group.flags || (group.flags && !group.flags.condition_readonly)) { - group.$el.find('>' + Selectors.group_condition).prop('disabled', group.rules.length <= 1) + group.$el.find('>' + QueryBuilder.selectors.group_condition).prop('disabled', group.rules.length <= 1) .parent().toggleClass('disabled', group.rules.length <= 1); } - group.each(function(rule) {}, function(group) { + group.each(null, function(group) { walk(group); }, this); }(this.model.root)); }; /** - * Add a new rule - * @param parent {Group} - * @param data {mixed,optional} rule custom data - * @param flags {object,optional} flags to apply to the rule - * @return rule {Rule} + * Adds a new rule + * @param {Group} parent + * @param {object} [data] - rule custom data + * @param {object} [flags] - flags to apply to the rule + * @returns {Rule} + * @fires QueryBuilder#beforeAddRule + * @fires QueryBuilder#afterAddRule */ QueryBuilder.prototype.addRule = function(parent, data, flags) { + /** + * Preventable + * @event QueryBuilder#beforeAddRule + * @param {Group} parent + */ var e = this.trigger('beforeAddRule', parent); if (e.isDefaultPrevented()) { return null; @@ -502,6 +588,10 @@ QueryBuilder.prototype.addRule = function(parent, data, flags) { model.__.flags = $.extend({}, this.settings.default_rule_flags, flags); + /** + * @event QueryBuilder#afterAddRule + * @param {Rule} rule + */ this.trigger('afterAddRule', model); this.createRuleFilters(model); @@ -517,15 +607,22 @@ QueryBuilder.prototype.addRule = function(parent, data, flags) { }; /** - * Delete a rule. - * @param rule {Rule} - * @return {boolean} true if the rule has been deleted + * Tries to delete a rule + * @param {Rule} rule + * @returns {boolean} if the rule has been deleted + * @fires QueryBuilder#beforeDeleteRule + * @fires QueryBuilder#afterDeleteRule */ QueryBuilder.prototype.deleteRule = function(rule) { if (rule.flags.no_delete) { return false; } + /** + * Preventable + * @event QueryBuilder#beforeDeleteRule + * @param {Rule} rule + */ var e = this.trigger('beforeDeleteRule', rule); if (e.isDefaultPrevented()) { return false; @@ -533,30 +630,41 @@ QueryBuilder.prototype.deleteRule = function(rule) { rule.drop(); + /** + * @event QueryBuilder#afterDeleteRule + */ this.trigger('afterDeleteRule'); return true; }; /** - * Create the filters for a rule and init the rule operator - * @param rule {Rule} + * Creates the operators for a rule and init the rule operator + * @param {Rule} rule + * @fires QueryBuilder#afterCreateRuleOperators + * @private */ QueryBuilder.prototype.createRuleOperators = function(rule) { - var $operatorContainer = rule.$el.find(Selectors.operator_container).empty(); + var $operatorContainer = rule.$el.find(QueryBuilder.selectors.operator_container).empty(); if (!rule.filter) { return; @@ -570,15 +678,22 @@ QueryBuilder.prototype.createRuleOperators = function(rule) { // set the operator without triggering update event rule.__.operator = operators[0]; + /** + * @event QueryBuilder#afterCreateRuleOperators + * @param {Rule} rule + * @param {object[]} operators + */ this.trigger('afterCreateRuleOperators', rule, operators); }; /** - * Create the main input for a rule - * @param rule {Rule} + * Creates the main input for a rule + * @param {Rule} rule + * @fires QueryBuilder#afterCreateRuleInput + * @private */ QueryBuilder.prototype.createRuleInput = function(rule) { - var $valueContainer = rule.$el.find(Selectors.value_container).empty(); + var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container).empty(); rule.__.value = undefined; @@ -611,6 +726,10 @@ QueryBuilder.prototype.createRuleInput = function(rule) { $inputs[filter.plugin](filter.plugin_config || {}); } + /** + * @event QueryBuilder#afterCreateRuleInput + * @param {Rule} rule + */ this.trigger('afterCreateRuleInput', rule); if (filter.default_value !== undefined) { @@ -624,31 +743,39 @@ QueryBuilder.prototype.createRuleInput = function(rule) { }; /** - * Perform action when rule's filter is changed - * @param rule {Rule} - * @param previousFilter {object} + * Performs action when a rule's filter changes + * @param {Rule} rule + * @param {object} previousFilter + * @fires QueryBuilder#afterUpdateRuleFilter + * @private */ QueryBuilder.prototype.updateRuleFilter = function(rule, previousFilter) { this.createRuleOperators(rule); this.createRuleInput(rule); - rule.$el.find(Selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1'); + rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1'); // clear rule data if the filter changed if (previousFilter && rule.filter && previousFilter.id !== rule.filter.id) { rule.data = undefined; } + /** + * @event QueryBuilder#afterUpdateRuleFilter + * @param {Rule} rule + */ this.trigger('afterUpdateRuleFilter', rule); }; /** - * Update main visibility when rule operator changes - * @param rule {Rule} - * @param previousOperator {object} + * Performs actions when a rule's operator changes + * @param {Rule} rule + * @param {object} previousOperator + * @fires QueryBuilder#afterUpdateRuleOperator + * @private */ QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) { - var $valueContainer = rule.$el.find(Selectors.value_container); + var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container); if (!rule.operator || rule.operator.nb_inputs === 0) { $valueContainer.hide(); @@ -667,32 +794,45 @@ QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) { } if (rule.operator) { - rule.$el.find(Selectors.rule_operator).val(rule.operator.type); + rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type); } + /** + * @event QueryBuilder#afterUpdateRuleOperator + * @param {Rule} rule + */ this.trigger('afterUpdateRuleOperator', rule); this.updateRuleValue(rule); }; /** - * Perform action when rule's value is changed - * @param rule {Rule} + * Performs actions when rule's value changes + * @param {Rule} rule + * @fires QueryBuilder#afterUpdateRuleValue + * @private */ QueryBuilder.prototype.updateRuleValue = function(rule) { if (!rule._updating_value) { this.setRuleInputValue(rule, rule.value); } + /** + * @event QueryBuilder#afterUpdateRuleValue + * @param {Rule} rule + */ this.trigger('afterUpdateRuleValue', rule); }; /** - * Change rules properties depending on flags. - * @param rule {Rule} + * Changes a rule's properties depending on its flags + * @param {Rule} rule + * @fires QueryBuilder#afterApplyRuleFlags + * @private */ QueryBuilder.prototype.applyRuleFlags = function(rule) { var flags = rule.flags; + var Selectors = QueryBuilder.selectors; if (flags.filter_readonly) { rule.$el.find(Selectors.rule_filter).prop('disabled', true); @@ -707,15 +847,22 @@ QueryBuilder.prototype.applyRuleFlags = function(rule) { rule.$el.find(Selectors.delete_rule).remove(); } + /** + * @event QueryBuilder#afterApplyRuleFlags + * @param {Rule} rule + */ this.trigger('afterApplyRuleFlags', rule); }; /** - * Change group properties depending on flags. - * @param group {Group} + * Changes group's properties depending on its flags + * @param {Group} group + * @fires QueryBuilder#afterApplyGroupFlags + * @private */ QueryBuilder.prototype.applyGroupFlags = function(group) { var flags = group.flags; + var Selectors = QueryBuilder.selectors; if (flags.condition_readonly) { group.$el.find('>' + Selectors.group_condition).prop('disabled', true) @@ -731,12 +878,16 @@ QueryBuilder.prototype.applyGroupFlags = function(group) { group.$el.find(Selectors.delete_group).remove(); } + /** + * @event QueryBuilder#afterApplyGroupFlags + * @param {Group} group + */ this.trigger('afterApplyGroupFlags', group); }; /** - * Clear all errors markers - * @param node {Node,optional} default is root Group + * Clears all errors markers + * @param {Node} [node] default is root Group */ QueryBuilder.prototype.clearErrors = function(node) { node = node || this.model.root; @@ -757,10 +908,12 @@ QueryBuilder.prototype.clearErrors = function(node) { }; /** - * Add/Remove class .has-error and update error title - * @param node {Node} + * Adds/Removes error on a Rule or Group + * @param {Node} node + * @fires QueryBuilder#filter:displayError + * @private */ -QueryBuilder.prototype.displayError = function(node) { +QueryBuilder.prototype.updateError = function(node) { if (this.settings.display_errors) { if (node.error === null) { node.$el.removeClass('has-error'); @@ -768,26 +921,43 @@ QueryBuilder.prototype.displayError = function(node) { else { var errorMessage = this.lang.errors[node.error[0]] || node.error[0]; errorMessage = Utils.fmt(errorMessage, node.error.slice(1)); + + /** + * @event QueryBuilder#filter:displayError + * @param {string} errorMessage + * @param {array} error + * @param {Node} node + * @returns {string} + */ errorMessage = this.change('displayError', errorMessage, node.error, node); node.$el.addClass('has-error') - .find(Selectors.error_container).eq(0) + .find(QueryBuilder.selectors.error_container).eq(0) .attr('title', errorMessage); } } }; /** - * Trigger a validation error event - * @param node {Node} - * @param error {array} - * @param value {mixed} + * Triggers a validation error event + * @param {Node} node + * @param {string|array} error + * @param {*} value + * @fires QueryBuilder#validationError + * @private */ QueryBuilder.prototype.triggerValidationError = function(node, error, value) { if (!$.isArray(error)) { error = [error]; } + /** + * Preventable + * @event QueryBuilder#validationError + * @param {Node} node + * @param {string} node + * @param {*} value + */ var e = this.trigger('validationError', node, error, value); if (!e.isDefaultPrevented()) { node.error = error; diff --git a/src/data.js b/src/data.js index 3aab4578..71e1c656 100644 --- a/src/data.js +++ b/src/data.js @@ -1,10 +1,9 @@ -/*jshint loopfunc:true */ - /** - * Check if a value is correct for a filter - * @param rule {Rule} - * @param value {string|string[]|undefined} - * @return {array|true} + * Performs value validation + * @param {Rule} rule + * @param {string|string[]} value + * @returns {array|boolean} true or error array + * @fires QueryBuilder#filter:validateValue */ QueryBuilder.prototype.validateValue = function(rule, value) { var validation = rule.filter.validation || {}; @@ -14,20 +13,28 @@ QueryBuilder.prototype.validateValue = function(rule, value) { result = validation.callback.call(this, value, rule); } else { - result = this.validateValueInternal(rule, value); + result = this._validateValue(rule, value); } + /** + * @event QueryBuilder#filter:validateValue + * @param {array|boolean} result + * @param {*} value + * @param {Rule} rule + * @returns {array|boolean} + */ return this.change('validateValue', result, value, rule); }; /** * Default validation function + * @param {Rule} rule + * @param {string|string[]} value + * @returns {array|boolean} true or error array * @throws ConfigError - * @param rule {Rule} - * @param value {string|string[]|undefined} - * @return {Array|boolean} error array or true + * @private */ -QueryBuilder.prototype.validateValueInternal = function(rule, value) { +QueryBuilder.prototype._validateValue = function(rule, value) { var filter = rule.filter; var operator = rule.operator; var validation = filter.validation || {}; @@ -217,7 +224,8 @@ QueryBuilder.prototype.validateValueInternal = function(rule, value) { /** * Returns an incremented group ID - * @return {string} + * @returns {string} + * @private */ QueryBuilder.prototype.nextGroupId = function() { return this.status.id + '_group_' + (this.status.group_id++); @@ -225,7 +233,8 @@ QueryBuilder.prototype.nextGroupId = function() { /** * Returns an incremented rule ID - * @return {string} + * @returns {string} + * @private */ QueryBuilder.prototype.nextRuleId = function() { return this.status.id + '_rule_' + (this.status.rule_id++); @@ -233,8 +242,10 @@ QueryBuilder.prototype.nextRuleId = function() { /** * Returns the operators for a filter - * @param filter {string|object} (filter id name or filter object) - * @return {object[]} + * @param {string|object} filter - filter id or filter object + * @returns {object[]} + * @fires QueryBuilder#filter:getOperators + * @private */ QueryBuilder.prototype.getOperators = function(filter) { if (typeof filter == 'string') { @@ -265,15 +276,22 @@ QueryBuilder.prototype.getOperators = function(filter) { }); } + /** + * QueryBuilder#filter:getOperators + * @param {object[]} operators + * @param {object} filter + * @returns {object[]} + */ return this.change('getOperators', result, filter); }; /** * Returns a particular filter by its id + * @param {string} id + * @param {boolean} [doThrow=true] + * @returns {object|null} * @throws UndefinedFilterError - * @param id {string} - * @param [doThrow=true] {boolean} - * @return {object|null} + * @private */ QueryBuilder.prototype.getFilterById = function(id, doThrow) { if (id == '-1') { @@ -292,11 +310,12 @@ QueryBuilder.prototype.getFilterById = function(id, doThrow) { }; /** - * Return a particular operator by its type + * Returns a particular operator by its type + * @param {string} type + * @param {boolean} [doThrow=true] + * @returns {object|null} * @throws UndefinedOperatorError - * @param type {string} - * @param [doThrow=true] {boolean} - * @return {object|null} + * @private */ QueryBuilder.prototype.getOperatorByType = function(type, doThrow) { if (type == '-1') { @@ -315,9 +334,11 @@ QueryBuilder.prototype.getOperatorByType = function(type, doThrow) { }; /** - * Returns rule's input value - * @param rule {Rule} - * @return {mixed} + * Returns rule's current input value + * @param {Rule} rule + * @returns {*} + * @fires QueryBuilder#filter:getRuleValue + * @private */ QueryBuilder.prototype.getRuleInputValue = function(rule) { var filter = rule.filter; @@ -328,7 +349,7 @@ QueryBuilder.prototype.getRuleInputValue = function(rule) { value = filter.valueGetter.call(this, rule); } else { - var $value = rule.$el.find(Selectors.value_container); + var $value = rule.$el.find(QueryBuilder.selectors.value_container); for (var i = 0; i < operator.nb_inputs; i++) { var name = Utils.escapeElementId(rule.id + '_value_' + i); @@ -341,18 +362,22 @@ 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 { @@ -381,13 +406,20 @@ QueryBuilder.prototype.getRuleInputValue = function(rule) { } } + /** + * @event QueryBuilder#filter:getRuleValue + * @param {*} value + * @param {Rule} rule + * @returns {*} + */ return this.change('getRuleValue', value, rule); }; /** - * Sets the value of a rule's input. - * @param rule {Rule} - * @param value {mixed} + * Sets the value of a rule's input + * @param {Rule} rule + * @param {*} value + * @private */ QueryBuilder.prototype.setRuleInputValue = function(rule, value) { var filter = rule.filter; @@ -403,7 +435,7 @@ QueryBuilder.prototype.setRuleInputValue = function(rule, value) { filter.valueSetter.call(this, rule, value); } else { - var $value = rule.$el.find(Selectors.value_container); + var $value = rule.$el.find(QueryBuilder.selectors.value_container); if (operator.nb_inputs == 1) { value = [value]; @@ -421,9 +453,11 @@ 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: @@ -440,9 +474,11 @@ QueryBuilder.prototype.setRuleInputValue = function(rule, value) { }; /** - * Clean rule flags. - * @param rule {object} - * @return {object} + * Parses rule flags + * @param {object} rule + * @returns {object} + * @fires QueryBuilder#filter:parseRuleFlags + * @private */ QueryBuilder.prototype.parseRuleFlags = function(rule) { var flags = $.extend({}, this.settings.default_rule_flags); @@ -460,14 +496,21 @@ QueryBuilder.prototype.parseRuleFlags = function(rule) { $.extend(flags, rule.flags); } + /** + * @event QueryBuilder#filter:parseRuleFlags + * @param {object} flags + * @param {object} rule + * @returns {object} + */ return this.change('parseRuleFlags', flags, rule); }; /** - * Get a copy of flags of a rule. + * Gets a copy of flags of a rule * @param {object} flags - * @param {boolean} all - true to return all flags, false to return only changes from default + * @param {boolean} [all=false] - return all flags or only changes from default flags * @returns {object} + * @private */ QueryBuilder.prototype.getRuleFlags = function(flags, all) { if (all) { @@ -485,9 +528,11 @@ QueryBuilder.prototype.getRuleFlags = function(flags, all) { }; /** - * Clean group flags. - * @param group {object} - * @return {object} + * Parses group flags + * @param {object} group + * @returns {object} + * @fires QueryBuilder#filter:parseGroupFlags + * @private */ QueryBuilder.prototype.parseGroupFlags = function(group) { var flags = $.extend({}, this.settings.default_group_flags); @@ -505,14 +550,21 @@ QueryBuilder.prototype.parseGroupFlags = function(group) { $.extend(flags, group.flags); } + /** + * @event QueryBuilder#filter:parseGroupFlags + * @param {object} flags + * @param {object} group + * @returns {object} + */ return this.change('parseGroupFlags', flags, group); }; /** - * Get a copy of flags of a group. + * Gets a copy of flags of a group * @param {object} flags - * @param {boolean} all - true to return all flags, false to return only changes from default + * @param {boolean} [all=false] - return all flags or only changes from default flags * @returns {object} + * @private */ QueryBuilder.prototype.getGroupFlags = function(flags, all) { if (all) { @@ -530,20 +582,22 @@ QueryBuilder.prototype.getGroupFlags = function(flags, all) { }; /** - * Translate a label - * @param label {string|object} - * @return string + * Translates a label + * @param {string|object} label + * @returns {string} + * @private */ -QueryBuilder.prototype.translateLabel = function(label) { +QueryBuilder.prototype.getTranslatedLabel = function(label) { return typeof label == 'object' ? (label[this.settings.lang_code] || label['en']) : label; }; /** - * Return a validation message + * Returns a validation message * @param {object} validation * @param {string} type * @param {string} def * @returns {string} + * @private */ QueryBuilder.prototype.getValidationMessage = function(validation, type, def) { return validation.messages && validation.messages[type] || def; diff --git a/src/defaults.js b/src/defaults.js index 1fd031a1..ed757eb6 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -1,5 +1,7 @@ /** * Allowed types and their internal representation + * @type {object.} + * @readonly */ QueryBuilder.types = { 'string': 'string', @@ -13,6 +15,8 @@ QueryBuilder.types = { /** * Allowed inputs + * @type {string[]} + * @readonly */ QueryBuilder.inputs = [ 'text', @@ -24,7 +28,9 @@ QueryBuilder.inputs = [ ]; /** - * Runtime modifiable options with `setOptions` method + * Runtime modifiable options with setOptions method + * @type {string[]} + * @readonly */ QueryBuilder.modifiable_options = [ 'display_errors', @@ -36,8 +42,10 @@ QueryBuilder.modifiable_options = [ /** * CSS selectors for common components + * @type {object.} + * @readonly */ -var Selectors = QueryBuilder.selectors = { +QueryBuilder.selectors = { group_container: '.rules-group-container', rule_container: '.rule-container', filter_container: '.rule-filter-container', @@ -65,17 +73,23 @@ var Selectors = QueryBuilder.selectors = { }; /** - * Template strings (see `template.js`) + * Template strings (see template.js) + * @type {object.} + * @readonly */ QueryBuilder.templates = {}; /** - * Localized strings (see `i18n/`) + * Localized strings (see i18n/) + * @type {object.} + * @readonly */ QueryBuilder.regional = {}; /** * Default operators + * @type {object.} + * @readonly */ QueryBuilder.OPERATORS = { equal: { type: 'equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] }, @@ -102,6 +116,8 @@ QueryBuilder.OPERATORS = { /** * Default configuration + * @type {object} + * @readonly */ QueryBuilder.DEFAULTS = { filters: [], diff --git a/src/main.js b/src/main.js index 4a2165fc..9390e212 100644 --- a/src/main.js +++ b/src/main.js @@ -1,15 +1,37 @@ -// CLASS DEFINITION -// =============================== +/** + * @param {jQuery} $el + * @param {object} options - see {@link http://querybuilder.js.org/#options} + * @constructor + * @fires QueryBuilder#afterInit + */ var QueryBuilder = function($el, options) { this.init($el, options); }; +$.extend(QueryBuilder.prototype, /** @lends QueryBuilder.prototype */ { + /** + * Triggers an event on the builder container + * @param {string} type + * @returns {$.Event} + */ + trigger: function(type) { + var event = new $.Event(this._tojQueryEvent(type), { + builder: this + }); + + this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 1)); + + return event; + }, -// EVENTS SYSTEM -// =============================== -$.extend(QueryBuilder.prototype, { + /** + * Triggers an event on the builder container and returns the modified value + * @param {string} type + * @param {*} value + * @returns {*} + */ change: function(type, value) { - var event = new $.Event(this.tojQueryEvent(type, true), { + var event = new $.Event(this._tojQueryEvent(type, true), { builder: this, value: value }); @@ -19,47 +41,69 @@ $.extend(QueryBuilder.prototype, { return event.value; }, - trigger: function(type) { - var event = new $.Event(this.tojQueryEvent(type), { - builder: this - }); - - this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 1)); - - return event; - }, - + /** + * Attaches an event listener on the builder container + * @param {string} type + * @param {function} cb + * @returns {QueryBuilder} + */ on: function(type, cb) { - this.$el.on(this.tojQueryEvent(type), cb); + this.$el.on(this._tojQueryEvent(type), cb); return this; }, + /** + * Removes an event listener from the builder container + * @param {string} type + * @param {function} [cb] + * @returns {QueryBuilder} + */ off: function(type, cb) { - this.$el.off(this.tojQueryEvent(type), cb); + this.$el.off(this._tojQueryEvent(type), cb); return this; }, + /** + * Attaches an event listener called once on the builder container + * @param {string} type + * @param {function} cb + * @returns {QueryBuilder} + */ once: function(type, cb) { - this.$el.one(this.tojQueryEvent(type), cb); + this.$el.one(this._tojQueryEvent(type), cb); return this; }, - tojQueryEvent: function(name, filter) { + /** + * Appends `.queryBuilder` and optionally `.filter` to the events names + * @param {string} name + * @param {boolean} [filter=false] + * @returns {string} + * @private + */ + _tojQueryEvent: function(name, filter) { return name.split(' ').map(function(type) { return type + '.queryBuilder' + (filter ? '.filter' : ''); }).join(' '); } }); +/** + * @typedef {object} QueryBuilder#Plugin + * @property {object} def - default options + * @property {function} fct - init function + */ -// PLUGINS SYSTEM -// =============================== +/** + * Definition of available plugins + * @type {object.} + */ QueryBuilder.plugins = {}; /** - * Get or extend the default configuration - * @param options {object,optional} new configuration, leave undefined to get the default config - * @return {undefined|object} nothing or configuration object (copy) + * Gets or extends the default configuration + * @param {object} [options] - new configuration + * @returns {undefined|object} nothing or configuration object (copy) */ QueryBuilder.defaults = function(options) { if (typeof options == 'object') { @@ -79,10 +123,10 @@ QueryBuilder.defaults = function(options) { }; /** - * Define a new plugin - * @param {string} - * @param {function} - * @param {object,optional} default configuration + * Registers a new plugin + * @param {string} name + * @param {function} fct - init function + * @param {object} [def] - default options */ QueryBuilder.define = function(name, fct, def) { QueryBuilder.plugins[name] = { @@ -92,41 +136,9 @@ QueryBuilder.define = function(name, fct, def) { }; /** - * Add new methods - * @param {object} + * Adds new methods to QueryBuilder prototypes + * @param {object.} methods */ QueryBuilder.extend = function(methods) { $.extend(QueryBuilder.prototype, methods); }; - -/** - * Init plugins for an instance - * @throws ConfigError - */ -QueryBuilder.prototype.initPlugins = function() { - if (!this.plugins) { - return; - } - - if ($.isArray(this.plugins)) { - var tmp = {}; - this.plugins.forEach(function(plugin) { - tmp[plugin] = null; - }); - this.plugins = tmp; - } - - Object.keys(this.plugins).forEach(function(plugin) { - if (plugin in QueryBuilder.plugins) { - this.plugins[plugin] = $.extend(true, {}, - QueryBuilder.plugins[plugin].def, - this.plugins[plugin] || {} - ); - - QueryBuilder.plugins[plugin].fct.call(this, this.plugins[plugin]); - } - else { - Utils.error('Config', 'Unable to find plugin "{0}"', plugin); - } - }, this); -}; diff --git a/src/model.js b/src/model.js index e67538e2..3e58ae09 100644 --- a/src/model.js +++ b/src/model.js @@ -1,37 +1,60 @@ -// Model CLASS -// =============================== /** - * Main object storing data model and emitting events - * --------- - * Access Node object stored in jQuery objects - * @param el {jQuery|Node} - * @return {Node} + * Main object storing data model and emitting model events + * @constructor */ -function Model(el) { - if (!(this instanceof Model)) { - return Model.getModel(el); - } - +function Model() { this.root = null; this.$ = $(this); } $.extend(Model.prototype, { + /** + * Triggers an event on the model + * @param {string} type + * @returns {$.Event} + * @memberof Model + * @instance + */ trigger: function(type) { - this.$.triggerHandler(type, Array.prototype.slice.call(arguments, 1)); - return this; + var event = new $.Event(type); + this.$.triggerHandler(event, Array.prototype.slice.call(arguments, 1)); + return event; }, + /** + * Attaches an event listener on the model + * @param {string} type + * @param {function} cb + * @returns {Model} + * @memberof Model + * @instance + */ on: function() { this.$.on.apply(this.$, Array.prototype.slice.call(arguments)); return this; }, + /** + * Removes an event listener from the model + * @param {string} type + * @param {function} [cb] + * @returns {Model} + * @memberof Model + * @instance + */ off: function() { this.$.off.apply(this.$, Array.prototype.slice.call(arguments)); return this; }, + /** + * Attaches an event listener called once on the model + * @param {string} type + * @param {function} cb + * @returns {Model} + * @memberof Model + * @instance + */ once: function() { this.$.one.apply(this.$, Array.prototype.slice.call(arguments)); return this; @@ -39,25 +62,11 @@ $.extend(Model.prototype, { }); /** - * Access Node object stored in jQuery objects - * @param el {jQuery|Node} - * @return {Node} - */ -Model.getModel = function(el) { - if (!el) { - return null; - } - else if (el instanceof Node) { - return el; - } - else { - return $(el).data('queryBuilderModel'); - } -}; - -/* - * Define Node properties with getter and setter - * Update events are emitted in the setter through root Model (if any) + * Defines properties on an Node prototype with getter and setter.
+ * Update events are emitted in the setter through root Model (if any).
+ * The object must have a `__` object, non enumerable property to store values. + * @param {function} obj + * @param {string[]} fields */ Model.defineModelProperties = function(obj, fields) { fields.forEach(function(field) { @@ -67,14 +76,21 @@ Model.defineModelProperties = function(obj, fields) { return this.__[field]; }, set: function(value) { - var oldValue = (this.__[field] !== null && typeof this.__[field] == 'object') ? - $.extend({}, this.__[field]) : - this.__[field]; + var previousValue = (this.__[field] !== null && typeof this.__[field] == 'object') ? + $.extend({}, this.__[field]) : + this.__[field]; this.__[field] = value; if (this.model !== null) { - this.model.trigger('update', this, field, value, oldValue); + /** + * @event Model#model:update + * @param {Node} node + * @param {string} field + * @param {*} value + * @param {*} previousValue + */ + this.model.trigger('update', this, field, value, previousValue); } } }); @@ -82,27 +98,77 @@ Model.defineModelProperties = function(obj, fields) { }; -// Node abstract CLASS -// =============================== /** - * @param {Node} - * @param {jQuery} + * Root abstract object + * @constructor + * @param {Node} [parent] + * @param {jQuery} $el */ var Node = function(parent, $el) { if (!(this instanceof Node)) { - return new Node(); + return new Node(parent, $el); } Object.defineProperty(this, '__', { value: {} }); $el.data('queryBuilderModel', this); + /** + * @name level + * @member {int} + * @memberof Node + * @instance + * @readonly + */ this.__.level = 1; + + /** + * @name error + * @member {string} + * @memberof Node + * @instance + */ this.__.error = null; + + /** + * @name flags + * @member {object} + * @memberof Node + * @instance + * @readonly + */ + this.__.flags = {}; + + /** + * @name data + * @member {object} + * @memberof Node + * @instance + */ this.__.data = undefined; + + /** + * @member {jQuery} + * @readonly + */ this.$el = $el; + + /** + * @member {string} + * @readonly + */ this.id = $el[0].id; + + /** + * @member {Model} + * @readonly + */ this.model = null; + + /** + * @member {Node} + * @readonly + */ this.parent = parent; }; @@ -121,16 +187,16 @@ Object.defineProperty(Node.prototype, 'parent', { }); /** - * Check if this Node is the root - * @return {boolean} + * Checks if this Node is the root + * @returns {boolean} */ Node.prototype.isRoot = function() { return (this.level === 1); }; /** - * Return node position inside parent - * @return {int} + * Returns the node position inside its parent + * @returns {int} */ Node.prototype.getPos = function() { if (this.isRoot()) { @@ -142,87 +208,103 @@ Node.prototype.getPos = function() { }; /** - * Delete self + * Deletes self + * @fires Model#model:drop */ Node.prototype.drop = function() { var model = this.model; - if (!this.isRoot()) { - this.parent._removeNode(this); + if (!!this.parent) { + this.parent.removeNode(this); } + this.$el.removeData('queryBuilderModel'); + if (model !== null) { + /** + * @event Model#model:drop + * @param {Node} node + */ model.trigger('drop', this); } }; /** - * Move itself after another Node - * @param {Node} - * @return {Node} self + * Moves itself after another Node + * @param {Node} target + * @fires Model#model:move */ -Node.prototype.moveAfter = function(node) { - if (this.isRoot()) return; - - this._move(node.parent, node.getPos() + 1); - - return this; +Node.prototype.moveAfter = function(target) { + if (!this.isRoot()) { + this.move(target.parent, target.getPos() + 1); + } }; /** - * Move itself at the beginning of parent or another Group - * @param {Group,optional} - * @return {Node} self + * Moves itself at the beginning of parent or another Group + * @param {Group} [target] + * @fires Model#model:move */ Node.prototype.moveAtBegin = function(target) { - if (this.isRoot()) return; + if (!this.isRoot()) { + if (target === undefined) { + target = this.parent; + } - if (target === undefined) { - target = this.parent; + this.move(target, 0); } - - this._move(target, 0); - - return this; }; /** - * Move itself at the end of parent or another Group - * @param {Group,optional} - * @return {Node} self + * Moves itself at the end of parent or another Group + * @param {Group} [target] + * @fires Model#model:move */ Node.prototype.moveAtEnd = function(target) { - if (this.isRoot()) return; + if (!this.isRoot()) { + if (target === undefined) { + target = this.parent; + } - if (target === undefined) { - target = this.parent; + this.move(target, target.length() === 0 ? 0 : target.length() - 1); } - - this._move(target, target.length() === 0 ? 0 : target.length() - 1); - - return this; }; /** - * Move itself at specific position of Group - * @param {Group} - * @param {int} + * Moves itself at specific position of Group + * @param {Group} target + * @param {int} index + * @fires Model#model:move */ -Node.prototype._move = function(group, index) { - this.parent._removeNode(this); - group._appendNode(this, index, false); +Node.prototype.move = function(target, index) { + if (!this.isRoot()) { + if (typeof target === 'number') { + index = target; + target = this.parent; + } - if (this.model !== null) { - this.model.trigger('move', this, group, index); + this.parent.removeNode(this); + target.insertNode(this, index, false); + + if (this.model !== null) { + /** + * @event Model#model:move + * @param {Node} node + * @param {Node} target + * @param {int} index + */ + this.model.trigger('move', this, target, index); + } } }; -// GROUP CLASS -// =============================== /** - * @param {Group} - * @param {jQuery} + * Group object + * @constructor + * @extends Node + * @param {Group} [parent] + * @param {jQuery} $el */ var Group = function(parent, $el) { if (!(this instanceof Group)) { @@ -231,7 +313,18 @@ var Group = function(parent, $el) { Node.call(this, parent, $el); + /** + * @member {object[]} + * @readonly + */ this.rules = []; + + /** + * @name condition + * @member {string} + * @memberof Group + * @instance + */ this.__.condition = null; }; @@ -241,7 +334,7 @@ Group.prototype.constructor = Group; Model.defineModelProperties(Group, ['condition']); /** - * Empty the Group + * Removes group's content */ Group.prototype.empty = function() { this.each('reverse', function(rule) { @@ -252,7 +345,7 @@ Group.prototype.empty = function() { }; /** - * Delete self + * Deletes self */ Group.prototype.drop = function() { this.empty(); @@ -260,21 +353,22 @@ Group.prototype.drop = function() { }; /** - * Return the number of children - * @return {int} + * Returns the number of children + * @returns {int} */ Group.prototype.length = function() { return this.rules.length; }; /** - * Add a Node at specified index - * @param {Node} - * @param {int,optional} - * @param {boolean,optional} - * @return {Node} the inserted node + * Adds a Node at specified index + * @param {Node} node + * @param {int} [index=end] + * @param {boolean} [trigger=false] - fire 'add' event + * @returns {Node} the inserted node + * @fires Model#model:add */ -Group.prototype._appendNode = function(node, index, trigger) { +Group.prototype.insertNode = function(node, index, trigger) { if (index === undefined) { index = this.length(); } @@ -283,65 +377,77 @@ Group.prototype._appendNode = function(node, index, trigger) { node.parent = this; if (trigger && this.model !== null) { - this.model.trigger('add', node, index); + /** + * @event Model#model:add + * @param {Node} parent + * @param {Node} node + * @param {int} index + */ + this.model.trigger('add', this, node, index); } return node; }; /** - * Add a Group by jQuery element at specified index - * @param {jQuery} - * @param {int,optional} - * @return {Group} the inserted group + * Adds a new Group at specified index + * @param {jQuery} $el + * @param {int} [index=end] + * @returns {Group} + * @fires Model#model:add */ Group.prototype.addGroup = function($el, index) { - return this._appendNode(new Group(this, $el), index, true); + return this.insertNode(new Group(this, $el), index, true); }; /** - * Add a Rule by jQuery element at specified index - * @param {jQuery} - * @param {int,optional} - * @return {Rule} the inserted rule + * Adds a new Rule at specified index + * @param {jQuery} $el + * @param {int} [index=end] + * @returns {Rule} + * @fires Model#model:add */ Group.prototype.addRule = function($el, index) { - return this._appendNode(new Rule(this, $el), index, true); + return this.insertNode(new Rule(this, $el), index, true); }; /** - * Delete a specific Node - * @param {Node} - * @return {Group} self + * Deletes a specific Node + * @param {Node} node */ -Group.prototype._removeNode = function(node) { +Group.prototype.removeNode = function(node) { var index = this.getNodePos(node); if (index !== -1) { node.parent = null; this.rules.splice(index, 1); } - - return this; }; /** - * Return position of a child Node - * @param {Node} - * @return {int} + * Returns the position of a child Node + * @param {Node} node + * @returns {int} */ Group.prototype.getNodePos = function(node) { return this.rules.indexOf(node); }; +/** + * @callback Model#GroupIteratee + * @param {Node} node + * @returns {boolean} stop the iteration + */ + /** * Iterate over all Nodes - * @param {boolean,optional} iterate in reverse order, required if you delete nodes - * @param {function} callback for Rules - * @param {function,optional} callback for Groups - * @return {boolean} + * @param {boolean} [reverse=false] - iterate in reverse order, required if you delete nodes + * @param {Model#GroupIteratee} cbRule - callback for Rules (can be null but not omitted) + * @param {Model#GroupIteratee} [cbGroup] - callback for Groups + * @param {object} [context] - context for callbacks + * @returns {boolean} if the iteration has been stopped by a callback */ Group.prototype.each = function(reverse, cbRule, cbGroup, context) { - if (typeof reverse == 'function') { + if (typeof reverse !== 'boolean' && typeof reverse !== 'string') { context = cbGroup; cbGroup = cbRule; cbRule = reverse; @@ -352,16 +458,18 @@ Group.prototype.each = function(reverse, cbRule, cbGroup, context) { var i = reverse ? this.rules.length - 1 : 0; var l = reverse ? 0 : this.rules.length - 1; var c = reverse ? -1 : 1; - var next = function() { return reverse ? i >= l : i <= l; }; + var next = function() { + return reverse ? i >= l : i <= l; + }; var stop = false; - for (; next(); i+= c) { + for (; next(); i += c) { if (this.rules[i] instanceof Group) { - if (cbGroup !== undefined) { + if (!!cbGroup) { stop = cbGroup.call(context, this.rules[i]) === false; } } - else { + else if (!!cbRule) { stop = cbRule.call(context, this.rules[i]) === false; } @@ -374,21 +482,21 @@ Group.prototype.each = function(reverse, cbRule, cbGroup, context) { }; /** - * Return true if the group contains a particular Node - * @param {Node} - * @param {boolean,optional} recursive search - * @return {boolean} + * Checks if the group contains a particular Node + * @param {Node} node + * @param {boolean} [recursive=false] + * @returns {boolean} */ -Group.prototype.contains = function(node, deep) { +Group.prototype.contains = function(node, recursive) { if (this.getNodePos(node) !== -1) { return true; } - else if (!deep) { + else if (!recursive) { return false; } else { // the loop will return with false as soon as the Node is found - return !this.each(function(rule) { + return !this.each(function() { return true; }, function(group) { return !group.contains(node, true); @@ -397,11 +505,12 @@ Group.prototype.contains = function(node, deep) { }; -// RULE CLASS -// =============================== /** - * @param {Group} - * @param {jQuery} + * Rule object + * @constructor + * @extends Node + * @param {Group} parent + * @param {jQuery} $el */ var Rule = function(parent, $el) { if (!(this instanceof Rule)) { @@ -413,9 +522,28 @@ var Rule = function(parent, $el) { this._updating_value = false; this._updating_input = false; + /** + * @name filter + * @member {object} + * @memberof Rule + * @instance + */ this.__.filter = null; + + /** + * @name operator + * @member {object} + * @memberof Rule + * @instance + */ this.__.operator = null; - this.__.flags = {}; + + /** + * @name value + * @member {*} + * @memberof Rule + * @instance + */ this.__.value = undefined; }; @@ -424,8 +552,15 @@ Rule.prototype.constructor = Rule; Model.defineModelProperties(Rule, ['filter', 'operator', 'value']); +/** + * Checks if this Node is the root + * @returns {boolean} always false + */ +Rule.prototype.isRoot = function() { + return false; +}; + -// EXPORT -// =============================== +// export QueryBuilder.Group = Group; QueryBuilder.Rule = Rule; diff --git a/src/plugins/bt-checkbox/plugin.js b/src/plugins/bt-checkbox/plugin.js index a35abd0e..fd5257af 100644 --- a/src/plugins/bt-checkbox/plugin.js +++ b/src/plugins/bt-checkbox/plugin.js @@ -1,22 +1,13 @@ -/*! - * jQuery QueryBuilder Awesome Bootstrap Checkbox - * Applies Awesome Bootstrap Checkbox for checkbox and radio inputs. +/** + * Applies Awesome Bootstrap Checkbox for checkbox and radio inputs + * @class BtCheckboxPlugin + * @param {object} [options] + * @param {string} [options.font=glypicons] + * @param {string} [options.color=default] */ - QueryBuilder.define('bt-checkbox', function(options) { if (options.font == 'glyphicons') { - var injectCSS = document.createElement('style'); - injectCSS.innerHTML = '\ -.checkbox input[type=checkbox]:checked + label:after { \ - font-family: "Glyphicons Halflings"; \ - content: "\\e013"; \ -} \ -.checkbox label:after { \ - padding-left: 4px; \ - padding-top: 2px; \ - font-size: 9px; \ -}'; - document.body.appendChild(injectCSS); + this.$el.addClass('bt-checkbox-glypicons'); } this.on('getRuleInput.filter', function(h, rule, name) { diff --git a/src/plugins/bt-checkbox/plugins.scss b/src/plugins/bt-checkbox/plugins.scss new file mode 100644 index 00000000..f2ba2375 --- /dev/null +++ b/src/plugins/bt-checkbox/plugins.scss @@ -0,0 +1,12 @@ +.query-builder.bt-checkbox-glypicons { + .checkbox input[type=checkbox]:checked + label::after { + font-family: 'Glyphicons Halflings'; + content: '\e013'; + } + + .checkbox label::after { + padding-left: 4px; + padding-top: 2px; + font-size: 9px; + } +} diff --git a/src/plugins/bt-selectpicker/plugin.js b/src/plugins/bt-selectpicker/plugin.js index e4ad3be6..8909b425 100644 --- a/src/plugins/bt-selectpicker/plugin.js +++ b/src/plugins/bt-selectpicker/plugin.js @@ -1,16 +1,20 @@ -/*! - * jQuery QueryBuilder Bootstrap Selectpicker - * Applies Bootstrap Select on filters and operators combo-boxes. - */ - /** - * @throws ConfigError + * Applies Bootstrap Select on filters and operators combo-boxes. + * @class BtSelectpickerPlugin + * @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); diff --git a/src/plugins/bt-tooltip-errors/plugin.js b/src/plugins/bt-tooltip-errors/plugin.js index 0d97c0b9..0da378de 100644 --- a/src/plugins/bt-tooltip-errors/plugin.js +++ b/src/plugins/bt-tooltip-errors/plugin.js @@ -1,10 +1,9 @@ -/*! - * jQuery QueryBuilder Bootstrap Tooltip errors - * Applies Bootstrap Tooltips on validation error messages. - */ - /** - * @throws ConfigError + * Applies Bootstrap Tooltips on validation error messages. + * @class BtTooltipErrorsPlugin + * @param {object} [options] + * @param {string} [options.placement=right] + * @throws MissingLibraryError */ QueryBuilder.define('bt-tooltip-errors', function(options) { if (!$.fn.tooltip || !$.fn.tooltip.Constructor || !$.fn.tooltip.Constructor.prototype.fixTitle) { @@ -16,14 +15,14 @@ QueryBuilder.define('bt-tooltip-errors', function(options) { // add BT Tooltip data this.on('getRuleTemplate.filter getGroupTemplate.filter', function(h) { var $h = $(h.value); - $h.find(Selectors.error_container).attr('data-toggle', 'tooltip'); + $h.find(QueryBuilder.selectors.error_container).attr('data-toggle', 'tooltip'); h.value = $h.prop('outerHTML'); }); // init/refresh tooltip when title changes this.model.on('update', function(e, node, field) { if (field == 'error' && self.settings.display_errors) { - node.$el.find(Selectors.error_container).eq(0) + node.$el.find(QueryBuilder.selectors.error_container).eq(0) .tooltip(options) .tooltip('hide') .tooltip('fixTitle'); diff --git a/src/plugins/change-filters/plugin.js b/src/plugins/change-filters/plugin.js index 27aa6881..f5de6f58 100644 --- a/src/plugins/change-filters/plugin.js +++ b/src/plugins/change-filters/plugin.js @@ -1,24 +1,30 @@ -/*! - * jQuery QueryBuilder Change Filters +/** * Allows to change available filters after plugin initialization. + * @class ChangeFiltersPlugin */ - -QueryBuilder.extend({ +QueryBuilder.extend(/** @lends ChangeFiltersPlugin.prototype */ { /** * Change the filters of the builder + * @param {boolean} [deleteOrphans=false] - delete rules using old filters + * @param {object[]} filters + * @fires ChangeFiltersPlugin#afterSetFilters * @throws ChangeFilterError - * @param {boolean,optional} delete rules using old filters - * @param {object[]} new filters */ - setFilters: function(delete_orphans, filters) { + setFilters: function(deleteOrphans, filters) { var self = this; if (filters === undefined) { - filters = delete_orphans; - delete_orphans = false; + filters = deleteOrphans; + deleteOrphans = false; } filters = this.checkFilters(filters); + + /** + * @event ChangeFiltersPlugin#filter:setFilters + * @param {object[]} filters + * @returns {object[]} + */ filters = this.change('setFilters', filters); var filtersIds = filters.map(function(filter) { @@ -26,7 +32,7 @@ QueryBuilder.extend({ }); // check for orphans - if (!delete_orphans) { + if (!deleteOrphans) { (function checkOrphans(node) { node.each( function(rule) { @@ -45,18 +51,18 @@ QueryBuilder.extend({ // apply on existing DOM (function updateBuilder(node) { node.each(true, - function(rule) { - if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) { - rule.drop(); - } - else { - self.createRuleFilters(rule); - - rule.$el.find(Selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1'); - self.trigger('afterUpdateRuleFilter', rule); - } - }, - updateBuilder + function(rule) { + if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) { + rule.drop(); + } + else { + self.createRuleFilters(rule); + + rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1'); + self.trigger('afterUpdateRuleFilter', rule); + } + }, + updateBuilder ); }(this.model.root)); @@ -66,7 +72,7 @@ QueryBuilder.extend({ this.updateDisabledFilters(); } if (this.settings.plugins['bt-selectpicker']) { - this.$el.find(Selectors.rule_filter).selectpicker('render'); + this.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render'); } } @@ -80,15 +86,21 @@ QueryBuilder.extend({ } } + /** + * @event ChangeFiltersPlugin#afterSetFilters + * @param {object[]} filters + */ this.trigger('afterSetFilters', filters); }, /** * Adds a new filter to the builder - * @param {object|object[]} the new filter - * @param {mixed,optional} numeric index or '#start' or '#end' + * @param {object|object[]} newFilters + * @param {*} [position=#end] - numeric index or '#start' or '#end' + * @fires ChangeFiltersPlugin#afterSetFilters + * @throws ChangeFilterError */ - addFilter: function(new_filters, position) { + addFilter: function(newFilters, position) { if (position === undefined || position == '#end') { position = this.filters.length; } @@ -96,29 +108,30 @@ QueryBuilder.extend({ position = 0; } - if (!$.isArray(new_filters)) { - new_filters = [new_filters]; + if (!$.isArray(newFilters)) { + newFilters = [newFilters]; } var filters = $.extend(true, [], this.filters); // numeric position if (parseInt(position) == position) { - Array.prototype.splice.apply(filters, [position, 0].concat(new_filters)); + Array.prototype.splice.apply(filters, [position, 0].concat(newFilters)); } else { // after filter by its id if (this.filters.some(function(filter, index) { - if (filter.id == position) { - position = index + 1; - return true; - } - })) { - Array.prototype.splice.apply(filters, [position, 0].concat(new_filters)); + if (filter.id == position) { + position = index + 1; + return true; + } + }) + ) { + Array.prototype.splice.apply(filters, [position, 0].concat(newFilters)); } // defaults to end of list else { - Array.prototype.push.apply(filters, new_filters); + Array.prototype.push.apply(filters, newFilters); } } @@ -127,19 +140,21 @@ QueryBuilder.extend({ /** * Removes a filter from the builder - * @param {string|string[]} the filter id - * @param {boolean,optional} delete rules using old filters + * @param {string|string[]} filterIds + * @param {boolean} [deleteOrphans=false] delete rules using old filters + * @fires ChangeFiltersPlugin#afterSetFilters + * @throws ChangeFilterError */ - removeFilter: function(filter_ids, delete_orphans) { + removeFilter: function(filterIds, deleteOrphans) { var filters = $.extend(true, [], this.filters); - if (typeof filter_ids === 'string') { - filter_ids = [filter_ids]; + if (typeof filterIds === 'string') { + filterIds = [filterIds]; } filters = filters.filter(function(filter) { - return filter_ids.indexOf(filter.id) === -1; + return filterIds.indexOf(filter.id) === -1; }); - this.setFilters(delete_orphans, filters); + this.setFilters(deleteOrphans, filters); } }); diff --git a/src/plugins/filter-description/plugin.js b/src/plugins/filter-description/plugin.js index c87beb97..5cbde11f 100644 --- a/src/plugins/filter-description/plugin.js +++ b/src/plugins/filter-description/plugin.js @@ -1,9 +1,9 @@ -/*! - * jQuery QueryBuilder Filter Description - * Provides three ways to display a description about a filter: inline, Bootsrap Popover or Bootbox. - */ - /** + * Provides three ways to display a description about a filter: inline, Bootsrap Popover or Bootbox. + * @class FilterDescriptionPlugin + * @param {object} [options] + * @param {string} [options.icon=glyphicon glyphicon-info-sign] + * @param {string} [options.mode=popover] - inline, popover or bootbox * @throws ConfigError */ QueryBuilder.define('filter-description', function(options) { @@ -53,7 +53,7 @@ QueryBuilder.define('filter-description', function(options) { else { if ($b.length === 0) { $b = $(''); - $b.prependTo(rule.$el.find(Selectors.rule_actions)); + $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions)); $b.popover({ placement: 'left', @@ -95,7 +95,7 @@ QueryBuilder.define('filter-description', function(options) { else { if ($b.length === 0) { $b = $(''); - $b.prependTo(rule.$el.find(Selectors.rule_actions)); + $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions)); $b.on('click', function() { bootbox.alert($b.data('description')); @@ -111,7 +111,7 @@ QueryBuilder.define('filter-description', function(options) { mode: 'popover' }); -QueryBuilder.extend({ +QueryBuilder.extend(/** @lends FilterDescriptionPlugin.prototype */ { /** * Returns the description of a filter for a particular rule (if present) * @param {object} filter diff --git a/src/plugins/invert/plugin.js b/src/plugins/invert/plugin.js index a4818e3e..1b53fdb2 100644 --- a/src/plugins/invert/plugin.js +++ b/src/plugins/invert/plugin.js @@ -1,6 +1,12 @@ -/*! - * jQuery QueryBuilder Invert +/** * Allows to invert a rule operator, a group condition or the entire builder. + * @class InvertPlugin + * @param {object} [options] + * @param {string} [options.icon='glyphicon glyphicon-random] + * @param {boolean} [options.recursive=true] + * @param {boolean} [options.invert_rules=true] + * @param {boolean} [options.display_rules_button=false] + * @param {boolean} [options.silent_fail=false] */ QueryBuilder.defaults({ @@ -35,6 +41,7 @@ QueryBuilder.defaults({ QueryBuilder.define('invert', function(options) { var self = this; + var Selectors = QueryBuilder.selectors; /** * Bind events @@ -42,13 +49,13 @@ QueryBuilder.define('invert', function(options) { this.on('afterInit', function() { self.$el.on('click.queryBuilder', '[data-invert=group]', function() { var $group = $(this).closest(Selectors.group_container); - self.invert(Model($group), options); + self.invert(self.getModel($group), options); }); if (options.display_rules_button && options.invert_rules) { self.$el.on('click.queryBuilder', '[data-invert=rule]', function() { var $rule = $(this).closest(Selectors.rule_container); - self.invert(Model($rule), options); + self.invert(self.getModel($rule), options); }); } }); @@ -77,12 +84,13 @@ QueryBuilder.define('invert', function(options) { silent_fail: false }); -QueryBuilder.extend({ +QueryBuilder.extend(/** @lends InvertPlugin.prototype */ { /** * Invert a Group, a Rule or the whole builder + * @param {Node} [node] + * @param {object} [options] {@link InvertPlugin} + * @fires InvertPlugin#afterInvert * @throws InvertConditionError, InvertOperatorError - * @param {Node,optional} - * @param {object,optional} */ invert: function(node, options) { if (!(node instanceof Node)) { @@ -135,6 +143,11 @@ QueryBuilder.extend({ } if (options.trigger) { + /** + * @event InvertPlugin#afterInvert + * @param {Node} node + * @param {object} options + */ this.trigger('afterInvert', node, options); } } diff --git a/src/plugins/mongodb-support/plugin.js b/src/plugins/mongodb-support/plugin.js index bf98ab4e..b9470992 100644 --- a/src/plugins/mongodb-support/plugin.js +++ b/src/plugins/mongodb-support/plugin.js @@ -1,10 +1,8 @@ -/*! - * jQuery QueryBuilder MongoDB Support +/** * Allows to export rules as a MongoDB find object as well as populating the builder from a MongoDB object. + * @class MongoDbSupportPlugin */ -// DEFAULT CONFIG -// =============================== QueryBuilder.defaults({ mongoOperators: { // @formatter:off @@ -66,26 +64,42 @@ QueryBuilder.defaults({ return { 'val': v, 'op': 'contains' }; } }, - between: function(v) { return { 'val': [v.$gte, v.$lte], 'op': 'between' }; }, - not_between: function(v) { return { 'val': [v.$lt, v.$gt], 'op': 'not_between' }; }, - $in: function(v) { return { 'val': v.$in, 'op': 'in' }; }, - $nin: function(v) { return { 'val': v.$nin, 'op': 'not_in' }; }, - $lt: function(v) { return { 'val': v.$lt, 'op': 'less' }; }, - $lte: function(v) { return { 'val': v.$lte, 'op': 'less_or_equal' }; }, - $gt: function(v) { return { 'val': v.$gt, 'op': 'greater' }; }, - $gte: function(v) { return { 'val': v.$gte, 'op': 'greater_or_equal' }; } + between: function(v) { + return { 'val': [v.$gte, v.$lte], 'op': 'between' }; + }, + not_between: function(v) { + return { 'val': [v.$lt, v.$gt], 'op': 'not_between' }; + }, + $in: function(v) { + return { 'val': v.$in, 'op': 'in' }; + }, + $nin: function(v) { + return { 'val': v.$nin, 'op': 'not_in' }; + }, + $lt: function(v) { + return { 'val': v.$lt, 'op': 'less' }; + }, + $lte: function(v) { + return { 'val': v.$lte, 'op': 'less_or_equal' }; + }, + $gt: function(v) { + return { 'val': v.$gt, 'op': 'greater' }; + }, + $gte: function(v) { + return { 'val': v.$gte, 'op': 'greater_or_equal' }; + } } }); - -// PUBLIC METHODS -// =============================== -QueryBuilder.extend({ +QueryBuilder.extend(/** @lends MongoDbSupportPlugin.prototype */ { /** - * Get rules as MongoDB query + * Returns rules as a MongoDB query + * @param {object} [data] - current rules by default + * @returns {object} + * @fires MongoDbSupportPlugin#filter:getMongoDBField + * @fires MongoDbSupportPlugin#filter:ruleToMongo + * @fires MongoDbSupportPlugin#filter:groupToMongo * @throws UndefinedMongoConditionError, UndefinedMongoOperatorError - * @param data {object} (optional) rules - * @return {object} */ getMongo: function(data) { data = (data === undefined) ? this.getRules() : data; @@ -129,41 +143,81 @@ QueryBuilder.extend({ }); } - var ruleExpression = {}; + /** + * @event MongoDbSupportPlugin#filter:getMongoDBField + * @param {string} field + * @param {Rule} rule + * @returns {string} + */ var field = self.change('getMongoDBField', rule.field, rule); + + var ruleExpression = {}; ruleExpression[field] = mdb.call(self, values); + + /** + * @event MongoDbSupportPlugin#filter:ruleToMongo + * @param {object} expression + * @param {Rule} rule + * @param {*} value + * @param {function} valueWrapper + * @returns {object} + */ parts.push(self.change('ruleToMongo', ruleExpression, rule, values, mdb)); } }); var groupExpression = {}; groupExpression['$' + group.condition.toLowerCase()] = parts; + + /** + * @event MongoDbSupportPlugin#filter:groupToMongo + * @param {object} expression + * @param {Group} group + * @returns {object} + */ return self.change('groupToMongo', groupExpression, group); }(data)); }, /** - * Convert MongoDB object to rules + * Converts a MongoDB query to rules + * @param {object} query + * @returns {object} + * @fires MongoDbSupportPlugin#filter:parseMongoNode + * @fires MongoDbSupportPlugin#filter:getMongoDBFieldID + * @fires MongoDbSupportPlugin#filter:mongoToRule + * @fires MongoDbSupportPlugin#filter:mongoToGroup * @throws MongoParseError, UndefinedMongoConditionError, UndefinedMongoOperatorError - * @param data {object} query object - * @return {object} */ - getRulesFromMongo: function(data) { - if (data === undefined || data === null) { + getRulesFromMongo: function(query) { + if (query === undefined || query === null) { return null; } var self = this; - // allow plugins to manually parse or handle special cases - data = self.change('parseMongoNode', data); + /** + * Allow plugins to manually parse or handle special cases + * @event MongoDbSupportPlugin#filter:parseMongoNode + * @param {object} expression + * @returns {object} expression, rule or group + */ + query = self.change('parseMongoNode', query); // a plugin returned a group - if ('rules' in data && 'condition' in data) { - return data; + if ('rules' in query && 'condition' in query) { + return query; + } + + // a plugin returned a rule + if ('id' in query && 'operator' in query && 'value' in query) { + return { + condition: this.settings.default_condition, + rules: [query] + }; } - var key = andOr(data); + var key = andOr(query); if (!key) { Utils.error('MongoParse', 'Invalid MongoDB query format'); } @@ -208,8 +262,22 @@ QueryBuilder.extend({ var opVal = mdbrl.call(self, value); + /** + * @event MongoDbSupportPlugin#filter:getMongoDBFieldID + * @param {string} field + * @param {*} value + * @returns {string} + */ + var id = self.change('getMongoDBFieldID', field, value); + + /** + * @event MongoDbSupportPlugin#filter:mongoToRule + * @param {object} rule + * @param {object} expression + * @returns {object} + */ var rule = self.change('mongoToRule', { - id: self.change('getMongoDBFieldID', field, value), + id: id, field: field, operator: opVal.op, value: opVal.val @@ -219,29 +287,36 @@ QueryBuilder.extend({ } }); + /** + * @event MongoDbSupportPlugin#filter:mongoToGroup + * @param {object} group + * @param {object} {expression} + * @returns {object} + */ return self.change('mongoToGroup', { condition: topKey.replace('$', '').toUpperCase(), rules: parts }, data); - }(data, key)); + }(query, key)); }, /** - * Set rules from MongoDB object - * @param data {object} + * Sets rules a from MongoDB query + * @see MongoDbSupportPlugin#getRulesFromMongo */ - setRulesFromMongo: function(data) { - this.setRules(this.getRulesFromMongo(data)); + setRulesFromMongo: function(query) { + this.setRules(this.getRulesFromMongo(query)); } }); /** - * Find which operator is used in a MongoDB sub-object - * @param {mixed} value - * @param {string} field - * @return {string|undefined} + * Finds which operator is used in a MongoDB sub-object + * @param {*} value + * @returns {string|undefined} + * @memberof MongoDbSupportPlugin + * @private */ -function determineMongoOperator(value, field) { +function determineMongoOperator(value) { if (value !== null && typeof value == 'object') { var subkeys = Object.keys(value); @@ -272,6 +347,8 @@ function determineMongoOperator(value, field) { * Returns the key corresponding to "$or" or "$and" * @param {object} data * @returns {string} + * @memberof MongoDbSupportPlugin + * @private */ function andOr(data) { var keys = Object.keys(data); diff --git a/src/plugins/not-group/plugin.js b/src/plugins/not-group/plugin.js index 3f7588a4..8ab3f4b4 100644 --- a/src/plugins/not-group/plugin.js +++ b/src/plugins/not-group/plugin.js @@ -1,12 +1,22 @@ -/*! - * jQuery QueryBuilder Not +/** * Adds a "Not" checkbox in front of group conditions. + * @class NotGroupPlugin + * @param {object} [options] + * @param {string} [options.icon_checked=glyphicon glyphicon-checked] + * @param {string} [options.icon_unchecked=glyphicon glyphicon-unchecked] */ -Selectors.group_not = Selectors.group_header + ' [data-not=group]'; - +/** + * From {@link NotGroupPlugin} + * @name not + * @member {boolean} + * @memberof Group + * @instance + */ Model.defineModelProperties(Group, ['not']); +QueryBuilder.selectors.group_not = QueryBuilder.selectors.group_header + ' [data-not=group]'; + QueryBuilder.define('not-group', function(options) { var self = this; @@ -15,8 +25,8 @@ QueryBuilder.define('not-group', function(options) { */ this.on('afterInit', function() { self.$el.on('click.queryBuilder', '[data-not=group]', function() { - var $group = $(this).closest(Selectors.group_container); - var group = Model($group); + var $group = $(this).closest(QueryBuilder.selectors.group_container); + var group = self.getModel($group); group.not = !group.not; }); @@ -39,7 +49,7 @@ QueryBuilder.define('not-group', function(options) { */ this.on('getGroupTemplate.filter', function(h, level) { var $h = $(h.value); - $h.find(Selectors.condition_container).prepend( + $h.find(QueryBuilder.selectors.condition_container).prepend( '' @@ -120,17 +130,23 @@ QueryBuilder.define('not-group', function(options) { icon_checked: 'glyphicon glyphicon-check' }); -QueryBuilder.extend({ +QueryBuilder.extend(/** @lends NotGroupPlugin.prototype */ { /** - * Apply the "not" property to the DOM - * @param group + * Performs actions when a group's not changes + * @param {Group} group + * @fires NotGroupPlugin#afterUpdateGroupNot + * @private */ updateGroupNot: function(group) { var options = this.plugins['not-group']; - group.$el.find('>' + Selectors.group_not) + group.$el.find('>' + QueryBuilder.selectors.group_not) .toggleClass('active', group.not) .find('i').attr('class', group.not ? options.icon_checked : options.icon_unchecked); + /** + * @event NotGroupPlugin#afterUpdateGroupNot + * @param {Group} group + */ this.trigger('afterUpdateGroupNot', group); } }); diff --git a/src/plugins/sortable/plugin.js b/src/plugins/sortable/plugin.js index ec94fe81..cbcde92c 100644 --- a/src/plugins/sortable/plugin.js +++ b/src/plugins/sortable/plugin.js @@ -1,10 +1,15 @@ -/*! - * jQuery QueryBuilder Sortable +/** * Enables drag & drop sort of rules. + * @class SortablePlugin + * @param {object} [options] + * @param {boolean} [options.inherit_no_drop=true] + * @param {boolean} [options.inherit_no_sortable=true] + * @param {string} [options.icon=glyphicon glyphicon-sort] + * @throws MissingLibraryError, ConfigError */ -Selectors.rule_and_group_containers = Selectors.rule_container + ', ' + Selectors.group_container; -Selectors.drag_handle = '.drag-handle'; +QueryBuilder.selectors.rule_and_group_containers = QueryBuilder.selectors.rule_container + ', ' + QueryBuilder.selectors.group_container; +QueryBuilder.selectors.drag_handle = '.drag-handle'; QueryBuilder.defaults({ default_rule_flags: { @@ -62,11 +67,11 @@ QueryBuilder.define('sortable', function(options) { */ if (!node.flags.no_sortable) { interact(node.$el[0]) - .allowFrom(Selectors.drag_handle) + .allowFrom(QueryBuilder.selectors.drag_handle) .draggable({ onstart: function(event) { // get model of dragged element - src = Model(event.target); + src = self.getModel(event.target); // create ghost ghost = src.$el.clone() @@ -100,6 +105,10 @@ QueryBuilder.define('sortable', function(options) { // show element src.$el.show(); + /** + * @event SortablePlugin#afterMove + * @param {Node} node + */ self.trigger('afterMove', src); } }); @@ -111,7 +120,7 @@ QueryBuilder.define('sortable', function(options) { */ interact(node.$el[0]) .dropzone({ - accept: Selectors.rule_and_group_containers, + accept: QueryBuilder.selectors.rule_and_group_containers, ondragenter: function(event) { moveSortableToTarget(placeholder, $(event.target)); }, @@ -124,9 +133,9 @@ QueryBuilder.define('sortable', function(options) { * Configure drop on group headers */ if (node instanceof Group) { - interact(node.$el.find(Selectors.group_header)[0]) + interact(node.$el.find(QueryBuilder.selectors.group_header)[0]) .dropzone({ - accept: Selectors.rule_and_group_containers, + accept: QueryBuilder.selectors.rule_and_group_containers, ondragenter: function(event) { moveSortableToTarget(placeholder, $(event.target)); }, @@ -146,7 +155,7 @@ QueryBuilder.define('sortable', function(options) { interact(node.$el[0]).unset(); if (node instanceof Group) { - interact(node.$el.find(Selectors.group_header)[0]).unset(); + interact(node.$el.find(QueryBuilder.selectors.group_header)[0]).unset(); } } }); @@ -166,14 +175,14 @@ QueryBuilder.define('sortable', function(options) { this.on('getGroupTemplate.filter', function(h, level) { if (level > 1) { var $h = $(h.value); - $h.find(Selectors.condition_container).after('
'); + $h.find(QueryBuilder.selectors.condition_container).after('
'); h.value = $h.prop('outerHTML'); } }); this.on('getRuleTemplate.filter', function(h) { var $h = $(h.value); - $h.find(Selectors.rule_header).after('
'); + $h.find(QueryBuilder.selectors.rule_header).after('
'); h.value = $h.prop('outerHTML'); }); }, { @@ -183,13 +192,16 @@ QueryBuilder.define('sortable', function(options) { }); /** - * Move an element (placeholder or actual object) depending on active target + * Moves an element (placeholder or actual object) depending on active target * @param {Node} node * @param {jQuery} target * @param {QueryBuilder} [builder] + * @memberof SortablePlugin + * @private */ function moveSortableToTarget(node, target, builder) { var parent, method; + var Selectors = QueryBuilder.selectors; // on rule parent = target.closest(Selectors.rule_container); @@ -215,7 +227,7 @@ function moveSortableToTarget(node, target, builder) { } if (method) { - node[method](Model(parent)); + node[method](builder.getModel(parent)); // refresh radio value if (builder && node instanceof Rule) { diff --git a/src/plugins/sql-support/plugin.js b/src/plugins/sql-support/plugin.js index 6413cf39..70b4135f 100644 --- a/src/plugins/sql-support/plugin.js +++ b/src/plugins/sql-support/plugin.js @@ -1,33 +1,31 @@ -/*! - * jQuery QueryBuilder SQL Support +/** * Allows to export rules as a SQL WHERE statement as well as populating the builder from an SQL query. + * @class SqlSupportPlugin */ -// DEFAULT CONFIG -// =============================== QueryBuilder.defaults({ /* operators for internal -> SQL conversion */ sqlOperators: { - equal: { op: '= ?' }, - not_equal: { op: '!= ?' }, - in: { op: 'IN(?)', sep: ', ' }, - not_in: { op: 'NOT IN(?)', sep: ', ' }, - less: { op: '< ?' }, - less_or_equal: { op: '<= ?' }, - greater: { op: '> ?' }, + equal: { op: '= ?' }, + not_equal: { op: '!= ?' }, + in: { op: 'IN(?)', sep: ', ' }, + not_in: { op: 'NOT IN(?)', sep: ', ' }, + less: { op: '< ?' }, + less_or_equal: { op: '<= ?' }, + greater: { op: '> ?' }, greater_or_equal: { op: '>= ?' }, - between: { op: 'BETWEEN ?', sep: ' AND ' }, - not_between: { op: 'NOT BETWEEN ?', sep: ' AND ' }, - begins_with: { op: 'LIKE(?)', mod: '{0}%' }, - not_begins_with: { op: 'NOT LIKE(?)', mod: '{0}%' }, - contains: { op: 'LIKE(?)', mod: '%{0}%' }, - not_contains: { op: 'NOT LIKE(?)', mod: '%{0}%' }, - ends_with: { op: 'LIKE(?)', mod: '%{0}' }, - not_ends_with: { op: 'NOT LIKE(?)', mod: '%{0}' }, - is_empty: { op: '= \'\'' }, - is_not_empty: { op: '!= \'\'' }, - is_null: { op: 'IS NULL' }, - is_not_null: { op: 'IS NOT NULL' } + between: { op: 'BETWEEN ?', sep: ' AND ' }, + not_between: { op: 'NOT BETWEEN ?', sep: ' AND ' }, + begins_with: { op: 'LIKE(?)', mod: '{0}%' }, + not_begins_with: { op: 'NOT LIKE(?)', mod: '{0}%' }, + contains: { op: 'LIKE(?)', mod: '%{0}%' }, + not_contains: { op: 'NOT LIKE(?)', mod: '%{0}%' }, + ends_with: { op: 'LIKE(?)', mod: '%{0}' }, + not_ends_with: { op: 'NOT LIKE(?)', mod: '%{0}' }, + is_empty: { op: '= \'\'' }, + is_not_empty: { op: '!= \'\'' }, + is_null: { op: 'IS NULL' }, + is_not_null: { op: 'IS NOT NULL' } }, /* operators for SQL -> internal conversion */ @@ -90,14 +88,30 @@ QueryBuilder.defaults({ Utils.error('SQLParse', 'Invalid value for NOT LIKE operator "{0}"', v); } }, - 'IN': function(v) { return { val: v, op: 'in' }; }, - 'NOT IN': function(v) { return { val: v, op: 'not_in' }; }, - '<': function(v) { return { val: v, op: 'less' }; }, - '<=': function(v) { return { val: v, op: 'less_or_equal' }; }, - '>': function(v) { return { val: v, op: 'greater' }; }, - '>=': function(v) { return { val: v, op: 'greater_or_equal' }; }, - 'BETWEEN': function(v) { return { val: v, op: 'between' }; }, - 'NOT BETWEEN': function(v) { return { val: v, op: 'not_between' }; }, + 'IN': function(v) { + return { val: v, op: 'in' }; + }, + 'NOT IN': function(v) { + return { val: v, op: 'not_in' }; + }, + '<': function(v) { + return { val: v, op: 'less' }; + }, + '<=': function(v) { + return { val: v, op: 'less_or_equal' }; + }, + '>': function(v) { + return { val: v, op: 'greater' }; + }, + '>=': function(v) { + return { val: v, op: 'greater_or_equal' }; + }, + 'BETWEEN': function(v) { + return { val: v, op: 'between' }; + }, + 'NOT BETWEEN': function(v) { + return { val: v, op: 'not_between' }; + }, 'IS': function(v) { if (v !== null) { Utils.error('SQLParse', 'Invalid value for IS operator'); @@ -205,21 +219,27 @@ QueryBuilder.defaults({ } }); +/** + * @typedef {object} SqlSupportPlugin#SqlQuery + * @property {string} sql + * @property {object} params + */ -// PUBLIC METHODS -// =============================== -QueryBuilder.extend({ +QueryBuilder.extend(/** @lends SqlSupportPlugin.prototype */ { /** - * Get rules as SQL query + * Returns rules as a SQL query + * @param {boolean|string} [stmt] - use prepared statements: false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)' + * @param {boolean} [nl=false] output with new lines + * @param {object} [data] - current rules by default + * @returns {SqlSupportPlugin#SqlQuery} + * @fires SqlSupportPlugin#filter:getSQLField + * @fires SqlSupportPlugin#filter:ruleToSQL + * @fires SqlSupportPlugin#filter:groupToSQL * @throws UndefinedSQLConditionError, UndefinedSQLOperatorError - * @param stmt {boolean|string} use prepared statements - false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)' - * @param nl {bool} output with new lines - * @param data {object} (optional) rules - * @return {object} */ getSQL: function(stmt, nl, data) { data = (data === undefined) ? this.getRules() : data; - nl = (nl === true) ? '\n' : ' '; + nl = !!nl ? '\n' : ' '; if (stmt === true) stmt = 'question_mark'; if (typeof stmt == 'string') { @@ -263,7 +283,7 @@ QueryBuilder.extend({ rule.value.forEach(function(v, i) { if (i > 0) { - value+= sql.sep; + value += sql.sep; } if (rule.type == 'integer' || rule.type == 'double' || rule.type == 'boolean') { @@ -278,14 +298,14 @@ QueryBuilder.extend({ } if (stmt) { - value+= stmt.add(rule, v); + value += stmt.add(rule, v); } else { if (typeof v == 'string') { v = '\'' + v + '\''; } - value+= v; + value += v; } }); } @@ -294,12 +314,36 @@ QueryBuilder.extend({ return sql.op.replace(/\?/, v); }; - var ruleExpression = self.change('getSQLField', rule.field, rule) + ' ' + sqlFn(value); + /** + * @event SqlSupportPlugin#filter:getSQLField + * @param {string} field + * @param {Rule} rule + * @returns {string} + */ + var field = self.change('getSQLField', rule.field, rule); + + var ruleExpression = field + ' ' + sqlFn(value); + + /** + * @event SqlSupportPlugin#filter:ruleToSQL + * @param {string} expression + * @param {Rule} rule + * @param {*} value + * @param {function} valueWrapper + * @returns {string} + */ parts.push(self.change('ruleToSQL', ruleExpression, rule, value, sqlFn)); } }); var groupExpression = parts.join(' ' + group.condition + nl); + + /** + * @event SqlSupportPlugin#filter:groupToSQL + * @param {string} expression + * @param {Group} group + * @returns {string} + */ return self.change('groupToSQL', groupExpression, group); }(data)); @@ -317,56 +361,73 @@ QueryBuilder.extend({ }, /** - * Convert SQL to rules - * @throws ConfigError, SQLParseError, UndefinedSQLOperatorError - * @param data {object} query object - * @param stmt {boolean|string} use prepared statements - false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)' - * @return {object} + * Convert a SQL query to rules + * @param {string|SqlSupportPlugin#SqlQuery} query + * @param {boolean|string} stmt + * @returns {object} + * @fires SqlSupportPlugin#filter:parseSQLNode + * @fires SqlSupportPlugin#filter:getSQLFieldID + * @fires SqlSupportPlugin#filter:sqlToRule + * @fires SqlSupportPlugin#filter:sqlToGroup + * @throws MissingLibraryError, SQLParseError, UndefinedSQLOperatorError */ - getRulesFromSQL: function(data, stmt) { + getRulesFromSQL: function(query, stmt) { if (!('SQLParser' in window)) { Utils.error('MissingLibrary', 'SQLParser is required to parse SQL queries. Get it here https://github.com/mistic100/sql-parser'); } var self = this; - if (typeof data == 'string') { - data = { sql: data }; + if (typeof query == 'string') { + query = { sql: query }; } if (stmt === true) stmt = 'question_mark'; if (typeof stmt == 'string') { var config = getStmtConfig(stmt); - stmt = this.settings.sqlRuleStatement[config[1]](data.params, config[2]); + stmt = this.settings.sqlRuleStatement[config[1]](query.params, config[2]); } if (stmt) { - data.sql = stmt.esc(data.sql); + query.sql = stmt.esc(query.sql); } - if (data.sql.toUpperCase().indexOf('SELECT') !== 0) { - data.sql = 'SELECT * FROM table WHERE ' + data.sql; + if (query.sql.toUpperCase().indexOf('SELECT') !== 0) { + query.sql = 'SELECT * FROM table WHERE ' + query.sql; } - var parsed = SQLParser.parse(data.sql); + var parsed = SQLParser.parse(query.sql); if (!parsed.where) { Utils.error('SQLParse', 'No WHERE clause found'); } - // allow plugins to manually parse or handle special cases - data = self.change('parseSQLNode', parsed.where.conditions); + /** + * Allow plugins to manually parse or handle special cases + * @event SqlSupportPlugin#filter:parseSQLNode + * @param {object} AST + * @returns {object} tree, rule or group + */ + query = self.change('parseSQLNode', parsed.where.conditions); // a plugin returned a group - if ('rules' in data && 'condition' in data) { - return data; + if ('rules' in query && 'condition' in query) { + return query; + } + + // a plugin returned a rule + if ('id' in query && 'operator' in query && 'value' in query) { + return { + condition: this.settings.default_condition, + rules: [query] + }; } // create root group var out = self.change('sqlToGroup', { condition: this.settings.default_condition, rules: [] - }, data); + }, query); // keep track of current group var curr = out; @@ -396,6 +457,12 @@ QueryBuilder.extend({ 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()) { + /** + * @event SqlSupportPlugin#filter:sqlToGroup + * @param {object} group + * @param {object} AST + * @returns {object} + */ var group = self.change('sqlToGroup', { condition: self.settings.default_condition, rules: [] @@ -456,8 +523,22 @@ QueryBuilder.extend({ var opVal = sqlrl.call(this, value, data.operation); var field = data.left.values.join('.'); + /** + * @event SqlSupportPlugin#filter:getSQLFieldID + * @param {string} field + * @param {*} value + * @returns {string} + */ + var id = self.change('getSQLFieldID', field, value); + + /** + * @event SqlSupportPlugin#filter:sqlToRule + * @param {object} rule + * @param {object} AST + * @retunrs {object} + */ var rule = self.change('sqlToRule', { - id: self.change('getSQLFieldID', field, value), + id: id, field: field, operator: opVal.op, value: opVal.val @@ -465,21 +546,27 @@ QueryBuilder.extend({ curr.rules.push(rule); } - }(data, 0)); + }(query, 0)); return out; }, /** - * Set rules from SQL - * @param data {object} - * @param stmt {boolean|string} + * Sets the rules from a SQL query + * @see SqlSupportPlugin#getRulesFromSQL */ - setRulesFromSQL: function(data, stmt) { - this.setRules(this.getRulesFromSQL(data, stmt)); + setRulesFromSQL: function(query, stmt) { + this.setRules(this.getRulesFromSQL(query, stmt)); } }); +/** + * Parses the statement configuration + * @param {string} stmt + * @returns {Array} null, mode, option + * @memberof SqlSupportPlugin + * @private + */ function getStmtConfig(stmt) { var config = stmt.match(/(question_mark|numbered|named)(?:\((.)\))?/); if (!config) config = [null, 'question_mark', undefined]; diff --git a/src/plugins/unique-filter/plugin.js b/src/plugins/unique-filter/plugin.js index aee31b7c..7a2d6c10 100644 --- a/src/plugins/unique-filter/plugin.js +++ b/src/plugins/unique-filter/plugin.js @@ -1,8 +1,7 @@ -/*! - * jQuery QueryBuilder Unique Filter +/** * Allows to define some filters as "unique": ie which can be used for only one rule, globally or in the same group. + * @class UniqueFilterPlugin */ - QueryBuilder.define('unique-filter', function() { this.status.used_filters = {}; @@ -30,17 +29,18 @@ QueryBuilder.define('unique-filter', function() { }); if (!found) { - Utils.error('UniqueFilter', 'No more non-unique filters available'); + Utils.error(false, 'UniqueFilter', 'No more non-unique filters available'); e.value = undefined; } } }); }); -QueryBuilder.extend({ +QueryBuilder.extend(/** @lends UniqueFilterPlugin.prototype*/ { /** - * Update the list of used filters - * @param [e] + * Updates the list of used filters + * @param {$.Event} [e] + * @private */ updateDisabledFilters: function(e) { var self = e ? e.builder : this; @@ -72,7 +72,8 @@ QueryBuilder.extend({ /** * Clear the list of used filters - * @param [e] + * @param {$.Event} [e] + * @private */ clearDisabledFilters: function(e) { var self = e ? e.builder : this; @@ -84,23 +85,24 @@ QueryBuilder.extend({ /** * Disabled filters depending on the list of used ones - * @param [e] + * @param {$.Event} [e] + * @private */ applyDisabledFilters: function(e) { var self = e ? e.builder : this; // re-enable everything - self.$el.find(Selectors.filter_container + ' option').prop('disabled', false); + self.$el.find(QueryBuilder.selectors.filter_container + ' option').prop('disabled', false); // disable some $.each(self.status.used_filters, function(filterId, groups) { if (groups.length === 0) { - self.$el.find(Selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true); + self.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true); } else { groups.forEach(function(group) { group.each(function(rule) { - rule.$el.find(Selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true); + rule.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true); }); }); } @@ -108,7 +110,7 @@ QueryBuilder.extend({ // update Selectpicker if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) { - self.$el.find(Selectors.rule_filter).selectpicker('render'); + self.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render'); } } }); diff --git a/src/public.js b/src/public.js index a2c1325f..2533be1e 100644 --- a/src/public.js +++ b/src/public.js @@ -1,7 +1,11 @@ /** - * Destroy the plugin + * Destroys the builder + * @fires QueryBuilder#beforeDestroy */ QueryBuilder.prototype.destroy = function() { + /** + * @event QueryBuilder#beforeDestroy + */ this.trigger('beforeDestroy'); if (this.status.generated_id) { @@ -20,10 +24,19 @@ QueryBuilder.prototype.destroy = function() { }; /** - * Reset the plugin + * Clear all rules and resets the root group + * @fires QueryBuilder#beforeReset + * @fires QueryBuilder#afterReset */ QueryBuilder.prototype.reset = function() { - this.trigger('beforeReset'); + /** + * Preventable + * @event QueryBuilder#beforeReset + */ + var e = this.trigger('beforeReset'); + if (e.isDefaultPrevented()) { + return; + } this.status.group_id = 1; this.status.rule_id = 0; @@ -32,14 +45,26 @@ QueryBuilder.prototype.reset = function() { this.addRule(this.model.root); + /** + * @event QueryBuilder#afterReset + */ this.trigger('afterReset'); }; /** - * Clear the plugin + * Clears all rules and removes the root group + * @fires QueryBuilder#beforeClear + * @fires QueryBuilder#afterClear */ QueryBuilder.prototype.clear = function() { - this.trigger('beforeClear'); + /** + * Preventable + * @event QueryBuilder#beforeClear + */ + var e = this.trigger('beforeClear'); + if (e.isDefaultPrevented()) { + return; + } this.status.group_id = 0; this.status.rule_id = 0; @@ -49,13 +74,16 @@ QueryBuilder.prototype.clear = function() { this.model.root = null; } + /** + * @event QueryBuilder#afterClear + */ this.trigger('afterClear'); }; /** - * Modify the builder configuration + * Modifies the builder configuration.
* Only options defined in QueryBuilder.modifiable_options are modifiable - * @param {object} + * @param {object} options */ QueryBuilder.prototype.setOptions = function(options) { $.each(options, function(opt, value) { @@ -66,19 +94,28 @@ QueryBuilder.prototype.setOptions = function(options) { }; /** - * Return the model associated to a DOM object, or root model - * @param {jQuery,optional} - * @return {Node} + * Returns the model associated to a DOM object, or the root model + * @param {jQuery} [target] + * @returns {Node} */ QueryBuilder.prototype.getModel = function(target) { - return !target ? this.model.root : Model(target); + if (!target) { + return this.model.root; + } + else if (target instanceof Node) { + return target; + } + else { + return $(target).data('queryBuilderModel'); + } }; /** - * Validate the whole builder - * @param {object} options - * - skip_empty: false[default] | true(skips validating rules that have no filter selected) - * @return {boolean} + * Validates the whole builder + * @param {object} [options] + * @param {boolean} [options.skip_empty=false] - skips validating rules that have no filter selected + * @returns {boolean} + * @fires QueryBuilder#filter:validate */ QueryBuilder.prototype.validate = function(options) { options = $.extend({ @@ -147,16 +184,24 @@ QueryBuilder.prototype.validate = function(options) { }(this.model.root)); + /** + * @event QueryBuilder#filter:validate + * @param {boolean} valid + * @returns {boolean} + */ return this.change('validate', valid); }; /** - * Get an object representing current rules - * @param {object} options - * - get_flags: false[default] | true(only changes from default flags) | 'all' - * - allow_invalid: false[default] | true(returns rules even if they are invalid) - * - skip_empty: false[default] | true(remove rules that have no filter selected) - * @return {object} + * Gets an object representing current rules + * @param {object} [options] + * @param {boolean|string} [options.get_flags=false] - export flags, true: only changes from default flags or 'all' + * @param {boolean} [options.allow_invalid=false] - returns rules even if they are invalid + * @param {boolean} [options.skip_empty=false] - remove rules that have no filter selected + * @returns {object} + * @fires QueryBuilder#filter:ruleToJson + * @fires QueryBuilder#filter:groupToJson + * @fires QueryBuilder#filter:getRules */ QueryBuilder.prototype.getRules = function(options) { options = $.extend({ @@ -219,6 +264,12 @@ QueryBuilder.prototype.getRules = function(options) { } } + /** + * @event QueryBuilder#filter:ruleToJson + * @param {object} json + * @param {Rule} rule + * @returns {object} + */ groupData.rules.push(self.change('ruleToJson', ruleData, rule)); }, function(model) { @@ -228,21 +279,36 @@ QueryBuilder.prototype.getRules = function(options) { } }, this); + /** + * @event QueryBuilder#filter:groupToJson + * @param {object} json + * @param {Group} group + * @returns {object} + */ return self.change('groupToJson', groupData, group); }(this.model.root)); out.valid = valid; + /** + * @avant QueryBuilder#filter:getRules + * @param {object} json + * @returns {object} + */ return this.change('getRules', out); }; /** - * Set rules from object + * Sets rules from object + * @param {object} data + * @param {object} [options] + * @param {boolean} [options.allow_invalid=false] - silent-fail if the data are invalid * @throws RulesError, UndefinedConditionError - * @param data {object} - * @param {object} options - * - allow_invalid: false[default] | true(silent-fail if the data are invalid) + * @fires QueryBuilder#filter:setRules + * @fires QueryBuilder#filter:jsonToRule + * @fires QueryBuilder#filter:jsonToGroup + * @fires QueryBuilder#afterSetRules */ QueryBuilder.prototype.setRules = function(data, options) { options = $.extend({ @@ -264,6 +330,11 @@ QueryBuilder.prototype.setRules = function(data, options) { this.setRoot(false, data.data, this.parseGroupFlags(data)); this.applyGroupFlags(this.model.root); + /** + * @event QueryBuilder#filter:setRules + * @param {object} json + * @returns {object} + */ data = this.change('setRules', data); var self = this; @@ -336,15 +407,30 @@ QueryBuilder.prototype.setRules = function(data, options) { self.applyRuleFlags(model); + /** + * @event QueryBuilder#filter:jsonToRule + * @param {Rule} rule + * @param {object} json + */ if (self.change('jsonToRule', model, item) != model) { Utils.error('RulesParse', 'Plugin tried to change rule reference'); } } }); + /** + * @event QueryBuilder#filter:jsonToGroup + * @param {Group} group + * @param {object} json + */ if (self.change('jsonToGroup', group, data) != group) { Utils.error('RulesParse', 'Plugin tried to change group reference'); } }(data, this.model.root)); + + /** + * @event QueryBuilder#afterSetRules + */ + this.trigger('afterSetRules'); }; diff --git a/src/template.js b/src/template.js index c5869c1a..6912f34d 100644 --- a/src/template.js +++ b/src/template.js @@ -88,10 +88,12 @@ QueryBuilder.templates.operatorSelect = '\ '; /** - * Returns group HTML - * @param group_id {string} - * @param level {int} - * @return {string} + * Returns group's HTML + * @param {string} group_id + * @param {int} level + * @returns {string} + * @fires QueryBuilder#filter:getGroupTemplate + * @private */ QueryBuilder.prototype.getGroupTemplate = function(group_id, level) { var h = this.templates.group({ @@ -104,13 +106,21 @@ QueryBuilder.prototype.getGroupTemplate = function(group_id, level) { settings: this.settings }); + /** + * @event QueryBuilder#filter:getGroupTemplate + * @param {string} html + * @param {int} level + * @returns {string} + */ return this.change('getGroupTemplate', h, level); }; /** - * Returns rule HTML - * @param rule_id {string} - * @return {string} + * Returns rule's HTML + * @param {string} rule_id + * @returns {string} + * @fires QueryBuilder#filter:getRuleTemplate + * @private */ QueryBuilder.prototype.getRuleTemplate = function(rule_id) { var h = this.templates.rule({ @@ -121,14 +131,21 @@ QueryBuilder.prototype.getRuleTemplate = function(rule_id) { settings: this.settings }); + /** + * @event QueryBuilder#filter:getRuleTemplate + * @param {string} html + * @returns {string} + */ return this.change('getRuleTemplate', h); }; /** - * Returns rule filter HTML - * @param rule {Rule} - * @param operators {object} - * @return {string} + * Returns rule's operator HTML + * @param {Rule} rule + * @param {object[]} operators + * @returns {string} + * @fires QueryBuilder#filter:getRuleOperatorTemplate + * @private */ QueryBuilder.prototype.getRuleOperatorSelect = function(rule, operators) { var h = this.templates.operatorSelect({ @@ -158,18 +183,25 @@ QueryBuilder.prototype.getRuleOperatorSelect = function(rule, operators) { icons: this.icons, lang: this.lang, settings: this.settings, - translate: this.translateLabel + translate: this.getTranslatedLabel }); + /** + * @event QueryBuilder#filter:getRuleOperatorTemplate + * @param {string} html + * @param {Rule} rule + * @returns {string} + */ return this.change('getRuleOperatorSelect', h, rule); }; /** - * Return the rule value HTML - * @param rule {Rule} - * @param filter {object} - * @param value_id {int} - * @return {string} + * Returns the rule's value HTML + * @param {Rule} rule + * @param {int} value_id + * @returns {string} + * @fires QueryBuilder#filter:getRuleInput + * @private */ QueryBuilder.prototype.getRuleInput = function(rule, value_id) { var filter = rule.filter; @@ -183,52 +215,60 @@ QueryBuilder.prototype.getRuleInput = function(rule, value_id) { } else { switch (filter.input) { - case 'radio': case 'checkbox': + case 'radio': + case 'checkbox': Utils.iterateOptions(filter.values, function(key, val) { - h+= ' ' + val + ' '; + h += ' ' + val + ' '; }); break; case 'select': - h+= ''; if (filter.placeholder) { - h+= ''; + h += ''; } Utils.iterateOptions(filter.values, function(key, val) { - h+= ' '; + h += ' '; }); - h+= ''; + h += ''; break; case 'textarea': - h+= '