diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..6750fc50 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ +## PR Description +Add a meaningful description here that will let us know what you want to fix with this PR or what functionality you want to add. + +## Steps before you submit a PR +- Please add tests for the code you add if it's possible. +- Please check out our contribution guide: https://docs.spryker.com/docs/dg/dev/code-contribution-guide.html +- Add a `contribution-license-agreement.txt` file with the following content: +`I hereby agree to Spryker\'s Contribution License Agreement in https://github.com/spryker/jquery-query-builder/blob/HASH_OF_COMMIT_YOU_ARE_BASING_YOUR_BRANCH_FROM_MASTER_BRANCH/CONTRIBUTING.md.` + +This is a mandatory step to make sure you are aware of the license agreement and agree to it. `HASH_OF_COMMIT_YOU_ARE_BASING_YOUR_BRANCH_FROM_MASTER_BRANCH` is a hash of the commit you are basing your branch from the master branch. You can take it from commits list of master branch before you submit a PR. + +## Checklist +- [x] I agree with the Code Contribution License Agreement in CONTRIBUTING.md diff --git a/LICENSE b/LICENSE index 99070e3d..1cb3d4f8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ The MIT License (MIT) Copyright (c) 2014-2015 Damien Sorel +Copyright (c) 2021-present Spryker Systems GmbH Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +19,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/composer.json b/composer.json index 359b2cf1..4e2d61a1 100644 --- a/composer.json +++ b/composer.json @@ -1,26 +1,28 @@ { - "name": "mistic100/jquery-querybuilder", - "version": "2.3.3", - "authors": [{ - "name": "Damien \"Mistic\" Sorel", - "email": "contact@git.strangeplanet.fr", - "homepage": "http://www.strangeplanet.fr" - }], - "description": "jQuery plugin for user friendly query/filter creator", - "require": { - "components/jquery": ">=1.9.0", - "moment/moment": ">=2.6.0", - "twbs/bootstrap": ">=3.1.0" - }, - "keywords": [ - "jquery", - "query", - "builder", - "filter" - ], - "license": "MIT", - "homepage": "https://github.com/mistic100/jQuery-QueryBuilder", - "support": { - "issues": "https://github.com/mistic100/jQuery-QueryBuilder/issues" - } + "name": "mistic100/jquery-querybuilder", + "version": "2.3.3", + "authors": [ + { + "name": "Damien \"Mistic\" Sorel", + "email": "contact@git.strangeplanet.fr", + "homepage": "http://www.strangeplanet.fr" + } + ], + "description": "jQuery plugin for user friendly query/filter creator", + "require": { + "components/jquery": ">=1.9.0", + "moment/moment": ">=2.6.0", + "twbs/bootstrap": ">=3.1.0" + }, + "keywords": [ + "jquery", + "query", + "builder", + "filter" + ], + "license": "MIT", + "homepage": "https://github.com/mistic100/jQuery-QueryBuilder", + "support": { + "issues": "https://github.com/mistic100/jQuery-QueryBuilder/issues" + } } diff --git a/dist/i18n/query-builder.ar.js b/dist/i18n/query-builder.ar.js index e8444c65..36b01256 100644 --- a/dist/i18n/query-builder.ar.js +++ b/dist/i18n/query-builder.ar.js @@ -7,7 +7,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -77,4 +77,4 @@ QueryBuilder.regional['ar'] = { }; QueryBuilder.defaults({ lang_code: 'ar' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.az.js b/dist/i18n/query-builder.az.js index 69e710f2..b27739f2 100644 --- a/dist/i18n/query-builder.az.js +++ b/dist/i18n/query-builder.az.js @@ -7,7 +7,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -76,4 +76,4 @@ QueryBuilder.regional['az'] = { }; QueryBuilder.defaults({ lang_code: 'az' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.cs.js b/dist/i18n/query-builder.cs.js index fa5b8039..caf93d37 100644 --- a/dist/i18n/query-builder.cs.js +++ b/dist/i18n/query-builder.cs.js @@ -7,7 +7,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -76,4 +76,4 @@ QueryBuilder.regional['cs'] = { }; QueryBuilder.defaults({ lang_code: 'cs' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.da.js b/dist/i18n/query-builder.da.js index 05a366e0..a7007188 100644 --- a/dist/i18n/query-builder.da.js +++ b/dist/i18n/query-builder.da.js @@ -7,7 +7,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -53,4 +53,4 @@ QueryBuilder.regional['da'] = { }; QueryBuilder.defaults({ lang_code: 'da' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.de.js b/dist/i18n/query-builder.de.js index 2ca6ed4c..32cdf437 100644 --- a/dist/i18n/query-builder.de.js +++ b/dist/i18n/query-builder.de.js @@ -7,7 +7,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -73,4 +73,4 @@ QueryBuilder.regional['de'] = { }; QueryBuilder.defaults({ lang_code: 'de' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.en.js b/dist/i18n/query-builder.en.js index c77da5d5..45685a59 100644 --- a/dist/i18n/query-builder.en.js +++ b/dist/i18n/query-builder.en.js @@ -7,7 +7,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -77,4 +77,4 @@ QueryBuilder.regional['en'] = { }; QueryBuilder.defaults({ lang_code: 'en' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.es.js b/dist/i18n/query-builder.es.js index 3aeaa70d..db743f57 100644 --- a/dist/i18n/query-builder.es.js +++ b/dist/i18n/query-builder.es.js @@ -7,7 +7,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -72,4 +72,4 @@ QueryBuilder.regional['es'] = { }; QueryBuilder.defaults({ lang_code: 'es' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.fa-IR.js b/dist/i18n/query-builder.fa-IR.js index 6c00e9de..cda793cd 100644 --- a/dist/i18n/query-builder.fa-IR.js +++ b/dist/i18n/query-builder.fa-IR.js @@ -7,7 +7,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -75,4 +75,4 @@ QueryBuilder.regional['fa-IR'] = { }; QueryBuilder.defaults({ lang_code: 'fa-IR' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.fr.js b/dist/i18n/query-builder.fr.js index e7c7e5b0..fafc8bfa 100644 --- a/dist/i18n/query-builder.fr.js +++ b/dist/i18n/query-builder.fr.js @@ -7,7 +7,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -77,4 +77,4 @@ QueryBuilder.regional['fr'] = { }; QueryBuilder.defaults({ lang_code: 'fr' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.it.js b/dist/i18n/query-builder.it.js index 04542dcd..1c9bd301 100644 --- a/dist/i18n/query-builder.it.js +++ b/dist/i18n/query-builder.it.js @@ -6,7 +6,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -49,4 +49,4 @@ QueryBuilder.regional['it'] = { }; QueryBuilder.defaults({ lang_code: 'it' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.nl.js b/dist/i18n/query-builder.nl.js index aef1f152..c0fdbf43 100644 --- a/dist/i18n/query-builder.nl.js +++ b/dist/i18n/query-builder.nl.js @@ -7,7 +7,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -72,4 +72,4 @@ QueryBuilder.regional['nl'] = { }; QueryBuilder.defaults({ lang_code: 'nl' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.no.js b/dist/i18n/query-builder.no.js index e4fe8fa0..1fc9637c 100644 --- a/dist/i18n/query-builder.no.js +++ b/dist/i18n/query-builder.no.js @@ -7,7 +7,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -51,4 +51,4 @@ QueryBuilder.regional['no'] = { }; QueryBuilder.defaults({ lang_code: 'no' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.pl.js b/dist/i18n/query-builder.pl.js index 9b5608dc..5be1f79d 100644 --- a/dist/i18n/query-builder.pl.js +++ b/dist/i18n/query-builder.pl.js @@ -7,7 +7,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -77,4 +77,4 @@ QueryBuilder.regional['pl'] = { }; QueryBuilder.defaults({ lang_code: 'pl' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.pt-BR.js b/dist/i18n/query-builder.pt-BR.js index 0f37269d..72d49a9c 100644 --- a/dist/i18n/query-builder.pt-BR.js +++ b/dist/i18n/query-builder.pt-BR.js @@ -7,7 +7,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -72,4 +72,4 @@ QueryBuilder.regional['pt-BR'] = { }; QueryBuilder.defaults({ lang_code: 'pt-BR' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.pt-PT.js b/dist/i18n/query-builder.pt-PT.js index 911858a2..14c0ae75 100644 --- a/dist/i18n/query-builder.pt-PT.js +++ b/dist/i18n/query-builder.pt-PT.js @@ -7,7 +7,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -72,4 +72,4 @@ QueryBuilder.regional['pt-PT'] = { }; QueryBuilder.defaults({ lang_code: 'pt-PT' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.ro.js b/dist/i18n/query-builder.ro.js index 31b81127..e601fb22 100644 --- a/dist/i18n/query-builder.ro.js +++ b/dist/i18n/query-builder.ro.js @@ -7,7 +7,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -51,4 +51,4 @@ QueryBuilder.regional['ro'] = { }; QueryBuilder.defaults({ lang_code: 'ro' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.ru.js b/dist/i18n/query-builder.ru.js index 0ee2091d..cb19f014 100644 --- a/dist/i18n/query-builder.ru.js +++ b/dist/i18n/query-builder.ru.js @@ -6,7 +6,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -74,4 +74,4 @@ QueryBuilder.regional['ru'] = { }; QueryBuilder.defaults({ lang_code: 'ru' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.sq.js b/dist/i18n/query-builder.sq.js index ba179c12..a6d434d2 100644 --- a/dist/i18n/query-builder.sq.js +++ b/dist/i18n/query-builder.sq.js @@ -7,7 +7,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -75,4 +75,4 @@ QueryBuilder.regional['sq'] = { }; QueryBuilder.defaults({ lang_code: 'sq' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.tr.js b/dist/i18n/query-builder.tr.js index dd9f9a2b..311b19a4 100644 --- a/dist/i18n/query-builder.tr.js +++ b/dist/i18n/query-builder.tr.js @@ -7,7 +7,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -77,4 +77,4 @@ QueryBuilder.regional['tr'] = { }; QueryBuilder.defaults({ lang_code: 'tr' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.ua.js b/dist/i18n/query-builder.ua.js index fd060991..75954cbe 100644 --- a/dist/i18n/query-builder.ua.js +++ b/dist/i18n/query-builder.ua.js @@ -7,7 +7,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -76,4 +76,4 @@ QueryBuilder.regional['ua'] = { }; QueryBuilder.defaults({ lang_code: 'ua' }); -})); \ No newline at end of file +})); diff --git a/dist/i18n/query-builder.zh-CN.js b/dist/i18n/query-builder.zh-CN.js index e7b7859c..4de3074f 100644 --- a/dist/i18n/query-builder.zh-CN.js +++ b/dist/i18n/query-builder.zh-CN.js @@ -7,7 +7,7 @@ (function(root, factory) { if (typeof define == 'function' && define.amd) { - define(['jquery', 'query-builder'], factory); + define(['jquery'], factory); } else { factory(root.jQuery); @@ -77,4 +77,4 @@ QueryBuilder.regional['zh-CN'] = { }; QueryBuilder.defaults({ lang_code: 'zh-CN' }); -})); \ No newline at end of file +})); diff --git a/index.js b/index.js new file mode 100644 index 00000000..9324e084 --- /dev/null +++ b/index.js @@ -0,0 +1,4371 @@ +"use strict"; + +var jQuery = require('jquery'); +var dot = require('dot/doT'); +require('jquery-extendext'); + +(function (factory) { + factory(jQuery, dot); +}(function ($, doT) { + +// CLASS DEFINITION +// =============================== + var QueryBuilder = function ($el, options) { + this.init($el, options); + }; + + +// EVENTS SYSTEM +// =============================== + $.extend(QueryBuilder.prototype, { + change: function (type, value) { + var event = new $.Event(type + '.queryBuilder.filter', { + builder: this, + value: value + }); + + this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 2)); + + return event.value; + }, + + trigger: function (type) { + var event = new $.Event(type + '.queryBuilder', { + builder: this + }); + + this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 1)); + + return event; + }, + + on: function (type, cb) { + this.$el.on(type + '.queryBuilder', cb); + return this; + }, + + off: function (type, cb) { + this.$el.off(type + '.queryBuilder', cb); + return this; + }, + + once: function (type, cb) { + this.$el.one(type + '.queryBuilder', cb); + return this; + } + }); + + +// PLUGINS SYSTEM +// =============================== + QueryBuilder.plugins = {}; + + /** + * Get or extend the default configuration + * @param options {object,optional} new configuration, leave undefined to get the default config + * @return {undefined|object} nothing or configuration object (copy) + */ + QueryBuilder.defaults = function (options) { + if (typeof options == 'object') { + $.extendext(true, 'replace', QueryBuilder.DEFAULTS, options); + } + else if (typeof options == 'string') { + if (typeof QueryBuilder.DEFAULTS[options] == 'object') { + return $.extend(true, {}, QueryBuilder.DEFAULTS[options]); + } + else { + return QueryBuilder.DEFAULTS[options]; + } + } + else { + return $.extend(true, {}, QueryBuilder.DEFAULTS); + } + }; + + /** + * Define a new plugin + * @param {string} + * @param {function} + * @param {object,optional} default configuration + */ + QueryBuilder.define = function (name, fct, def) { + QueryBuilder.plugins[name] = { + fct: fct, + def: def || {} + }; + }; + + /** + * Add new methods + * @param {object} + */ + QueryBuilder.extend = function (methods) { + $.extend(QueryBuilder.prototype, methods); + }; + + /** + * Init plugins for an instance + * @throws ConfigError + */ + QueryBuilder.prototype.initPlugins = function () { + if (!this.plugins) { + return; + } + + if ($.isArray(this.plugins)) { + var tmp = {}; + this.plugins.forEach(function (plugin) { + tmp[plugin] = null; + }); + this.plugins = tmp; + } + + Object.keys(this.plugins).forEach(function (plugin) { + if (plugin in QueryBuilder.plugins) { + this.plugins[plugin] = $.extend(true, {}, + QueryBuilder.plugins[plugin].def, + this.plugins[plugin] || {} + ); + + QueryBuilder.plugins[plugin].fct.call(this, this.plugins[plugin]); + } + else { + Utils.error('Config', 'Unable to find plugin "{0}"', plugin); + } + }, this); + }; + + + /** + * Allowed types and their internal representation + */ + QueryBuilder.types = { + 'string': 'string', + 'integer': 'number', + 'double': 'number', + 'date': 'datetime', + 'time': 'datetime', + 'datetime': 'datetime', + 'boolean': 'boolean' + }; + + /** + * Allowed inputs + */ + QueryBuilder.inputs = [ + 'text', + 'textarea', + 'radio', + 'checkbox', + 'select' + ]; + + /** + * Runtime modifiable options with `setOptions` method + */ + QueryBuilder.modifiable_options = [ + 'display_errors', + 'allow_groups', + 'allow_empty', + 'default_condition', + 'default_filter' + ]; + + /** + * CSS selectors for common components + */ + var Selectors = QueryBuilder.selectors = { + group_container: '.rules-group-container', + rule_container: '.rule-container', + filter_container: '.rule-filter-container', + operator_container: '.rule-operator-container', + value_container: '.rule-value-container', + error_container: '.error-container', + condition_container: '.rules-group-header .group-conditions', + + rule_header: '.rule-header', + group_header: '.rules-group-header', + group_actions: '.group-actions', + rule_actions: '.rule-actions', + + rules_list: '.rules-group-body>.rules-list', + + group_condition: '.rules-group-header [name$=_cond]', + rule_filter: '.rule-filter-container [name$=_filter]', + rule_operator: '.rule-operator-container [name$=_operator]', + rule_value: '.rule-value-container [name*=_value_]', + + add_rule: '[data-add=rule]', + delete_rule: '[data-delete=rule]', + add_group: '[data-add=group]', + delete_group: '[data-delete=group]' + }; + + /** + * Template strings (see `template.js`) + */ + QueryBuilder.templates = {}; + + /** + * Localized strings (see `i18n/`) + */ + QueryBuilder.regional = {}; + + /** + * Default operators + */ + QueryBuilder.OPERATORS = { + equal: {type: 'equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean']}, + not_equal: { + type: 'not_equal', + nb_inputs: 1, + multiple: false, + apply_to: ['string', 'number', 'datetime', 'boolean'] + }, + in: {type: 'in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime']}, + not_in: {type: 'not_in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime']}, + less: {type: 'less', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime']}, + less_or_equal: {type: 'less_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime']}, + greater: {type: 'greater', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime']}, + greater_or_equal: {type: 'greater_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime']}, + between: {type: 'between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime']}, + not_between: {type: 'not_between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime']}, + begins_with: {type: 'begins_with', nb_inputs: 1, multiple: false, apply_to: ['string']}, + not_begins_with: {type: 'not_begins_with', nb_inputs: 1, multiple: false, apply_to: ['string']}, + contains: {type: 'contains', nb_inputs: 1, multiple: false, apply_to: ['string']}, + not_contains: {type: 'not_contains', nb_inputs: 1, multiple: false, apply_to: ['string']}, + ends_with: {type: 'ends_with', nb_inputs: 1, multiple: false, apply_to: ['string']}, + not_ends_with: {type: 'not_ends_with', nb_inputs: 1, multiple: false, apply_to: ['string']}, + is_empty: {type: 'is_empty', nb_inputs: 0, multiple: false, apply_to: ['string']}, + is_not_empty: {type: 'is_not_empty', nb_inputs: 0, multiple: false, apply_to: ['string']}, + is_null: { + type: 'is_null', + nb_inputs: 0, + multiple: false, + apply_to: ['string', 'number', 'datetime', 'boolean'] + }, + is_not_null: { + type: 'is_not_null', + nb_inputs: 0, + multiple: false, + apply_to: ['string', 'number', 'datetime', 'boolean'] + } + }; + + /** + * Default configuration + */ + QueryBuilder.DEFAULTS = { + filters: [], + plugins: [], + + sort_filters: false, + display_errors: true, + allow_groups: -1, + allow_empty: false, + conditions: ['AND', 'OR'], + default_condition: 'AND', + inputs_separator: ' , ', + select_placeholder: '------', + display_empty_filter: true, + default_filter: null, + optgroups: {}, + + default_rule_flags: { + filter_readonly: false, + operator_readonly: false, + value_readonly: false, + no_delete: false + }, + + default_group_flags: { + condition_readonly: false, + no_delete: false + }, + + templates: { + group: null, + rule: null, + filterSelect: null, + operatorSelect: null + }, + + lang_code: 'en', + lang: {}, + + operators: [ + 'equal', + 'not_equal', + 'in', + 'not_in', + 'less', + 'less_or_equal', + 'greater', + 'greater_or_equal', + 'between', + 'not_between', + 'begins_with', + 'not_begins_with', + 'contains', + 'not_contains', + 'ends_with', + 'not_ends_with', + 'is_empty', + 'is_not_empty', + 'is_null', + 'is_not_null' + ], + + icons: { + add_group: 'glyphicon glyphicon-plus-sign', + add_rule: 'glyphicon glyphicon-plus', + remove_group: 'glyphicon glyphicon-remove', + remove_rule: 'glyphicon glyphicon-remove', + error: 'glyphicon glyphicon-warning-sign' + } + }; + + + /** + * Init the builder + */ + QueryBuilder.prototype.init = function ($el, options) { + $el[0].queryBuilder = this; + this.$el = $el; + + // PROPERTIES + this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options); + this.model = new Model(); + this.status = { + group_id: 0, + rule_id: 0, + generated_id: false, + has_optgroup: false, + has_operator_oprgroup: false, + id: null, + updating_value: false + }; + + // "allow_groups" can be boolean or int + if (this.settings.allow_groups === false) { + this.settings.allow_groups = 0; + } + else if (this.settings.allow_groups === true) { + this.settings.allow_groups = -1; + } + + // SETTINGS SHORTCUTS + this.filters = this.settings.filters; + this.icons = this.settings.icons; + this.operators = this.settings.operators; + this.templates = this.settings.templates; + this.plugins = this.settings.plugins; + + // translations : english << 'lang_code' << custom + if (QueryBuilder.regional['en'] === undefined) { + Utils.error('Config', '"i18n/en.js" not loaded.'); + } + this.lang = $.extendext(true, 'replace', {}, QueryBuilder.regional['en'], QueryBuilder.regional[this.settings.lang_code], this.settings.lang); + + // init templates + Object.keys(this.templates).forEach(function (tpl) { + if (!this.templates[tpl]) { + this.templates[tpl] = QueryBuilder.templates[tpl]; + } + if (typeof this.templates[tpl] == 'string') { + this.templates[tpl] = doT.template(this.templates[tpl]); + } + }, this); + + // ensure we have a container id + if (!this.$el.attr('id')) { + this.$el.attr('id', 'qb_' + Math.floor(Math.random() * 99999)); + this.status.generated_id = true; + } + this.status.id = this.$el.attr('id'); + + // INIT + this.$el.addClass('query-builder'); + + this.filters = this.checkFilters(this.filters); + this.operators = this.checkOperators(this.operators); + this.bindEvents(); + this.initPlugins(); + + this.trigger('afterInit'); + + if (options.rules) { + this.setRules(options.rules); + delete this.settings.rules; + } + else { + this.setRoot(true); + } + }; + + /** + * Checks the configuration of each filter + * @throws ConfigError + */ + QueryBuilder.prototype.checkFilters = function (filters) { + var definedFilters = []; + + if (!filters || filters.length === 0) { + Utils.error('Config', 'Missing filters list'); + } + + filters.forEach(function (filter, i) { + if (!filter.id) { + Utils.error('Config', 'Missing filter {0} id', i); + } + if (definedFilters.indexOf(filter.id) != -1) { + Utils.error('Config', 'Filter "{0}" already defined', filter.id); + } + definedFilters.push(filter.id); + + if (!filter.type) { + filter.type = 'string'; + } + else if (!QueryBuilder.types[filter.type]) { + Utils.error('Config', 'Invalid type "{0}"', filter.type); + } + + if (!filter.input) { + filter.input = 'text'; + } + else if (typeof filter.input != 'function' && QueryBuilder.inputs.indexOf(filter.input) == -1) { + Utils.error('Config', 'Invalid input "{0}"', filter.input); + } + + if (filter.operators) { + filter.operators.forEach(function (operator) { + if (typeof operator != 'string') { + Utils.error('Config', 'Filter operators must be global operators types (string)'); + } + }); + } + + if (!filter.field) { + filter.field = filter.id; + } + if (!filter.label) { + filter.label = filter.field; + } + + if (!filter.optgroup) { + filter.optgroup = null; + } + else { + this.status.has_optgroup = true; + + // register optgroup if needed + if (!this.settings.optgroups[filter.optgroup]) { + this.settings.optgroups[filter.optgroup] = filter.optgroup; + } + } + + switch (filter.input) { + case 'radio': + case 'checkbox': + if (!filter.values || filter.values.length < 1) { + Utils.error('Config', 'Missing filter "{0}" values', filter.id); + } + break; + + case 'select': + if (filter.placeholder) { + if (filter.placeholder_value === undefined) { + filter.placeholder_value = -1; + } + Utils.iterateOptions(filter.values, function (key) { + if (key == filter.placeholder_value) { + Utils.error('Config', 'Placeholder of filter "{0}" overlaps with one of its values', filter.id); + } + }); + } + break; + } + }, this); + + if (this.settings.sort_filters) { + if (typeof this.settings.sort_filters == 'function') { + filters.sort(this.settings.sort_filters); + } + else { + var self = this; + filters.sort(function (a, b) { + return self.translateLabel(a.label).localeCompare(self.translateLabel(b.label)); + }); + } + } + + if (this.status.has_optgroup) { + filters = Utils.groupSort(filters, 'optgroup'); + } + + return filters; + }; + + /** + * Checks the configuration of each operator + * @throws ConfigError + */ + QueryBuilder.prototype.checkOperators = function (operators) { + var definedOperators = []; + + operators.forEach(function (operator, i) { + if (typeof operator == 'string') { + if (!QueryBuilder.OPERATORS[operator]) { + Utils.error('Config', 'Unknown operator "{0}"', operator); + } + + operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator]); + } + else { + if (!operator.type) { + Utils.error('Config', 'Missing "type" for operator {0}', i); + } + + if (QueryBuilder.OPERATORS[operator.type]) { + operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator.type], operator); + } + + if (operator.nb_inputs === undefined || operator.apply_to === undefined) { + Utils.error('Config', 'Missing "nb_inputs" and/or "apply_to" for operator "{0}"', operator.type); + } + } + + if (definedOperators.indexOf(operator.type) != -1) { + Utils.error('Config', 'Operator "{0}" already defined', operator.type); + } + definedOperators.push(operator.type); + + if (!operator.optgroup) { + operator.optgroup = null; + } + else { + this.status.has_operator_optgroup = true; + + // register optgroup if needed + if (!this.settings.optgroups[operator.optgroup]) { + this.settings.optgroups[operator.optgroup] = operator.optgroup; + } + } + }, this); + + if (this.status.has_operator_optgroup) { + operators = Utils.groupSort(operators, 'optgroup'); + } + + return operators; + }; + + /** + * Add all events listeners + */ + QueryBuilder.prototype.bindEvents = function () { + var self = this; + + // 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(); + } + }); + + // 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()); + }); + + // 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()); + }); + + // add rule button + this.$el.on('click.queryBuilder', Selectors.add_rule, function () { + var $group = $(this).closest(Selectors.group_container); + self.addRule(Model($group)); + }); + + // delete rule button + this.$el.on('click.queryBuilder', Selectors.delete_rule, function () { + var $rule = $(this).closest(Selectors.rule_container); + self.deleteRule(Model($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)); + }); + + // delete group button + this.$el.on('click.queryBuilder', Selectors.delete_group, function () { + var $group = $(this).closest(Selectors.group_container); + self.deleteGroup(Model($group)); + }); + } + + // model events + this.model.on({ + 'drop': function (e, node) { + node.$el.remove(); + self.refreshGroupsConditions(); + }, + 'add': function (e, node, index) { + if (index === 0) { + node.$el.prependTo(node.parent.$el.find('>' + Selectors.rules_list)); + } + else { + node.$el.insertAfter(node.parent.rules[index - 1].$el); + } + self.refreshGroupsConditions(); + }, + 'move': function (e, node, group, index) { + node.$el.detach(); + + if (index === 0) { + node.$el.prependTo(group.$el.find('>' + Selectors.rules_list)); + } + else { + node.$el.insertAfter(group.rules[index - 1].$el); + } + self.refreshGroupsConditions(); + }, + 'update': function (e, node, field, value, oldValue) { + if (node instanceof Rule) { + switch (field) { + case 'error': + self.displayError(node); + break; + + case 'flags': + self.applyRuleFlags(node); + break; + + case 'filter': + self.updateRuleFilter(node); + break; + + case 'operator': + self.updateRuleOperator(node, oldValue); + break; + + case 'value': + self.updateRuleValue(node); + break; + } + } + else { + switch (field) { + case 'error': + self.displayError(node); + break; + + case 'flags': + self.applyGroupFlags(node); + break; + + case 'condition': + self.updateGroupCondition(node); + break; + } + } + } + }); + }; + + /** + * 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} + */ + QueryBuilder.prototype.setRoot = function (addRule, data, flags) { + addRule = (addRule === undefined || addRule === true); + + var group_id = this.nextGroupId(); + var $group = $(this.getGroupTemplate(group_id, 1)); + + this.$el.append($group); + this.model.root = new Group(null, $group); + this.model.root.model = this.model; + + this.model.root.data = data; + this.model.root.flags = $.extend({}, this.settings.default_group_flags, flags); + + this.trigger('afterAddGroup', this.model.root); + + this.model.root.condition = this.settings.default_condition; + + if (addRule) { + this.addRule(this.model.root); + } + + return this.model.root; + }; + + /** + * 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} + */ + QueryBuilder.prototype.addGroup = function (parent, addRule, data, flags) { + addRule = (addRule === undefined || addRule === true); + + var level = parent.level + 1; + + var e = this.trigger('beforeAddGroup', parent, addRule, level); + if (e.isDefaultPrevented()) { + return null; + } + + var group_id = this.nextGroupId(); + var $group = $(this.getGroupTemplate(group_id, level)); + var model = parent.addGroup($group); + + model.data = data; + model.flags = $.extend({}, this.settings.default_group_flags, flags); + + this.trigger('afterAddGroup', model); + + model.condition = this.settings.default_condition; + + if (addRule) { + this.addRule(model); + } + + return model; + }; + + /** + * 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 + */ + QueryBuilder.prototype.deleteGroup = function (group) { + if (group.isRoot()) { + return false; + } + + var e = this.trigger('beforeDeleteGroup', group); + if (e.isDefaultPrevented()) { + return false; + } + + var del = true; + + group.each('reverse', function (rule) { + del &= this.deleteRule(rule); + }, function (group) { + del &= this.deleteGroup(group); + }, this); + + if (del) { + group.drop(); + this.trigger('afterDeleteGroup'); + } + + return del; + }; + + /** + * Changes the condition of a group + * @param group {Group} + */ + QueryBuilder.prototype.updateGroupCondition = function (group) { + group.$el.find('>' + Selectors.group_condition).each(function () { + var $this = $(this); + $this.prop('checked', $this.val() === group.condition); + $this.parent().toggleClass('active', $this.val() === group.condition); + }); + + this.trigger('afterUpdateGroupCondition', group); + }; + + /** + * Update visibility of conditions based on number of rules inside each group + */ + 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) + .parent().toggleClass('disabled', group.rules.length <= 1); + } + + group.each(function (rule) { + }, 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} + */ + QueryBuilder.prototype.addRule = function (parent, data, flags) { + var e = this.trigger('beforeAddRule', parent); + if (e.isDefaultPrevented()) { + return null; + } + + var rule_id = this.nextRuleId(); + var $rule = $(this.getRuleTemplate(rule_id)); + var model = parent.addRule($rule); + + if (data !== undefined) { + model.data = data; + } + + model.flags = $.extend({}, this.settings.default_rule_flags, flags); + + this.trigger('afterAddRule', model); + + this.createRuleFilters(model); + + if (this.settings.default_filter || !this.settings.display_empty_filter) { + model.filter = this.getFilterById(this.settings.default_filter || this.filters[0].id); + } + + return model; + }; + + /** + * Delete a rule. + * @param rule {Rule} + * @return {boolean} true if the rule has been deleted + */ + QueryBuilder.prototype.deleteRule = function (rule) { + if (rule.flags.no_delete) { + return false; + } + + var e = this.trigger('beforeDeleteRule', rule); + if (e.isDefaultPrevented()) { + return false; + } + + rule.drop(); + + this.trigger('afterDeleteRule'); + + return true; + }; + + /** + * Create the filters for a rule and init the rule operator + * @param rule {Rule} + */ + QueryBuilder.prototype.createRuleOperators = function (rule) { + var $operatorContainer = rule.$el.find(Selectors.operator_container).empty(); + + if (!rule.filter) { + return; + } + + var operators = this.getOperators(rule.filter); + var $operatorSelect = $(this.getRuleOperatorSelect(rule, operators)); + + $operatorContainer.html($operatorSelect); + + // set the operator without triggering update event + rule.__.operator = operators[0]; + + this.trigger('afterCreateRuleOperators', rule, operators); + }; + + /** + * Create the main input for a rule + * @param rule {Rule} + */ + QueryBuilder.prototype.createRuleInput = function (rule) { + var $valueContainer = rule.$el.find(Selectors.value_container).empty(); + + rule.__.value = undefined; + + if (!rule.filter || !rule.operator || rule.operator.nb_inputs === 0) { + return; + } + + var self = this; + var $inputs = $(); + var filter = rule.filter; + + for (var i = 0; i < rule.operator.nb_inputs; i++) { + var $ruleInput = $(this.getRuleInput(rule, i)); + if (i > 0) $valueContainer.append(this.settings.inputs_separator); + $valueContainer.append($ruleInput); + $inputs = $inputs.add($ruleInput); + } + + $valueContainer.show(); + + $inputs.on('change ' + (filter.input_event || ''), function () { + self.status.updating_value = true; + rule.value = self.getRuleValue(rule); + self.status.updating_value = false; + }); + + if (filter.plugin) { + $inputs[filter.plugin](filter.plugin_config || {}); + } + + this.trigger('afterCreateRuleInput', rule); + + if (filter.default_value !== undefined) { + rule.value = filter.default_value; + } + else { + self.status.updating_value = true; + rule.value = self.getRuleValue(rule); + self.status.updating_value = false; + } + }; + + /** + * Perform action when rule's filter is changed + * @param rule {Rule} + */ + QueryBuilder.prototype.updateRuleFilter = function (rule) { + this.createRuleOperators(rule); + this.createRuleInput(rule); + + rule.$el.find(Selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1'); + + this.trigger('afterUpdateRuleFilter', rule); + }; + + /** + * Update main visibility when rule operator changes + * @param rule {Rule} + * @param previousOperator {object} + */ + QueryBuilder.prototype.updateRuleOperator = function (rule, previousOperator) { + var $valueContainer = rule.$el.find(Selectors.value_container); + + if (!rule.operator || rule.operator.nb_inputs === 0) { + $valueContainer.hide(); + + rule.__.value = undefined; + } + else { + $valueContainer.show(); + + if ($valueContainer.is(':empty') || rule.operator.nb_inputs !== previousOperator.nb_inputs) { + this.createRuleInput(rule); + } + } + + if (rule.operator) { + rule.$el.find(Selectors.rule_operator).val(rule.operator.type); + } + + this.trigger('afterUpdateRuleOperator', rule); + }; + + /** + * Perform action when rule's value is changed + * @param rule {Rule} + */ + QueryBuilder.prototype.updateRuleValue = function (rule) { + if (!this.status.updating_value) { + this.setRuleValue(rule, rule.value); + } + + this.trigger('afterUpdateRuleValue', rule); + }; + + /** + * Change rules properties depending on flags. + * @param rule {Rule} + */ + QueryBuilder.prototype.applyRuleFlags = function (rule) { + var flags = rule.flags; + + if (flags.filter_readonly) { + rule.$el.find(Selectors.rule_filter).prop('disabled', true); + } + if (flags.operator_readonly) { + rule.$el.find(Selectors.rule_operator).prop('disabled', true); + } + if (flags.value_readonly) { + rule.$el.find(Selectors.rule_value).prop('disabled', true); + } + if (flags.no_delete) { + rule.$el.find(Selectors.delete_rule).remove(); + } + + this.trigger('afterApplyRuleFlags', rule); + }; + + /** + * Change group properties depending on flags. + * @param group {Group} + */ + QueryBuilder.prototype.applyGroupFlags = function (group) { + var flags = group.flags; + + if (flags.condition_readonly) { + group.$el.find('>' + Selectors.group_condition).prop('disabled', true) + .parent().addClass('readonly'); + } + if (flags.no_delete) { + group.$el.find(Selectors.delete_group).remove(); + } + + this.trigger('afterApplyGroupFlags', group); + }; + + /** + * Clear all errors markers + * @param node {Node,optional} default is root Group + */ + QueryBuilder.prototype.clearErrors = function (node) { + node = node || this.model.root; + + if (!node) { + return; + } + + node.error = null; + + if (node instanceof Group) { + node.each(function (rule) { + rule.error = null; + }, function (group) { + this.clearErrors(group); + }, this); + } + }; + + /** + * Add/Remove class .has-error and update error title + * @param node {Node} + */ + QueryBuilder.prototype.displayError = function (node) { + if (this.settings.display_errors) { + if (node.error === null) { + node.$el.removeClass('has-error'); + } + else { + // translate the text without modifying event array + var error = $.extend([], node.error, [ + this.lang.errors[node.error[0]] || node.error[0] + ]); + + node.$el.addClass('has-error') + .find(Selectors.error_container).eq(0) + .attr('title', Utils.fmt.apply(null, error)); + } + } + }; + + /** + * Trigger a validation error event + * @param node {Node} + * @param error {array} + * @param value {mixed} + */ + QueryBuilder.prototype.triggerValidationError = function (node, error, value) { + if (!$.isArray(error)) { + error = [error]; + } + + var e = this.trigger('validationError', node, error, value); + if (!e.isDefaultPrevented()) { + node.error = error; + } + }; + + + /** + * Destroy the plugin + */ + QueryBuilder.prototype.destroy = function () { + this.trigger('beforeDestroy'); + + if (this.status.generated_id) { + this.$el.removeAttr('id'); + } + + this.clear(); + this.model = null; + + this.$el + .off('.queryBuilder') + .removeClass('query-builder') + .removeData('queryBuilder'); + + delete this.$el[0].queryBuilder; + }; + + /** + * Reset the plugin + */ + QueryBuilder.prototype.reset = function () { + this.status.group_id = 1; + this.status.rule_id = 0; + + this.model.root.empty(); + + this.addRule(this.model.root); + + this.trigger('afterReset'); + }; + + /** + * Clear the plugin + */ + QueryBuilder.prototype.clear = function () { + this.status.group_id = 0; + this.status.rule_id = 0; + + if (this.model.root) { + this.model.root.drop(); + this.model.root = null; + } + + this.trigger('afterClear'); + }; + + /** + * Modify the builder configuration + * Only options defined in QueryBuilder.modifiable_options are modifiable + * @param {object} + */ + QueryBuilder.prototype.setOptions = function (options) { + // use jQuery utils to filter options keys + $.makeArray($(Object.keys(options)).filter(QueryBuilder.modifiable_options)) + .forEach(function (opt) { + this.settings[opt] = options[opt]; + }, this); + }; + + /** + * Return the model associated to a DOM object, or root model + * @param {jQuery,optional} + * @return {Node} + */ + QueryBuilder.prototype.getModel = function (target) { + return !target ? this.model.root : Model(target); + }; + + /** + * Validate the whole builder + * @return {boolean} + */ + QueryBuilder.prototype.validate = function () { + this.clearErrors(); + + var self = this; + + var valid = (function parse(group) { + var done = 0; + var errors = 0; + + group.each(function (rule) { + if (!rule.filter) { + self.triggerValidationError(rule, 'no_filter', null); + errors++; + return; + } + + if (rule.operator.nb_inputs !== 0) { + var valid = self.validateValue(rule, rule.value); + + if (valid !== true) { + self.triggerValidationError(rule, valid, rule.value); + errors++; + return; + } + } + + done++; + + }, function (group) { + if (parse(group)) { + done++; + } + else { + errors++; + } + }); + + if (errors > 0) { + return false; + } + else if (done === 0 && (!self.settings.allow_empty || !group.isRoot())) { + self.triggerValidationError(group, 'empty_group', null); + return false; + } + + return true; + + }(this.model.root)); + + return this.change('validate', valid); + }; + + /** + * Get an object representing current rules + * @param {object} options + * - get_flags: false[default] | true(only changes from default flags) | 'all' + * @return {object} + */ + QueryBuilder.prototype.getRules = function (options) { + options = $.extend({ + get_flags: false + }, options); + + if (!this.validate()) { + return {}; + } + + var self = this; + + var out = (function parse(group) { + var data = { + condition: group.condition, + rules: [] + }; + + if (group.data) { + data.data = $.extendext(true, 'replace', {}, group.data); + } + + if (options.get_flags) { + var flags = self.getGroupFlags(group.flags, options.get_flags === 'all'); + if (!$.isEmptyObject(flags)) { + data.flags = flags; + } + } + + group.each(function (model) { + var value = null; + if (model.operator.nb_inputs !== 0) { + value = model.value; + } + + var rule = { + id: model.filter.id, + field: model.filter.field, + type: model.filter.type, + input: model.filter.input, + operator: model.operator.type, + value: value + }; + + if (model.filter.data || model.data) { + rule.data = $.extendext(true, 'replace', {}, model.filter.data, model.data); + } + + if (options.get_flags) { + var flags = self.getRuleFlags(model.flags, options.get_flags === 'all'); + if (!$.isEmptyObject(flags)) { + rule.flags = flags; + } + } + + data.rules.push(rule); + + }, function (model) { + data.rules.push(parse(model)); + }); + + return data; + + }(this.model.root)); + + return this.change('getRules', out); + }; + + /** + * Set rules from object + * @throws RulesError, UndefinedConditionError + * @param data {object} + */ + QueryBuilder.prototype.setRules = function (data) { + if ($.isArray(data)) { + data = { + condition: this.settings.default_condition, + rules: data + }; + } + + if (!data || !data.rules || (data.rules.length === 0 && !this.settings.allow_empty)) { + Utils.error('RulesParse', 'Incorrect data object passed'); + } + + this.clear(); + this.setRoot(false, data.data, this.parseGroupFlags(data)); + + data = this.change('setRules', data); + + var self = this; + + (function add(data, group) { + if (group === null) { + return; + } + + if (data.condition === undefined) { + data.condition = self.settings.default_condition; + } + else if (self.settings.conditions.indexOf(data.condition) == -1) { + Utils.error('UndefinedCondition', 'Invalid condition "{0}"', data.condition); + } + + group.condition = data.condition; + + data.rules.forEach(function (item) { + var model; + if (item.rules && item.rules.length > 0) { + if (self.settings.allow_groups !== -1 && self.settings.allow_groups < group.level) { + self.reset(); + Utils.error('RulesParse', 'No more than {0} groups are allowed', self.settings.allow_groups); + } + else { + model = self.addGroup(group, false, item.data, self.parseGroupFlags(item)); + if (model === null) { + return; + } + + add(item, model); + } + } + else { + if (item.id === undefined) { + Utils.error('RulesParse', 'Missing rule field id'); + } + if (item.operator === undefined) { + item.operator = 'equal'; + } + + model = self.addRule(group, item.data); + if (model === null) { + return; + } + + model.filter = self.getFilterById(item.id); + model.operator = self.getOperatorByType(item.operator); + model.flags = self.parseRuleFlags(item); + + if (model.operator.nb_inputs !== 0 && item.value !== undefined) { + model.value = item.value; + } + } + }); + + }(data, this.model.root)); + }; + + + /** + * Check if a value is correct for a filter + * @param rule {Rule} + * @param value {string|string[]|undefined} + * @return {array|true} + */ + QueryBuilder.prototype.validateValue = function (rule, value) { + var validation = rule.filter.validation || {}; + var result = true; + + if (validation.callback) { + result = validation.callback.call(this, value, rule); + } + else { + result = this.validateValueInternal(rule, value); + } + + return this.change('validateValue', result, value, rule); + }; + + /** + * Default validation function + * @throws ConfigError + * @param rule {Rule} + * @param value {string|string[]|undefined} + * @return {array|true} + */ + QueryBuilder.prototype.validateValueInternal = function (rule, value) { + var filter = rule.filter; + var operator = rule.operator; + var validation = filter.validation || {}; + var result = true; + var tmp; + + if (rule.operator.nb_inputs === 1) { + value = [value]; + } + else { + value = value; + } + + for (var i = 0; i < operator.nb_inputs; i++) { + switch (filter.input) { + case 'radio': + if (value[i] === undefined) { + result = ['radio_empty']; + break; + } + break; + + case 'checkbox': + if (value[i] === undefined || value[i].length === 0) { + result = ['checkbox_empty']; + break; + } + else if (!operator.multiple && value[i].length > 1) { + result = ['operator_not_multiple', operator.type]; + break; + } + break; + + case 'select': + if (filter.multiple) { + if (value[i] === undefined || value[i].length === 0 || (filter.placeholder && value[i] == filter.placeholder_value)) { + result = ['select_empty']; + break; + } + else if (!operator.multiple && value[i].length > 1) { + result = ['operator_not_multiple', operator.type]; + break; + } + } + else { + if (value[i] === undefined || (filter.placeholder && value[i] == filter.placeholder_value)) { + result = ['select_empty']; + break; + } + } + break; + + default: + switch (QueryBuilder.types[filter.type]) { + case 'string': + if (value[i] === undefined || value[i].length === 0) { + result = ['string_empty']; + break; + } + if (validation.min !== undefined) { + if (value[i].length < parseInt(validation.min)) { + result = ['string_exceed_min_length', validation.min]; + break; + } + } + if (validation.max !== undefined) { + if (value[i].length > parseInt(validation.max)) { + result = ['string_exceed_max_length', validation.max]; + break; + } + } + if (validation.format) { + if (typeof validation.format == 'string') { + validation.format = new RegExp(validation.format); + } + if (!validation.format.test(value[i])) { + result = ['string_invalid_format', validation.format]; + break; + } + } + break; + + case 'number': + if (value[i] === undefined || isNaN(value[i])) { + result = ['number_nan']; + break; + } + if (filter.type == 'integer') { + if (parseInt(value[i]) != value[i]) { + result = ['number_not_integer']; + break; + } + } + else { + if (parseFloat(value[i]) != value[i]) { + result = ['number_not_double']; + break; + } + } + if (validation.min !== undefined) { + if (value[i] < parseFloat(validation.min)) { + result = ['number_exceed_min', validation.min]; + break; + } + } + if (validation.max !== undefined) { + if (value[i] > parseFloat(validation.max)) { + result = ['number_exceed_max', validation.max]; + break; + } + } + if (validation.step !== undefined && validation.step !== 'any') { + var v = (value[i] / validation.step).toPrecision(14); + if (parseInt(v) != v) { + result = ['number_wrong_step', validation.step]; + break; + } + } + break; + + case 'datetime': + if (value[i] === undefined || value[i].length === 0) { + result = ['datetime_empty']; + break; + } + + // we need MomentJS + if (validation.format) { + if (!('moment' in window)) { + Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com'); + } + + var datetime = moment(value[i], validation.format); + if (!datetime.isValid()) { + result = ['datetime_invalid', validation.format]; + break; + } + else { + if (validation.min) { + if (datetime < moment(validation.min, validation.format)) { + result = ['datetime_exceed_min', validation.min]; + break; + } + } + if (validation.max) { + if (datetime > moment(validation.max, validation.format)) { + result = ['datetime_exceed_max', validation.max]; + break; + } + } + } + } + break; + + case 'boolean': + 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; + } + } + + return result; + }; + + /** + * Returns an incremented group ID + * @return {string} + */ + QueryBuilder.prototype.nextGroupId = function () { + return this.status.id + '_group_' + (this.status.group_id++); + }; + + /** + * Returns an incremented rule ID + * @return {string} + */ + QueryBuilder.prototype.nextRuleId = function () { + return this.status.id + '_rule_' + (this.status.rule_id++); + }; + + /** + * Returns the operators for a filter + * @param filter {string|object} (filter id name or filter object) + * @return {object[]} + */ + QueryBuilder.prototype.getOperators = function (filter) { + if (typeof filter == 'string') { + filter = this.getFilterById(filter); + } + + var result = []; + + for (var i = 0, l = this.operators.length; i < l; i++) { + // filter operators check + if (filter.operators) { + if (filter.operators.indexOf(this.operators[i].type) == -1) { + continue; + } + } + // type check + else if (this.operators[i].apply_to.indexOf(QueryBuilder.types[filter.type]) == -1) { + continue; + } + + result.push(this.operators[i]); + } + + // keep sort order defined for the filter + if (filter.operators) { + result.sort(function (a, b) { + return filter.operators.indexOf(a.type) - filter.operators.indexOf(b.type); + }); + } + + return this.change('getOperators', result, filter); + }; + + /** + * Returns a particular filter by its id + * @throws UndefinedFilterError + * @param filterId {string} + * @return {object|null} + */ + QueryBuilder.prototype.getFilterById = function (id) { + if (id == '-1') { + return null; + } + + for (var i = 0, l = this.filters.length; i < l; i++) { + if (this.filters[i].id == id) { + return this.filters[i]; + } + } + + Utils.error('UndefinedFilter', 'Undefined filter "{0}"', id); + }; + + /** + * Return a particular operator by its type + * @throws UndefinedOperatorError + * @param type {string} + * @return {object|null} + */ + QueryBuilder.prototype.getOperatorByType = function (type) { + if (type == '-1') { + return null; + } + + for (var i = 0, l = this.operators.length; i < l; i++) { + if (this.operators[i].type == type) { + return this.operators[i]; + } + } + + Utils.error('UndefinedOperator', 'Undefined operator "{0}"', type); + }; + + /** + * Returns rule value + * @param rule {Rule} + * @return {mixed} + */ + QueryBuilder.prototype.getRuleValue = function (rule) { + var filter = rule.filter; + var operator = rule.operator; + var value = []; + + if (filter.valueGetter) { + value = filter.valueGetter.call(this, rule); + } + else { + var $value = rule.$el.find(Selectors.value_container); + + for (var i = 0; i < operator.nb_inputs; i++) { + var name = Utils.escapeElementId(rule.id + '_value_' + i); + var tmp; + + switch (filter.input) { + case 'radio': + value.push($value.find('[name=' + name + ']:checked').val()); + break; + + case 'checkbox': + tmp = []; + $value.find('[name=' + name + ']:checked').each(function () { + tmp.push($(this).val()); + }); + value.push(tmp); + break; + + case 'select': + if (filter.multiple) { + tmp = []; + $value.find('[name=' + name + '] option:selected').each(function () { + tmp.push($(this).val()); + }); + value.push(tmp); + } + else { + value.push($value.find('[name=' + name + '] option:selected').val()); + } + break; + + default: + value.push($value.find('[name=' + name + ']').val()); + } + } + + if (operator.nb_inputs === 1) { + value = value[0]; + } + + // @deprecated + if (filter.valueParser) { + value = filter.valueParser.call(this, rule, value); + } + } + + return this.change('getRuleValue', value, rule); + }; + + /** + * Sets the value of a rule. + * @param rule {Rule} + * @param value {mixed} + */ + QueryBuilder.prototype.setRuleValue = function (rule, value) { + var filter = rule.filter; + var operator = rule.operator; + + if (filter.valueSetter) { + filter.valueSetter.call(this, rule, value); + } + else { + var $value = rule.$el.find(Selectors.value_container); + + if (operator.nb_inputs == 1) { + value = [value]; + } + else { + value = value; + } + + for (var i = 0; i < operator.nb_inputs; i++) { + var name = Utils.escapeElementId(rule.id + '_value_' + i); + + switch (filter.input) { + case 'radio': + $value.find('[name=' + name + '][value="' + value[i] + '"]').prop('checked', true).trigger('change'); + break; + + case 'checkbox': + if (!$.isArray(value[i])) { + value[i] = [value[i]]; + } + value[i].forEach(function (value) { + $value.find('[name=' + name + '][value="' + value + '"]').prop('checked', true).trigger('change'); + }); + break; + + default: + $value.find('[name=' + name + ']').val(value[i]).trigger('change'); + break; + } + } + } + }; + + /** + * Clean rule flags. + * @param rule {object} + * @return {object} + */ + QueryBuilder.prototype.parseRuleFlags = function (rule) { + var flags = $.extend({}, this.settings.default_rule_flags); + + if (rule.readonly) { + $.extend(flags, { + filter_readonly: true, + operator_readonly: true, + value_readonly: true, + no_delete: true + }); + } + + if (rule.flags) { + $.extend(flags, rule.flags); + } + + return this.change('parseRuleFlags', flags, rule); + }; + + /** + * Get 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 + * @returns {object} + */ + QueryBuilder.prototype.getRuleFlags = function (flags, all) { + if (all) { + return $.extend({}, flags); + } + else { + var ret = {}; + $.each(this.settings.default_rule_flags, function (key, value) { + if (flags[key] !== value) { + ret[key] = flags[key]; + } + }); + return ret; + } + }; + + /** + * Clean group flags. + * @param group {object} + * @return {object} + */ + QueryBuilder.prototype.parseGroupFlags = function (group) { + var flags = $.extend({}, this.settings.default_group_flags); + + if (group.readonly) { + $.extend(flags, { + condition_readonly: true, + no_delete: true + }); + } + + if (group.flags) { + $.extend(flags, group.flags); + } + + return this.change('parseGroupFlags', flags, group); + }; + + /** + * Get 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 + * @returns {object} + */ + QueryBuilder.prototype.getGroupFlags = function (flags, all) { + if (all) { + return $.extend({}, flags); + } + else { + var ret = {}; + $.each(this.settings.default_group_flags, function (key, value) { + if (flags[key] !== value) { + ret[key] = flags[key]; + } + }); + return ret; + } + }; + + /** + * Translate a label + * @param label {string|object} + * @return string + */ + QueryBuilder.prototype.translateLabel = function (label) { + return typeof label == 'object' ? (label[this.settings.lang_code] || label['en']) : label; + }; + + + QueryBuilder.templates.group = '\ +
\ +
\ +
\ + \ + {{? it.settings.allow_groups===-1 || it.settings.allow_groups>=it.level }} \ + \ + {{?}} \ + {{? it.level>1 }} \ + \ + {{?}} \ +
\ +
\ + {{~ it.conditions: condition }} \ + \ + {{~}} \ +
\ + {{? it.settings.display_errors }} \ +
\ + {{?}} \ +
\ +
\ + \ +
\ +
'; + + QueryBuilder.templates.rule = '\ +
  • \ +
    \ +
    \ + \ +
    \ +
    \ + {{? it.settings.display_errors }} \ +
    \ + {{?}} \ +
    \ +
    \ +
    \ +
  • '; + + QueryBuilder.templates.filterSelect = '\ +{{ var optgroup = null; }} \ +'; + + QueryBuilder.templates.operatorSelect = '\ +{{ var optgroup = null; }} \ +'; + + /** + * Returns group HTML + * @param group_id {string} + * @param level {int} + * @return {string} + */ + QueryBuilder.prototype.getGroupTemplate = function (group_id, level) { + var h = this.templates.group({ + builder: this, + group_id: group_id, + level: level, + conditions: this.settings.conditions, + icons: this.icons, + lang: this.lang, + settings: this.settings + }); + + return this.change('getGroupTemplate', h, level); + }; + + /** + * Returns rule HTML + * @param rule_id {string} + * @return {string} + */ + QueryBuilder.prototype.getRuleTemplate = function (rule_id) { + var h = this.templates.rule({ + builder: this, + rule_id: rule_id, + icons: this.icons, + lang: this.lang, + settings: this.settings + }); + + return this.change('getRuleTemplate', h); + }; + + /** + * Returns rule filter HTML + * @param rule {Rule} + * @param operators {object} + * @return {string} + */ + QueryBuilder.prototype.getRuleOperatorSelect = function (rule, operators) { + var h = this.templates.operatorSelect({ + builder: this, + rule: rule, + operators: operators, + icons: this.icons, + lang: this.lang, + settings: this.settings, + translate: this.translateLabel + }); + + return this.change('getRuleOperatorSelect', h, rule); + }; + + /** + * Return the rule value HTML + * @param rule {Rule} + * @param filter {object} + * @param value_id {int} + * @return {string} + */ + QueryBuilder.prototype.getRuleInput = function (rule, value_id) { + var filter = rule.filter; + var validation = rule.filter.validation || {}; + var name = rule.id + '_value_' + value_id; + var c = filter.vertical ? ' class=block' : ''; + var h = ''; + + if (typeof filter.input == 'function') { + h = filter.input.call(this, rule, name); + } + else { + switch (filter.input) { + case 'radio': + case 'checkbox': + Utils.iterateOptions(filter.values, function (key, val) { + h += ' ' + val + ' '; + }); + break; + + case 'select': + h += ''; + break; + + case 'textarea': + h += '