diff --git a/examples/index.html b/examples/index.html
index 205d2a06..7699fbd1 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -124,6 +124,127 @@
Output
//default_filter: 'name',
sort_filters: true,
+ sections: [
+ {
+ id: 'subquery-a',
+ label: 'Subquery A',
+ base_sql: 'SELECT a_id FROM section_a WHERE parent_id = 10 AND ',
+ filters: [
+ {
+ id: 'sqa_name',
+ label: {
+ en: 'Subquery A Name',
+ fr: 'Nom'
+ },
+ type: 'string',
+ optgroup: 'core',
+ default_value: 'Subquery Name',
+ size: 30,
+ unique: true
+ },
+ {
+ id: 'sqa_category',
+ label: 'Subquery A Category',
+ type: 'integer',
+ input: 'checkbox',
+ optgroup: 'core',
+ values: {
+ 1: 'Books',
+ 2: 'Movies',
+ 3: 'Music',
+ 4: 'Tools',
+ 5: 'Goodies',
+ 6: 'Clothes'
+ },
+ colors: {
+ 1: 'foo',
+ 2: 'warning',
+ 5: 'success'
+ },
+ operators: ['in', 'not_in', 'equal', 'not_equal', 'is_null', 'is_not_null']
+ }
+ ]
+ },
+ {
+ id: 'subquery-b',
+ label: 'Subquery B',
+ filters: [
+ {
+ id: 'sqb_name',
+ label: {
+ en: 'Subquery B Name',
+ fr: 'Nom'
+ },
+ type: 'string',
+ optgroup: 'core',
+ default_value: 'Subquery Name',
+ size: 30,
+ unique: true
+ },
+ {
+ id: 'sqb_category',
+ label: 'Subquery B Category',
+ type: 'integer',
+ input: 'checkbox',
+ optgroup: 'core',
+ values: {
+ 1: 'Books',
+ 2: 'Movies',
+ 3: 'Music',
+ 4: 'Tools',
+ 5: 'Goodies',
+ 6: 'Clothes'
+ },
+ colors: {
+ 1: 'foo',
+ 2: 'warning',
+ 5: 'success'
+ },
+ operators: ['in', 'not_in', 'equal', 'not_equal', 'is_null', 'is_not_null']
+ }
+ ]
+ },
+ {
+ id: 'subquery-c',
+ label: 'Subquery C',
+ filters: [
+ {
+ id: 'sqc_name',
+ label: {
+ en: 'Subquery C Name',
+ fr: 'Nom'
+ },
+ type: 'string',
+ optgroup: 'core',
+ default_value: 'Subquery Name',
+ size: 30,
+ unique: true
+ },
+ {
+ id: 'sqc_category',
+ label: 'Subquery C Category',
+ type: 'integer',
+ input: 'checkbox',
+ optgroup: 'core',
+ values: {
+ 1: 'Books',
+ 2: 'Movies',
+ 3: 'Music',
+ 4: 'Tools',
+ 5: 'Goodies',
+ 6: 'Clothes'
+ },
+ colors: {
+ 1: 'foo',
+ 2: 'warning',
+ 5: 'success'
+ },
+ operators: ['in', 'not_in', 'equal', 'not_equal', 'is_null', 'is_not_null']
+ }
+ ]
+ }
+ ],
+
optgroups: {
core: {
en: 'Core',
@@ -446,8 +567,7 @@ Output
});
// set rules
-$('.set').on('click', function() {
- $('#builder').queryBuilder('setRules', {
+var json_rules = {
condition: 'AND',
flags: {
condition_readonly: true
@@ -463,6 +583,21 @@ Output
data: {
unit: '€'
}
+ }, {
+ section: 'subquery-a',
+ exists: 'EXISTS',
+ group: {
+ condition: 'AND',
+ rules: [{
+ id: 'sqa-name',
+ operator: 'equal',
+ value: 'Name Within Subquery A'
+ }, {
+ id: 'sqa-category',
+ operator: 'in',
+ value: [2,3]
+ }]
+ }
}, {
id: 'state',
operator: 'equal',
@@ -489,7 +624,10 @@ Output
}, {
empty: true
}]
- });
+ };
+
+$('.set').on('click', function() {
+ $('#builder').queryBuilder('setRules', json_rules);
});
// set rules from MongoDB
diff --git a/src/core.js b/src/core.js
index 33138413..0359c407 100644
--- a/src/core.js
+++ b/src/core.js
@@ -9,6 +9,7 @@ QueryBuilder.prototype.init = function($el, options) {
this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options);
this.model = new Model();
this.status = {
+ section_id: 0,
group_id: 0,
rule_id: 0,
generated_id: false,
@@ -28,6 +29,7 @@ QueryBuilder.prototype.init = function($el, options) {
// SETTINGS SHORTCUTS
this.filters = this.settings.filters;
+ this.sections = this.settings.sections;
this.icons = this.settings.icons;
this.operators = this.settings.operators;
this.templates = this.settings.templates;
@@ -60,7 +62,13 @@ QueryBuilder.prototype.init = function($el, options) {
this.$el.addClass('query-builder form-inline');
this.filters = this.checkFilters(this.filters);
+ this.sections = this.checkSections(this.sections);
this.operators = this.checkOperators(this.operators);
+
+ if (this.sections.length > 0) {
+ this.settings.has_sections = true;
+ }
+
this.bindEvents();
this.initPlugins();
@@ -79,19 +87,20 @@ QueryBuilder.prototype.init = function($el, options) {
* Checks the configuration of each filter
* @throws ConfigError
*/
-QueryBuilder.prototype.checkFilters = function(filters) {
+QueryBuilder.prototype.checkFilters = function(filters, section) {
var definedFilters = [];
+ var sectiontag = function(i) { if (section) { return ' [section: {' + i + '}]'; } };
if (!filters || filters.length === 0) {
- Utils.error('Config', 'Missing filters list');
+ Utils.error('Config', 'Missing filters list' + sectiontag(0), section);
}
filters.forEach(function(filter, i) {
if (!filter.id) {
- Utils.error('Config', 'Missing filter {0} id', i);
+ Utils.error('Config', 'Missing filter {0} id' + sectiontag(1), i, section);
}
if (definedFilters.indexOf(filter.id) != -1) {
- Utils.error('Config', 'Filter "{0}" already defined', filter.id);
+ Utils.error('Config', 'Filter "{0}" already defined' + sectiontag(1), filter.id, section);
}
definedFilters.push(filter.id);
@@ -99,20 +108,20 @@ QueryBuilder.prototype.checkFilters = function(filters) {
filter.type = 'string';
}
else if (!QueryBuilder.types[filter.type]) {
- Utils.error('Config', 'Invalid type "{0}"', filter.type);
+ Utils.error('Config', 'Invalid type "{0}"' + sectiontag(1), filter.type, section);
}
if (!filter.input) {
filter.input = 'text';
}
else if (typeof filter.input != 'function' && QueryBuilder.inputs.indexOf(filter.input) == -1) {
- Utils.error('Config', 'Invalid input "{0}"', filter.input);
+ Utils.error('Config', 'Invalid input "{0}"' + sectiontag(1), filter.input, section);
}
if (filter.operators) {
filter.operators.forEach(function(operator) {
if (typeof operator != 'string') {
- Utils.error('Config', 'Filter operators must be global operators types (string)');
+ Utils.error('Config', 'Filter operators must be global operators types (string)' + sectiontag(0), section);
}
});
}
@@ -139,7 +148,7 @@ QueryBuilder.prototype.checkFilters = function(filters) {
switch (filter.input) {
case 'radio': case 'checkbox':
if (!filter.values || filter.values.length < 1) {
- Utils.error('Config', 'Missing filter "{0}" values', filter.id);
+ Utils.error('Config', 'Missing filter "{0}" values' + sectiontag(1), filter.id, section);
}
break;
@@ -150,7 +159,7 @@ QueryBuilder.prototype.checkFilters = function(filters) {
}
Utils.iterateOptions(filter.values, function(key) {
if (key == filter.placeholder_value) {
- Utils.error('Config', 'Placeholder of filter "{0}" overlaps with one of its values', filter.id);
+ Utils.error('Config', 'Placeholder of filter "{0}" overlaps with one of its values' + sectiontag(1), filter.id, section);
}
});
}
@@ -177,6 +186,32 @@ QueryBuilder.prototype.checkFilters = function(filters) {
return filters;
};
+/**
+ * Checks the configuration of each section
+ * @throws ConfigError
+ */
+QueryBuilder.prototype.checkSections = function(sections) {
+ if (!this.settings.allow_sections) {
+ return [];
+ }
+
+ var definedSections = [];
+
+ sections.forEach(function(section, i) {
+ if (!section.id) {
+ Utils.error('Config', 'Missing section {0} id', i);
+ }
+ if (definedSections.indexOf(section.id) != -1) {
+ Utils.error('Config', 'Section "{0}" already defined', section.id);
+ }
+ sections[i].filters = this.checkFilters(sections[i].filters, sections[i].id);
+ definedSections.push(section.id);
+ }, this);
+
+ return sections;
+};
+
+
/**
* Checks the configuration of each operator
* @throws ConfigError
@@ -248,7 +283,8 @@ QueryBuilder.prototype.bindEvents = function() {
// rule filter change
this.$el.on('change.queryBuilder', Selectors.rule_filter, function() {
var $rule = $(this).closest(Selectors.rule_container);
- Model($rule).filter = self.getFilterById($(this).val());
+ var m = Model($rule);
+ m.filter = self.getFilterById($(this).val(), m.section_type_id);
});
// rule operator change
@@ -283,6 +319,37 @@ QueryBuilder.prototype.bindEvents = function() {
});
}
+ if (this.settings.allow_sections && this.settings.has_sections) {
+ // section exists change
+ this.$el.on('change.queryBuilder', Selectors.section_exists_flag, function() {
+ if ($(this).is(':checked')) {
+ var $section = $(this).closest(Selectors.section_container);
+ Model($section).exists = $(this).val();
+ }
+ });
+
+ // section type change
+ this.$el.on('change.queryBuilder', Selectors.rule_stype, function() {
+ var sid = $(this).val();
+ var $section = $(this).closest(Selectors.section_container);
+ var model = Model($section);
+ model.type_id = sid;
+ self.refreshSection(model);
+ });
+
+ // add section button
+ this.$el.on('click.queryBuilder', Selectors.add_section, function() {
+ var $group = $(this).closest(Selectors.group_container);
+ self.addSection(Model($group));
+ });
+
+ // delete section button
+ this.$el.on('click.queryBuilder', Selectors.delete_section, function() {
+ var $section = $(this).closest(Selectors.section_container);
+ self.deleteSection(Model($section));
+ });
+ }
+
// model events
this.model.on({
'drop': function(e, node) {
@@ -298,6 +365,10 @@ QueryBuilder.prototype.bindEvents = function() {
}
self.refreshGroupsConditions();
},
+ 'set': function(e, node) {
+ node.parent.$el.find('>' + Selectors.section_body).empty().append(node.$el);
+ self.updateSectionExistsFlag(node.parent);
+ },
'move': function(e, node, group, index) {
node.$el.detach();
@@ -331,9 +402,13 @@ QueryBuilder.prototype.bindEvents = function() {
case 'value':
self.updateRuleValue(node);
break;
+
+ case 'section_type_id':
+ self.updateRuleSectionTypeId(node);
+ break;
}
}
- else {
+ else if (node instanceof Group) {
switch (field) {
case 'error':
self.displayError(node);
@@ -346,6 +421,29 @@ QueryBuilder.prototype.bindEvents = function() {
case 'condition':
self.updateGroupCondition(node);
break;
+
+ case 'section_type_id':
+ self.updateGroupSectionTypeId(node);
+ break;
+ }
+ }
+ else if (node instanceof Section) {
+ switch (field) {
+ case 'error':
+ self.displayError(node);
+ break;
+
+ case 'type_id':
+ self.updateSectionTypeId(node);
+ break;
+
+ case 'exists':
+ self.updateSectionExistsFlag(node);
+ break;
+
+ case 'flags':
+ self.applySectionFlags(node);
+ break;
}
}
}
@@ -402,8 +500,17 @@ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) {
}
var group_id = this.nextGroupId();
- var $group = $(this.getGroupTemplate(group_id, level));
- var model = parent.addGroup($group);
+ var section_root = parent instanceof Section;
+ var stype = section_root ? parent.type_id : parent.section_type_id;
+ var in_section = section_root || stype !== undefined;
+ var $group = $(this.getGroupTemplate(group_id, level, stype, in_section, section_root));
+ var model = null;
+ if (parent instanceof Section) {
+ model = parent.setGroup($group);
+ }
+ else {
+ model = parent.addGroup($group);
+ }
model.data = data;
model.flags = $.extend({}, this.settings.default_group_flags, flags);
@@ -440,6 +547,8 @@ QueryBuilder.prototype.deleteGroup = function(group) {
del&= this.deleteRule(rule);
}, function(group) {
del&= this.deleteGroup(group);
+ }, function(section) {
+ del&= this.deleteSection(section);
}, this);
if (del) {
@@ -476,10 +585,168 @@ QueryBuilder.prototype.refreshGroupsConditions = function() {
group.each(function(rule) {}, function(group) {
walk(group);
+ }, function(section) {
+ if (section.group) {
+ walk(section.group);
+ }
}, this);
}(this.model.root));
};
+/**
+ * Add a new section
+ * @param parent {Group}
+ * @param addRule {bool,optional} add a default empty rule
+ * @param data {mixed,optional} section custom data
+ * @param flags {object,optional} flags to apply to the section
+ * @return section {Section}
+ */
+QueryBuilder.prototype.addSection = function(parent, addRule, data, flags) {
+ addRule = (addRule === undefined || addRule === true);
+
+ var level = parent.level + 1;
+
+ var e = this.trigger('beforeAddSection', parent, addRule, level);
+ if (e.isDefaultPrevented()) {
+ return null;
+ }
+
+ var section_id = this.nextSectionId();
+ var $section = $(this.getSectionTemplate(section_id, level));
+ var model = parent.addSection($section);
+
+ model.data = data;
+ model.flags = $.extend({}, this.settings.default_section_flags, flags);
+
+ this.trigger('afterAddSection', model);
+
+ model.exists = this.settings.default_exists;
+
+ this.createSectionTypes(model);
+
+ if (addRule && this.settings.default_section) {
+ model.type_id = this.settings.default_section;
+ this.addGroup(model, true);
+ }
+
+ return model;
+};
+
+/**
+ * Tries to delete a section. The section is not deleted if at least one rule is no_delete.
+ * @param section {Section}
+ * @return {boolean} true if the section has been deleted
+ */
+QueryBuilder.prototype.deleteSection = function(section) {
+ var e = this.trigger('beforeDeleteSection', section);
+ if (e.isDefaultPrevented()) {
+ return false;
+ }
+
+ if (section.group) {
+ if (!this.deleteGroup(section.group)) {
+ return false;
+ }
+ }
+
+ section.drop();
+ this.trigger('afterDeleteSection');
+
+ return true;
+};
+
+/**
+ * Changes the type setting of a section
+ * @param section {Section}
+ */
+QueryBuilder.prototype.updateSectionTypeId = function(section) {
+ section.$el.find(Selectors.rule_stype).val(section.type_id ? section.type_id : '-1');
+ section.$el.attr('data-stype', section.type_id);
+ this.trigger('afterUpdateSectionTypeId', section);
+};
+
+/**
+ * Changes the section type setting of a group
+ * @param section {Section}
+ */
+QueryBuilder.prototype.updateGroupSectionTypeId = function(group) {
+ group.$el.attr('data-stype', group.section_type_id);
+ this.trigger('afterUpdateGroupSectionTypeId', group);
+};
+
+/**
+ * Changes the section type setting of a rule
+ * @param section {Section}
+ */
+QueryBuilder.prototype.updateRuleSectionTypeId = function(rule) {
+ rule.$el.attr('data-stype', rule.section_type_id);
+ this.trigger('afterUpdateRuleSectionTypeId', rule);
+};
+
+/**
+ * Changes the exists setting of a section
+ * @param section {Section}
+ */
+QueryBuilder.prototype.updateSectionExistsFlag = function(section) {
+ section.$el.find('>' + Selectors.section_exists_flag).each(function() {
+ var $this = $(this);
+ $this.prop('checked', $this.val() === section.exists);
+ $this.parent().toggleClass('active', $this.val() === section.exists);
+ });
+
+ this.trigger('afterUpdateSectionExistsFlag', section);
+};
+
+/**
+ * Create the type