jquery_query_builder-rails 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +4 -0
  3. data/lib/jquery_query_builder/rails/version.rb +1 -1
  4. data/lib/jquery_query_builder/rule.rb +4 -2
  5. data/vendor/assets/javascripts/i18n/query-builder.ar.js +2 -2
  6. data/vendor/assets/javascripts/i18n/query-builder.az.js +2 -2
  7. data/vendor/assets/javascripts/i18n/query-builder.bg.js +2 -2
  8. data/vendor/assets/javascripts/i18n/query-builder.cs.js +2 -2
  9. data/vendor/assets/javascripts/i18n/query-builder.da.js +2 -2
  10. data/vendor/assets/javascripts/i18n/query-builder.de.js +2 -2
  11. data/vendor/assets/javascripts/i18n/query-builder.el.js +2 -2
  12. data/vendor/assets/javascripts/i18n/query-builder.en.js +4 -2
  13. data/vendor/assets/javascripts/i18n/query-builder.es.js +9 -3
  14. data/vendor/assets/javascripts/i18n/query-builder.fa-IR.js +3 -2
  15. data/vendor/assets/javascripts/i18n/query-builder.fr.js +12 -10
  16. data/vendor/assets/javascripts/i18n/query-builder.he.js +2 -2
  17. data/vendor/assets/javascripts/i18n/query-builder.it.js +29 -2
  18. data/vendor/assets/javascripts/i18n/query-builder.nl.js +2 -2
  19. data/vendor/assets/javascripts/i18n/query-builder.no.js +2 -2
  20. data/vendor/assets/javascripts/i18n/query-builder.pl.js +4 -4
  21. data/vendor/assets/javascripts/i18n/query-builder.pt-BR.js +2 -2
  22. data/vendor/assets/javascripts/i18n/query-builder.pt-PT.js +2 -2
  23. data/vendor/assets/javascripts/i18n/query-builder.ro.js +2 -2
  24. data/vendor/assets/javascripts/i18n/query-builder.ru.js +2 -2
  25. data/vendor/assets/javascripts/i18n/query-builder.sq.js +2 -2
  26. data/vendor/assets/javascripts/i18n/query-builder.tr.js +8 -6
  27. data/vendor/assets/javascripts/i18n/query-builder.ua.js +2 -2
  28. data/vendor/assets/javascripts/i18n/query-builder.zh-CN.js +2 -2
  29. data/vendor/assets/javascripts/query-builder.js +521 -228
  30. data/vendor/assets/stylesheets/query-builder.dark.css +53 -8
  31. data/vendor/assets/stylesheets/query-builder.default.css +53 -8
  32. metadata +7 -8
@@ -1,8 +1,8 @@
1
1
  /*!
2
- * jQuery QueryBuilder 2.4.4
2
+ * jQuery QueryBuilder 2.5.2
3
3
  * Locale: Ukrainian (ua)
4
4
  * Author: Megaplan, mborisv <bm@megaplan.ru>
5
- * Licensed under MIT (http://opensource.org/licenses/MIT)
5
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
6
6
  */
7
7
 
8
8
  (function(root, factory) {
@@ -1,8 +1,8 @@
1
1
  /*!
2
- * jQuery QueryBuilder 2.4.4
2
+ * jQuery QueryBuilder 2.5.2
3
3
  * Locale: Simplified Chinese (zh_CN)
4
4
  * Author: shadowwind, shatteredwindgo@gmail.com
5
- * Licensed under MIT (http://opensource.org/licenses/MIT)
5
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
6
6
  */
7
7
 
8
8
  (function(root, factory) {
@@ -1,7 +1,7 @@
1
1
  /*!
2
- * jQuery QueryBuilder 2.4.4
3
- * Copyright 2014-2017 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
4
- * Licensed under MIT (http://opensource.org/licenses/MIT)
2
+ * jQuery QueryBuilder 2.5.2
3
+ * Copyright 2014-2018 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
4
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
5
5
  */
6
6
  (function(root, factory) {
7
7
  if (typeof define == 'function' && define.amd) {
@@ -32,7 +32,6 @@
32
32
  * @param {jQuery} $el
33
33
  * @param {object} options - see {@link http://querybuilder.js.org/#options}
34
34
  * @constructor
35
- * @fires QueryBuilder.afterInit
36
35
  */
37
36
  var QueryBuilder = function($el, options) {
38
37
  $el[0].queryBuilder = this;
@@ -159,21 +158,6 @@ var QueryBuilder = function($el, options) {
159
158
  this.operators = this.checkOperators(this.operators);
160
159
  this.bindEvents();
161
160
  this.initPlugins();
162
-
163
- /**
164
- * When the initilization is done, just before creating the root group
165
- * @event afterInit
166
- * @memberof QueryBuilder
167
- */
168
- this.trigger('afterInit');
169
-
170
- if (options.rules) {
171
- this.setRules(options.rules);
172
- delete this.settings.rules;
173
- }
174
- else {
175
- this.setRoot(true);
176
- }
177
161
  };
178
162
 
179
163
  $.extend(QueryBuilder.prototype, /** @lends QueryBuilder.prototype */ {
@@ -415,7 +399,8 @@ QueryBuilder.DEFAULTS = {
415
399
  group: null,
416
400
  rule: null,
417
401
  filterSelect: null,
418
- operatorSelect: null
402
+ operatorSelect: null,
403
+ ruleValueSelect: null
419
404
  },
420
405
 
421
406
  lang_code: 'en',
@@ -570,6 +555,29 @@ QueryBuilder.prototype.getPluginOptions = function(name, property) {
570
555
  };
571
556
 
572
557
 
558
+ /**
559
+ * Final initialisation of the builder
560
+ * @param {object} [rules]
561
+ * @fires QueryBuilder.afterInit
562
+ * @private
563
+ */
564
+ QueryBuilder.prototype.init = function(rules) {
565
+ /**
566
+ * When the initilization is done, just before creating the root group
567
+ * @event afterInit
568
+ * @memberof QueryBuilder
569
+ */
570
+ this.trigger('afterInit');
571
+
572
+ if (rules) {
573
+ this.setRules(rules);
574
+ delete this.settings.rules;
575
+ }
576
+ else {
577
+ this.setRoot(true);
578
+ }
579
+ };
580
+
573
581
  /**
574
582
  * Checks the configuration of each filter
575
583
  * @param {QueryBuilder.Filter[]} filters
@@ -642,12 +650,40 @@ QueryBuilder.prototype.checkFilters = function(filters) {
642
650
  break;
643
651
 
644
652
  case 'select':
653
+ var cleanValues = [];
654
+ filter.has_optgroup = false;
655
+
656
+ Utils.iterateOptions(filter.values, function(value, label, optgroup) {
657
+ cleanValues.push({
658
+ value: value,
659
+ label: label,
660
+ optgroup: optgroup || null
661
+ });
662
+
663
+ if (optgroup) {
664
+ filter.has_optgroup = true;
665
+
666
+ // register optgroup if needed
667
+ if (!this.settings.optgroups[optgroup]) {
668
+ this.settings.optgroups[optgroup] = optgroup;
669
+ }
670
+ }
671
+ }.bind(this));
672
+
673
+ if (filter.has_optgroup) {
674
+ filter.values = Utils.groupSort(cleanValues, 'optgroup');
675
+ }
676
+ else {
677
+ filter.values = cleanValues;
678
+ }
679
+
645
680
  if (filter.placeholder) {
646
681
  if (filter.placeholder_value === undefined) {
647
682
  filter.placeholder_value = -1;
648
683
  }
649
- Utils.iterateOptions(filter.values, function(key) {
650
- if (key == filter.placeholder_value) {
684
+
685
+ filter.values.forEach(function(entry) {
686
+ if (entry.value == filter.placeholder_value) {
651
687
  Utils.error('Config', 'Placeholder of filter "{0}" overlaps with one of its values', filter.id);
652
688
  }
653
689
  });
@@ -831,7 +867,7 @@ QueryBuilder.prototype.bindEvents = function() {
831
867
  break;
832
868
 
833
869
  case 'value':
834
- self.updateRuleValue(node);
870
+ self.updateRuleValue(node, oldValue);
835
871
  break;
836
872
  }
837
873
  }
@@ -846,7 +882,7 @@ QueryBuilder.prototype.bindEvents = function() {
846
882
  break;
847
883
 
848
884
  case 'condition':
849
- self.updateGroupCondition(node);
885
+ self.updateGroupCondition(node, oldValue);
850
886
  break;
851
887
  }
852
888
  }
@@ -873,12 +909,11 @@ QueryBuilder.prototype.setRoot = function(addRule, data, flags) {
873
909
  this.model.root.model = this.model;
874
910
 
875
911
  this.model.root.data = data;
876
- this.model.root.__.flags = $.extend({}, this.settings.default_group_flags, flags);
912
+ this.model.root.flags = $.extend({}, this.settings.default_group_flags, flags);
913
+ this.model.root.condition = this.settings.default_condition;
877
914
 
878
915
  this.trigger('afterAddGroup', this.model.root);
879
916
 
880
- this.model.root.condition = this.settings.default_condition;
881
-
882
917
  if (addRule) {
883
918
  this.addRule(this.model.root);
884
919
  }
@@ -919,7 +954,8 @@ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) {
919
954
  var model = parent.addGroup($group);
920
955
 
921
956
  model.data = data;
922
- model.__.flags = $.extend({}, this.settings.default_group_flags, flags);
957
+ model.flags = $.extend({}, this.settings.default_group_flags, flags);
958
+ model.condition = this.settings.default_condition;
923
959
 
924
960
  /**
925
961
  * Just after adding a group
@@ -929,7 +965,12 @@ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) {
929
965
  */
930
966
  this.trigger('afterAddGroup', model);
931
967
 
932
- model.condition = this.settings.default_condition;
968
+ /**
969
+ * After any change in the rules
970
+ * @event rulesChanged
971
+ * @memberof QueryBuilder
972
+ */
973
+ this.trigger('rulesChanged');
933
974
 
934
975
  if (addRule) {
935
976
  this.addRule(model);
@@ -978,6 +1019,8 @@ QueryBuilder.prototype.deleteGroup = function(group) {
978
1019
  * @memberof QueryBuilder
979
1020
  */
980
1021
  this.trigger('afterDeleteGroup');
1022
+
1023
+ this.trigger('rulesChanged');
981
1024
  }
982
1025
 
983
1026
  return del;
@@ -986,10 +1029,11 @@ QueryBuilder.prototype.deleteGroup = function(group) {
986
1029
  /**
987
1030
  * Performs actions when a group's condition changes
988
1031
  * @param {Group} group
1032
+ * @param {object} previousCondition
989
1033
  * @fires QueryBuilder.afterUpdateGroupCondition
990
1034
  * @private
991
1035
  */
992
- QueryBuilder.prototype.updateGroupCondition = function(group) {
1036
+ QueryBuilder.prototype.updateGroupCondition = function(group, previousCondition) {
993
1037
  group.$el.find('>' + QueryBuilder.selectors.group_condition).each(function() {
994
1038
  var $this = $(this);
995
1039
  $this.prop('checked', $this.val() === group.condition);
@@ -1001,8 +1045,11 @@ QueryBuilder.prototype.updateGroupCondition = function(group) {
1001
1045
  * @event afterUpdateGroupCondition
1002
1046
  * @memberof QueryBuilder
1003
1047
  * @param {Group} group
1048
+ * @param {object} previousCondition
1004
1049
  */
1005
- this.trigger('afterUpdateGroupCondition', group);
1050
+ this.trigger('afterUpdateGroupCondition', group, previousCondition);
1051
+
1052
+ this.trigger('rulesChanged');
1006
1053
  };
1007
1054
 
1008
1055
  /**
@@ -1048,11 +1095,8 @@ QueryBuilder.prototype.addRule = function(parent, data, flags) {
1048
1095
  var $rule = $(this.getRuleTemplate(rule_id));
1049
1096
  var model = parent.addRule($rule);
1050
1097
 
1051
- if (data !== undefined) {
1052
- model.data = data;
1053
- }
1054
-
1055
- model.__.flags = $.extend({}, this.settings.default_rule_flags, flags);
1098
+ model.data = data;
1099
+ model.flags = $.extend({}, this.settings.default_rule_flags, flags);
1056
1100
 
1057
1101
  /**
1058
1102
  * Just after adding a rule
@@ -1062,6 +1106,8 @@ QueryBuilder.prototype.addRule = function(parent, data, flags) {
1062
1106
  */
1063
1107
  this.trigger('afterAddRule', model);
1064
1108
 
1109
+ this.trigger('rulesChanged');
1110
+
1065
1111
  this.createRuleFilters(model);
1066
1112
 
1067
1113
  if (this.settings.default_filter || !this.settings.display_empty_filter) {
@@ -1114,6 +1160,8 @@ QueryBuilder.prototype.deleteRule = function(rule) {
1114
1160
  */
1115
1161
  this.trigger('afterDeleteRule');
1116
1162
 
1163
+ this.trigger('rulesChanged');
1164
+
1117
1165
  return true;
1118
1166
  };
1119
1167
 
@@ -1145,6 +1193,8 @@ QueryBuilder.prototype.createRuleFilters = function(rule) {
1145
1193
  * @param {Rule} rule
1146
1194
  */
1147
1195
  this.trigger('afterCreateRuleFilters', rule);
1196
+
1197
+ this.applyRuleFlags(rule);
1148
1198
  };
1149
1199
 
1150
1200
  /**
@@ -1166,7 +1216,14 @@ QueryBuilder.prototype.createRuleOperators = function(rule) {
1166
1216
  $operatorContainer.html($operatorSelect);
1167
1217
 
1168
1218
  // set the operator without triggering update event
1169
- rule.__.operator = operators[0];
1219
+ if (rule.filter.default_operator) {
1220
+ rule.__.operator = this.getOperatorByType(rule.filter.default_operator);
1221
+ }
1222
+ else {
1223
+ rule.__.operator = operators[0];
1224
+ }
1225
+
1226
+ rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
1170
1227
 
1171
1228
  /**
1172
1229
  * After creating the dropdown for operators
@@ -1176,6 +1233,8 @@ QueryBuilder.prototype.createRuleOperators = function(rule) {
1176
1233
  * @param {QueryBuilder.Operator[]} operators - allowed operators for this rule
1177
1234
  */
1178
1235
  this.trigger('afterCreateRuleOperators', rule, operators);
1236
+
1237
+ this.applyRuleFlags(rule);
1179
1238
  };
1180
1239
 
1181
1240
  /**
@@ -1204,10 +1263,10 @@ QueryBuilder.prototype.createRuleInput = function(rule) {
1204
1263
  $inputs = $inputs.add($ruleInput);
1205
1264
  }
1206
1265
 
1207
- $valueContainer.show();
1266
+ $valueContainer.css('display', '');
1208
1267
 
1209
1268
  $inputs.on('change ' + (filter.input_event || ''), function() {
1210
- if (!this._updating_input) {
1269
+ if (!rule._updating_input) {
1211
1270
  rule._updating_value = true;
1212
1271
  rule.value = self.getRuleInputValue(rule);
1213
1272
  rule._updating_value = false;
@@ -1234,6 +1293,8 @@ QueryBuilder.prototype.createRuleInput = function(rule) {
1234
1293
  rule.value = self.getRuleInputValue(rule);
1235
1294
  rule._updating_value = false;
1236
1295
  }
1296
+
1297
+ this.applyRuleFlags(rule);
1237
1298
  };
1238
1299
 
1239
1300
  /**
@@ -1259,8 +1320,11 @@ QueryBuilder.prototype.updateRuleFilter = function(rule, previousFilter) {
1259
1320
  * @event afterUpdateRuleFilter
1260
1321
  * @memberof QueryBuilder
1261
1322
  * @param {Rule} rule
1323
+ * @param {object} previousFilter
1262
1324
  */
1263
- this.trigger('afterUpdateRuleFilter', rule);
1325
+ this.trigger('afterUpdateRuleFilter', rule, previousFilter);
1326
+
1327
+ this.trigger('rulesChanged');
1264
1328
  };
1265
1329
 
1266
1330
  /**
@@ -1279,7 +1343,7 @@ QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) {
1279
1343
  rule.__.value = undefined;
1280
1344
  }
1281
1345
  else {
1282
- $valueContainer.show();
1346
+ $valueContainer.css('display', '');
1283
1347
 
1284
1348
  if ($valueContainer.is(':empty') || !previousOperator ||
1285
1349
  rule.operator.nb_inputs !== previousOperator.nb_inputs ||
@@ -1291,6 +1355,9 @@ QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) {
1291
1355
 
1292
1356
  if (rule.operator) {
1293
1357
  rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
1358
+
1359
+ // refresh value if the format changed for this operator
1360
+ rule.__.value = this.getRuleInputValue(rule);
1294
1361
  }
1295
1362
 
1296
1363
  /**
@@ -1298,19 +1365,21 @@ QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) {
1298
1365
  * @event afterUpdateRuleOperator
1299
1366
  * @memberof QueryBuilder
1300
1367
  * @param {Rule} rule
1368
+ * @param {object} previousOperator
1301
1369
  */
1302
- this.trigger('afterUpdateRuleOperator', rule);
1370
+ this.trigger('afterUpdateRuleOperator', rule, previousOperator);
1303
1371
 
1304
- this.updateRuleValue(rule);
1372
+ this.trigger('rulesChanged');
1305
1373
  };
1306
1374
 
1307
1375
  /**
1308
1376
  * Performs actions when rule's value changes
1309
1377
  * @param {Rule} rule
1378
+ * @param {object} previousValue
1310
1379
  * @fires QueryBuilder.afterUpdateRuleValue
1311
1380
  * @private
1312
1381
  */
1313
- QueryBuilder.prototype.updateRuleValue = function(rule) {
1382
+ QueryBuilder.prototype.updateRuleValue = function(rule, previousValue) {
1314
1383
  if (!rule._updating_value) {
1315
1384
  this.setRuleInputValue(rule, rule.value);
1316
1385
  }
@@ -1320,8 +1389,11 @@ QueryBuilder.prototype.updateRuleValue = function(rule) {
1320
1389
  * @event afterUpdateRuleValue
1321
1390
  * @memberof QueryBuilder
1322
1391
  * @param {Rule} rule
1392
+ * @param {*} previousValue
1323
1393
  */
1324
- this.trigger('afterUpdateRuleValue', rule);
1394
+ this.trigger('afterUpdateRuleValue', rule, previousValue);
1395
+
1396
+ this.trigger('rulesChanged');
1325
1397
  };
1326
1398
 
1327
1399
  /**
@@ -1334,15 +1406,10 @@ QueryBuilder.prototype.applyRuleFlags = function(rule) {
1334
1406
  var flags = rule.flags;
1335
1407
  var Selectors = QueryBuilder.selectors;
1336
1408
 
1337
- if (flags.filter_readonly) {
1338
- rule.$el.find(Selectors.rule_filter).prop('disabled', true);
1339
- }
1340
- if (flags.operator_readonly) {
1341
- rule.$el.find(Selectors.rule_operator).prop('disabled', true);
1342
- }
1343
- if (flags.value_readonly) {
1344
- rule.$el.find(Selectors.rule_value).prop('disabled', true);
1345
- }
1409
+ rule.$el.find(Selectors.rule_filter).prop('disabled', flags.filter_readonly);
1410
+ rule.$el.find(Selectors.rule_operator).prop('disabled', flags.operator_readonly);
1411
+ rule.$el.find(Selectors.rule_value).prop('disabled', flags.value_readonly);
1412
+
1346
1413
  if (flags.no_delete) {
1347
1414
  rule.$el.find(Selectors.delete_rule).remove();
1348
1415
  }
@@ -1366,10 +1433,9 @@ QueryBuilder.prototype.applyGroupFlags = function(group) {
1366
1433
  var flags = group.flags;
1367
1434
  var Selectors = QueryBuilder.selectors;
1368
1435
 
1369
- if (flags.condition_readonly) {
1370
- group.$el.find('>' + Selectors.group_condition).prop('disabled', true)
1371
- .parent().addClass('readonly');
1372
- }
1436
+ group.$el.find('>' + Selectors.group_condition).prop('disabled', flags.condition_readonly)
1437
+ .parent().toggleClass('readonly', flags.condition_readonly);
1438
+
1373
1439
  if (flags.no_add_rule) {
1374
1440
  group.$el.find(Selectors.add_rule).remove();
1375
1441
  }
@@ -1520,6 +1586,10 @@ QueryBuilder.prototype.reset = function() {
1520
1586
 
1521
1587
  this.model.root.empty();
1522
1588
 
1589
+ this.model.root.data = undefined;
1590
+ this.model.root.flags = $.extend({}, this.settings.default_group_flags);
1591
+ this.model.root.condition = this.settings.default_condition;
1592
+
1523
1593
  this.addRule(this.model.root);
1524
1594
 
1525
1595
  /**
@@ -1528,6 +1598,8 @@ QueryBuilder.prototype.reset = function() {
1528
1598
  * @memberof QueryBuilder
1529
1599
  */
1530
1600
  this.trigger('afterReset');
1601
+
1602
+ this.trigger('rulesChanged');
1531
1603
  };
1532
1604
 
1533
1605
  /**
@@ -1560,6 +1632,8 @@ QueryBuilder.prototype.clear = function() {
1560
1632
  * @memberof QueryBuilder
1561
1633
  */
1562
1634
  this.trigger('afterClear');
1635
+
1636
+ this.trigger('rulesChanged');
1563
1637
  };
1564
1638
 
1565
1639
  /**
@@ -1818,7 +1892,6 @@ QueryBuilder.prototype.setRules = function(data, options) {
1818
1892
 
1819
1893
  this.clear();
1820
1894
  this.setRoot(false, data.data, this.parseGroupFlags(data));
1821
- this.applyGroupFlags(this.model.root);
1822
1895
 
1823
1896
  /**
1824
1897
  * Modifies data before the {@link QueryBuilder#setRules} method
@@ -1861,8 +1934,6 @@ QueryBuilder.prototype.setRules = function(data, options) {
1861
1934
  return;
1862
1935
  }
1863
1936
 
1864
- self.applyGroupFlags(model);
1865
-
1866
1937
  add(item, model);
1867
1938
  }
1868
1939
  }
@@ -1884,21 +1955,24 @@ QueryBuilder.prototype.setRules = function(data, options) {
1884
1955
 
1885
1956
  if (!item.empty) {
1886
1957
  model.filter = self.getFilterById(item.id, !options.allow_invalid);
1958
+ }
1887
1959
 
1888
- if (model.filter) {
1889
- model.operator = self.getOperatorByType(item.operator, !options.allow_invalid);
1960
+ if (model.filter) {
1961
+ model.operator = self.getOperatorByType(item.operator, !options.allow_invalid);
1890
1962
 
1891
- if (!model.operator) {
1892
- model.operator = self.getOperators(model.filter)[0];
1893
- }
1894
-
1895
- if (model.operator && model.operator.nb_inputs !== 0 && item.value !== undefined) {
1896
- model.value = item.value;
1897
- }
1963
+ if (!model.operator) {
1964
+ model.operator = self.getOperators(model.filter)[0];
1898
1965
  }
1899
1966
  }
1900
1967
 
1901
- self.applyRuleFlags(model);
1968
+ if (model.operator && model.operator.nb_inputs !== 0) {
1969
+ if (item.value !== undefined) {
1970
+ model.value = item.value;
1971
+ }
1972
+ else if (model.filter.default_value !== undefined) {
1973
+ model.value = model.filter.default_value;
1974
+ }
1975
+ }
1902
1976
 
1903
1977
  /**
1904
1978
  * Modifies the Rule object generated from the JSON
@@ -2160,6 +2234,29 @@ QueryBuilder.prototype._validateValue = function(rule, value) {
2160
2234
  }
2161
2235
  }
2162
2236
 
2237
+ if ((rule.operator.type === 'between' || rule.operator.type === 'not_between') && value.length === 2) {
2238
+ switch (QueryBuilder.types[filter.type]) {
2239
+ case 'number':
2240
+ if (value[0] > value[1]) {
2241
+ result = ['number_between_invalid', value[0], value[1]];
2242
+ }
2243
+ break;
2244
+
2245
+ case 'datetime':
2246
+ // we need MomentJS
2247
+ if (validation.format) {
2248
+ if (!('moment' in window)) {
2249
+ Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
2250
+ }
2251
+
2252
+ if (moment(value[0], validation.format).isAfter(moment(value[1], validation.format))) {
2253
+ result = ['datetime_between_invalid', value[0], value[1]];
2254
+ }
2255
+ }
2256
+ break;
2257
+ }
2258
+ }
2259
+
2163
2260
  return result;
2164
2261
  };
2165
2262
 
@@ -2333,11 +2430,20 @@ QueryBuilder.prototype.getRuleInputValue = function(rule) {
2333
2430
  }
2334
2431
  }
2335
2432
 
2336
- if (operator.multiple && filter.value_separator) {
2337
- value = value.map(function(val) {
2338
- return val.split(filter.value_separator);
2339
- });
2340
- }
2433
+ value = value.map(function(val) {
2434
+ if (operator.multiple && filter.value_separator && typeof val == 'string') {
2435
+ val = val.split(filter.value_separator);
2436
+ }
2437
+
2438
+ if ($.isArray(val)) {
2439
+ return val.map(function(subval) {
2440
+ return Utils.changeType(subval, filter.type);
2441
+ });
2442
+ }
2443
+ else {
2444
+ return Utils.changeType(val, filter.type);
2445
+ }
2446
+ });
2341
2447
 
2342
2448
  if (operator.nb_inputs === 1) {
2343
2449
  value = value[0];
@@ -2374,7 +2480,7 @@ QueryBuilder.prototype.setRuleInputValue = function(rule, value) {
2374
2480
  return;
2375
2481
  }
2376
2482
 
2377
- this._updating_input = true;
2483
+ rule._updating_input = true;
2378
2484
 
2379
2485
  if (filter.valueSetter) {
2380
2486
  filter.valueSetter.call(this, rule, value);
@@ -2415,7 +2521,7 @@ QueryBuilder.prototype.setRuleInputValue = function(rule, value) {
2415
2521
  }
2416
2522
  }
2417
2523
 
2418
- this._updating_input = false;
2524
+ rule._updating_input = false;
2419
2525
  };
2420
2526
 
2421
2527
  /**
@@ -2577,8 +2683,8 @@ QueryBuilder.prototype.getValidationMessage = function(validation, type, def) {
2577
2683
 
2578
2684
 
2579
2685
  QueryBuilder.templates.group = '\
2580
- <dl id="{{= it.group_id }}" class="rules-group-container"> \
2581
- <dt class="rules-group-header"> \
2686
+ <div id="{{= it.group_id }}" class="rules-group-container"> \
2687
+ <div class="rules-group-header"> \
2582
2688
  <div class="btn-group pull-right group-actions"> \
2583
2689
  <button type="button" class="btn btn-xs btn-success" data-add="rule"> \
2584
2690
  <i class="{{= it.icons.add_rule }}"></i> {{= it.translate("add_rule") }} \
@@ -2604,14 +2710,14 @@ QueryBuilder.templates.group = '\
2604
2710
  {{? it.settings.display_errors }} \
2605
2711
  <div class="error-container"><i class="{{= it.icons.error }}"></i></div> \
2606
2712
  {{?}} \
2607
- </dt> \
2608
- <dd class=rules-group-body> \
2609
- <ul class=rules-list></ul> \
2610
- </dd> \
2611
- </dl>';
2713
+ </div> \
2714
+ <div class=rules-group-body> \
2715
+ <div class=rules-list></div> \
2716
+ </div> \
2717
+ </div>';
2612
2718
 
2613
2719
  QueryBuilder.templates.rule = '\
2614
- <li id="{{= it.rule_id }}" class="rule-container"> \
2720
+ <div id="{{= it.rule_id }}" class="rule-container"> \
2615
2721
  <div class="rule-header"> \
2616
2722
  <div class="btn-group pull-right rule-actions"> \
2617
2723
  <button type="button" class="btn btn-xs btn-danger" data-delete="rule"> \
@@ -2625,7 +2731,7 @@ QueryBuilder.templates.rule = '\
2625
2731
  <div class="rule-filter-container"></div> \
2626
2732
  <div class="rule-operator-container"></div> \
2627
2733
  <div class="rule-value-container"></div> \
2628
- </li>';
2734
+ </div>';
2629
2735
 
2630
2736
  QueryBuilder.templates.filterSelect = '\
2631
2737
  {{ var optgroup = null; }} \
@@ -2640,7 +2746,7 @@ QueryBuilder.templates.filterSelect = '\
2640
2746
  <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
2641
2747
  {{?}} \
2642
2748
  {{?}} \
2643
- <option value="{{= filter.id }}">{{= it.translate(filter.label) }}</option> \
2749
+ <option value="{{= filter.id }}" {{? filter.icon}}data-icon="{{= filter.icon}}"{{?}}>{{= it.translate(filter.label) }}</option> \
2644
2750
  {{~}} \
2645
2751
  {{? optgroup !== null }}</optgroup>{{?}} \
2646
2752
  </select>';
@@ -2660,7 +2766,25 @@ QueryBuilder.templates.operatorSelect = '\
2660
2766
  <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
2661
2767
  {{?}} \
2662
2768
  {{?}} \
2663
- <option value="{{= operator.type }}">{{= it.translate("operators", operator.type) }}</option> \
2769
+ <option value="{{= operator.type }}" {{? operator.icon}}data-icon="{{= operator.icon}}"{{?}}>{{= it.translate("operators", operator.type) }}</option> \
2770
+ {{~}} \
2771
+ {{? optgroup !== null }}</optgroup>{{?}} \
2772
+ </select>';
2773
+
2774
+ QueryBuilder.templates.ruleValueSelect = '\
2775
+ {{ var optgroup = null; }} \
2776
+ <select class="form-control" name="{{= it.name }}" {{? it.rule.filter.multiple }}multiple{{?}}> \
2777
+ {{? it.rule.filter.placeholder }} \
2778
+ <option value="{{= it.rule.filter.placeholder_value }}" disabled selected>{{= it.rule.filter.placeholder }}</option> \
2779
+ {{?}} \
2780
+ {{~ it.rule.filter.values: entry }} \
2781
+ {{? optgroup !== entry.optgroup }} \
2782
+ {{? optgroup !== null }}</optgroup>{{?}} \
2783
+ {{? (optgroup = entry.optgroup) !== null }} \
2784
+ <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
2785
+ {{?}} \
2786
+ {{?}} \
2787
+ <option value="{{= entry.value }}">{{= entry.label }}</option> \
2664
2788
  {{~}} \
2665
2789
  {{? optgroup !== null }}</optgroup>{{?}} \
2666
2790
  </select>';
@@ -2741,7 +2865,7 @@ QueryBuilder.prototype.getRuleFilterSelect = function(rule, filters) {
2741
2865
 
2742
2866
  /**
2743
2867
  * Modifies the raw HTML of the rule's filter dropdown
2744
- * @event changer:getRuleFilterTemplate
2868
+ * @event changer:getRuleFilterSelect
2745
2869
  * @memberof QueryBuilder
2746
2870
  * @param {string} html
2747
2871
  * @param {Rule} rule
@@ -2771,7 +2895,7 @@ QueryBuilder.prototype.getRuleOperatorSelect = function(rule, operators) {
2771
2895
 
2772
2896
  /**
2773
2897
  * Modifies the raw HTML of the rule's operator dropdown
2774
- * @event changer:getRuleOperatorTemplate
2898
+ * @event changer:getRuleOperatorSelect
2775
2899
  * @memberof QueryBuilder
2776
2900
  * @param {string} html
2777
2901
  * @param {Rule} rule
@@ -2781,6 +2905,36 @@ QueryBuilder.prototype.getRuleOperatorSelect = function(rule, operators) {
2781
2905
  return this.change('getRuleOperatorSelect', h, rule, operators);
2782
2906
  };
2783
2907
 
2908
+ /**
2909
+ * Returns the rule's value select HTML
2910
+ * @param {string} name
2911
+ * @param {Rule} rule
2912
+ * @returns {string}
2913
+ * @fires QueryBuilder.changer:getRuleValueSelect
2914
+ * @private
2915
+ */
2916
+ QueryBuilder.prototype.getRuleValueSelect = function(name, rule) {
2917
+ var h = this.templates.ruleValueSelect({
2918
+ builder: this,
2919
+ name: name,
2920
+ rule: rule,
2921
+ icons: this.icons,
2922
+ settings: this.settings,
2923
+ translate: this.translate.bind(this)
2924
+ });
2925
+
2926
+ /**
2927
+ * Modifies the raw HTML of the rule's value dropdown (in case of a "select filter)
2928
+ * @event changer:getRuleValueSelect
2929
+ * @memberof QueryBuilder
2930
+ * @param {string} html
2931
+ * @param [string} name
2932
+ * @param {Rule} rule
2933
+ * @returns {string}
2934
+ */
2935
+ return this.change('getRuleValueSelect', h, name, rule);
2936
+ };
2937
+
2784
2938
  /**
2785
2939
  * Returns the rule's value HTML
2786
2940
  * @param {Rule} rule
@@ -2809,14 +2963,7 @@ QueryBuilder.prototype.getRuleInput = function(rule, value_id) {
2809
2963
  break;
2810
2964
 
2811
2965
  case 'select':
2812
- h += '<select class="form-control" name="' + name + '"' + (filter.multiple ? ' multiple' : '') + '>';
2813
- if (filter.placeholder) {
2814
- h += '<option value="' + filter.placeholder_value + '" disabled selected>' + filter.placeholder + '</option>';
2815
- }
2816
- Utils.iterateOptions(filter.values, function(key, val) {
2817
- h += '<option value="' + key + '">' + val + '</option> ';
2818
- });
2819
- h += '</select>';
2966
+ h = this.getRuleValueSelect(name, rule);
2820
2967
  break;
2821
2968
 
2822
2969
  case 'textarea':
@@ -2878,10 +3025,11 @@ QueryBuilder.utils = Utils;
2878
3025
  * @callback Utils#OptionsIteratee
2879
3026
  * @param {string} key
2880
3027
  * @param {string} value
3028
+ * @param {string} [optgroup]
2881
3029
  */
2882
3030
 
2883
3031
  /**
2884
- * Iterates over radio/checkbox/selection options, it accept three formats
3032
+ * Iterates over radio/checkbox/selection options, it accept four formats
2885
3033
  *
2886
3034
  * @example
2887
3035
  * // array of values
@@ -2892,6 +3040,9 @@ QueryBuilder.utils = Utils;
2892
3040
  * @example
2893
3041
  * // array of 1-element maps
2894
3042
  * options = [{1: 'one'}, {2: 'two'}, {3: 'three'}]
3043
+ * @example
3044
+ * // array of elements
3045
+ * options = [{value: 1, label: 'one', optgroup: 'group'}, {value: 2, label: 'two'}]
2895
3046
  *
2896
3047
  * @param {object|array} options
2897
3048
  * @param {Utils#OptionsIteratee} tpl
@@ -2900,12 +3051,18 @@ Utils.iterateOptions = function(options, tpl) {
2900
3051
  if (options) {
2901
3052
  if ($.isArray(options)) {
2902
3053
  options.forEach(function(entry) {
2903
- // array of one-element maps
2904
3054
  if ($.isPlainObject(entry)) {
2905
- $.each(entry, function(key, val) {
2906
- tpl(key, val);
2907
- return false; // break after first entry
2908
- });
3055
+ // array of elements
3056
+ if ('value' in entry) {
3057
+ tpl(entry.value, entry.label || entry.value, entry.optgroup);
3058
+ }
3059
+ // array of one-element maps
3060
+ else {
3061
+ $.each(entry, function(key, val) {
3062
+ tpl(key, val);
3063
+ return false; // break after first entry
3064
+ });
3065
+ }
2909
3066
  }
2910
3067
  // array of values
2911
3068
  else {
@@ -2967,19 +3124,32 @@ Utils.error = function() {
2967
3124
  * Changes the type of a value to int, float or bool
2968
3125
  * @param {*} value
2969
3126
  * @param {string} type - 'integer', 'double', 'boolean' or anything else (passthrough)
2970
- * @param {boolean} [boolAsInt=false] - return 0 or 1 for booleans
2971
3127
  * @returns {*}
2972
3128
  */
2973
- Utils.changeType = function(value, type, boolAsInt) {
3129
+ Utils.changeType = function(value, type) {
3130
+ if (value === '' || value === undefined) {
3131
+ return undefined;
3132
+ }
3133
+
2974
3134
  switch (type) {
2975
3135
  // @formatter:off
2976
- case 'integer': return parseInt(value);
2977
- case 'double': return parseFloat(value);
2978
- case 'boolean':
2979
- var bool = value.trim().toLowerCase() === 'true' || value.trim() === '1' || value === 1;
2980
- return boolAsInt ? (bool ? 1 : 0) : bool;
2981
- default: return value;
2982
- // @formatter:on
3136
+ case 'integer':
3137
+ if (typeof value === 'string' && !/^-?\d+$/.test(value)) {
3138
+ return value;
3139
+ }
3140
+ return parseInt(value);
3141
+ case 'double':
3142
+ if (typeof value === 'string' && !/^-?\d+\.?\d*$/.test(value)) {
3143
+ return value;
3144
+ }
3145
+ return parseFloat(value);
3146
+ case 'boolean':
3147
+ if (typeof value === 'string' && !/^(0|1|true|false){1}$/i.test(value)) {
3148
+ return value;
3149
+ }
3150
+ return value === true || value === 1 || value.toLowerCase() === 'true' || value === '1';
3151
+ default: return value;
3152
+ // @formatter:on
2983
3153
  }
2984
3154
  };
2985
3155
 
@@ -2997,12 +3167,12 @@ Utils.escapeString = function(value) {
2997
3167
  .replace(/[\0\n\r\b\\\'\"]/g, function(s) {
2998
3168
  switch (s) {
2999
3169
  // @formatter:off
3000
- case '\0': return '\\0';
3001
- case '\n': return '\\n';
3002
- case '\r': return '\\r';
3003
- case '\b': return '\\b';
3004
- default: return '\\' + s;
3005
- // @formatter:off
3170
+ case '\0': return '\\0';
3171
+ case '\n': return '\\n';
3172
+ case '\r': return '\\r';
3173
+ case '\b': return '\\b';
3174
+ default: return '\\' + s;
3175
+ // @formatter:off
3006
3176
  }
3007
3177
  })
3008
3178
  // uglify compliant
@@ -3689,7 +3859,9 @@ $.fn.queryBuilder = function(option) {
3689
3859
  return this;
3690
3860
  }
3691
3861
  if (!data) {
3692
- this.data('queryBuilder', new QueryBuilder(this, options));
3862
+ var builder = new QueryBuilder(this, options);
3863
+ this.data('queryBuilder', builder);
3864
+ builder.init(options.rules);
3693
3865
  }
3694
3866
  if (typeof option == 'string') {
3695
3867
  return data[option].apply(data, Array.prototype.slice.call(arguments, 1));
@@ -3926,6 +4098,8 @@ QueryBuilder.extend(/** @lends module:plugins.ChangeFilters.prototype */ {
3926
4098
  function(rule) {
3927
4099
  if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
3928
4100
  rule.drop();
4101
+
4102
+ self.trigger('rulesChanged');
3929
4103
  }
3930
4104
  else {
3931
4105
  self.createRuleFilters(rule);
@@ -4036,6 +4210,50 @@ QueryBuilder.extend(/** @lends module:plugins.ChangeFilters.prototype */ {
4036
4210
  });
4037
4211
 
4038
4212
 
4213
+ /**
4214
+ * @class ChosenSelectpicker
4215
+ * @memberof module:plugins
4216
+ * @descriptioon Applies chosen-js Select on filters and operators combo-boxes.
4217
+ * @param {object} [options] Supports all the options for chosen
4218
+ * @throws MissingLibraryError
4219
+ */
4220
+ QueryBuilder.define('chosen-selectpicker', function(options) {
4221
+
4222
+ if (!$.fn.chosen) {
4223
+ Utils.error('MissingLibrary', 'chosen is required to use "chosen-selectpicker" plugin. Get it here: https://github.com/harvesthq/chosen');
4224
+ }
4225
+
4226
+ if (this.settings.plugins['bt-selectpicker']) {
4227
+ Utils.error('Conflict', 'bt-selectpicker is already selected as the dropdown plugin. Please remove chosen-selectpicker from the plugin list');
4228
+ }
4229
+
4230
+ var Selectors = QueryBuilder.selectors;
4231
+
4232
+ // init selectpicker
4233
+ this.on('afterCreateRuleFilters', function(e, rule) {
4234
+ rule.$el.find(Selectors.rule_filter).removeClass('form-control').chosen(options);
4235
+ });
4236
+
4237
+ this.on('afterCreateRuleOperators', function(e, rule) {
4238
+ rule.$el.find(Selectors.rule_operator).removeClass('form-control').chosen(options);
4239
+ });
4240
+
4241
+ // update selectpicker on change
4242
+ this.on('afterUpdateRuleFilter', function(e, rule) {
4243
+ rule.$el.find(Selectors.rule_filter).trigger('chosen:updated');
4244
+ });
4245
+
4246
+ this.on('afterUpdateRuleOperator', function(e, rule) {
4247
+ rule.$el.find(Selectors.rule_operator).trigger('chosen:updated');
4248
+ });
4249
+
4250
+ this.on('beforeDeleteRule', function(e, rule) {
4251
+ rule.$el.find(Selectors.rule_filter).chosen('destroy');
4252
+ rule.$el.find(Selectors.rule_operator).chosen('destroy');
4253
+ });
4254
+ });
4255
+
4256
+
4039
4257
  /**
4040
4258
  * @class FilterDescription
4041
4259
  * @memberof module:plugins
@@ -4061,7 +4279,7 @@ QueryBuilder.define('filter-description', function(options) {
4061
4279
  $p.appendTo(rule.$el);
4062
4280
  }
4063
4281
  else {
4064
- $p.show();
4282
+ $p.css('display', '');
4065
4283
  }
4066
4284
 
4067
4285
  $p.html('<i class="' + options.icon + '"></i> ' + description);
@@ -4101,7 +4319,7 @@ QueryBuilder.define('filter-description', function(options) {
4101
4319
  });
4102
4320
  }
4103
4321
  else {
4104
- $b.show();
4322
+ $b.css('display', '');
4105
4323
  }
4106
4324
 
4107
4325
  $b.data('bs.popover').options.content = description;
@@ -4134,6 +4352,9 @@ QueryBuilder.define('filter-description', function(options) {
4134
4352
  bootbox.alert($b.data('description'));
4135
4353
  });
4136
4354
  }
4355
+ else {
4356
+ $b.css('display', '');
4357
+ }
4137
4358
 
4138
4359
  $b.data('description', description);
4139
4360
  }
@@ -4197,25 +4418,36 @@ QueryBuilder.define('invert', function(options) {
4197
4418
  });
4198
4419
 
4199
4420
  // Modify templates
4200
- this.on('getGroupTemplate.filter', function(h, level) {
4201
- var $h = $(h.value);
4202
- $h.find(Selectors.condition_container).after('<button type="button" class="btn btn-xs btn-default" data-invert="group"><i class="' + options.icon + '"></i> ' + self.translate('invert') + '</button>');
4203
- h.value = $h.prop('outerHTML');
4204
- });
4205
-
4206
- if (options.display_rules_button && options.invert_rules) {
4207
- this.on('getRuleTemplate.filter', function(h) {
4421
+ if (!options.disable_template) {
4422
+ this.on('getGroupTemplate.filter', function(h) {
4208
4423
  var $h = $(h.value);
4209
- $h.find(Selectors.rule_actions).prepend('<button type="button" class="btn btn-xs btn-default" data-invert="rule"><i class="' + options.icon + '"></i> ' + self.translate('invert') + '</button>');
4424
+ $h.find(Selectors.condition_container).after(
4425
+ '<button type="button" class="btn btn-xs btn-default" data-invert="group">' +
4426
+ '<i class="' + options.icon + '"></i> ' + self.translate('invert') +
4427
+ '</button>'
4428
+ );
4210
4429
  h.value = $h.prop('outerHTML');
4211
4430
  });
4431
+
4432
+ if (options.display_rules_button && options.invert_rules) {
4433
+ this.on('getRuleTemplate.filter', function(h) {
4434
+ var $h = $(h.value);
4435
+ $h.find(Selectors.rule_actions).prepend(
4436
+ '<button type="button" class="btn btn-xs btn-default" data-invert="rule">' +
4437
+ '<i class="' + options.icon + '"></i> ' + self.translate('invert') +
4438
+ '</button>'
4439
+ );
4440
+ h.value = $h.prop('outerHTML');
4441
+ });
4442
+ }
4212
4443
  }
4213
4444
  }, {
4214
4445
  icon: 'glyphicon glyphicon-random',
4215
4446
  recursive: true,
4216
4447
  invert_rules: true,
4217
4448
  display_rules_button: false,
4218
- silent_fail: false
4449
+ silent_fail: false,
4450
+ disable_template: false
4219
4451
  });
4220
4452
 
4221
4453
  QueryBuilder.defaults({
@@ -4315,6 +4547,8 @@ QueryBuilder.extend(/** @lends module:plugins.Invert.prototype */ {
4315
4547
  * @param {object} options
4316
4548
  */
4317
4549
  this.trigger('afterInvert', node, options);
4550
+
4551
+ this.trigger('rulesChanged');
4318
4552
  }
4319
4553
  }
4320
4554
  });
@@ -4353,17 +4587,17 @@ QueryBuilder.defaults({
4353
4587
  },
4354
4588
 
4355
4589
  mongoRuleOperators: {
4356
- $ne: function(v) {
4357
- v = v.$ne;
4590
+ $eq: function(v) {
4358
4591
  return {
4359
4592
  'val': v,
4360
- 'op': v === null ? 'is_not_null' : (v === '' ? 'is_not_empty' : 'not_equal')
4593
+ 'op': v === null ? 'is_null' : (v === '' ? 'is_empty' : 'equal')
4361
4594
  };
4362
4595
  },
4363
- eq: function(v) {
4596
+ $ne: function(v) {
4597
+ v = v.$ne;
4364
4598
  return {
4365
4599
  'val': v,
4366
- 'op': v === null ? 'is_null' : (v === '' ? 'is_empty' : 'equal')
4600
+ 'op': v === null ? 'is_not_null' : (v === '' ? 'is_not_empty' : 'not_equal')
4367
4601
  };
4368
4602
  },
4369
4603
  $regex: function(v) {
@@ -4427,6 +4661,10 @@ QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ {
4427
4661
  getMongo: function(data) {
4428
4662
  data = (data === undefined) ? this.getRules() : data;
4429
4663
 
4664
+ if (!data) {
4665
+ return null;
4666
+ }
4667
+
4430
4668
  var self = this;
4431
4669
 
4432
4670
  return (function parse(group) {
@@ -4450,7 +4688,6 @@ QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ {
4450
4688
  else {
4451
4689
  var mdb = self.settings.mongoOperators[rule.operator];
4452
4690
  var ope = self.getOperatorByType(rule.operator);
4453
- var values = [];
4454
4691
 
4455
4692
  if (mdb === undefined) {
4456
4693
  Utils.error('UndefinedMongoOperator', 'Unknown MongoDB operation for operator "{0}"', rule.operator);
@@ -4460,10 +4697,6 @@ QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ {
4460
4697
  if (!(rule.value instanceof Array)) {
4461
4698
  rule.value = [rule.value];
4462
4699
  }
4463
-
4464
- rule.value.forEach(function(v) {
4465
- values.push(Utils.changeType(v, rule.type, false));
4466
- });
4467
4700
  }
4468
4701
 
4469
4702
  /**
@@ -4477,7 +4710,7 @@ QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ {
4477
4710
  var field = self.change('getMongoDBField', rule.field, rule);
4478
4711
 
4479
4712
  var ruleExpression = {};
4480
- ruleExpression[field] = mdb.call(self, values);
4713
+ ruleExpression[field] = mdb.call(self, rule.value);
4481
4714
 
4482
4715
  /**
4483
4716
  * Modifies the MongoDB expression generated for a rul
@@ -4489,7 +4722,7 @@ QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ {
4489
4722
  * @param {function} valueWrapper - function that takes the value and adds the operator
4490
4723
  * @returns {object}
4491
4724
  */
4492
- parts.push(self.change('ruleToMongo', ruleExpression, rule, values, mdb));
4725
+ parts.push(self.change('ruleToMongo', ruleExpression, rule, rule.value, mdb));
4493
4726
  }
4494
4727
  });
4495
4728
 
@@ -4547,7 +4780,7 @@ QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ {
4547
4780
  };
4548
4781
  }
4549
4782
 
4550
- var key = andOr(query);
4783
+ var key = self.getMongoCondition(query);
4551
4784
  if (!key) {
4552
4785
  Utils.error('MongoParse', 'Invalid MongoDB query format');
4553
4786
  }
@@ -4572,7 +4805,7 @@ QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ {
4572
4805
  return;
4573
4806
  }
4574
4807
 
4575
- var key = andOr(data);
4808
+ var key = self.getMongoCondition(data);
4576
4809
  if (key) {
4577
4810
  parts.push(parse(data, key));
4578
4811
  }
@@ -4580,7 +4813,7 @@ QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ {
4580
4813
  var field = Object.keys(data)[0];
4581
4814
  var value = data[field];
4582
4815
 
4583
- var operator = determineMongoOperator(value, field);
4816
+ var operator = self.getMongoOperator(value);
4584
4817
  if (operator === undefined) {
4585
4818
  Utils.error('MongoParse', 'Invalid MongoDB query format');
4586
4819
  }
@@ -4667,61 +4900,53 @@ QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ {
4667
4900
  }
4668
4901
 
4669
4902
  return id;
4670
- }
4671
- });
4672
-
4673
- /**
4674
- * Finds which operator is used in a MongoDB sub-object
4675
- * @memberof module:plugins.MongoDbSupport
4676
- * @param {*} value
4677
- * @returns {string|undefined}
4678
- * @private
4679
- */
4680
- function determineMongoOperator(value) {
4681
- if (value !== null && typeof value == 'object') {
4682
- var subkeys = Object.keys(value);
4903
+ },
4683
4904
 
4684
- if (subkeys.length === 1) {
4685
- return subkeys[0];
4686
- }
4687
- else {
4688
- if (value.$gte !== undefined && value.$lte !== undefined) {
4905
+ /**
4906
+ * Finds which operator is used in a MongoDB sub-object
4907
+ * @param {*} data
4908
+ * @returns {string|undefined}
4909
+ * @private
4910
+ */
4911
+ getMongoOperator: function(data) {
4912
+ if (data !== null && typeof data === 'object') {
4913
+ if (data.$gte !== undefined && data.$lte !== undefined) {
4689
4914
  return 'between';
4690
4915
  }
4691
- if (value.$lt !== undefined && value.$gt !== undefined) {
4916
+ if (data.$lt !== undefined && data.$gt !== undefined) {
4692
4917
  return 'not_between';
4693
4918
  }
4694
- else if (value.$regex !== undefined) { // optional $options
4695
- return '$regex';
4696
- }
4697
- else {
4698
- return;
4919
+
4920
+ var knownKeys = Object.keys(data).filter(function(key) {
4921
+ return !!this.settings.mongoRuleOperators[key];
4922
+ }.bind(this));
4923
+
4924
+ if (knownKeys.length === 1) {
4925
+ return knownKeys[0];
4699
4926
  }
4700
4927
  }
4701
- }
4702
- else {
4703
- return 'eq';
4704
- }
4705
- }
4928
+ else {
4929
+ return '$eq';
4930
+ }
4931
+ },
4706
4932
 
4707
- /**
4708
- * Returns the key corresponding to "$or" or "$and"
4709
- * @memberof module:plugins.MongoDbSupport
4710
- * @param {object} data
4711
- * @returns {string}
4712
- * @private
4713
- */
4714
- function andOr(data) {
4715
- var keys = Object.keys(data);
4716
4933
 
4717
- for (var i = 0, l = keys.length; i < l; i++) {
4718
- if (keys[i].toLowerCase() == '$or' || keys[i].toLowerCase() == '$and') {
4719
- return keys[i];
4934
+ /**
4935
+ * Returns the key corresponding to "$or" or "$and"
4936
+ * @param {object} data
4937
+ * @returns {string|undefined}
4938
+ * @private
4939
+ */
4940
+ getMongoCondition: function(data) {
4941
+ var keys = Object.keys(data);
4942
+
4943
+ for (var i = 0, l = keys.length; i < l; i++) {
4944
+ if (keys[i].toLowerCase() === '$or' || keys[i].toLowerCase() === '$and') {
4945
+ return keys[i];
4946
+ }
4720
4947
  }
4721
4948
  }
4722
-
4723
- return undefined;
4724
- }
4949
+ });
4725
4950
 
4726
4951
 
4727
4952
  /**
@@ -4756,15 +4981,17 @@ QueryBuilder.define('not-group', function(options) {
4756
4981
  });
4757
4982
 
4758
4983
  // Modify templates
4759
- this.on('getGroupTemplate.filter', function(h, level) {
4760
- var $h = $(h.value);
4761
- $h.find(QueryBuilder.selectors.condition_container).prepend(
4762
- '<button type="button" class="btn btn-xs btn-default" data-not="group">' +
4763
- '<i class="' + options.icon_unchecked + '"></i> ' + self.translate('NOT') +
4764
- '</button>'
4765
- );
4766
- h.value = $h.prop('outerHTML');
4767
- });
4984
+ if (!options.disable_template) {
4985
+ this.on('getGroupTemplate.filter', function(h) {
4986
+ var $h = $(h.value);
4987
+ $h.find(QueryBuilder.selectors.condition_container).prepend(
4988
+ '<button type="button" class="btn btn-xs btn-default" data-not="group">' +
4989
+ '<i class="' + options.icon_unchecked + '"></i> ' + self.translate('NOT') +
4990
+ '</button>'
4991
+ );
4992
+ h.value = $h.prop('outerHTML');
4993
+ });
4994
+ }
4768
4995
 
4769
4996
  // Export "not" to JSON
4770
4997
  this.on('groupToJson.filter', function(e, group) {
@@ -4787,10 +5014,27 @@ QueryBuilder.define('not-group', function(options) {
4787
5014
  this.on('parseSQLNode.filter', function(e) {
4788
5015
  if (e.value.name && e.value.name.toUpperCase() == 'NOT') {
4789
5016
  e.value = e.value.arguments.value[0];
5017
+
5018
+ // if the there is no sub-group, create one
5019
+ if (['AND', 'OR'].indexOf(e.value.operation.toUpperCase()) === -1) {
5020
+ e.value = new SQLParser.nodes.Op(
5021
+ self.settings.default_condition,
5022
+ e.value,
5023
+ null
5024
+ );
5025
+ }
5026
+
4790
5027
  e.value.not = true;
4791
5028
  }
4792
5029
  });
4793
5030
 
5031
+ // Request to create sub-group if the "not" flag is set
5032
+ this.on('sqlGroupsDistinct.filter', function(e, group, data, i) {
5033
+ if (data.not && i > 0) {
5034
+ e.value = true;
5035
+ }
5036
+ });
5037
+
4794
5038
  // Read "not" from parsed SQL
4795
5039
  this.on('sqlToGroup.filter', function(e, data) {
4796
5040
  e.value.not = !!data.not;
@@ -4820,7 +5064,8 @@ QueryBuilder.define('not-group', function(options) {
4820
5064
  });
4821
5065
  }, {
4822
5066
  icon_unchecked: 'glyphicon glyphicon-unchecked',
4823
- icon_checked: 'glyphicon glyphicon-check'
5067
+ icon_checked: 'glyphicon glyphicon-check',
5068
+ disable_template: false
4824
5069
  });
4825
5070
 
4826
5071
  /**
@@ -4854,6 +5099,8 @@ QueryBuilder.extend(/** @lends module:plugins.NotGroup.prototype */ {
4854
5099
  * @param {Group} group
4855
5100
  */
4856
5101
  this.trigger('afterUpdateGroupNot', group);
5102
+
5103
+ this.trigger('rulesChanged');
4857
5104
  }
4858
5105
  });
4859
5106
 
@@ -4887,6 +5134,7 @@ QueryBuilder.define('sortable', function(options) {
4887
5134
  var placeholder;
4888
5135
  var ghost;
4889
5136
  var src;
5137
+ var moved;
4890
5138
 
4891
5139
  // Init drag and drop
4892
5140
  this.on('afterAddRule afterAddGroup', function(e, node) {
@@ -4907,9 +5155,11 @@ QueryBuilder.define('sortable', function(options) {
4907
5155
  // Configure drag
4908
5156
  if (!node.flags.no_sortable) {
4909
5157
  interact(node.$el[0])
4910
- .allowFrom(QueryBuilder.selectors.drag_handle)
4911
5158
  .draggable({
5159
+ allowFrom: QueryBuilder.selectors.drag_handle,
4912
5160
  onstart: function(event) {
5161
+ moved = false;
5162
+
4913
5163
  // get model of dragged element
4914
5164
  src = self.getModel(event.target);
4915
5165
 
@@ -4933,7 +5183,13 @@ QueryBuilder.define('sortable', function(options) {
4933
5183
  ghost[0].style.top = event.clientY - 15 + 'px';
4934
5184
  ghost[0].style.left = event.clientX - 15 + 'px';
4935
5185
  },
4936
- onend: function() {
5186
+ onend: function(event) {
5187
+ // starting from Interact 1.3.3, onend is called before ondrop
5188
+ if (event.dropzone) {
5189
+ moveSortableToTarget(src, $(event.relatedTarget), self);
5190
+ moved = true;
5191
+ }
5192
+
4937
5193
  // remove ghost
4938
5194
  ghost.remove();
4939
5195
  ghost = undefined;
@@ -4943,7 +5199,7 @@ QueryBuilder.define('sortable', function(options) {
4943
5199
  placeholder = undefined;
4944
5200
 
4945
5201
  // show element
4946
- src.$el.show();
5202
+ src.$el.css('display', '');
4947
5203
 
4948
5204
  /**
4949
5205
  * After a node has been moved with {@link module:plugins.Sortable}
@@ -4952,6 +5208,8 @@ QueryBuilder.define('sortable', function(options) {
4952
5208
  * @param {Node} node
4953
5209
  */
4954
5210
  self.trigger('afterMove', src);
5211
+
5212
+ self.trigger('rulesChanged');
4955
5213
  }
4956
5214
  });
4957
5215
  }
@@ -4965,7 +5223,9 @@ QueryBuilder.define('sortable', function(options) {
4965
5223
  moveSortableToTarget(placeholder, $(event.target), self);
4966
5224
  },
4967
5225
  ondrop: function(event) {
4968
- moveSortableToTarget(src, $(event.target), self);
5226
+ if (!moved) {
5227
+ moveSortableToTarget(src, $(event.target), self);
5228
+ }
4969
5229
  }
4970
5230
  });
4971
5231
 
@@ -4978,7 +5238,9 @@ QueryBuilder.define('sortable', function(options) {
4978
5238
  moveSortableToTarget(placeholder, $(event.target), self);
4979
5239
  },
4980
5240
  ondrop: function(event) {
4981
- moveSortableToTarget(src, $(event.target), self);
5241
+ if (!moved) {
5242
+ moveSortableToTarget(src, $(event.target), self);
5243
+ }
4982
5244
  }
4983
5245
  });
4984
5246
  }
@@ -5004,23 +5266,26 @@ QueryBuilder.define('sortable', function(options) {
5004
5266
  });
5005
5267
 
5006
5268
  // Modify templates
5007
- this.on('getGroupTemplate.filter', function(h, level) {
5008
- if (level > 1) {
5269
+ if (!options.disable_template) {
5270
+ this.on('getGroupTemplate.filter', function(h, level) {
5271
+ if (level > 1) {
5272
+ var $h = $(h.value);
5273
+ $h.find(QueryBuilder.selectors.condition_container).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>');
5274
+ h.value = $h.prop('outerHTML');
5275
+ }
5276
+ });
5277
+
5278
+ this.on('getRuleTemplate.filter', function(h) {
5009
5279
  var $h = $(h.value);
5010
- $h.find(QueryBuilder.selectors.condition_container).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>');
5280
+ $h.find(QueryBuilder.selectors.rule_header).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>');
5011
5281
  h.value = $h.prop('outerHTML');
5012
- }
5013
- });
5014
-
5015
- this.on('getRuleTemplate.filter', function(h) {
5016
- var $h = $(h.value);
5017
- $h.find(QueryBuilder.selectors.rule_header).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>');
5018
- h.value = $h.prop('outerHTML');
5019
- });
5282
+ });
5283
+ }
5020
5284
  }, {
5021
5285
  inherit_no_sortable: true,
5022
5286
  inherit_no_drop: true,
5023
- icon: 'glyphicon glyphicon-sort'
5287
+ icon: 'glyphicon glyphicon-sort',
5288
+ disable_template: false
5024
5289
  });
5025
5290
 
5026
5291
  QueryBuilder.selectors.rule_and_group_containers = QueryBuilder.selectors.rule_container + ', ' + QueryBuilder.selectors.group_container;
@@ -5333,10 +5598,17 @@ QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ {
5333
5598
  */
5334
5599
  getSQL: function(stmt, nl, data) {
5335
5600
  data = (data === undefined) ? this.getRules() : data;
5601
+
5602
+ if (!data) {
5603
+ return null;
5604
+ }
5605
+
5336
5606
  nl = !!nl ? '\n' : ' ';
5337
5607
  var boolean_as_integer = this.getPluginOptions('sql-support', 'boolean_as_integer');
5338
5608
 
5339
- if (stmt === true) stmt = 'question_mark';
5609
+ if (stmt === true) {
5610
+ stmt = 'question_mark';
5611
+ }
5340
5612
  if (typeof stmt == 'string') {
5341
5613
  var config = getStmtConfig(stmt);
5342
5614
  stmt = this.settings.sqlStatements[config[1]](config[2]);
@@ -5381,10 +5653,10 @@ QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ {
5381
5653
  value += sql.sep;
5382
5654
  }
5383
5655
 
5384
- if (rule.type == 'integer' || rule.type == 'double' || rule.type == 'boolean') {
5385
- v = Utils.changeType(v, rule.type, boolean_as_integer);
5656
+ if (rule.type == 'boolean' && boolean_as_integer) {
5657
+ v = v ? 1 : 0;
5386
5658
  }
5387
- else if (!stmt) {
5659
+ else if (!stmt && rule.type !== 'integer' && rule.type !== 'double' && rule.type !== 'boolean') {
5388
5660
  v = Utils.escapeString(v);
5389
5661
  }
5390
5662
 
@@ -5406,7 +5678,9 @@ QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ {
5406
5678
  }
5407
5679
 
5408
5680
  var sqlFn = function(v) {
5409
- return sql.op.replace(/\?/, v);
5681
+ return sql.op.replace('?', function() {
5682
+ return v;
5683
+ });
5410
5684
  };
5411
5685
 
5412
5686
  /**
@@ -5535,6 +5809,10 @@ QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ {
5535
5809
  var curr = out;
5536
5810
 
5537
5811
  (function flatten(data, i) {
5812
+ if (data === null) {
5813
+ return;
5814
+ }
5815
+
5538
5816
  // allow plugins to manually parse or handle special cases
5539
5817
  data = self.change('parseSQLNode', data);
5540
5818
 
@@ -5558,7 +5836,20 @@ QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ {
5558
5836
  // it's a node
5559
5837
  if (['AND', 'OR'].indexOf(data.operation.toUpperCase()) !== -1) {
5560
5838
  // create a sub-group if the condition is not the same and it's not the first level
5561
- if (i > 0 && curr.condition != data.operation.toUpperCase()) {
5839
+
5840
+ /**
5841
+ * Given an existing group and an AST node, determines if a sub-group must be created
5842
+ * @event changer:sqlGroupsDistinct
5843
+ * @memberof module:plugins.SqlSupport
5844
+ * @param {boolean} create - true by default if the group condition is different
5845
+ * @param {object} group
5846
+ * @param {object} AST
5847
+ * @param {int} current group level
5848
+ * @returns {boolean}
5849
+ */
5850
+ var createGroup = self.change('sqlGroupsDistinct', i > 0 && curr.condition != data.operation.toUpperCase(), curr, data, i);
5851
+
5852
+ if (createGroup) {
5562
5853
  /**
5563
5854
  * Modifies the group generated from the SQL expression (this is called before the group is filled with rules)
5564
5855
  * @event changer:sqlToGroup
@@ -5681,7 +5972,7 @@ QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ {
5681
5972
  */
5682
5973
  getSQLFieldID: function(field, value) {
5683
5974
  var matchingFilters = this.filters.filter(function(filter) {
5684
- return filter.field === field;
5975
+ return filter.field.toLowerCase() === field.toLowerCase();
5685
5976
  });
5686
5977
 
5687
5978
  var id;
@@ -5835,10 +6126,10 @@ QueryBuilder.extend(/** @lends module:plugins.UniqueFilter.prototype */ {
5835
6126
 
5836
6127
 
5837
6128
  /*!
5838
- * jQuery QueryBuilder 2.4.4
6129
+ * jQuery QueryBuilder 2.5.2
5839
6130
  * Locale: English (en)
5840
6131
  * Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr
5841
- * Licensed under MIT (http://opensource.org/licenses/MIT)
6132
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
5842
6133
  */
5843
6134
 
5844
6135
  QueryBuilder.regional['en'] = {
@@ -5890,10 +6181,12 @@ QueryBuilder.regional['en'] = {
5890
6181
  "number_exceed_min": "Must be greater than {0}",
5891
6182
  "number_exceed_max": "Must be lower than {0}",
5892
6183
  "number_wrong_step": "Must be a multiple of {0}",
6184
+ "number_between_invalid": "Invalid values, {0} is greater than {1}",
5893
6185
  "datetime_empty": "Empty value",
5894
6186
  "datetime_invalid": "Invalid date format ({0})",
5895
6187
  "datetime_exceed_min": "Must be after {0}",
5896
6188
  "datetime_exceed_max": "Must be before {0}",
6189
+ "datetime_between_invalid": "Invalid values, {0} is greater than {1}",
5897
6190
  "boolean_not_valid": "Not a boolean",
5898
6191
  "operator_not_multiple": "Operator \"{1}\" cannot accept multiple values"
5899
6192
  },