Skip to content

Commit b5f05ef

Browse files
committed
Fix #209 Optgroups for operators
1 parent 9b4efc2 commit b5f05ef

File tree

7 files changed

+231
-52
lines changed

7 files changed

+231
-52
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ I want to keep the core clean of extra (and certainly awesome) functionalities.
1010

1111
Check the doc about [creating plugins](http://mistic100.github.io/jQuery-QueryBuilder/dev/plugins.html).
1212

13-
I reserve the right to refuse any plugin I think is not useful for many people. Particularly, only import/export plugins for mainstream data storages will be intergated in the main repository. Others should be in a separated repository. But it's totally possible to add a link to your repository in the documentation.
13+
I reserve the right to refuse any plugin I think is not useful for many people. Particularly, only import/export plugins for mainstream data storages will be integrated in the main repository. Others should be in a separated repository. But it's totally possible to add a link to your repository in the documentation.
1414

1515
## Unit tests
1616
Any big feature must have it's own QUnit tests suite. Of course existing tests must still pass after changes.

examples/index.html

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,30 @@ <h3>Output</h3>
132132
'invert': null
133133
},
134134

135+
// standard operators in custom optgroups
136+
operators: [
137+
{type: 'equal', optgroup: 'basic'},
138+
{type: 'not_equal', optgroup: 'basic'},
139+
{type: 'in', optgroup: 'basic'},
140+
{type: 'not_in', optgroup: 'basic'},
141+
{type: 'less', optgroup: 'numbers'},
142+
{type: 'less_or_equal', optgroup: 'numbers'},
143+
{type: 'greater', optgroup: 'numbers'},
144+
{type: 'greater_or_equal', optgroup: 'numbers'},
145+
{type: 'between', optgroup: 'numbers'},
146+
{type: 'not_between', optgroup: 'numbers'},
147+
{type: 'begins_with', optgroup: 'strings'},
148+
{type: 'not_begins_with', optgroup: 'strings'},
149+
{type: 'contains', optgroup: 'strings'},
150+
{type: 'not_contains', optgroup: 'strings'},
151+
{type: 'ends_with', optgroup: 'strings'},
152+
{type: 'not_ends_with', optgroup: 'strings'},
153+
{type: 'is_empty' },
154+
{type: 'is_not_empty' },
155+
{type: 'is_null' },
156+
{type: 'is_not_null' }
157+
],
158+
135159
filters: [
136160
/*
137161
* basic

src/core.js

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ QueryBuilder.prototype.init = function($el, options) {
1313
rule_id: 0,
1414
generated_id: false,
1515
has_optgroup: false,
16+
has_operator_oprgroup: false,
1617
id: null,
1718
updating_value: false
1819
};
@@ -59,6 +60,7 @@ QueryBuilder.prototype.init = function($el, options) {
5960
this.$el.addClass('query-builder form-inline');
6061

6162
this.filters = this.checkFilters(this.filters);
63+
this.operators = this.checkOperators(this.operators);
6264
this.bindEvents();
6365
this.initPlugins();
6466

@@ -120,7 +122,7 @@ QueryBuilder.prototype.checkFilters = function(filters) {
120122
else {
121123
this.status.has_optgroup = true;
122124

123-
// backward compatiblity, register optgroup if needed
125+
// register optgroup if needed
124126
if (!this.settings.optgroups[filter.optgroup]) {
125127
this.settings.optgroups[filter.optgroup] = filter.optgroup;
126128
}
@@ -138,7 +140,7 @@ QueryBuilder.prototype.checkFilters = function(filters) {
138140
if (filter.placeholder_value === undefined) {
139141
filter.placeholder_value = -1;
140142
}
141-
Utils.iterateOptions(filter.values, function(key, val) {
143+
Utils.iterateOptions(filter.values, function(key) {
142144
if (key == filter.placeholder_value) {
143145
Utils.error('Config', 'Placeholder of filter "{0}" overlaps with one of its values', filter.id);
144146
}
@@ -148,36 +150,65 @@ QueryBuilder.prototype.checkFilters = function(filters) {
148150
}
149151
}, this);
150152

151-
// group filters with same optgroup, preserving declaration order when possible
152153
if (this.status.has_optgroup) {
153-
var optgroups = [],
154-
newFilters = [];
154+
filters = Utils.groupSort(filters, 'optgroup');
155+
}
155156

156-
filters.forEach(function(filter, i) {
157-
var idx;
157+
return filters;
158+
};
158159

159-
if (filter.optgroup) {
160-
idx = optgroups.lastIndexOf(filter.optgroup);
160+
/**
161+
* Checks the configuration of each operator
162+
* @throws ConfigError
163+
*/
164+
QueryBuilder.prototype.checkOperators = function(operators) {
165+
var definedOperators = [];
161166

162-
if (idx == -1) {
163-
idx = optgroups.length;
164-
}
165-
else {
166-
idx++;
167-
}
167+
operators.forEach(function(operator, i) {
168+
if (typeof operator === 'string') {
169+
if (!QueryBuilder.OPERATORS[operator]) {
170+
Utils.error('Config', 'Unknown operator "{0}"', operator);
168171
}
169-
else {
170-
idx = optgroups.length;
172+
173+
operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator]);
174+
}
175+
else {
176+
if (!operator.type) {
177+
Utils.error('Config', 'Missing "type" for operator {0}', i);
171178
}
172179

173-
optgroups.splice(idx, 0, filter.optgroup);
174-
newFilters.splice(idx, 0, filter);
175-
});
180+
if (QueryBuilder.OPERATORS[operator.type]) {
181+
operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator.type], operator);
182+
}
176183

177-
filters = newFilters;
184+
if (operator.nb_inputs === undefined || operator.apply_to === undefined) {
185+
Utils.error('Config', 'Missing "nb_inputs" and/or "apply_to" for operator "{0}"', operator.type);
186+
}
187+
}
188+
189+
if (definedOperators.indexOf(operator.type) != -1) {
190+
Utils.error('Config', 'Operator "{0}" already defined', operator.type);
191+
}
192+
definedOperators.push(operator.type);
193+
194+
if (!operator.optgroup) {
195+
operator.optgroup = null;
196+
}
197+
else {
198+
this.status.has_operator_optgroup = true;
199+
200+
// register optgroup if needed
201+
if (!this.settings.optgroups[operator.optgroup]) {
202+
this.settings.optgroups[operator.optgroup] = operator.optgroup;
203+
}
204+
}
205+
}, this);
206+
207+
if (this.status.has_operator_optgroup) {
208+
operators = Utils.groupSort(operators, 'optgroup');
178209
}
179210

180-
return filters;
211+
return operators;
181212
};
182213

183214
/**
@@ -689,4 +720,4 @@ QueryBuilder.prototype.triggerValidationError = function(node, error, value) {
689720
if (!e.isDefaultPrevented()) {
690721
node.error = error;
691722
}
692-
};
723+
};

src/defaults.js

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,32 @@ QueryBuilder.templates = {};
7373
*/
7474
QueryBuilder.regional = {};
7575

76+
/**
77+
* Default operators
78+
*/
79+
QueryBuilder.OPERATORS = {
80+
equal: {type: 'equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean']},
81+
not_equal: {type: 'not_equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean']},
82+
in: {type: 'in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime']},
83+
not_in: {type: 'not_in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime']},
84+
less: {type: 'less', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime']},
85+
less_or_equal: {type: 'less_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime']},
86+
greater: {type: 'greater', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime']},
87+
greater_or_equal: {type: 'greater_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime']},
88+
between: {type: 'between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime']},
89+
not_between: {type: 'not_between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime']},
90+
begins_with: {type: 'begins_with', nb_inputs: 1, multiple: false, apply_to: ['string']},
91+
not_begins_with: {type: 'not_begins_with', nb_inputs: 1, multiple: false, apply_to: ['string']},
92+
contains: {type: 'contains', nb_inputs: 1, multiple: false, apply_to: ['string']},
93+
not_contains: {type: 'not_contains', nb_inputs: 1, multiple: false, apply_to: ['string']},
94+
ends_with: {type: 'ends_with', nb_inputs: 1, multiple: false, apply_to: ['string']},
95+
not_ends_with: {type: 'not_ends_with', nb_inputs: 1, multiple: false, apply_to: ['string']},
96+
is_empty: {type: 'is_empty', nb_inputs: 0, multiple: false, apply_to: ['string']},
97+
is_not_empty: {type: 'is_not_empty', nb_inputs: 0, multiple: false, apply_to: ['string']},
98+
is_null: {type: 'is_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean']},
99+
is_not_null: {type: 'is_not_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean']}
100+
};
101+
76102
/**
77103
* Default configuration
78104
*/
@@ -114,26 +140,26 @@ QueryBuilder.DEFAULTS = {
114140
lang: {},
115141

116142
operators: [
117-
{type: 'equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean']},
118-
{type: 'not_equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean']},
119-
{type: 'in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime']},
120-
{type: 'not_in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime']},
121-
{type: 'less', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime']},
122-
{type: 'less_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime']},
123-
{type: 'greater', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime']},
124-
{type: 'greater_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime']},
125-
{type: 'between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime']},
126-
{type: 'not_between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime']},
127-
{type: 'begins_with', nb_inputs: 1, multiple: false, apply_to: ['string']},
128-
{type: 'not_begins_with', nb_inputs: 1, multiple: false, apply_to: ['string']},
129-
{type: 'contains', nb_inputs: 1, multiple: false, apply_to: ['string']},
130-
{type: 'not_contains', nb_inputs: 1, multiple: false, apply_to: ['string']},
131-
{type: 'ends_with', nb_inputs: 1, multiple: false, apply_to: ['string']},
132-
{type: 'not_ends_with', nb_inputs: 1, multiple: false, apply_to: ['string']},
133-
{type: 'is_empty', nb_inputs: 0, multiple: false, apply_to: ['string']},
134-
{type: 'is_not_empty', nb_inputs: 0, multiple: false, apply_to: ['string']},
135-
{type: 'is_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean']},
136-
{type: 'is_not_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean']}
143+
'equal',
144+
'not_equal',
145+
'in',
146+
'not_in',
147+
'less',
148+
'less_or_equal',
149+
'greater',
150+
'greater_or_equal',
151+
'between',
152+
'not_between',
153+
'begins_with',
154+
'not_begins_with',
155+
'contains',
156+
'not_contains',
157+
'ends_with',
158+
'not_ends_with',
159+
'is_empty',
160+
'is_not_empty',
161+
'is_null',
162+
'is_not_null'
137163
],
138164

139165
icons: {
@@ -143,4 +169,4 @@ QueryBuilder.DEFAULTS = {
143169
remove_rule: 'glyphicon glyphicon-remove',
144170
error: 'glyphicon glyphicon-warning-sign'
145171
}
146-
};
172+
};

src/template.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ QueryBuilder.templates.filterSelect = '\
6060
{{~ it.filters: filter }} \
6161
{{? optgroup !== filter.optgroup }} \
6262
{{? optgroup !== null }}</optgroup>{{?}} \
63-
{{? (optgroup = filter.optgroup) !== null}} \
63+
{{? (optgroup = filter.optgroup) !== null }} \
6464
<optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
6565
{{?}} \
6666
{{?}} \
@@ -70,10 +70,18 @@ QueryBuilder.templates.filterSelect = '\
7070
</select>';
7171

7272
QueryBuilder.templates.operatorSelect = '\
73+
{{ var optgroup = null; }} \
7374
<select class="form-control" name="{{= it.rule.id }}_operator"> \
7475
{{~ it.operators: operator }} \
76+
{{? optgroup !== operator.optgroup }} \
77+
{{? optgroup !== null }}</optgroup>{{?}} \
78+
{{? (optgroup = operator.optgroup) !== null }} \
79+
<optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
80+
{{?}} \
81+
{{?}} \
7582
<option value="{{= operator.type }}">{{= it.lang.operators[operator.type] || operator.type }}</option> \
7683
{{~}} \
84+
{{? optgroup !== null }}</optgroup>{{?}} \
7785
</select>';
7886

7987
/**
@@ -146,7 +154,8 @@ QueryBuilder.prototype.getRuleOperatorSelect = function(rule, operators) {
146154
operators: operators,
147155
icons: this.icons,
148156
lang: this.lang,
149-
settings: this.settings
157+
settings: this.settings,
158+
translate: this.translateLabel
150159
});
151160

152161
return this.change('getRuleOperatorSelect', h, rule);
@@ -222,4 +231,4 @@ QueryBuilder.prototype.getRuleInput = function(rule, value_id) {
222231
}
223232

224233
return this.change('getRuleInput', h, rule, name);
225-
};
234+
};

src/utils.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,38 @@ Utils.escapeString = function(value) {
110110
*/
111111
Utils.escapeRegExp = function(str) {
112112
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
113-
};
113+
};
114+
115+
/**
116+
* Sort objects by grouping them by {key}, preserving initial order when possible
117+
* @param {object[]} items
118+
* @param {string} key
119+
* @returns {object[]}
120+
*/
121+
Utils.groupSort = function(items, key) {
122+
var optgroups = [],
123+
newItems = [];
124+
125+
items.forEach(function(item) {
126+
var idx;
127+
128+
if (item[key]) {
129+
idx = optgroups.lastIndexOf(item[key]);
130+
131+
if (idx == -1) {
132+
idx = optgroups.length;
133+
}
134+
else {
135+
idx++;
136+
}
137+
}
138+
else {
139+
idx = optgroups.length;
140+
}
141+
142+
optgroups.splice(idx, 0, item[key]);
143+
newItems.splice(idx, 0, item);
144+
});
145+
146+
return newItems;
147+
};

0 commit comments

Comments
 (0)