diff --git a/Gruntfile.js b/Gruntfile.js index 36ab0d62..3704fcb6 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -150,7 +150,7 @@ module.exports = function(grunt) { dev: { options: { host: '0.0.0.0', - port: 9000, + port: 9100, livereload: true } } diff --git a/bower.json b/bower.json index bf6b6519..ba495e77 100644 --- a/bower.json +++ b/bower.json @@ -1,10 +1,9 @@ { - "name": "jQuery-QueryBuilder", + "name": "bdt-jQuery-QueryBuilder", + "version": "0.1.0", "authors": [ { - "name": "Damien \"Mistic\" Sorel", - "email": "contact@git.strangeplanet.fr", - "homepage": "http://www.strangeplanet.fr" + "name": "Michele Monti" } ], "description": "jQuery plugin for user friendly query/filter creator", diff --git a/examples/index.html b/examples/index.html index 205d2a06..29bfbf82 100644 --- a/examples/index.html +++ b/examples/index.html @@ -183,6 +183,37 @@

Output

size: 30, unique: true }, + /* + * result + */ + { + id: 'result', + label: 'Result', + type: 'integer', + input: 'select', + values: { + 1: 'Books', + 2: 'Movies', + 3: 'Music', + 4: 'Tools', + 5: 'Goodies', + 6: 'Clothes' + }, + operators: ['equal'], + type2: 'string', + operators2: ['equal', 'less', 'less_or_equal', 'greater', 'greater_or_equal'], + type3: 'integer', + input3: 'select', + values3: { + 1: 'Books', + 2: 'Movies', + 3: 'Music', + 4: 'Tools', + 5: 'Goodies', + 6: 'Clothes' + }, + operators3: [] + }, /* * textarea */ diff --git a/package.json b/package.json index c546cfad..7a62f282 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,8 @@ { - "name": "jQuery-QueryBuilder", - "version": "2.4.0", + "name": "bdt-jQuery-QueryBuilder", + "version": "0.1.0", "author": { - "name": "Damien \"Mistic\" Sorel", - "email": "contact@git.strangeplanet.fr", - "url": "http://www.strangeplanet.fr" + "name": "BDT" }, "description": "jQuery plugin for user friendly query/filter creator", "dependencies": { diff --git a/src/core.js b/src/core.js index 33138413..03878f7b 100644 --- a/src/core.js +++ b/src/core.js @@ -30,6 +30,8 @@ QueryBuilder.prototype.init = function($el, options) { this.filters = this.settings.filters; this.icons = this.settings.icons; this.operators = this.settings.operators; + this.operators2 = this.settings.operators2; + this.operators3 = this.settings.operators3; this.templates = this.settings.templates; this.plugins = this.settings.plugins; @@ -61,6 +63,8 @@ QueryBuilder.prototype.init = function($el, options) { this.filters = this.checkFilters(this.filters); this.operators = this.checkOperators(this.operators); + this.operators2 = this.checkOperators(this.operators2); + this.operators3 = this.checkOperators(this.operators3); this.bindEvents(); this.initPlugins(); @@ -574,6 +578,50 @@ QueryBuilder.prototype.createRuleOperators = function(rule) { this.trigger('afterCreateRuleOperators', rule, operators); }; +/** + * Create the operators for a rule and init the rule operator + * @param rule {Rule} + */ +QueryBuilder.prototype.createRuleOperators3 = function(rule) { + var $operator3Container = rule.$el.find(Selectors.operator3_container).empty(); + + if (!rule.filter) { + return; + } + + var operators3 = this.getOperators3(rule.filter); + var $operator3Select = $(this.getRuleOperatorSelect(rule, operators3)); + + $operator3Container.html($operator3Select); + + // set the operator without triggering update event + rule.__.operator3 = operators3[0]; + + this.trigger('afterCreateRuleOperators3', rule, operators3); +}; + /** * Create the main input for a rule * @param rule {Rule} @@ -622,6 +670,104 @@ QueryBuilder.prototype.createRuleInput = function(rule) { } }; +/** + * Create the main input for a rule + * @param rule {Rule} + */ +QueryBuilder.prototype.createRuleInput2 = function(rule) { + var $valueContainer = rule.$el.find(Selectors.value2_container).empty(); + + rule.__.value2 = 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.getRuleInput2(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.value2 = self.getRuleValue2(rule); + self.status.updating_value = false; + }); + + if (filter.plugin) { + $inputs[filter.plugin](filter.plugin_config || {}); + } + + this.trigger('afterCreateRuleInput2', rule); + + if (filter.default_value !== undefined) { + rule.value2 = filter.default_value; + } + else { + self.status.updating_value = true; + rule.value2 = self.getRuleValue2(rule); + self.status.updating_value = false; + } +}; + +/** + * Create the main input for a rule + * @param rule {Rule} + */ +QueryBuilder.prototype.createRuleInput3 = function(rule) { + var $valueContainer = rule.$el.find(Selectors.value3_container).empty(); + + rule.__.value3 = 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.getRuleInput3(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.value3 = self.getRuleValue3(rule); + self.status.updating_value = false; + }); + + if (filter.plugin) { + $inputs[filter.plugin](filter.plugin_config || {}); + } + + this.trigger('afterCreateRuleInput3', rule); + + if (filter.default_value !== undefined) { + rule.value3 = filter.default_value; + } + else { + self.status.updating_value = true; + rule.value3 = self.getRuleValue3(rule); + self.status.updating_value = false; + } +}; + /** * Perform action when rule's filter is changed * @param rule {Rule} @@ -629,7 +775,11 @@ QueryBuilder.prototype.createRuleInput = function(rule) { */ QueryBuilder.prototype.updateRuleFilter = function(rule, previousFilter) { this.createRuleOperators(rule); + this.createRuleOperators2(rule); + this.createRuleOperators3(rule); this.createRuleInput(rule); + this.createRuleInput2(rule); + this.createRuleInput3(rule); rule.$el.find(Selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1'); diff --git a/src/data.js b/src/data.js index 6cb5a28e..7980fed8 100644 --- a/src/data.js +++ b/src/data.js @@ -249,6 +249,80 @@ QueryBuilder.prototype.getOperators = function(filter) { return this.change('getOperators', result, filter); }; +/** + * Returns the operators for a filter + * @param filter {string|object} (filter id name or filter object) + * @return {object[]} + */ +QueryBuilder.prototype.getOperators2 = function(filter) { + if (typeof filter == 'string') { + filter = this.getFilterById(filter); + } + + var result = []; + + for (var i = 0, l = this.operators2.length; i < l; i++) { + // filter operators2 check + if (filter.operators2) { + if (filter.operators2.indexOf(this.operators2[i].type) == -1) { + continue; + } + } + // type check + else if (this.operators2[i].apply_to.indexOf(QueryBuilder.types[filter.type]) == -1) { + continue; + } + + result.push(this.operators2[i]); + } + + // keep sort order defined for the filter + if (filter.operators2) { + result.sort(function(a, b) { + return filter.operators2.indexOf(a.type) - filter.operators2.indexOf(b.type); + }); + } + + return this.change('getoperators2', result, filter); +}; + +/** + * Returns the operators for a filter + * @param filter {string|object} (filter id name or filter object) + * @return {object[]} + */ +QueryBuilder.prototype.getOperators3 = function(filter) { + if (typeof filter == 'string') { + filter = this.getFilterById(filter); + } + + var result = []; + + for (var i = 0, l = this.operators3.length; i < l; i++) { + // filter operators3 check + if (filter.operators3) { + if (filter.operators3.indexOf(this.operators3[i].type) == -1) { + continue; + } + } + // type check + else if (this.operators3[i].apply_to.indexOf(QueryBuilder.types[filter.type]) == -1) { + continue; + } + + result.push(this.operators3[i]); + } + + // keep sort order defined for the filter + if (filter.operators3) { + result.sort(function(a, b) { + return filter.operators3.indexOf(a.type) - filter.operators3.indexOf(b.type); + }); + } + + return this.change('getoperators3', result, filter); +}; + /** * Returns a particular filter by its id * @throws UndefinedFilterError @@ -289,6 +363,26 @@ QueryBuilder.prototype.getOperatorByType = function(type) { Utils.error('UndefinedOperator', 'Undefined operator "{0}"', type); }; +/** + * Return a particular operator by its type + * @throws UndefinedOperatorError + * @param type {string} + * @return {object|null} + */ +QueryBuilder.prototype.getOperator2ByType = function(type) { + if (type == '-1') { + return null; + } + + for (var i = 0, l = this.operators2.length; i < l; i++) { + if (this.operators2[i].type == type) { + return this.operators2[i]; + } + } + + Utils.error('UndefinedOperator', 'Undefined operator "{0}"', type); +}; + /** * Returns rule value * @param rule {Rule} @@ -359,6 +453,146 @@ QueryBuilder.prototype.getRuleValue = function(rule) { return this.change('getRuleValue', value, rule); }; +/** + * Returns rule value2 + * @param rule {Rule} + * @return {mixed} + */ +QueryBuilder.prototype.getRuleValue2 = 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.value2_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.multiple && filter.value_separator) { + value = value.map(function(val) { + return val.split(filter.value_separator); + }); + } + + if (operator.nb_inputs === 1) { + value = value[0]; + } + + // @deprecated + if (filter.valueParser) { + value = filter.valueParser.call(this, rule, value); + } + } + + return this.change('getRuleValue2', value, rule); +}; + +/** + * Returns rule value3 + * @param rule {Rule} + * @return {mixed} + */ +QueryBuilder.prototype.getRuleValue3 = 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.value3_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.multiple && filter.value_separator) { + value = value.map(function(val) { + return val.split(filter.value_separator); + }); + } + + if (operator.nb_inputs === 1) { + value = value[0]; + } + + // @deprecated + if (filter.valueParser) { + value = filter.valueParser.call(this, rule, value); + } + } + + return this.change('getRuleValue3', value, rule); +}; + /** * Sets the value of a rule. * @param rule {Rule} @@ -409,6 +643,56 @@ QueryBuilder.prototype.setRuleValue = function(rule, value) { } }; +/** + * Sets the value2 of a rule. + * @param rule {Rule} + * @param value {mixed} + */ +QueryBuilder.prototype.setRuleValue2 = function(rule, value) { + var filter = rule.filter; + var operator = rule.operator2; + + if (filter.valueSetter) { + filter.valueSetter.call(this, rule, value); + } + else { + var $value = rule.$el.find(Selectors.value2_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: + if (operator.multiple && filter.value_separator && $.isArray(value[i])) { + value[i] = value[i].join(filter.value_separator); + } + $value.find('[name=' + name + ']').val(value[i]).trigger('change'); + break; + } + } + } +}; + /** * Clean rule flags. * @param rule {object} diff --git a/src/defaults.js b/src/defaults.js index fffde6f5..ce578104 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -41,7 +41,11 @@ var Selectors = QueryBuilder.selectors = { rule_container: '.rule-container', filter_container: '.rule-filter-container', operator_container: '.rule-operator-container', + operator2_container: '.rule-operator2-container', + operator3_container: '.rule-operator3-container', value_container: '.rule-value-container', + value2_container: '.rule-value2-container', + value3_container: '.rule-value3-container', error_container: '.error-container', condition_container: '.rules-group-header .group-conditions', @@ -55,7 +59,11 @@ var Selectors = QueryBuilder.selectors = { group_condition: '.rules-group-header [name$=_cond]', rule_filter: '.rule-filter-container [name$=_filter]', rule_operator: '.rule-operator-container [name$=_operator]', + rule_operator2: '.rule-operator2-container [name$=_operator2]', + rule_operator3: '.rule-operator3-container [name$=_operator2]', rule_value: '.rule-value-container [name*=_value_]', + rule_value2: '.rule-value2-container [name*=_value2_]', + rule_value3: '.rule-value3-container [name*=_value3_]', add_rule: '[data-add=rule]', delete_rule: '[data-delete=rule]', @@ -99,6 +107,32 @@ QueryBuilder.OPERATORS = { is_not_null: { type: 'is_not_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] } }; +/** + * Default operators + */ +QueryBuilder.OPERATORS2 = { + 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 */ @@ -136,7 +170,9 @@ QueryBuilder.DEFAULTS = { group: null, rule: null, filterSelect: null, - operatorSelect: null + operatorSelect: null, + operator2Select: null, + operator3Select: null }, lang_code: 'en', @@ -165,6 +201,52 @@ QueryBuilder.DEFAULTS = { 'is_not_null' ], + operators2: [ + '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' + ], + + operators3: [ + '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', diff --git a/src/scss/default.scss b/src/scss/default.scss index ceebadd7..a8568c0c 100644 --- a/src/scss/default.scss +++ b/src/scss/default.scss @@ -17,6 +17,7 @@ $rule-border: 1px solid $rule-border-color !default; $rule-padding: 5px !default; // scss-lint:disable ColorVariable $rule-value-separator: 1px solid #DDD !default; +$rule-value2-separator: 1px solid #DDD !default; // errors $error-icon-color: #F00 !default; @@ -92,11 +93,15 @@ $ticks-position: 5px, 10px !default; .rule-filter-container, .rule-operator-container, - .rule-value-container { + .rule-operator2-container, + .rule-value-container, + .rule-value2-container, + .rule-value3-container { @extend %rule-component; } } + .rule-value2-container, .rule-value-container { border-left: $rule-value-separator; padding-left: 5px; diff --git a/src/template.js b/src/template.js index 3a2756e4..c459030d 100644 --- a/src/template.js +++ b/src/template.js @@ -47,6 +47,9 @@ QueryBuilder.templates.rule = '\
\
\
\ +
\ +
\ +
\ '; QueryBuilder.templates.filterSelect = '\ @@ -87,6 +90,26 @@ QueryBuilder.templates.operatorSelect = '\ {{? optgroup !== null }}{{?}} \ '; +QueryBuilder.templates.operator2Select = '\ +{{? it.operators2.length === 1 }} \ + \ +{{= it.lang.operators[it.operators2[0].type] || it.operators2[0].type }} \ + \ +{{?}} \ +{{ var optgroup = null; }} \ +'; + /** * Returns group HTML * @param group_id {string} @@ -235,3 +258,147 @@ QueryBuilder.prototype.getRuleInput = function(rule, value_id) { return this.change('getRuleInput', h, rule, name); }; + +/** + * Return the rule value HTML + * @param rule {Rule} + * @param filter {object} + * @param value_id {int} + * @return {string} + */ +QueryBuilder.prototype.getRuleInput2 = function(rule, value_id) { + var filter = rule.filter; + var validation = rule.filter.validation || {}; + var name = rule.id + '_value2_' + value_id; + var c = filter.vertical ? ' class=block' : ''; + var h = ''; + + if (typeof filter.input2 == 'function') { + h = filter.input.call(this, rule, name); + } + else { + switch (filter.input2) { + case 'radio': case 'checkbox': + Utils.iterateOptions(filter.values, function(key, val) { + h+= ' ' + val + ' '; + }); + break; + + case 'select': + h+= ''; + break; + + case 'textarea': + h+= ''; + break; + + default: + switch (QueryBuilder.types[filter.type2]) { + case 'number': + h+= '