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 {{= it.lang.exist_options[option] || option }} \ + \ + {{~}} \ + \ + {{? it.settings.display_errors }} \ +
\ + {{?}} \ + \ +
\ +
\ +
\ +'; + +QueryBuilder.templates.stypeSelect = '\ +'; + QueryBuilder.templates.rule = '\ -
  • \ +
  • \
    \
    \