From baa79ce9ca0691f335cd771c68b8b7967ba98e78 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Fri, 14 Oct 2016 12:57:08 -0400 Subject: [PATCH 01/44] Began work on adding sections (placeholder commit) --- src/core.js | 118 ++++++++++++++++++++++++++++++++++++++++++++++- src/data.js | 8 ++++ src/defaults.js | 36 ++++++++++++--- src/i18n/en.json | 9 +++- src/model.js | 66 +++++++++++++++++++++++++- src/template.js | 56 +++++++++++++++++++++- 6 files changed, 281 insertions(+), 12 deletions(-) diff --git a/src/core.js b/src/core.js index 5ee8e0ac..fd63b573 100644 --- a/src/core.js +++ b/src/core.js @@ -9,9 +9,11 @@ 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, + has_sections: false, has_optgroup: false, has_operator_oprgroup: false, id: null, @@ -28,6 +30,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 +63,13 @@ QueryBuilder.prototype.init = function($el, options) { this.$el.addClass('query-builder form-inline'); this.filters = this.checkFilters(this.filters); + for (var k in this.sections) { + if (this.sections.hasOwnProperty(k)) { + this.sections[k].filters = this.checkFilters(this.sections[k].filters); + } + } this.operators = this.checkOperators(this.operators); + this.bindEvents(); this.initPlugins(); @@ -245,6 +254,14 @@ QueryBuilder.prototype.bindEvents = function() { } }); + // section exists change + this.$el.on('change.queryBuilder', Selectors.section_exists, function() { + if ($(this).is(':checked')) { + var $section = $(this).closest(Selectors.section_exists); + Model($section).condition = $(this).val(); + } + }); + // rule filter change this.$el.on('change.queryBuilder', Selectors.rule_filter, function() { var $rule = $(this).closest(Selectors.rule_container); @@ -283,6 +300,20 @@ QueryBuilder.prototype.bindEvents = function() { }); } + if (this.settings.allow_sections !== 0) { + // add section button + this.$el.on('click.queryBuilder', Selectors.add_section, function() { + var $section = $(this).closest(Selectors.group_container); + self.addSection(Model($section)); + }); + + // 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 +329,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.section); + }, 'move': function(e, node, group, index) { node.$el.detach(); @@ -402,8 +437,14 @@ 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 $group = $(this.getGroupTemplate(group_id, level, parent instanceof Section || parent.section ? true : false)); + console.log(parent); + if (parent instanceof Section) { + var model = parent.setGroup($group); + } else { + var model = parent.addGroup($group); + } + console.log(model); model.data = data; model.flags = $.extend({}, this.settings.default_group_flags, flags); @@ -480,6 +521,79 @@ QueryBuilder.prototype.refreshGroupsConditions = function() { }(this.model.root)); }; +//--section + +/** + * 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.addGroup(model, true, data, flags); + + 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 (!this.deleteGroup(section.group)) { + return false; + } + + section.drop(); + this.trigger('afterDeleteSection'); + + return true; +}; + +/** + * 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); +}; + +//--section + /** * Add a new rule * @param parent {Group} diff --git a/src/data.js b/src/data.js index 2f1752dc..73f40ec8 100644 --- a/src/data.js +++ b/src/data.js @@ -215,6 +215,14 @@ QueryBuilder.prototype.nextRuleId = function() { return this.status.id + '_rule_' + (this.status.rule_id++); }; +/** + * Returns an incremented section ID + * @return {string} + */ +QueryBuilder.prototype.nextSectionId = function() { + return this.status.id + '_section_' + (this.status.section_id++); +}; + /** * Returns the operators for a filter * @param filter {string|object} (filter id name or filter object) diff --git a/src/defaults.js b/src/defaults.js index fffde6f5..b2dbd84d 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -28,6 +28,7 @@ QueryBuilder.inputs = [ QueryBuilder.modifiable_options = [ 'display_errors', 'allow_groups', + 'allow_sections', 'allow_empty', 'default_condition', 'default_filter' @@ -37,6 +38,7 @@ QueryBuilder.modifiable_options = [ * CSS selectors for common components */ var Selectors = QueryBuilder.selectors = { + section_container: '.rules-section-container', group_container: '.rules-group-container', rule_container: '.rule-container', filter_container: '.rule-filter-container', @@ -45,13 +47,17 @@ var Selectors = QueryBuilder.selectors = { error_container: '.error-container', condition_container: '.rules-group-header .group-conditions', - rule_header: '.rule-header', + section_header: '.rules-section-header', group_header: '.rules-group-header', + rule_header: '.rule-header', + section_actions: '.section-actions', group_actions: '.group-actions', rule_actions: '.rule-actions', + section_body: '.rules-section-body', rules_list: '.rules-group-body>.rules-list', + section_exists_flag: '.rules-section-header [name$=_exists]', group_condition: '.rules-group-header [name$=_cond]', rule_filter: '.rule-filter-container [name$=_filter]', rule_operator: '.rule-operator-container [name$=_operator]', @@ -60,7 +66,9 @@ var Selectors = QueryBuilder.selectors = { add_rule: '[data-add=rule]', delete_rule: '[data-delete=rule]', add_group: '[data-add=group]', - delete_group: '[data-delete=group]' + delete_group: '[data-delete=group]', + add_section: '[data-add=section]', + delete_section: '[data-delete=section]' }; /** @@ -104,14 +112,18 @@ QueryBuilder.OPERATORS = { */ QueryBuilder.DEFAULTS = { filters: [], + sections: [], plugins: [], sort_filters: false, display_errors: true, + allow_sections: -1, allow_groups: -1, allow_empty: false, conditions: ['AND', 'OR'], default_condition: 'AND', + exist_options: ['EXISTS', 'DOES NOT EXIST'], + default_exists: 'EXISTS', inputs_separator: ' , ', select_placeholder: '------', display_empty_filter: true, @@ -132,7 +144,15 @@ QueryBuilder.DEFAULTS = { no_delete: false }, + default_section_flags: { + exists_readonly: false, + no_add_rule: false, + no_add_group: false, + no_delete: false + }, + templates: { + section: null, group: null, rule: null, filterSelect: null, @@ -166,10 +186,12 @@ QueryBuilder.DEFAULTS = { ], icons: { - add_group: 'glyphicon glyphicon-plus-sign', - add_rule: 'glyphicon glyphicon-plus', - remove_group: 'glyphicon glyphicon-remove', - remove_rule: 'glyphicon glyphicon-remove', - error: 'glyphicon glyphicon-warning-sign' + add_section: 'glyphicon glyphicon-plus-sign', + add_group: 'glyphicon glyphicon-plus-sign', + add_rule: 'glyphicon glyphicon-plus', + remove_section: 'glyphicon glyphicon-remove', + remove_group: 'glyphicon glyphicon-remove', + remove_rule: 'glyphicon glyphicon-remove', + error: 'glyphicon glyphicon-warning-sign' } }; diff --git a/src/i18n/en.json b/src/i18n/en.json index b5828139..60c0d07e 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -4,14 +4,21 @@ "add_rule": "Add rule", "add_group": "Add group", + "add_section": "Add section", "delete_rule": "Delete", "delete_group": "Delete", + "delete_section": "Delete", "conditions": { "AND": "AND", "OR": "OR" }, + "exist_options": { + "EXISTS": "EXISTS", + "NOT EXISTS": "DOES NOT EXIST" + }, + "operators": { "equal": "equal", "not_equal": "not equal", @@ -58,4 +65,4 @@ "boolean_not_valid": "Not a boolean", "operator_not_multiple": "Operator {0} cannot accept multiple values" } -} \ No newline at end of file +} diff --git a/src/model.js b/src/model.js index f93dc8e3..df02b19d 100644 --- a/src/model.js +++ b/src/model.js @@ -221,7 +221,7 @@ Node.prototype._move = function(group, index) { // GROUP CLASS // =============================== /** - * @param {Group} + * @param {Group|Section} * @param {jQuery} */ var Group = function(parent, $el) { @@ -232,6 +232,7 @@ var Group = function(parent, $el) { Node.call(this, parent, $el); this.rules = []; + this.section = null; this.__.condition = null; }; @@ -281,6 +282,9 @@ Group.prototype._appendNode = function(node, index, trigger) { this.rules.splice(index, 0, node); node.parent = this; + if (!(node instanceof Section)) { + node.section = this.section; + } if (trigger && this.model !== null) { this.model.trigger('add', node, index); @@ -309,6 +313,17 @@ Group.prototype.addRule = function($el, index) { return this._appendNode(new Rule(this, $el), index, true); }; +/** + * Add a Section by jQuery element at specified index + * @param {jQuery} + * @param {int,optional} + * @return {Section} the inserted section + */ +Group.prototype.addSection = function($el, index) { + console.log($el); + return this._appendNode(new Section(this, $el), index, true); +}; + /** * Delete a specific Node * @param {Node} @@ -410,6 +425,8 @@ var Rule = function(parent, $el) { Node.call(this, parent, $el); + this.section = null; + this.__.filter = null; this.__.operator = null; this.__.flags = {}; @@ -421,8 +438,55 @@ Rule.prototype.constructor = Rule; defineModelProperties(Rule, ['filter', 'operator', 'value']); +// Section CLASS +// =============================== +/** + * @param {Section} + * @param {jQuery} + */ +var Section = function(parent, $el) { + if (!(this instanceof Section)) { + return new Section(parent, $el); + } + + Node.call(this, parent, $el); + + this.group = null; + this.__.exists = null; +}; + +Section.prototype = Object.create(Node.prototype); +Section.prototype.constructor = Section; + +defineModelProperties(Section, ['exists']); + +/** + * Set the root group of the section by jQuery element + * @param {jQuery} + * @return {Group} the new root group + */ +Section.prototype.setGroup = function($el) { + this.group = new Group(this, $el); + this.group.parent = this; + this.group.section = this; + if (this.model !== null) { + this.model.trigger('set', this.group); + } + return this.group; +}; + +/** + * Delete self + */ +Section.prototype.drop = function() { + this.group.empty(); + Node.prototype.drop.call(this); +}; + // EXPORT // =============================== QueryBuilder.Group = Group; QueryBuilder.Rule = Rule; +QueryBuilder.Section = Section; + diff --git a/src/template.js b/src/template.js index 3a2756e4..b8808303 100644 --- a/src/template.js +++ b/src/template.js @@ -10,6 +10,11 @@ QueryBuilder.templates.group = '\ {{= it.lang.add_group }} \ \ {{?}} \ + {{? !it.in_section }} \ + \ + {{?}} \ {{? it.level>1 }} \ \ + {{?}} \ + \ +
\ + {{~ it.exist_options: option }} \ + \ + {{~}} \ +
\ + {{? it.settings.display_errors }} \ +
\ + {{?}} \ + \ +
\ +
\ +'; + QueryBuilder.templates.rule = '\
  • \
    \ @@ -91,13 +121,16 @@ QueryBuilder.templates.operatorSelect = '\ * Returns group HTML * @param group_id {string} * @param level {int} + * @param in_section {bool} * @return {string} */ -QueryBuilder.prototype.getGroupTemplate = function(group_id, level) { +QueryBuilder.prototype.getGroupTemplate = function(group_id, level, in_section) { + console.log(in_section); var h = this.templates.group({ builder: this, group_id: group_id, level: level, + in_section: in_section, conditions: this.settings.conditions, icons: this.icons, lang: this.lang, @@ -124,6 +157,27 @@ QueryBuilder.prototype.getRuleTemplate = function(rule_id) { return this.change('getRuleTemplate', h); }; +/** + * Returns section HTML + * @param group_id {string} + * @param level {int} + * @param in_section {bool} + * @return {string} + */ +QueryBuilder.prototype.getSectionTemplate = function(section_id, level) { + var h = this.templates.section({ + builder: this, + section_id: section_id, + level: level, + exist_options: this.settings.exist_options, + icons: this.icons, + lang: this.lang, + settings: this.settings + }); + + return this.change('getSectionTemplate', h); +}; + /** * Returns rule filter for a section + * @param section {Section} + */ +QueryBuilder.prototype.createSectionTypes = function(section) { + var stypes = this.change('getSectionTypes', this.sections, section); + var $stypesSelect = $(this.getSectionTypeSelect(section, stypes)); + + section.$el.find(Selectors.stype_container).html($stypesSelect); + + this.trigger('afterCreateSectionStypes', section); +}; + +/** + * Refreshes a section after a type change + * @param section {Section} + */ +QueryBuilder.prototype.refreshSection = function(model) { + + // Clear out the section if there's any rule that don't belong + var ok = true; + model.$el.find(Selectors.rule_container).each(function() { + var rule = Model($(this)); + if (rule.section_id != model.id) { + ok = false; + } + }); + if (!ok) { + model.empty(); + } + + this.trigger('afterRefreshSection', model); +}; + //--section /** @@ -658,7 +708,18 @@ QueryBuilder.prototype.deleteRule = function(rule) { * @param rule {Rule} */ QueryBuilder.prototype.createRuleFilters = function(rule) { - var filters = this.change('getRuleFilters', this.filters, rule); + if (rule.section_id) { + console.log('on create rule filters'); + console.log(rule.section_id); + var section = this.getSectionById(rule.section_id); + if (section) { + var filters = this.change('getRuleFilters', section.filters, rule); + } else { + var filters = this.change('getRuleFilters', [], rule); + } + } else { + var filters = this.change('getRuleFilters', this.filters, rule); + } var $filterSelect = $(this.getRuleFilterSelect(rule, filters)); rule.$el.find(Selectors.filter_container).html($filterSelect); diff --git a/src/data.js b/src/data.js index 73f40ec8..0c67e255 100644 --- a/src/data.js +++ b/src/data.js @@ -223,6 +223,26 @@ QueryBuilder.prototype.nextSectionId = function() { return this.status.id + '_section_' + (this.status.section_id++); }; +/** + * Returns a particular section by its id + * @throws UndefinedSectionError + * @param sectionId {string} + * @return {object|null} + */ +QueryBuilder.prototype.getSectionById = function(id) { + if (id == '-1') { + return null; + } + + for (var i = 0, l = this.sections.length; i < l; i++) { + if (this.sections[i].id == id) { + return this.sections[i]; + } + } + + Utils.error('UndefinedSection', 'Undefined section "{0}"', id); +}; + /** * Returns the operators for a filter * @param filter {string|object} (filter id name or filter object) @@ -264,16 +284,25 @@ QueryBuilder.prototype.getOperators = function(filter) { * Returns a particular filter by its id * @throws UndefinedFilterError * @param filterId {string} + * @param sectionId {string|null} * @return {object|null} */ -QueryBuilder.prototype.getFilterById = function(id) { +QueryBuilder.prototype.getFilterById = function(id, sectionId) { if (id == '-1') { return null; } - for (var i = 0, l = this.filters.length; i < l; i++) { - if (this.filters[i].id == id) { - return this.filters[i]; + if (sectionId) { + console.log('on getFilterById'); + console.log(sectionId); + var s = this.getSectionById(sectionId); + var filters = s ? s.filters : []; + } else { + var filters = this.filters; + } + for (var i = 0, l = filters.length; i < l; i++) { + if (filters[i].id == id) { + return filters[i]; } } diff --git a/src/defaults.js b/src/defaults.js index b2dbd84d..f495a2db 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -41,6 +41,7 @@ var Selectors = QueryBuilder.selectors = { section_container: '.rules-section-container', group_container: '.rules-group-container', rule_container: '.rule-container', + stype_container: '.rule-stype-container', filter_container: '.rule-filter-container', operator_container: '.rule-operator-container', value_container: '.rule-value-container', @@ -59,6 +60,7 @@ var Selectors = QueryBuilder.selectors = { section_exists_flag: '.rules-section-header [name$=_exists]', group_condition: '.rules-group-header [name$=_cond]', + rule_stype: '.rule-stype-container [name$=_section_type]', rule_filter: '.rule-filter-container [name$=_filter]', rule_operator: '.rule-operator-container [name$=_operator]', rule_value: '.rule-value-container [name*=_value_]', @@ -153,6 +155,7 @@ QueryBuilder.DEFAULTS = { templates: { section: null, + stypeSelect: null, group: null, rule: null, filterSelect: null, diff --git a/src/model.js b/src/model.js index df02b19d..a716050d 100644 --- a/src/model.js +++ b/src/model.js @@ -232,7 +232,7 @@ var Group = function(parent, $el) { Node.call(this, parent, $el); this.rules = []; - this.section = null; + this.section_id = null; this.__.condition = null; }; @@ -283,7 +283,7 @@ Group.prototype._appendNode = function(node, index, trigger) { this.rules.splice(index, 0, node); node.parent = this; if (!(node instanceof Section)) { - node.section = this.section; + node.section_id = this.section_id; } if (trigger && this.model !== null) { @@ -320,7 +320,6 @@ Group.prototype.addRule = function($el, index) { * @return {Section} the inserted section */ Group.prototype.addSection = function($el, index) { - console.log($el); return this._appendNode(new Section(this, $el), index, true); }; @@ -425,7 +424,7 @@ var Rule = function(parent, $el) { Node.call(this, parent, $el); - this.section = null; + this.section_id = null; this.__.filter = null; this.__.operator = null; @@ -452,6 +451,7 @@ var Section = function(parent, $el) { Node.call(this, parent, $el); this.group = null; + this.__.id = null; this.__.exists = null; }; @@ -468,13 +468,20 @@ defineModelProperties(Section, ['exists']); Section.prototype.setGroup = function($el) { this.group = new Group(this, $el); this.group.parent = this; - this.group.section = this; + this.group.section_id = this.id; if (this.model !== null) { this.model.trigger('set', this.group); } return this.group; }; +/** + * Clear out all rules + */ +Section.prototype.empty = function() { + this.group.empty(); +}; + /** * Delete self */ diff --git a/src/template.js b/src/template.js index b8808303..56d32fd4 100644 --- a/src/template.js +++ b/src/template.js @@ -47,7 +47,7 @@ QueryBuilder.templates.section = '\ \ {{?}} \
    \ -
    \ +
    \ {{~ it.exist_options: option }} \
  • \
    \ @@ -125,7 +136,6 @@ QueryBuilder.templates.operatorSelect = '\ * @return {string} */ QueryBuilder.prototype.getGroupTemplate = function(group_id, level, in_section) { - console.log(in_section); var h = this.templates.group({ builder: this, group_id: group_id, @@ -178,6 +188,26 @@ QueryBuilder.prototype.getSectionTemplate = function(section_id, level) { return this.change('getSectionTemplate', h); }; +/** + * Returns section type HTML * @param rule {Rule} From ad69458e7f797c758cc7e9588d43f1da779f4030 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Thu, 20 Oct 2016 12:13:29 -0400 Subject: [PATCH 04/44] Fixed a bug in swapping the section-exists flag --- src/core.js | 17 +++++++++++------ src/data.js | 2 -- src/model.js | 1 + 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/core.js b/src/core.js index 092858c6..d3c9c402 100644 --- a/src/core.js +++ b/src/core.js @@ -255,10 +255,10 @@ QueryBuilder.prototype.bindEvents = function() { }); // section exists change - this.$el.on('change.queryBuilder', Selectors.section_exists, function() { + this.$el.on('change.queryBuilder', Selectors.section_exists_flag, function() { if ($(this).is(':checked')) { - var $section = $(this).closest(Selectors.section_exists); - Model($section).condition = $(this).val(); + var $section = $(this).closest(Selectors.section_container); + Model($section).exists = $(this).val(); } }); @@ -378,7 +378,7 @@ QueryBuilder.prototype.bindEvents = function() { break; } } - else { + else if (node instanceof Group) { switch (field) { case 'error': self.displayError(node); @@ -393,6 +393,13 @@ QueryBuilder.prototype.bindEvents = function() { break; } } + else if (node instanceof Section) { + switch (field) { + case 'exists': + self.updateSectionExistsFlag(node); + break; + } + } } }); }; @@ -709,8 +716,6 @@ QueryBuilder.prototype.deleteRule = function(rule) { */ QueryBuilder.prototype.createRuleFilters = function(rule) { if (rule.section_id) { - console.log('on create rule filters'); - console.log(rule.section_id); var section = this.getSectionById(rule.section_id); if (section) { var filters = this.change('getRuleFilters', section.filters, rule); diff --git a/src/data.js b/src/data.js index 0c67e255..8cb46d1a 100644 --- a/src/data.js +++ b/src/data.js @@ -293,8 +293,6 @@ QueryBuilder.prototype.getFilterById = function(id, sectionId) { } if (sectionId) { - console.log('on getFilterById'); - console.log(sectionId); var s = this.getSectionById(sectionId); var filters = s ? s.filters : []; } else { diff --git a/src/model.js b/src/model.js index a716050d..29d09b71 100644 --- a/src/model.js +++ b/src/model.js @@ -479,6 +479,7 @@ Section.prototype.setGroup = function($el) { * Clear out all rules */ Section.prototype.empty = function() { + this.group.section_id = this.id; this.group.empty(); }; From b3e29dc092ab79985b1b906e7a97878610482f98 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Thu, 20 Oct 2016 14:01:22 -0400 Subject: [PATCH 05/44] Set up json building and validation for sections --- src/core.js | 8 ++++++++ src/model.js | 15 +++++++++++++-- src/public.js | 14 ++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/core.js b/src/core.js index d3c9c402..270f0ba7 100644 --- a/src/core.js +++ b/src/core.js @@ -496,6 +496,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) { @@ -532,6 +534,10 @@ QueryBuilder.prototype.refreshGroupsConditions = function() { group.each(function(rule) {}, function(group) { walk(group); + }, function(section) { + if (section.group) { + walk(section.group); + } }, this); }(this.model.root)); }; @@ -920,6 +926,8 @@ QueryBuilder.prototype.clearErrors = function(node) { rule.error = null; }, function(group) { this.clearErrors(group); + }, function(section) { + this.clearErrors(section.group); }, this); } }; diff --git a/src/model.js b/src/model.js index 29d09b71..e220705c 100644 --- a/src/model.js +++ b/src/model.js @@ -249,6 +249,8 @@ Group.prototype.empty = function() { rule.drop(); }, function(group) { group.drop(); + }, function(section) { + section.drop(); }); }; @@ -352,11 +354,13 @@ Group.prototype.getNodePos = function(node) { * @param {boolean,optional} iterate in reverse order, required if you delete nodes * @param {function} callback for Rules * @param {function,optional} callback for Groups + * @param {function,optional} callback for Sections * @return {boolean} */ -Group.prototype.each = function(reverse, cbRule, cbGroup, context) { +Group.prototype.each = function(reverse, cbRule, cbGroup, cbSection, context) { if (typeof reverse == 'function') { - context = cbGroup; + context = cbSection; + cbSection = cbGroup; cbGroup = cbRule; cbRule = reverse; reverse = false; @@ -375,6 +379,11 @@ Group.prototype.each = function(reverse, cbRule, cbGroup, context) { stop = cbGroup.call(context, this.rules[i]) === false; } } + else if (this.rules[i] instanceof Section) { + if (cbSection !== undefined) { + stop = cbSection.call(context, this.rules[i]) === false; + } + } else { stop = cbRule.call(context, this.rules[i]) === false; } @@ -406,6 +415,8 @@ Group.prototype.contains = function(node, deep) { return true; }, function(group) { return !group.contains(node, true); + }, function(section) { + return !section.group.contains(node, true); }); } }; diff --git a/src/public.js b/src/public.js index d74a67b2..673c6972 100644 --- a/src/public.js +++ b/src/public.js @@ -109,6 +109,13 @@ QueryBuilder.prototype.validate = function() { else { errors++; } + }, function(section) { + if (parse(section.group)) { + done++; + } + else { + errors++; + } }); if (errors > 0) { @@ -190,6 +197,13 @@ QueryBuilder.prototype.getRules = function(options) { }, function(model) { data.rules.push(parse(model)); + }, function(model) { + var rule = { + section: model.id, + exists: model.exists, + }; + rule.group = parse(model.group); + data.rules.push(rule); }); return data; From 9a34dfa6ef23bf6ab26d07498607a7446cd332b6 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Thu, 20 Oct 2016 14:10:46 -0400 Subject: [PATCH 06/44] When switching section types, add an empty rule to the group after we clear it out --- src/core.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/core.js b/src/core.js index 270f0ba7..a395465c 100644 --- a/src/core.js +++ b/src/core.js @@ -542,8 +542,6 @@ QueryBuilder.prototype.refreshGroupsConditions = function() { }(this.model.root)); }; -//--section - /** * Add a new section * @param parent {Group} @@ -650,13 +648,12 @@ QueryBuilder.prototype.refreshSection = function(model) { }); if (!ok) { model.empty(); + this.addRule(model.group); } this.trigger('afterRefreshSection', model); }; -//--section - /** * Add a new rule * @param parent {Group} From 248f2d9543694ac67005f95125ded31c9c1dd7c7 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Mon, 24 Oct 2016 09:19:33 -0400 Subject: [PATCH 07/44] Implemented allow_sections and section flags --- src/core.js | 75 ++++++++++++++++++++++++++++++++++--------------- src/defaults.js | 3 +- src/template.js | 2 +- 3 files changed, 56 insertions(+), 24 deletions(-) diff --git a/src/core.js b/src/core.js index a395465c..87b89e83 100644 --- a/src/core.js +++ b/src/core.js @@ -13,7 +13,6 @@ QueryBuilder.prototype.init = function($el, options) { group_id: 0, rule_id: 0, generated_id: false, - has_sections: false, has_optgroup: false, has_operator_oprgroup: false, id: null, @@ -63,9 +62,11 @@ QueryBuilder.prototype.init = function($el, options) { this.$el.addClass('query-builder form-inline'); this.filters = this.checkFilters(this.filters); - for (var k in this.sections) { - if (this.sections.hasOwnProperty(k)) { - this.sections[k].filters = this.checkFilters(this.sections[k].filters); + if (this.settings.allow_sections) { + for (var k in this.sections) { + if (this.sections.hasOwnProperty(k)) { + this.sections[k].filters = this.checkFilters(this.sections[k].filters); + } } } this.operators = this.checkOperators(this.operators); @@ -254,23 +255,6 @@ QueryBuilder.prototype.bindEvents = function() { } }); - // 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.id = sid; - self.refreshSection(model); - }); - // rule filter change this.$el.on('change.queryBuilder', Selectors.rule_filter, function() { var $rule = $(this).closest(Selectors.rule_container); @@ -310,7 +294,24 @@ QueryBuilder.prototype.bindEvents = function() { }); } - if (this.settings.allow_sections !== 0) { + if (this.settings.allow_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.id = sid; + self.refreshSection(model); + }); + // add section button this.$el.on('click.queryBuilder', Selectors.add_section, function() { var $section = $(this).closest(Selectors.group_container); @@ -398,6 +399,9 @@ QueryBuilder.prototype.bindEvents = function() { case 'exists': self.updateSectionExistsFlag(node); break; + case 'flags': + self.applySectionFlags(node); + break; } } } @@ -905,6 +909,33 @@ QueryBuilder.prototype.applyGroupFlags = function(group) { this.trigger('afterApplyGroupFlags', group); }; +/** + * Change section properties depending on flags. + * @param section {Section} + */ +QueryBuilder.prototype.applySectionFlags = function(section) { + var flags = section.flags; + + if (flags.exists_readonly) { + section.$el.find('>' + Selectors.section_exists_flag).prop('disabled', true) + .parent().addClass('readonly'); + } + if (flags.no_add_rule) { + section.$el.find(Selectors.add_rule).remove(); + } + if (flags.no_add_group) { + section.$el.find(Selectors.add_group).remove(); + } + if (flags.no_delete) { + section.$el.find(Selectors.delete_section).remove(); + } + this.trigger('afterApplySectionFlags', section); + + if (section.group) { + this.applyGroupFlags(section.group); + } +}; + /** * Clear all errors markers * @param node {Node,optional} default is root Group diff --git a/src/defaults.js b/src/defaults.js index f495a2db..1991b532 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -119,11 +119,12 @@ QueryBuilder.DEFAULTS = { sort_filters: false, display_errors: true, - allow_sections: -1, allow_groups: -1, allow_empty: false, conditions: ['AND', 'OR'], default_condition: 'AND', + allow_sections: false, + default_section: null, exist_options: ['EXISTS', 'DOES NOT EXIST'], default_exists: 'EXISTS', inputs_separator: ' , ', diff --git a/src/template.js b/src/template.js index 56d32fd4..b5e87599 100644 --- a/src/template.js +++ b/src/template.js @@ -10,7 +10,7 @@ QueryBuilder.templates.group = '\ {{= it.lang.add_group }} \ \ {{?}} \ - {{? !it.in_section }} \ + {{? it.settings.allow_sections && !it.in_section }} \ \ From 989967e6b0624fe6d0cc16a6d3737efa1607f676 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Mon, 24 Oct 2016 09:19:59 -0400 Subject: [PATCH 08/44] Set allow_sections in the example --- examples/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/index.html b/examples/index.html index 900113fe..e7515571 100644 --- a/examples/index.html +++ b/examples/index.html @@ -119,6 +119,7 @@

    Output

    var options = { allow_empty: true, + allow_sections: true, //default_filter: 'name', sort_filters: true, @@ -520,7 +521,6 @@

    Output

    } }] }; -console.log(options); // init $('#builder').queryBuilder(options); From 6d249e8b2a9c904798d03caa3dcb13735dba6876 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Mon, 24 Oct 2016 12:31:44 -0400 Subject: [PATCH 09/44] Unless we have a default section id, create the section with no group and add one when an id is selected --- src/core.js | 37 ++++++++++++++++++++++++------------- src/defaults.js | 1 + src/model.js | 8 ++++---- src/template.js | 8 +++----- 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/core.js b/src/core.js index 87b89e83..1dcf3a80 100644 --- a/src/core.js +++ b/src/core.js @@ -579,12 +579,9 @@ QueryBuilder.prototype.addSection = function(parent, addRule, data, flags) { if (this.settings.default_section) { model.id = this.settings.default_section; - } else { - model.id = this.sections[0].id; + this.addGroup(model, true, data, flags); } - this.addGroup(model, true, data, flags); - return model; }; @@ -642,17 +639,31 @@ QueryBuilder.prototype.createSectionTypes = function(section) { */ QueryBuilder.prototype.refreshSection = function(model) { - // Clear out the section if there's any rule that don't belong - var ok = true; - model.$el.find(Selectors.rule_container).each(function() { - var rule = Model($(this)); - if (rule.section_id != model.id) { - ok = false; + if (model.id) { + // Clear out the section if there's any group or rule that don't belong + var ok = true; + model.$el.find(Selectors.group_container).each(function() { + var group = Model($(this)); + if (group.section_id != model.id) { + ok = false; + } + }); + if (ok) { + model.$el.find(Selectors.rule_container).each(function() { + var rule = Model($(this)); + if (rule.section_id != model.id) { + ok = false; + } + }); } - }); - if (!ok) { + if (!ok) { + model.empty(); + } + if (!model.group) { + var group = this.addGroup(model, true); + } + } else { model.empty(); - this.addRule(model.group); } this.trigger('afterRefreshSection', model); diff --git a/src/defaults.js b/src/defaults.js index 1991b532..0288cfd4 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -130,6 +130,7 @@ QueryBuilder.DEFAULTS = { inputs_separator: ' , ', select_placeholder: '------', display_empty_filter: true, + display_empty_stype_filter: true, default_filter: null, optgroups: {}, diff --git a/src/model.js b/src/model.js index e220705c..6b6f2b6f 100644 --- a/src/model.js +++ b/src/model.js @@ -147,7 +147,7 @@ Node.prototype.getPos = function() { Node.prototype.drop = function() { var model = this.model; - if (!this.isRoot()) { + if (!this.isRoot() && this.parent instanceof Group) { this.parent._removeNode(this); } @@ -490,15 +490,15 @@ Section.prototype.setGroup = function($el) { * Clear out all rules */ Section.prototype.empty = function() { - this.group.section_id = this.id; - this.group.empty(); + this.group.drop(); + this.group = null; }; /** * Delete self */ Section.prototype.drop = function() { - this.group.empty(); + this.group.drop(); Node.prototype.drop.call(this); }; diff --git a/src/template.js b/src/template.js index b5e87599..6c128d80 100644 --- a/src/template.js +++ b/src/template.js @@ -41,11 +41,9 @@ QueryBuilder.templates.section = '\
    \
    \
    \ - {{? it.level>1 }} \ - \ - {{?}} \ + \
    \
    \ {{~ it.exist_options: option }} \ From 6e1d3ad1641184e4df98ad95f6f8786410054dbd Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Fri, 14 Oct 2016 12:57:08 -0400 Subject: [PATCH 10/44] Began work on adding sections (placeholder commit) --- src/core.js | 118 ++++++++++++++++++++++++++++++++++++++++++++++- src/data.js | 8 ++++ src/defaults.js | 36 ++++++++++++--- src/i18n/en.json | 9 +++- src/model.js | 66 +++++++++++++++++++++++++- src/template.js | 56 +++++++++++++++++++++- 6 files changed, 281 insertions(+), 12 deletions(-) diff --git a/src/core.js b/src/core.js index 33138413..7efca268 100644 --- a/src/core.js +++ b/src/core.js @@ -9,9 +9,11 @@ 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, + has_sections: false, has_optgroup: false, has_operator_oprgroup: false, id: null, @@ -28,6 +30,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 +63,13 @@ QueryBuilder.prototype.init = function($el, options) { this.$el.addClass('query-builder form-inline'); this.filters = this.checkFilters(this.filters); + for (var k in this.sections) { + if (this.sections.hasOwnProperty(k)) { + this.sections[k].filters = this.checkFilters(this.sections[k].filters); + } + } this.operators = this.checkOperators(this.operators); + this.bindEvents(); this.initPlugins(); @@ -245,6 +254,14 @@ QueryBuilder.prototype.bindEvents = function() { } }); + // section exists change + this.$el.on('change.queryBuilder', Selectors.section_exists, function() { + if ($(this).is(':checked')) { + var $section = $(this).closest(Selectors.section_exists); + Model($section).condition = $(this).val(); + } + }); + // rule filter change this.$el.on('change.queryBuilder', Selectors.rule_filter, function() { var $rule = $(this).closest(Selectors.rule_container); @@ -283,6 +300,20 @@ QueryBuilder.prototype.bindEvents = function() { }); } + if (this.settings.allow_sections !== 0) { + // add section button + this.$el.on('click.queryBuilder', Selectors.add_section, function() { + var $section = $(this).closest(Selectors.group_container); + self.addSection(Model($section)); + }); + + // 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 +329,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.section); + }, 'move': function(e, node, group, index) { node.$el.detach(); @@ -402,8 +437,14 @@ 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 $group = $(this.getGroupTemplate(group_id, level, parent instanceof Section || parent.section ? true : false)); + console.log(parent); + if (parent instanceof Section) { + var model = parent.setGroup($group); + } else { + var model = parent.addGroup($group); + } + console.log(model); model.data = data; model.flags = $.extend({}, this.settings.default_group_flags, flags); @@ -480,6 +521,79 @@ QueryBuilder.prototype.refreshGroupsConditions = function() { }(this.model.root)); }; +//--section + +/** + * 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.addGroup(model, true, data, flags); + + 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 (!this.deleteGroup(section.group)) { + return false; + } + + section.drop(); + this.trigger('afterDeleteSection'); + + return true; +}; + +/** + * 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); +}; + +//--section + /** * Add a new rule * @param parent {Group} diff --git a/src/data.js b/src/data.js index 6cb5a28e..d8082985 100644 --- a/src/data.js +++ b/src/data.js @@ -212,6 +212,14 @@ QueryBuilder.prototype.nextRuleId = function() { return this.status.id + '_rule_' + (this.status.rule_id++); }; +/** + * Returns an incremented section ID + * @return {string} + */ +QueryBuilder.prototype.nextSectionId = function() { + return this.status.id + '_section_' + (this.status.section_id++); +}; + /** * Returns the operators for a filter * @param filter {string|object} (filter id name or filter object) diff --git a/src/defaults.js b/src/defaults.js index fffde6f5..b2dbd84d 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -28,6 +28,7 @@ QueryBuilder.inputs = [ QueryBuilder.modifiable_options = [ 'display_errors', 'allow_groups', + 'allow_sections', 'allow_empty', 'default_condition', 'default_filter' @@ -37,6 +38,7 @@ QueryBuilder.modifiable_options = [ * CSS selectors for common components */ var Selectors = QueryBuilder.selectors = { + section_container: '.rules-section-container', group_container: '.rules-group-container', rule_container: '.rule-container', filter_container: '.rule-filter-container', @@ -45,13 +47,17 @@ var Selectors = QueryBuilder.selectors = { error_container: '.error-container', condition_container: '.rules-group-header .group-conditions', - rule_header: '.rule-header', + section_header: '.rules-section-header', group_header: '.rules-group-header', + rule_header: '.rule-header', + section_actions: '.section-actions', group_actions: '.group-actions', rule_actions: '.rule-actions', + section_body: '.rules-section-body', rules_list: '.rules-group-body>.rules-list', + section_exists_flag: '.rules-section-header [name$=_exists]', group_condition: '.rules-group-header [name$=_cond]', rule_filter: '.rule-filter-container [name$=_filter]', rule_operator: '.rule-operator-container [name$=_operator]', @@ -60,7 +66,9 @@ var Selectors = QueryBuilder.selectors = { add_rule: '[data-add=rule]', delete_rule: '[data-delete=rule]', add_group: '[data-add=group]', - delete_group: '[data-delete=group]' + delete_group: '[data-delete=group]', + add_section: '[data-add=section]', + delete_section: '[data-delete=section]' }; /** @@ -104,14 +112,18 @@ QueryBuilder.OPERATORS = { */ QueryBuilder.DEFAULTS = { filters: [], + sections: [], plugins: [], sort_filters: false, display_errors: true, + allow_sections: -1, allow_groups: -1, allow_empty: false, conditions: ['AND', 'OR'], default_condition: 'AND', + exist_options: ['EXISTS', 'DOES NOT EXIST'], + default_exists: 'EXISTS', inputs_separator: ' , ', select_placeholder: '------', display_empty_filter: true, @@ -132,7 +144,15 @@ QueryBuilder.DEFAULTS = { no_delete: false }, + default_section_flags: { + exists_readonly: false, + no_add_rule: false, + no_add_group: false, + no_delete: false + }, + templates: { + section: null, group: null, rule: null, filterSelect: null, @@ -166,10 +186,12 @@ QueryBuilder.DEFAULTS = { ], icons: { - add_group: 'glyphicon glyphicon-plus-sign', - add_rule: 'glyphicon glyphicon-plus', - remove_group: 'glyphicon glyphicon-remove', - remove_rule: 'glyphicon glyphicon-remove', - error: 'glyphicon glyphicon-warning-sign' + add_section: 'glyphicon glyphicon-plus-sign', + add_group: 'glyphicon glyphicon-plus-sign', + add_rule: 'glyphicon glyphicon-plus', + remove_section: 'glyphicon glyphicon-remove', + remove_group: 'glyphicon glyphicon-remove', + remove_rule: 'glyphicon glyphicon-remove', + error: 'glyphicon glyphicon-warning-sign' } }; diff --git a/src/i18n/en.json b/src/i18n/en.json index b5828139..60c0d07e 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -4,14 +4,21 @@ "add_rule": "Add rule", "add_group": "Add group", + "add_section": "Add section", "delete_rule": "Delete", "delete_group": "Delete", + "delete_section": "Delete", "conditions": { "AND": "AND", "OR": "OR" }, + "exist_options": { + "EXISTS": "EXISTS", + "NOT EXISTS": "DOES NOT EXIST" + }, + "operators": { "equal": "equal", "not_equal": "not equal", @@ -58,4 +65,4 @@ "boolean_not_valid": "Not a boolean", "operator_not_multiple": "Operator {0} cannot accept multiple values" } -} \ No newline at end of file +} diff --git a/src/model.js b/src/model.js index 39db5988..e532ac6c 100644 --- a/src/model.js +++ b/src/model.js @@ -221,7 +221,7 @@ Node.prototype._move = function(group, index) { // GROUP CLASS // =============================== /** - * @param {Group} + * @param {Group|Section} * @param {jQuery} */ var Group = function(parent, $el) { @@ -232,6 +232,7 @@ var Group = function(parent, $el) { Node.call(this, parent, $el); this.rules = []; + this.section = null; this.__.condition = null; }; @@ -281,6 +282,9 @@ Group.prototype._appendNode = function(node, index, trigger) { this.rules.splice(index, 0, node); node.parent = this; + if (!(node instanceof Section)) { + node.section = this.section; + } if (trigger && this.model !== null) { this.model.trigger('add', node, index); @@ -309,6 +313,17 @@ Group.prototype.addRule = function($el, index) { return this._appendNode(new Rule(this, $el), index, true); }; +/** + * Add a Section by jQuery element at specified index + * @param {jQuery} + * @param {int,optional} + * @return {Section} the inserted section + */ +Group.prototype.addSection = function($el, index) { + console.log($el); + return this._appendNode(new Section(this, $el), index, true); +}; + /** * Delete a specific Node * @param {Node} @@ -410,6 +425,8 @@ var Rule = function(parent, $el) { Node.call(this, parent, $el); + this.section = null; + this.__.filter = null; this.__.operator = null; this.__.flags = {}; @@ -421,8 +438,55 @@ Rule.prototype.constructor = Rule; Model.defineModelProperties(Rule, ['filter', 'operator', 'value']); +// Section CLASS +// =============================== +/** + * @param {Section} + * @param {jQuery} + */ +var Section = function(parent, $el) { + if (!(this instanceof Section)) { + return new Section(parent, $el); + } + + Node.call(this, parent, $el); + + this.group = null; + this.__.exists = null; +}; + +Section.prototype = Object.create(Node.prototype); +Section.prototype.constructor = Section; + +defineModelProperties(Section, ['exists']); + +/** + * Set the root group of the section by jQuery element + * @param {jQuery} + * @return {Group} the new root group + */ +Section.prototype.setGroup = function($el) { + this.group = new Group(this, $el); + this.group.parent = this; + this.group.section = this; + if (this.model !== null) { + this.model.trigger('set', this.group); + } + return this.group; +}; + +/** + * Delete self + */ +Section.prototype.drop = function() { + this.group.empty(); + Node.prototype.drop.call(this); +}; + // EXPORT // =============================== QueryBuilder.Group = Group; QueryBuilder.Rule = Rule; +QueryBuilder.Section = Section; + diff --git a/src/template.js b/src/template.js index 3a2756e4..b8808303 100644 --- a/src/template.js +++ b/src/template.js @@ -10,6 +10,11 @@ QueryBuilder.templates.group = '\ {{= it.lang.add_group }} \ \ {{?}} \ + {{? !it.in_section }} \ + \ + {{?}} \ {{? it.level>1 }} \
    '; +QueryBuilder.templates.section = '\ +
    \ +
    \ +
    \ + {{? it.level>1 }} \ + \ + {{?}} \ +
    \ +
    \ + {{~ it.exist_options: option }} \ + \ + {{~}} \ +
    \ + {{? it.settings.display_errors }} \ +
    \ + {{?}} \ +
    \ +
    \ +
    \ +
    '; + QueryBuilder.templates.rule = '\
  • \
    \ @@ -91,13 +121,16 @@ QueryBuilder.templates.operatorSelect = '\ * Returns group HTML * @param group_id {string} * @param level {int} + * @param in_section {bool} * @return {string} */ -QueryBuilder.prototype.getGroupTemplate = function(group_id, level) { +QueryBuilder.prototype.getGroupTemplate = function(group_id, level, in_section) { + console.log(in_section); var h = this.templates.group({ builder: this, group_id: group_id, level: level, + in_section: in_section, conditions: this.settings.conditions, icons: this.icons, lang: this.lang, @@ -124,6 +157,27 @@ QueryBuilder.prototype.getRuleTemplate = function(rule_id) { return this.change('getRuleTemplate', h); }; +/** + * Returns section HTML + * @param group_id {string} + * @param level {int} + * @param in_section {bool} + * @return {string} + */ +QueryBuilder.prototype.getSectionTemplate = function(section_id, level) { + var h = this.templates.section({ + builder: this, + section_id: section_id, + level: level, + exist_options: this.settings.exist_options, + icons: this.icons, + lang: this.lang, + settings: this.settings + }); + + return this.change('getSectionTemplate', h); +}; + /** * Returns rule filter for a section + * @param section {Section} + */ +QueryBuilder.prototype.createSectionTypes = function(section) { + var stypes = this.change('getSectionTypes', this.sections, section); + var $stypesSelect = $(this.getSectionTypeSelect(section, stypes)); + + section.$el.find(Selectors.stype_container).html($stypesSelect); + + this.trigger('afterCreateSectionStypes', section); +}; + +/** + * Refreshes a section after a type change + * @param section {Section} + */ +QueryBuilder.prototype.refreshSection = function(model) { + + // Clear out the section if there's any rule that don't belong + var ok = true; + model.$el.find(Selectors.rule_container).each(function() { + var rule = Model($(this)); + if (rule.section_id != model.id) { + ok = false; + } + }); + if (!ok) { + model.empty(); + } + + this.trigger('afterRefreshSection', model); +}; + //--section /** @@ -658,7 +708,18 @@ QueryBuilder.prototype.deleteRule = function(rule) { * @param rule {Rule} */ QueryBuilder.prototype.createRuleFilters = function(rule) { - var filters = this.change('getRuleFilters', this.filters, rule); + if (rule.section_id) { + console.log('on create rule filters'); + console.log(rule.section_id); + var section = this.getSectionById(rule.section_id); + if (section) { + var filters = this.change('getRuleFilters', section.filters, rule); + } else { + var filters = this.change('getRuleFilters', [], rule); + } + } else { + var filters = this.change('getRuleFilters', this.filters, rule); + } var $filterSelect = $(this.getRuleFilterSelect(rule, filters)); rule.$el.find(Selectors.filter_container).html($filterSelect); diff --git a/src/data.js b/src/data.js index d8082985..d76f4487 100644 --- a/src/data.js +++ b/src/data.js @@ -220,6 +220,26 @@ QueryBuilder.prototype.nextSectionId = function() { return this.status.id + '_section_' + (this.status.section_id++); }; +/** + * Returns a particular section by its id + * @throws UndefinedSectionError + * @param sectionId {string} + * @return {object|null} + */ +QueryBuilder.prototype.getSectionById = function(id) { + if (id == '-1') { + return null; + } + + for (var i = 0, l = this.sections.length; i < l; i++) { + if (this.sections[i].id == id) { + return this.sections[i]; + } + } + + Utils.error('UndefinedSection', 'Undefined section "{0}"', id); +}; + /** * Returns the operators for a filter * @param filter {string|object} (filter id name or filter object) @@ -261,16 +281,25 @@ QueryBuilder.prototype.getOperators = function(filter) { * Returns a particular filter by its id * @throws UndefinedFilterError * @param filterId {string} + * @param sectionId {string|null} * @return {object|null} */ -QueryBuilder.prototype.getFilterById = function(id) { +QueryBuilder.prototype.getFilterById = function(id, sectionId) { if (id == '-1') { return null; } - for (var i = 0, l = this.filters.length; i < l; i++) { - if (this.filters[i].id == id) { - return this.filters[i]; + if (sectionId) { + console.log('on getFilterById'); + console.log(sectionId); + var s = this.getSectionById(sectionId); + var filters = s ? s.filters : []; + } else { + var filters = this.filters; + } + for (var i = 0, l = filters.length; i < l; i++) { + if (filters[i].id == id) { + return filters[i]; } } diff --git a/src/defaults.js b/src/defaults.js index b2dbd84d..f495a2db 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -41,6 +41,7 @@ var Selectors = QueryBuilder.selectors = { section_container: '.rules-section-container', group_container: '.rules-group-container', rule_container: '.rule-container', + stype_container: '.rule-stype-container', filter_container: '.rule-filter-container', operator_container: '.rule-operator-container', value_container: '.rule-value-container', @@ -59,6 +60,7 @@ var Selectors = QueryBuilder.selectors = { section_exists_flag: '.rules-section-header [name$=_exists]', group_condition: '.rules-group-header [name$=_cond]', + rule_stype: '.rule-stype-container [name$=_section_type]', rule_filter: '.rule-filter-container [name$=_filter]', rule_operator: '.rule-operator-container [name$=_operator]', rule_value: '.rule-value-container [name*=_value_]', @@ -153,6 +155,7 @@ QueryBuilder.DEFAULTS = { templates: { section: null, + stypeSelect: null, group: null, rule: null, filterSelect: null, diff --git a/src/model.js b/src/model.js index e532ac6c..f2202fc6 100644 --- a/src/model.js +++ b/src/model.js @@ -232,7 +232,7 @@ var Group = function(parent, $el) { Node.call(this, parent, $el); this.rules = []; - this.section = null; + this.section_id = null; this.__.condition = null; }; @@ -283,7 +283,7 @@ Group.prototype._appendNode = function(node, index, trigger) { this.rules.splice(index, 0, node); node.parent = this; if (!(node instanceof Section)) { - node.section = this.section; + node.section_id = this.section_id; } if (trigger && this.model !== null) { @@ -320,7 +320,6 @@ Group.prototype.addRule = function($el, index) { * @return {Section} the inserted section */ Group.prototype.addSection = function($el, index) { - console.log($el); return this._appendNode(new Section(this, $el), index, true); }; @@ -425,7 +424,7 @@ var Rule = function(parent, $el) { Node.call(this, parent, $el); - this.section = null; + this.section_id = null; this.__.filter = null; this.__.operator = null; @@ -452,6 +451,7 @@ var Section = function(parent, $el) { Node.call(this, parent, $el); this.group = null; + this.__.id = null; this.__.exists = null; }; @@ -468,13 +468,20 @@ defineModelProperties(Section, ['exists']); Section.prototype.setGroup = function($el) { this.group = new Group(this, $el); this.group.parent = this; - this.group.section = this; + this.group.section_id = this.id; if (this.model !== null) { this.model.trigger('set', this.group); } return this.group; }; +/** + * Clear out all rules + */ +Section.prototype.empty = function() { + this.group.empty(); +}; + /** * Delete self */ diff --git a/src/template.js b/src/template.js index b8808303..56d32fd4 100644 --- a/src/template.js +++ b/src/template.js @@ -47,7 +47,7 @@ QueryBuilder.templates.section = '\ \ {{?}} \
    \ -
    \ +
    \ {{~ it.exist_options: option }} \
  • \
    \ @@ -125,7 +136,6 @@ QueryBuilder.templates.operatorSelect = '\ * @return {string} */ QueryBuilder.prototype.getGroupTemplate = function(group_id, level, in_section) { - console.log(in_section); var h = this.templates.group({ builder: this, group_id: group_id, @@ -178,6 +188,26 @@ QueryBuilder.prototype.getSectionTemplate = function(section_id, level) { return this.change('getSectionTemplate', h); }; +/** + * Returns section type HTML * @param rule {Rule} From 323bfc49f8356e08a2bf03059ab1847577b44067 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Thu, 20 Oct 2016 12:13:29 -0400 Subject: [PATCH 13/44] Fixed a bug in swapping the section-exists flag --- src/core.js | 17 +++++++++++------ src/data.js | 2 -- src/model.js | 1 + 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/core.js b/src/core.js index 6ec52beb..a65a1f98 100644 --- a/src/core.js +++ b/src/core.js @@ -255,10 +255,10 @@ QueryBuilder.prototype.bindEvents = function() { }); // section exists change - this.$el.on('change.queryBuilder', Selectors.section_exists, function() { + this.$el.on('change.queryBuilder', Selectors.section_exists_flag, function() { if ($(this).is(':checked')) { - var $section = $(this).closest(Selectors.section_exists); - Model($section).condition = $(this).val(); + var $section = $(this).closest(Selectors.section_container); + Model($section).exists = $(this).val(); } }); @@ -378,7 +378,7 @@ QueryBuilder.prototype.bindEvents = function() { break; } } - else { + else if (node instanceof Group) { switch (field) { case 'error': self.displayError(node); @@ -393,6 +393,13 @@ QueryBuilder.prototype.bindEvents = function() { break; } } + else if (node instanceof Section) { + switch (field) { + case 'exists': + self.updateSectionExistsFlag(node); + break; + } + } } }); }; @@ -709,8 +716,6 @@ QueryBuilder.prototype.deleteRule = function(rule) { */ QueryBuilder.prototype.createRuleFilters = function(rule) { if (rule.section_id) { - console.log('on create rule filters'); - console.log(rule.section_id); var section = this.getSectionById(rule.section_id); if (section) { var filters = this.change('getRuleFilters', section.filters, rule); diff --git a/src/data.js b/src/data.js index d76f4487..ef544cc9 100644 --- a/src/data.js +++ b/src/data.js @@ -290,8 +290,6 @@ QueryBuilder.prototype.getFilterById = function(id, sectionId) { } if (sectionId) { - console.log('on getFilterById'); - console.log(sectionId); var s = this.getSectionById(sectionId); var filters = s ? s.filters : []; } else { diff --git a/src/model.js b/src/model.js index f2202fc6..4f980f55 100644 --- a/src/model.js +++ b/src/model.js @@ -479,6 +479,7 @@ Section.prototype.setGroup = function($el) { * Clear out all rules */ Section.prototype.empty = function() { + this.group.section_id = this.id; this.group.empty(); }; From f945379288f92e35266947e45020819911112317 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Thu, 20 Oct 2016 14:01:22 -0400 Subject: [PATCH 14/44] Set up json building and validation for sections --- src/core.js | 8 ++++++++ src/model.js | 15 +++++++++++++-- src/public.js | 16 +++++++++++++++- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/core.js b/src/core.js index a65a1f98..cf0a7f70 100644 --- a/src/core.js +++ b/src/core.js @@ -496,6 +496,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) { @@ -532,6 +534,10 @@ QueryBuilder.prototype.refreshGroupsConditions = function() { group.each(function(rule) {}, function(group) { walk(group); + }, function(section) { + if (section.group) { + walk(section.group); + } }, this); }(this.model.root)); }; @@ -928,6 +934,8 @@ QueryBuilder.prototype.clearErrors = function(node) { rule.error = null; }, function(group) { this.clearErrors(group); + }, function(section) { + this.clearErrors(section.group); }, this); } }; diff --git a/src/model.js b/src/model.js index 4f980f55..d01edb9c 100644 --- a/src/model.js +++ b/src/model.js @@ -249,6 +249,8 @@ Group.prototype.empty = function() { rule.drop(); }, function(group) { group.drop(); + }, function(section) { + section.drop(); }); }; @@ -352,11 +354,13 @@ Group.prototype.getNodePos = function(node) { * @param {boolean,optional} iterate in reverse order, required if you delete nodes * @param {function} callback for Rules * @param {function,optional} callback for Groups + * @param {function,optional} callback for Sections * @return {boolean} */ -Group.prototype.each = function(reverse, cbRule, cbGroup, context) { +Group.prototype.each = function(reverse, cbRule, cbGroup, cbSection, context) { if (typeof reverse == 'function') { - context = cbGroup; + context = cbSection; + cbSection = cbGroup; cbGroup = cbRule; cbRule = reverse; reverse = false; @@ -375,6 +379,11 @@ Group.prototype.each = function(reverse, cbRule, cbGroup, context) { stop = cbGroup.call(context, this.rules[i]) === false; } } + else if (this.rules[i] instanceof Section) { + if (cbSection !== undefined) { + stop = cbSection.call(context, this.rules[i]) === false; + } + } else { stop = cbRule.call(context, this.rules[i]) === false; } @@ -406,6 +415,8 @@ Group.prototype.contains = function(node, deep) { return true; }, function(group) { return !group.contains(node, true); + }, function(section) { + return !section.group.contains(node, true); }); } }; diff --git a/src/public.js b/src/public.js index 9c503043..431ab2b7 100644 --- a/src/public.js +++ b/src/public.js @@ -109,6 +109,13 @@ QueryBuilder.prototype.validate = function() { else { errors++; } + }, function(section) { + if (parse(section.group)) { + done++; + } + else { + errors++; + } }); if (errors > 0) { @@ -190,7 +197,14 @@ QueryBuilder.prototype.getRules = function(options) { }, function(model) { groupData.rules.push(parse(model)); - }, this); + }, function(model) { + var rule = { + section: model.id, + exists: model.exists, + }; + rule.group = parse(model.group); + groupData.rules.push(rule); + }); return self.change('groupToJson', groupData, group); From e7e09dfbde37b7ee487c0e96c81a561cd1ef8ab0 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Thu, 20 Oct 2016 14:10:46 -0400 Subject: [PATCH 15/44] When switching section types, add an empty rule to the group after we clear it out --- src/core.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/core.js b/src/core.js index cf0a7f70..4f1cba89 100644 --- a/src/core.js +++ b/src/core.js @@ -542,8 +542,6 @@ QueryBuilder.prototype.refreshGroupsConditions = function() { }(this.model.root)); }; -//--section - /** * Add a new section * @param parent {Group} @@ -650,13 +648,12 @@ QueryBuilder.prototype.refreshSection = function(model) { }); if (!ok) { model.empty(); + this.addRule(model.group); } this.trigger('afterRefreshSection', model); }; -//--section - /** * Add a new rule * @param parent {Group} From a771cf4016692ba5ec32752c3c3699b69edb62b3 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Mon, 24 Oct 2016 09:19:33 -0400 Subject: [PATCH 16/44] Implemented allow_sections and section flags --- src/core.js | 75 ++++++++++++++++++++++++++++++++++--------------- src/defaults.js | 3 +- src/template.js | 2 +- 3 files changed, 56 insertions(+), 24 deletions(-) diff --git a/src/core.js b/src/core.js index 4f1cba89..d216a696 100644 --- a/src/core.js +++ b/src/core.js @@ -13,7 +13,6 @@ QueryBuilder.prototype.init = function($el, options) { group_id: 0, rule_id: 0, generated_id: false, - has_sections: false, has_optgroup: false, has_operator_oprgroup: false, id: null, @@ -63,9 +62,11 @@ QueryBuilder.prototype.init = function($el, options) { this.$el.addClass('query-builder form-inline'); this.filters = this.checkFilters(this.filters); - for (var k in this.sections) { - if (this.sections.hasOwnProperty(k)) { - this.sections[k].filters = this.checkFilters(this.sections[k].filters); + if (this.settings.allow_sections) { + for (var k in this.sections) { + if (this.sections.hasOwnProperty(k)) { + this.sections[k].filters = this.checkFilters(this.sections[k].filters); + } } } this.operators = this.checkOperators(this.operators); @@ -254,23 +255,6 @@ QueryBuilder.prototype.bindEvents = function() { } }); - // 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.id = sid; - self.refreshSection(model); - }); - // rule filter change this.$el.on('change.queryBuilder', Selectors.rule_filter, function() { var $rule = $(this).closest(Selectors.rule_container); @@ -310,7 +294,24 @@ QueryBuilder.prototype.bindEvents = function() { }); } - if (this.settings.allow_sections !== 0) { + if (this.settings.allow_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.id = sid; + self.refreshSection(model); + }); + // add section button this.$el.on('click.queryBuilder', Selectors.add_section, function() { var $section = $(this).closest(Selectors.group_container); @@ -398,6 +399,9 @@ QueryBuilder.prototype.bindEvents = function() { case 'exists': self.updateSectionExistsFlag(node); break; + case 'flags': + self.applySectionFlags(node); + break; } } } @@ -913,6 +917,33 @@ QueryBuilder.prototype.applyGroupFlags = function(group) { this.trigger('afterApplyGroupFlags', group); }; +/** + * Change section properties depending on flags. + * @param section {Section} + */ +QueryBuilder.prototype.applySectionFlags = function(section) { + var flags = section.flags; + + if (flags.exists_readonly) { + section.$el.find('>' + Selectors.section_exists_flag).prop('disabled', true) + .parent().addClass('readonly'); + } + if (flags.no_add_rule) { + section.$el.find(Selectors.add_rule).remove(); + } + if (flags.no_add_group) { + section.$el.find(Selectors.add_group).remove(); + } + if (flags.no_delete) { + section.$el.find(Selectors.delete_section).remove(); + } + this.trigger('afterApplySectionFlags', section); + + if (section.group) { + this.applyGroupFlags(section.group); + } +}; + /** * Clear all errors markers * @param node {Node,optional} default is root Group diff --git a/src/defaults.js b/src/defaults.js index f495a2db..1991b532 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -119,11 +119,12 @@ QueryBuilder.DEFAULTS = { sort_filters: false, display_errors: true, - allow_sections: -1, allow_groups: -1, allow_empty: false, conditions: ['AND', 'OR'], default_condition: 'AND', + allow_sections: false, + default_section: null, exist_options: ['EXISTS', 'DOES NOT EXIST'], default_exists: 'EXISTS', inputs_separator: ' , ', diff --git a/src/template.js b/src/template.js index 56d32fd4..b5e87599 100644 --- a/src/template.js +++ b/src/template.js @@ -10,7 +10,7 @@ QueryBuilder.templates.group = '\ {{= it.lang.add_group }} \ \ {{?}} \ - {{? !it.in_section }} \ + {{? it.settings.allow_sections && !it.in_section }} \ \ From d9959dd99ce3e6248a599f0b5ffe316948db05ae Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Mon, 24 Oct 2016 09:19:59 -0400 Subject: [PATCH 17/44] Set allow_sections in the example --- examples/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/index.html b/examples/index.html index 9a098689..c714a377 100644 --- a/examples/index.html +++ b/examples/index.html @@ -120,6 +120,7 @@

    Output

    var options = { allow_empty: true, + allow_sections: true, //default_filter: 'name', sort_filters: true, @@ -526,7 +527,6 @@

    Output

    } }] }; -console.log(options); // init $('#builder').queryBuilder(options); From 543b87becbd78b5c94695d9990679f679061c78f Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Mon, 24 Oct 2016 12:31:44 -0400 Subject: [PATCH 18/44] Unless we have a default section id, create the section with no group and add one when an id is selected --- src/core.js | 37 ++++++++++++++++++++++++------------- src/defaults.js | 1 + src/model.js | 8 ++++---- src/template.js | 8 +++----- 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/core.js b/src/core.js index d216a696..1557e1bd 100644 --- a/src/core.js +++ b/src/core.js @@ -579,12 +579,9 @@ QueryBuilder.prototype.addSection = function(parent, addRule, data, flags) { if (this.settings.default_section) { model.id = this.settings.default_section; - } else { - model.id = this.sections[0].id; + this.addGroup(model, true, data, flags); } - this.addGroup(model, true, data, flags); - return model; }; @@ -642,17 +639,31 @@ QueryBuilder.prototype.createSectionTypes = function(section) { */ QueryBuilder.prototype.refreshSection = function(model) { - // Clear out the section if there's any rule that don't belong - var ok = true; - model.$el.find(Selectors.rule_container).each(function() { - var rule = Model($(this)); - if (rule.section_id != model.id) { - ok = false; + if (model.id) { + // Clear out the section if there's any group or rule that don't belong + var ok = true; + model.$el.find(Selectors.group_container).each(function() { + var group = Model($(this)); + if (group.section_id != model.id) { + ok = false; + } + }); + if (ok) { + model.$el.find(Selectors.rule_container).each(function() { + var rule = Model($(this)); + if (rule.section_id != model.id) { + ok = false; + } + }); } - }); - if (!ok) { + if (!ok) { + model.empty(); + } + if (!model.group) { + var group = this.addGroup(model, true); + } + } else { model.empty(); - this.addRule(model.group); } this.trigger('afterRefreshSection', model); diff --git a/src/defaults.js b/src/defaults.js index 1991b532..0288cfd4 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -130,6 +130,7 @@ QueryBuilder.DEFAULTS = { inputs_separator: ' , ', select_placeholder: '------', display_empty_filter: true, + display_empty_stype_filter: true, default_filter: null, optgroups: {}, diff --git a/src/model.js b/src/model.js index d01edb9c..b07db08f 100644 --- a/src/model.js +++ b/src/model.js @@ -147,7 +147,7 @@ Node.prototype.getPos = function() { Node.prototype.drop = function() { var model = this.model; - if (!this.isRoot()) { + if (!this.isRoot() && this.parent instanceof Group) { this.parent._removeNode(this); } @@ -490,15 +490,15 @@ Section.prototype.setGroup = function($el) { * Clear out all rules */ Section.prototype.empty = function() { - this.group.section_id = this.id; - this.group.empty(); + this.group.drop(); + this.group = null; }; /** * Delete self */ Section.prototype.drop = function() { - this.group.empty(); + this.group.drop(); Node.prototype.drop.call(this); }; diff --git a/src/template.js b/src/template.js index b5e87599..6c128d80 100644 --- a/src/template.js +++ b/src/template.js @@ -41,11 +41,9 @@ QueryBuilder.templates.section = '\
    \
    \
    \ - {{? it.level>1 }} \ - \ - {{?}} \ + \
    \
    \ {{~ it.exist_options: option }} \ From 3943f67b692577d22784d41158889dd6744b9ad5 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Mon, 24 Oct 2016 16:31:52 -0400 Subject: [PATCH 19/44] Fixed a merge error --- src/core.js | 4 ++-- src/model.js | 56 +--------------------------------------------------- 2 files changed, 3 insertions(+), 57 deletions(-) diff --git a/src/core.js b/src/core.js index 1557e1bd..d8a4e71e 100644 --- a/src/core.js +++ b/src/core.js @@ -314,8 +314,8 @@ QueryBuilder.prototype.bindEvents = function() { // add section button this.$el.on('click.queryBuilder', Selectors.add_section, function() { - var $section = $(this).closest(Selectors.group_container); - self.addSection(Model($section)); + var $group = $(this).closest(Selectors.group_container); + self.addSection(Model($group)); }); // delete section button diff --git a/src/model.js b/src/model.js index 671624ff..4c96f5d0 100644 --- a/src/model.js +++ b/src/model.js @@ -469,61 +469,7 @@ var Section = function(parent, $el) { Section.prototype = Object.create(Node.prototype); Section.prototype.constructor = Section; -defineModelProperties(Section, ['exists']); - -/** - * Set the root group of the section by jQuery element - * @param {jQuery} - * @return {Group} the new root group - */ -Section.prototype.setGroup = function($el) { - this.group = new Group(this, $el); - this.group.parent = this; - this.group.section_id = this.id; - if (this.model !== null) { - this.model.trigger('set', this.group); - } - return this.group; -}; - -/** - * Clear out all rules - */ -Section.prototype.empty = function() { - this.group.drop(); - this.group = null; -}; - -/** - * Delete self - */ -Section.prototype.drop = function() { - this.group.drop(); - Node.prototype.drop.call(this); -}; - -// Section CLASS -// =============================== -/** - * @param {Section} - * @param {jQuery} - */ -var Section = function(parent, $el) { - if (!(this instanceof Section)) { - return new Section(parent, $el); - } - - Node.call(this, parent, $el); - - this.group = null; - this.__.id = null; - this.__.exists = null; -}; - -Section.prototype = Object.create(Node.prototype); -Section.prototype.constructor = Section; - -defineModelProperties(Section, ['exists']); +Model.defineModelProperties(Section, ['exists']); /** * Set the root group of the section by jQuery element From 3dfdf2929ec150ce30a93a81abcf505893bbc41a Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Mon, 24 Oct 2016 16:37:25 -0400 Subject: [PATCH 20/44] Fixed linting and style errors --- src/core.js | 28 +++++++++++++++++----------- src/data.js | 8 +++++--- src/public.js | 2 +- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/core.js b/src/core.js index d8a4e71e..0a6812f8 100644 --- a/src/core.js +++ b/src/core.js @@ -307,7 +307,7 @@ QueryBuilder.prototype.bindEvents = function() { this.$el.on('change.queryBuilder', Selectors.rule_stype, function() { var sid = $(this).val(); var $section = $(this).closest(Selectors.section_container); - var model = Model($section) + var model = Model($section); model.id = sid; self.refreshSection(model); }); @@ -459,10 +459,12 @@ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) { var group_id = this.nextGroupId(); var $group = $(this.getGroupTemplate(group_id, level, parent instanceof Section || parent.section_id ? true : false)); + var model = null; if (parent instanceof Section) { - var model = parent.setGroup($group); - } else { - var model = parent.addGroup($group); + model = parent.setGroup($group); + } + else { + model = parent.addGroup($group); } model.data = data; @@ -500,7 +502,7 @@ QueryBuilder.prototype.deleteGroup = function(group) { del&= this.deleteRule(rule); }, function(group) { del&= this.deleteGroup(group); - }, function (section) { + }, function(section) { del&= this.deleteSection(section); }, this); @@ -662,7 +664,8 @@ QueryBuilder.prototype.refreshSection = function(model) { if (!model.group) { var group = this.addGroup(model, true); } - } else { + } + else { model.empty(); } @@ -733,15 +736,18 @@ QueryBuilder.prototype.deleteRule = function(rule) { * @param rule {Rule} */ QueryBuilder.prototype.createRuleFilters = function(rule) { + var filters = []; if (rule.section_id) { var section = this.getSectionById(rule.section_id); if (section) { - var filters = this.change('getRuleFilters', section.filters, rule); - } else { - var filters = this.change('getRuleFilters', [], rule); + filters = this.change('getRuleFilters', section.filters, rule); } - } else { - var filters = this.change('getRuleFilters', this.filters, rule); + else { + filters = this.change('getRuleFilters', [], rule); + } + } + else { + filters = this.change('getRuleFilters', this.filters, rule); } var $filterSelect = $(this.getRuleFilterSelect(rule, filters)); diff --git a/src/data.js b/src/data.js index ef544cc9..b5feeead 100644 --- a/src/data.js +++ b/src/data.js @@ -289,11 +289,13 @@ QueryBuilder.prototype.getFilterById = function(id, sectionId) { return null; } + var filters = []; if (sectionId) { var s = this.getSectionById(sectionId); - var filters = s ? s.filters : []; - } else { - var filters = this.filters; + filters = s ? s.filters : []; + } + else { + filters = this.filters; } for (var i = 0, l = filters.length; i < l; i++) { if (filters[i].id == id) { diff --git a/src/public.js b/src/public.js index 431ab2b7..f3c1e6ff 100644 --- a/src/public.js +++ b/src/public.js @@ -200,7 +200,7 @@ QueryBuilder.prototype.getRules = function(options) { }, function(model) { var rule = { section: model.id, - exists: model.exists, + exists: model.exists }; rule.group = parse(model.group); groupData.rules.push(rule); From 1a3fa8733ab570d2f2b073bb86a105cc2cc08c99 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Wed, 26 Oct 2016 11:51:08 -0400 Subject: [PATCH 21/44] You can now set section data from JSON --- examples/index.html | 23 ++++++++++++++++--- src/core.js | 54 +++++++++++++++++++++++++++++++-------------- src/data.js | 49 ++++++++++++++++++++++++++++++++++++++-- src/model.js | 12 +++++----- src/public.js | 27 ++++++++++++++++++++--- src/template.js | 8 +++---- 6 files changed, 139 insertions(+), 34 deletions(-) diff --git a/examples/index.html b/examples/index.html index c714a377..c35aca98 100644 --- a/examples/index.html +++ b/examples/index.html @@ -567,8 +567,7 @@

    Output

    }); // set rules -$('.set').on('click', function() { - $('#builder').queryBuilder('setRules', { +var json_rules = { condition: 'AND', flags: { condition_readonly: true @@ -584,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', @@ -610,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 0a6812f8..5e5e9ff2 100644 --- a/src/core.js +++ b/src/core.js @@ -259,7 +259,7 @@ QueryBuilder.prototype.bindEvents = function() { this.$el.on('change.queryBuilder', Selectors.rule_filter, function() { var $rule = $(this).closest(Selectors.rule_container); var m = Model($rule); - m.filter = self.getFilterById($(this).val(), m.section_id); + m.filter = self.getFilterById($(this).val(), m.section_type_id); }); // rule operator change @@ -396,6 +396,9 @@ QueryBuilder.prototype.bindEvents = function() { } else if (node instanceof Section) { switch (field) { + case 'type_id': + self.updateSectionTypeId(node); + break; case 'exists': self.updateSectionExistsFlag(node); break; @@ -458,7 +461,9 @@ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) { } var group_id = this.nextGroupId(); - var $group = $(this.getGroupTemplate(group_id, level, parent instanceof Section || parent.section_id ? true : false)); + var section_root = parent instanceof Section; + var in_section = section_root || parent.section_type_id; + var $group = $(this.getGroupTemplate(group_id, level, in_section, section_root)); var model = null; if (parent instanceof Section) { model = parent.setGroup($group); @@ -487,7 +492,7 @@ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) { * @return {boolean} true if the group has been deleted */ QueryBuilder.prototype.deleteGroup = function(group) { - if (group.isRoot()) { + if (group.isRoot() || group.parent instanceof Section) { return false; } @@ -579,9 +584,9 @@ QueryBuilder.prototype.addSection = function(parent, addRule, data, flags) { this.createSectionTypes(model); - if (this.settings.default_section) { - model.id = this.settings.default_section; - this.addGroup(model, true, data, flags); + if (addRule && this.settings.default_section) { + model.type_id = this.settings.default_section; + this.addGroup(model, true); } return model; @@ -608,6 +613,15 @@ QueryBuilder.prototype.deleteSection = function(section) { 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'); + this.trigger('afterUpdateSectionTypeId', section); +}; + /** * Changes the exists setting of a section * @param section {Section} @@ -646,14 +660,14 @@ QueryBuilder.prototype.refreshSection = function(model) { var ok = true; model.$el.find(Selectors.group_container).each(function() { var group = Model($(this)); - if (group.section_id != model.id) { + if (group.section_type_id != model.type_id) { ok = false; } }); if (ok) { model.$el.find(Selectors.rule_container).each(function() { var rule = Model($(this)); - if (rule.section_id != model.id) { + if (rule.section_type_id != model.type_id) { ok = false; } }); @@ -699,11 +713,19 @@ QueryBuilder.prototype.addRule = function(parent, data, flags) { this.createRuleFilters(model); - if (this.settings.default_filter || !this.settings.display_empty_filter) { - model.filter = this.change('getDefaultFilter', - this.getFilterById(this.settings.default_filter || this.filters[0].id), - model - ); + if (model.section_type_id) { + var s = this.getSectionById(model.section_type_id); + if (s.default_filter || !this.settings.display_empty_filter) { + var d = s.default_filter || s.filters[0].id; + model.filter = this.change('getDefaultFilter', this.getFilterById(d, s.id), model); + } + } else { + if (this.settings.default_filter || !this.settings.display_empty_filter) { + model.filter = this.change('getDefaultFilter', + this.getFilterById(this.settings.default_filter || this.filters[0].id), + model + ); + } } return model; @@ -737,8 +759,8 @@ QueryBuilder.prototype.deleteRule = function(rule) { */ QueryBuilder.prototype.createRuleFilters = function(rule) { var filters = []; - if (rule.section_id) { - var section = this.getSectionById(rule.section_id); + if (rule.section_type_id) { + var section = this.getSectionById(rule.section_type_id); if (section) { filters = this.change('getRuleFilters', section.filters, rule); } @@ -767,7 +789,7 @@ QueryBuilder.prototype.createRuleOperators = function(rule) { return; } - var operators = this.getOperators(rule.filter); + var operators = this.getOperators(rule.filter, rule.section_type_id); var $operatorSelect = $(this.getRuleOperatorSelect(rule, operators)); $operatorContainer.html($operatorSelect); diff --git a/src/data.js b/src/data.js index b5feeead..47eb0003 100644 --- a/src/data.js +++ b/src/data.js @@ -245,9 +245,9 @@ QueryBuilder.prototype.getSectionById = function(id) { * @param filter {string|object} (filter id name or filter object) * @return {object[]} */ -QueryBuilder.prototype.getOperators = function(filter) { +QueryBuilder.prototype.getOperators = function(filter, sectionId) { if (typeof filter == 'string') { - filter = this.getFilterById(filter); + filter = this.getFilterById(filter, sectionId); } var result = []; @@ -536,6 +536,51 @@ QueryBuilder.prototype.getGroupFlags = function(flags, all) { } }; +/** + * Clean section flags. + * @param section {object} + * @return {object} + */ +QueryBuilder.prototype.parseSectionFlags = function(section) { + var flags = $.extend({}, this.settings.default_section_flags); + + if (section.readonly) { + $.extend(flags, { + exists_readonly: true, + no_add_rule: true, + no_add_group: true, + no_delete: true + }); + } + + if (section.flags) { + $.extend(flags, section.flags); + } + + return this.change('parseSectionFlags', flags, section); +}; + +/** + * Get a copy of flags of a section. + * @param {object} flags + * @param {boolean} all - true to return all flags, false to return only changes from default + * @returns {object} + */ +QueryBuilder.prototype.getSectionFlags = function(flags, all) { + if (all) { + return $.extend({}, flags); + } + else { + var ret = {}; + $.each(this.settings.default_section_flags, function(key, value) { + if (flags[key] !== value) { + ret[key] = flags[key]; + } + }); + return ret; + } +}; + /** * Translate a label * @param label {string|object} diff --git a/src/model.js b/src/model.js index 4c96f5d0..7e286641 100644 --- a/src/model.js +++ b/src/model.js @@ -232,7 +232,7 @@ var Group = function(parent, $el) { Node.call(this, parent, $el); this.rules = []; - this.section_id = null; + this.section_type_id = null; this.__.condition = null; }; @@ -285,7 +285,7 @@ Group.prototype._appendNode = function(node, index, trigger) { this.rules.splice(index, 0, node); node.parent = this; if (!(node instanceof Section)) { - node.section_id = this.section_id; + node.section_type_id = this.section_type_id; } if (trigger && this.model !== null) { @@ -435,7 +435,7 @@ var Rule = function(parent, $el) { Node.call(this, parent, $el); - this.section_id = null; + this.section_type_id = null; this.__.filter = null; this.__.operator = null; @@ -462,14 +462,14 @@ var Section = function(parent, $el) { Node.call(this, parent, $el); this.group = null; - this.__.id = null; + this.__.type_id = null; this.__.exists = null; }; Section.prototype = Object.create(Node.prototype); Section.prototype.constructor = Section; -Model.defineModelProperties(Section, ['exists']); +Model.defineModelProperties(Section, ['type_id', 'exists']); /** * Set the root group of the section by jQuery element @@ -479,7 +479,7 @@ Model.defineModelProperties(Section, ['exists']); Section.prototype.setGroup = function($el) { this.group = new Group(this, $el); this.group.parent = this; - this.group.section_id = this.id; + this.group.section_type_id = this.type_id; if (this.model !== null) { this.model.trigger('set', this.group); } diff --git a/src/public.js b/src/public.js index f3c1e6ff..38b9c734 100644 --- a/src/public.js +++ b/src/public.js @@ -121,7 +121,7 @@ QueryBuilder.prototype.validate = function() { if (errors > 0) { return false; } - else if (done === 0 && (!self.settings.allow_empty || !group.isRoot())) { + else if (done === 0 && (!self.settings.allow_empty || !group.isRoot() || !group.parent instanceof Section)) { self.triggerValidationError(group, 'empty_group', null); return false; } @@ -199,7 +199,7 @@ QueryBuilder.prototype.getRules = function(options) { groupData.rules.push(parse(model)); }, function(model) { var rule = { - section: model.id, + section: model.type_id, exists: model.exists }; rule.group = parse(model.group); @@ -268,6 +268,27 @@ QueryBuilder.prototype.setRules = function(data) { add(item, model); } } + else if (item.section !== undefined) { + if (!self.settings.allow_sections) { + self.reset(); + Utils.error('RulesParse', 'No sections are allowed'); + } + else { + var section = self.addSection(group, false, item.data, self.parseSectionFlags(item)); + if (section === null) { + return; + } + section.type_id = item.section; + section.exists = item.exists; + if (item.group !== undefined) { + var gmodel = self.addGroup(section, false, item.group.data, self.parseGroupFlags(item.group)); + if (gmodel === null) { + return; + } + add(item.group, gmodel); + } + } + } else { if (!item.empty) { if (item.id === undefined) { @@ -284,7 +305,7 @@ QueryBuilder.prototype.setRules = function(data) { } if (!item.empty) { - model.filter = self.getFilterById(item.id); + model.filter = self.getFilterById(item.id, group.section_type_id); model.operator = self.getOperatorByType(item.operator); if (model.operator.nb_inputs !== 0 && item.value !== undefined) { diff --git a/src/template.js b/src/template.js index 6c128d80..d33bf46a 100644 --- a/src/template.js +++ b/src/template.js @@ -15,7 +15,7 @@ QueryBuilder.templates.group = '\ {{= it.lang.add_section }} \ \ {{?}} \ - {{? it.level>1 }} \ + {{? it.level>1 && !it.section_root }} \ \ @@ -133,12 +133,13 @@ QueryBuilder.templates.operatorSelect = '\ * @param in_section {bool} * @return {string} */ -QueryBuilder.prototype.getGroupTemplate = function(group_id, level, in_section) { +QueryBuilder.prototype.getGroupTemplate = function(group_id, level, in_section, section_root) { var h = this.templates.group({ builder: this, group_id: group_id, level: level, in_section: in_section, + section_root: section_root, conditions: this.settings.conditions, icons: this.icons, lang: this.lang, @@ -167,9 +168,8 @@ QueryBuilder.prototype.getRuleTemplate = function(rule_id) { /** * Returns section HTML - * @param group_id {string} + * @param section_id {string} * @param level {int} - * @param in_section {bool} * @return {string} */ QueryBuilder.prototype.getSectionTemplate = function(section_id, level) { From ff606d438bb9963ef7a27fc9395b9f56d5ef14eb Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Wed, 26 Oct 2016 11:53:19 -0400 Subject: [PATCH 22/44] Fixed linting/style warnings --- src/core.js | 3 ++- src/public.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core.js b/src/core.js index 5e5e9ff2..d8f61cde 100644 --- a/src/core.js +++ b/src/core.js @@ -719,7 +719,8 @@ QueryBuilder.prototype.addRule = function(parent, data, flags) { var d = s.default_filter || s.filters[0].id; model.filter = this.change('getDefaultFilter', this.getFilterById(d, s.id), model); } - } else { + } + else { if (this.settings.default_filter || !this.settings.display_empty_filter) { model.filter = this.change('getDefaultFilter', this.getFilterById(this.settings.default_filter || this.filters[0].id), diff --git a/src/public.js b/src/public.js index 38b9c734..dd31fa39 100644 --- a/src/public.js +++ b/src/public.js @@ -121,7 +121,7 @@ QueryBuilder.prototype.validate = function() { if (errors > 0) { return false; } - else if (done === 0 && (!self.settings.allow_empty || !group.isRoot() || !group.parent instanceof Section)) { + else if (done === 0 && (!self.settings.allow_empty || !group.isRoot() || !(group.parent instanceof Section))) { self.triggerValidationError(group, 'empty_group', null); return false; } From 1c16555afd49721dbe933645b2ef5b4cee05d97d Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Sat, 29 Oct 2016 09:20:35 -0400 Subject: [PATCH 23/44] More bug fixes for sections --- src/core.js | 16 ++++++++++------ src/i18n/en.json | 1 + src/model.js | 15 ++++++++++++--- src/public.js | 14 ++++++++++++-- 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/core.js b/src/core.js index d8f61cde..e92fb702 100644 --- a/src/core.js +++ b/src/core.js @@ -308,7 +308,7 @@ QueryBuilder.prototype.bindEvents = function() { var sid = $(this).val(); var $section = $(this).closest(Selectors.section_container); var model = Model($section); - model.id = sid; + model.type_id = sid; self.refreshSection(model); }); @@ -492,7 +492,7 @@ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) { * @return {boolean} true if the group has been deleted */ QueryBuilder.prototype.deleteGroup = function(group) { - if (group.isRoot() || group.parent instanceof Section) { + if (group.isRoot()) { return false; } @@ -603,8 +603,10 @@ QueryBuilder.prototype.deleteSection = function(section) { return false; } - if (!this.deleteGroup(section.group)) { - return false; + if (section.group) { + if (!this.deleteGroup(section.group)) { + return false; + } } section.drop(); @@ -655,7 +657,7 @@ QueryBuilder.prototype.createSectionTypes = function(section) { */ QueryBuilder.prototype.refreshSection = function(model) { - if (model.id) { + if (model.type_id && model.type_id != '-1') { // Clear out the section if there's any group or rule that don't belong var ok = true; model.$el.find(Selectors.group_container).each(function() { @@ -1003,7 +1005,9 @@ QueryBuilder.prototype.clearErrors = function(node) { }, function(group) { this.clearErrors(group); }, function(section) { - this.clearErrors(section.group); + if (section.group) { + this.clearErrors(section.group); + } }, this); } }; diff --git a/src/i18n/en.json b/src/i18n/en.json index 60c0d07e..ff0e3260 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -44,6 +44,7 @@ "errors": { "no_filter": "No filter selected", + "no_type": "No section type selected", "empty_group": "The group is empty", "radio_empty": "No value selected", "checkbox_empty": "No value selected", diff --git a/src/model.js b/src/model.js index 7e286641..0a325e40 100644 --- a/src/model.js +++ b/src/model.js @@ -416,7 +416,12 @@ Group.prototype.contains = function(node, deep) { }, function(group) { return !group.contains(node, true); }, function(section) { - return !section.group.contains(node, true); + if (section.group) { + return !section.group.contains(node, true); + } + else { + return true; + } }); } }; @@ -490,7 +495,9 @@ Section.prototype.setGroup = function($el) { * Clear out all rules */ Section.prototype.empty = function() { - this.group.drop(); + if (this.group) { + this.group.drop(); + } this.group = null; }; @@ -498,7 +505,9 @@ Section.prototype.empty = function() { * Delete self */ Section.prototype.drop = function() { - this.group.drop(); + if (this.group) { + this.group.drop(); + } Node.prototype.drop.call(this); }; diff --git a/src/public.js b/src/public.js index dd31fa39..611f3364 100644 --- a/src/public.js +++ b/src/public.js @@ -110,6 +110,11 @@ QueryBuilder.prototype.validate = function() { errors++; } }, function(section) { + if (!section.type_id || section.type_id == '-1' || !section.group) { + self.triggerValidationError(section, 'no_type', null); + errors++; + return; + } if (parse(section.group)) { done++; } @@ -121,7 +126,7 @@ QueryBuilder.prototype.validate = function() { if (errors > 0) { return false; } - else if (done === 0 && (!self.settings.allow_empty || !group.isRoot() || !(group.parent instanceof Section))) { + else if (done === 0 && (!self.settings.allow_empty || !group.isRoot())) { self.triggerValidationError(group, 'empty_group', null); return false; } @@ -202,7 +207,12 @@ QueryBuilder.prototype.getRules = function(options) { section: model.type_id, exists: model.exists }; - rule.group = parse(model.group); + if (model.group) { + rule.group = parse(model.group); + } + else { + rule.group = null; + } groupData.rules.push(rule); }); From 6df52ff3e8b4fefa771bf9a1dfa23bb2289a7657 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Mon, 31 Oct 2016 23:05:25 -0400 Subject: [PATCH 24/44] Invert plugin passes tests and works for sections --- src/plugins/invert/plugin.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/plugins/invert/plugin.js b/src/plugins/invert/plugin.js index a4818e3e..06179dd2 100644 --- a/src/plugins/invert/plugin.js +++ b/src/plugins/invert/plugin.js @@ -30,6 +30,11 @@ QueryBuilder.defaults({ conditionOpposites: { 'AND': 'OR', 'OR': 'AND' + }, + + existsOpposites: { + 'EXISTS': 'DOES NOT EXIST', + 'DOES NOT EXIST': 'EXISTS' } }); @@ -115,6 +120,8 @@ QueryBuilder.extend({ } }, function(group) { this.invert(group, tempOpts); + }, function(section) { + this.invert(section, tempOpts); }, this); } } @@ -133,6 +140,21 @@ QueryBuilder.extend({ } } } + else if (node instanceof Section) { + // invert exists setting + if (this.settings.existsOpposites[node.exists]) { + node.exists = this.settings.existsOpposites[node.exists]; + } + else if (!options.silent_fail) { + Utils.error('InvertExists', 'Unknown inverse of exists "{0}"', node.exists); + } + + // recursive call + if (options.recursive && node.group) { + var tempOpts = $.extend({}, options, { trigger: false }); + this.invert(node.group, tempOpts); + } + } if (options.trigger) { this.trigger('afterInvert', node, options); From 0ac032668787453a3f71e4ac6ab4b75e47b155d7 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Sun, 6 Nov 2016 15:03:00 -0500 Subject: [PATCH 25/44] Broke section checking into its own method and added section detail to filter error messages --- src/core.js | 57 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/src/core.js b/src/core.js index e92fb702..553c5bf9 100644 --- a/src/core.js +++ b/src/core.js @@ -62,13 +62,7 @@ QueryBuilder.prototype.init = function($el, options) { this.$el.addClass('query-builder form-inline'); this.filters = this.checkFilters(this.filters); - if (this.settings.allow_sections) { - for (var k in this.sections) { - if (this.sections.hasOwnProperty(k)) { - this.sections[k].filters = this.checkFilters(this.sections[k].filters); - } - } - } + this.sections = this.checkSections(this.sections); this.operators = this.checkOperators(this.operators); this.bindEvents(); @@ -89,19 +83,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); @@ -109,20 +104,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); } }); } @@ -149,7 +144,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; @@ -160,7 +155,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); } }); } @@ -187,6 +182,36 @@ 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 = []; + + if (!sections || sections.length === 0) { + Utils.error('Config', 'Missing sections list'); + } + + 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 From b7fc5807585b2f7486c7ad32b9639fbeed0366d8 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Mon, 7 Nov 2016 16:49:19 -0500 Subject: [PATCH 26/44] Fixed a logical error in setRules --- src/public.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/public.js b/src/public.js index 611f3364..6a6fff19 100644 --- a/src/public.js +++ b/src/public.js @@ -264,21 +264,7 @@ QueryBuilder.prototype.setRules = function(data) { data.rules.forEach(function(item) { var model; - if (item.rules !== undefined) { - if (self.settings.allow_groups !== -1 && self.settings.allow_groups < group.level) { - self.reset(); - Utils.error('RulesParse', 'No more than {0} groups are allowed', self.settings.allow_groups); - } - else { - model = self.addGroup(group, false, item.data, self.parseGroupFlags(item)); - if (model === null) { - return; - } - - add(item, model); - } - } - else if (item.section !== undefined) { + if (item.section !== undefined) { if (!self.settings.allow_sections) { self.reset(); Utils.error('RulesParse', 'No sections are allowed'); @@ -299,6 +285,20 @@ QueryBuilder.prototype.setRules = function(data) { } } } + else if (item.rules !== undefined) { + if (self.settings.allow_groups !== -1 && self.settings.allow_groups < group.level) { + self.reset(); + Utils.error('RulesParse', 'No more than {0} groups are allowed', self.settings.allow_groups); + } + else { + model = self.addGroup(group, false, item.data, self.parseGroupFlags(item)); + if (model === null) { + return; + } + + add(item, model); + } + } else { if (!item.empty) { if (item.id === undefined) { From 8e6b42ef618bd59c9ea7394825681cf220480017 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Mon, 7 Nov 2016 17:57:40 -0500 Subject: [PATCH 27/44] First pass at sections test suite (only currently passing tests for now) --- tests/common.js | 78 ++++++++++++++ tests/index.html | 1 + tests/sections.module.js | 228 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 307 insertions(+) create mode 100644 tests/sections.module.js diff --git a/tests/common.js b/tests/common.js index 26111ecb..3712ecc9 100644 --- a/tests/common.js +++ b/tests/common.js @@ -294,6 +294,58 @@ var basic_filters = [{ } }]; +var basic_sections = [{ + id: 'partner', + label: 'Partner', + filters: [{ + id: 'name', + label: 'Partner Name', + type: 'string' + }, { + id: 'status', + label: 'Partnership Status', + type: 'string', + input: 'select', + multiple: true, + values: { + 'ac': 'Active', + 'in': 'Inactive', + 'tr': 'Terminated' + } + }] +}, { + id: 'related', + label: 'Releated Products', + filters: [{ + id: 'name', + label: 'Name', + type: 'string' + }, { + id: 'price', + label: 'Price', + type: 'double', + validation: { + min: 0, + step: 0.01 + } + }, { + id: 'category', + label: 'Category', + type: 'string', + input: 'select', + multiple: true, + values: { + 'bk': 'Books', + 'mo': 'Movies', + 'mu': 'Music', + 'to': 'Tools', + 'go': 'Goodies', + 'cl': 'Clothes' + }, + operators: ['in', 'not_in', 'equal', 'not_equal', 'is_null', 'is_not_null'] + }] +}]; + var basic_rules = { condition: 'AND', rules: [{ @@ -321,3 +373,29 @@ var basic_rules = { }] }] }; + +var section_rules = { + condition: 'AND', + rules: [{ + id: 'price', + field: 'price', + operator: 'less', + value: 10.25 + }, { + section: 'partner', + group: { + condition: 'AND', + rules: [{ + id: 'name', + field: 'name', + operator: 'begins_with', + value: 'Best' + }, { + id: 'status', + field: 'status', + operator: 'in', + value: [ 'ac', 'in' ] + }] + } + }] +}; diff --git a/tests/index.html b/tests/index.html index 4c395d25..058ce194 100644 --- a/tests/index.html +++ b/tests/index.html @@ -56,6 +56,7 @@ + diff --git a/tests/sections.module.js b/tests/sections.module.js new file mode 100644 index 00000000..5a33884a --- /dev/null +++ b/tests/sections.module.js @@ -0,0 +1,228 @@ +$(function(){ + var $b = $('#builder'); + + QUnit.module('sections', { + afterEach: function() { + $b.queryBuilder('destroy'); + } + }); + + /** + * Disallow sections + */ + QUnit.test('Disallow sections', function(assert) { + $b.queryBuilder({ + filters: basic_filters, + sections: basic_sections + }); + + assert.equal( + $('#builder_group_0 .rules-group-header .group-actions button[data-add="section"]').length, + 0, + 'Should not have the add section button' + ); + }); + + /** + * Allow sections + */ + QUnit.test('Allow sections', function(assert) { + $b.queryBuilder({ + allow_sections: true, + filters: basic_filters, + sections: basic_sections + }); + + assert.equal( + $('#builder_group_0 .rules-group-header .group-actions button[data-add="section"]').length, + 1, + 'Should have the add section button' + ); + }); + + /** + * Test invalid sections + */ + QUnit.test('Invalid sections', function(assert) { + + assert.initError($b, + {allow_sections: true, + sections: [], + filters: basic_filters}, + /Missing sections list/ + ); + + assert.initError($b, + {allow_sections: true, + sections: [{}], + filters: basic_filters}, + /Missing section 0 id/ + ); + + assert.initError($b, + {allow_sections: true, + sections: [ + {id: 'foo', filters: basic_filters}, + {id: 'foo', filters: basic_filters} + ], + filters: basic_filters}, + /Section "foo" already defined/ + ); + + assert.initError($b, + {allow_sections: true, + sections: [ + {id: 'foo'} + ], + filters: basic_filters}, + /Missing filters list \[section: foo\]/ + ); + + assert.initError($b, + {allow_sections: true, + sections: [ + {id: 'foo', filters: [{}]} + ], + filters: basic_filters}, + /Missing filter 0 id \[section: foo\]/ + ); + + assert.initError($b, + {allow_sections: true, + sections: [ + {id: 'baz', filters: [ + {id: 'foo'}, + {id: 'foo'} + ]} + ], + filters: basic_filters}, + /Filter "foo" already defined \[section: baz\]/ + ); + + assert.initError($b, + {allow_sections: true, + sections: [ + {id: 'baz', filters: [ + {id: 'foo', type: 'bar'} + ]} + ], + filters: basic_filters}, + /Invalid type "bar" \[section: baz\]/ + ); + + assert.initError($b, + {allow_sections: true, + sections: [ + {id: 'baz', filters: [ + {id: 'foo', input: 'bar'} + ]} + ], + filters: basic_filters}, + /Invalid input "bar" \[section: baz\]/ + ); + + assert.initError($b, + {allow_sections: true, + sections: [ + {id: 'baz', filters: [ + {id: 'foo', input: 'radio'} + ]} + ], + filters: basic_filters}, + /Missing filter "foo" values \[section: baz\]/ + ); + + assert.initError($b, + {allow_sections: true, + sections: [ + {id: 'baz', filters: [ + {id:'foo', input: 'select', values:[1,2,3], placeholder: 1, placeholder_value: 1} + ]} + ], + filters: basic_filters}, + /Placeholder of filter "foo" overlaps with one of its values \[section: baz\]/ + ); + + assert.initError($b, + {allow_sections: true, + sections: [ + {id: 'baz', filters: [ + {id: 'foo', operators: ['equal', + {type: 'geo', nb_inputs: 3, multiple: false, apply_to: ['string'] } + ]} + ]} + ], + filters: basic_filters}, + /Filter operators must be global operators types \(string\) \[section: baz\]/ + ); + + }); + + /** + * Test setRules and getRules with sections + */ + QUnit.test('Set/get rules with sections', function(assert) { + $b.queryBuilder({ + filters: basic_filters, + allow_sections: true, + sections: basic_sections, + rules: section_rules + }); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + section_rules, + 'Should return object with rules for sections' + ); + }); + + /** + * Test default section + */ + QUnit.test('Default section', function(assert) { + $b.queryBuilder({ + allow_sections: true, + default_section: 'related', + sections: basic_sections, + filters: basic_filters + }); + + $('#builder_group_0>.rules-group-header>.group-actions [data-add=section]').trigger('click'); + + assert.equal( + $('[name=builder_section_0_section_type] [value="-1"]').length, + 1, + 'Should have the placeholder section type' + ); + + assert.equal( + $('[name=builder_section_0_section_type]').val(), + 'related', + 'Sould have used "related" as default section type' + ); + + $b.queryBuilder('destroy'); + + $b.queryBuilder({ + allow_sections: true, + display_empty_stype_filter: false, + sections: basic_sections, + filters: basic_filters + }); + + $('#builder_group_0>.rules-group-header>.group-actions [data-add=section]').trigger('click'); + + assert.equal( + $('[name=builder_section_0_section_type] [value="-1"]').length, + 0, + 'Should not have the placeholder section type' + ); + + assert.equal( + $('[name=builder_section_0_section_type]').val(), + 'partner', + 'Sould have used the first section as default one' + ); + }); + +}); From d0a8ec6c608f17eb29e03c054189f36c2a641d17 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Tue, 15 Nov 2016 12:17:56 -0500 Subject: [PATCH 28/44] Rounded out section tests and made allow_sections default to true --- src/core.js | 10 +- src/defaults.js | 3 +- src/public.js | 2 +- src/template.js | 2 +- tests/sections.module.js | 543 ++++++++++++++++++++++++++++++++++++--- 5 files changed, 519 insertions(+), 41 deletions(-) diff --git a/src/core.js b/src/core.js index 553c5bf9..f0f16da5 100644 --- a/src/core.js +++ b/src/core.js @@ -65,6 +65,10 @@ QueryBuilder.prototype.init = function($el, options) { 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(); @@ -193,10 +197,6 @@ QueryBuilder.prototype.checkSections = function(sections) { var definedSections = []; - if (!sections || sections.length === 0) { - Utils.error('Config', 'Missing sections list'); - } - sections.forEach(function(section, i) { if (!section.id) { Utils.error('Config', 'Missing section {0} id', i); @@ -319,7 +319,7 @@ QueryBuilder.prototype.bindEvents = function() { }); } - if (this.settings.allow_sections) { + 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')) { diff --git a/src/defaults.js b/src/defaults.js index 0288cfd4..59c244dc 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -123,7 +123,8 @@ QueryBuilder.DEFAULTS = { allow_empty: false, conditions: ['AND', 'OR'], default_condition: 'AND', - allow_sections: false, + allow_sections: true, + has_sections: false, default_section: null, exist_options: ['EXISTS', 'DOES NOT EXIST'], default_exists: 'EXISTS', diff --git a/src/public.js b/src/public.js index 6a6fff19..3a3f0ea3 100644 --- a/src/public.js +++ b/src/public.js @@ -265,7 +265,7 @@ QueryBuilder.prototype.setRules = function(data) { var model; if (item.section !== undefined) { - if (!self.settings.allow_sections) { + if (!self.settings.allow_sections || !self.settings.has_sections) { self.reset(); Utils.error('RulesParse', 'No sections are allowed'); } diff --git a/src/template.js b/src/template.js index d33bf46a..f4e4ceb7 100644 --- a/src/template.js +++ b/src/template.js @@ -10,7 +10,7 @@ QueryBuilder.templates.group = '\ {{= it.lang.add_group }} \ \ {{?}} \ - {{? it.settings.allow_sections && !it.in_section }} \ + {{? it.settings.allow_sections && it.settings.has_sections && !it.in_section }} \ \ diff --git a/tests/sections.module.js b/tests/sections.module.js index 5a33884a..8e83bba7 100644 --- a/tests/sections.module.js +++ b/tests/sections.module.js @@ -13,6 +13,7 @@ $(function(){ QUnit.test('Disallow sections', function(assert) { $b.queryBuilder({ filters: basic_filters, + allow_sections: false, sections: basic_sections }); @@ -28,7 +29,6 @@ $(function(){ */ QUnit.test('Allow sections', function(assert) { $b.queryBuilder({ - allow_sections: true, filters: basic_filters, sections: basic_sections }); @@ -38,30 +38,34 @@ $(function(){ 1, 'Should have the add section button' ); + + $b.queryBuilder('destroy'); + + $b.queryBuilder({ + allow_sections: false, + filters: basic_filters, + sections: basic_sections + }); + + assert.equal( + $('#builder_group_0 .rules-group-header .group-actions button[data-add="section"]').length, + 0, + 'Should not have the add section button' + ); }); /** * Test invalid sections */ QUnit.test('Invalid sections', function(assert) { - - assert.initError($b, - {allow_sections: true, - sections: [], - filters: basic_filters}, - /Missing sections list/ - ); - assert.initError($b, - {allow_sections: true, - sections: [{}], + {sections: [{}], filters: basic_filters}, /Missing section 0 id/ ); assert.initError($b, - {allow_sections: true, - sections: [ + {sections: [ {id: 'foo', filters: basic_filters}, {id: 'foo', filters: basic_filters} ], @@ -70,8 +74,7 @@ $(function(){ ); assert.initError($b, - {allow_sections: true, - sections: [ + {sections: [ {id: 'foo'} ], filters: basic_filters}, @@ -79,8 +82,7 @@ $(function(){ ); assert.initError($b, - {allow_sections: true, - sections: [ + {sections: [ {id: 'foo', filters: [{}]} ], filters: basic_filters}, @@ -88,8 +90,7 @@ $(function(){ ); assert.initError($b, - {allow_sections: true, - sections: [ + {sections: [ {id: 'baz', filters: [ {id: 'foo'}, {id: 'foo'} @@ -100,8 +101,7 @@ $(function(){ ); assert.initError($b, - {allow_sections: true, - sections: [ + {sections: [ {id: 'baz', filters: [ {id: 'foo', type: 'bar'} ]} @@ -111,8 +111,7 @@ $(function(){ ); assert.initError($b, - {allow_sections: true, - sections: [ + {sections: [ {id: 'baz', filters: [ {id: 'foo', input: 'bar'} ]} @@ -122,8 +121,7 @@ $(function(){ ); assert.initError($b, - {allow_sections: true, - sections: [ + {sections: [ {id: 'baz', filters: [ {id: 'foo', input: 'radio'} ]} @@ -133,8 +131,7 @@ $(function(){ ); assert.initError($b, - {allow_sections: true, - sections: [ + {sections: [ {id: 'baz', filters: [ {id:'foo', input: 'select', values:[1,2,3], placeholder: 1, placeholder_value: 1} ]} @@ -144,8 +141,7 @@ $(function(){ ); assert.initError($b, - {allow_sections: true, - sections: [ + {sections: [ {id: 'baz', filters: [ {id: 'foo', operators: ['equal', {type: 'geo', nb_inputs: 3, multiple: false, apply_to: ['string'] } @@ -155,7 +151,6 @@ $(function(){ filters: basic_filters}, /Filter operators must be global operators types \(string\) \[section: baz\]/ ); - }); /** @@ -164,7 +159,6 @@ $(function(){ QUnit.test('Set/get rules with sections', function(assert) { $b.queryBuilder({ filters: basic_filters, - allow_sections: true, sections: basic_sections, rules: section_rules }); @@ -181,7 +175,6 @@ $(function(){ */ QUnit.test('Default section', function(assert) { $b.queryBuilder({ - allow_sections: true, default_section: 'related', sections: basic_sections, filters: basic_filters @@ -204,7 +197,6 @@ $(function(){ $b.queryBuilder('destroy'); $b.queryBuilder({ - allow_sections: true, display_empty_stype_filter: false, sections: basic_sections, filters: basic_filters @@ -225,4 +217,489 @@ $(function(){ ); }); + /** + * Test UI events + */ + QUnit.test('UI events', function(assert) { + $b.queryBuilder({ + sections: basic_sections, + filters: basic_filters + }); + + // set the top-level rule + $('[name=builder_rule_0_filter]').val('name').trigger('change'); + $('[name=builder_rule_0_operator]').val('not_equal').trigger('change'); + $('[name=builder_rule_0_value_0]').val('foo').trigger('change'); + + // add a section + $('#builder_group_0>.rules-group-header>.group-actions [data-add=section]').trigger('click'); + + // set the first rule in the section + $('[name=builder_section_0_section_type]').val('partner').trigger('change'); + $('[name=builder_rule_1_filter]').val('name').trigger('change'); + $('[name=builder_rule_1_operator]').val('begins_with').trigger('change'); + $('[name=builder_rule_1_value_0]').val('bar').trigger('change'); + + // add a second rule to the section and set it + $('#builder_section_0 #builder_group_1>.rules-group-header>.group-actions [data-add=rule]').trigger('click'); + $('[name=builder_rule_2_filter]').val('status').trigger('change'); + $('[name=builder_rule_2_operator]').val('is_null').trigger('change'); + + // set the section group to "or" + $('#builder_section_0 #builder_group_1>.rules-group-header>.group-conditions [value=OR]').trigger('click'); + + // set the section exists to "does not exist" + $('#builder_section_0>.rules-section-header>.section-exists-options [value="DOES NOT EXIST"]').trigger('click'); + + // add a section rule and delete it + $('#builder_section_0 #builder_group_1>.rules-group-header>.group-actions [data-add=rule]').trigger('click'); + $('#builder_section_0 #builder_rule_3 [data-delete=rule]').trigger('click'); + + // add a section group and delete it + $('#builder_section_0 #builder_group_1>.rules-group-header>.group-actions [data-add=group]').trigger('click'); + $('#builder_section_0 #builder_group_2 [data-delete=group]').trigger('click'); + + // add a section and delete it + $('#builder_group_0>.rules-group-header>.group-actions [data-add=section]').trigger('click'); + $('#builder_section_1 [data-delete=section]').trigger('click'); + + var generated = $b.queryBuilder('getRules'); + var expected = { + condition: 'AND', + rules: [{ + id: 'name', + operator: 'not_equal', + value: 'foo' + }, { + section: 'partner', + exists: 'DOES NOT EXIST', + group: { + condition: 'OR', + rules: [{ + id: 'name', + operator: 'begins_with', + value: 'bar' + },{ + id: 'status', + operator: 'is_null' + }] + } + }] + }; + + assert.rulesMatch( + generated, + expected, + 'Should return correct rules after UI events' + ); + assert.rulesMatch( + generated.rules != undefined && generated.rules[1] != undefined ? generated.rules[1].group : 'missing', + expected.rules[1].group, + 'Should return correct section group after UI events' + ); + }); + + /** + * Test filter.operators within sections + */ + QUnit.test('Change operators within sections', function(assert) { + $b.queryBuilder({ + filters: [{ + id: 'name', + type: 'string' + }], + sections: [{ + id: 'test', + filters: [{ + id: 'name', + type: 'string' + }, { + id: 'price', + type: 'double' + }, { + id: 'release', + type: 'date', + operators: ['before', 'equal', 'after'] + }] + }], + rules: { + condition: 'AND', + rules: [{ + section: 'test', + exists: 'EXISTS', + group: { + condition: 'AND', + rules: [{ + id: 'name', + operator: 'equal', + value: 'foo' + }, { + id: 'price', + operator: 'less', + value: 10 + }, { + id: 'release', + operator: 'before', + value: '1995-5-1' + }] + } + }] + }, + operators: [ + {type: 'equal', nb_inputs: 1, apply_to: ['string']}, + {type: 'not_equal', nb_inputs: 1, apply_to: ['string']}, + {type: 'less', nb_inputs: 1, apply_to: ['number']}, + {type: 'greater', nb_inputs: 1, apply_to: ['number']}, + {type: 'before', nb_inputs: 1, apply_to: ['datetime']}, + {type: 'after', nb_inputs: 1, apply_to: ['datetime']} + ] + }); + + assert.optionsMatch( + $('#builder_rule_0 [name$=_operator] option'), + ['equal', 'not_equal'], + '"name" filter should have "equal" & "not_equal" operators' + ); + + assert.optionsMatch( + $('#builder_rule_1 [name$=_operator] option'), + ['less', 'greater'], + '"price" filter should have "less" & "greater" operators' + ); + + assert.optionsMatch( + $('#builder_rule_2 [name$=_operator] option'), + ['before', 'equal', 'after'], + '"release" filter should have "before" & "equal" & "after" operators' + ); + }); + + /** + * Test custom conditions within sections + */ + QUnit.test('Change conditions within sections', function(assert) { + var rules = { + condition: 'NAND', + rules: [{ + section: 'partner', + exists: 'EXISTS', + group: { + condition: 'NAND', + rules: [{ + id: 'name', + operator: 'equal', + value: 'foo' + }, { + condition: 'XOR', + rules: [{ + id: 'name', + operator: 'equal', + value: 'bar' + }] + }] + } + }] + }; + + $b.queryBuilder({ + filters: basic_filters, + sections: basic_sections, + rules: rules, + conditions: ['NAND', 'XOR'], + default_condition: 'NAND' + }); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + rules, + 'Should return correct rules' + ); + + assert.optionsMatch( + $('#builder_group_0 > .rules-group-header [name$=_cond]'), + ['NAND', 'XOR'], + 'Available onditions should be NAND & XOR' + ); + + assert.equal( + $('#builder_group_2 [name$=_cond]:checked').val(), + 'XOR', + 'The second group should have "XOR" condition selected' + ); + }); + + /** + * Test readonly + */ + QUnit.test('Readonly within sections', function(assert) { + $b.queryBuilder({ + filters: basic_filters, + sections: basic_sections, + rules: { + condition: 'AND', + flags: { + condition_readonly: true + }, + rules: [{ + section: 'partner', + exists: 'EXISTS', + flags: { + exists_readonly: true + }, + group: { + condition: 'AND', + flags: { + condition_readonly: true + }, + rules: [{ + id: 'name', + operator: 'not_equal', + value: 'foo', + flags: { + no_delete: true + } + }, { + condition: 'OR', + rules: [{ + id: 'status', + operator: 'not_equal', + value: 'ac', + readonly: true + }] + }, { + condition: 'AND', + readonly: true, + rules: [{ + id: 'name', + operator: 'not_equal', + value: 'bar' + }] + }] + } + }] + } + }); + + assert.equal( + $('#builder_section_0 #builder_group_1>.rules-group-header input:not(:disabled)').length, 0, + 'Should disable group condition radio buttons' + ); + + assert.equal( + $('#builder_section_0>.rules-section-header input:not(:disabled)').length, 0, + 'Should disable section exists radio buttons' + ); + + assert.equal( + $('#builder_section_0 #builder_rule_0 [data-delete=rule]').length, 0, + 'Should hide delete button of "no_delete" rule' + ); + + assert.equal( + $('#builder_section_0 #builder_rule_0').find('input:disabled, select:disabled').length, 0, + 'Should not disable inputs of "no_delete" rule' + ); + + assert.equal( + $('#builder_section_0 #builder_rule_1 [data-delete=rule]').length, 0, + 'Should hide delete button of "readonly" rule' + ); + + assert.equal( + $('#builder_section_0 #builder_rule_1').find('input:disabled, select:disabled').length, 3, + 'Should disable inputs of "readonly" rule' + ); + + assert.equal( + $('#builder_section_0 #builder_group_3').find('[data-delete=group], [data-add=rule], [data-add=group]').length, 0, + 'Should hide all buttons of "readonly" group' + ); + + $('#builder_section_0 #builder_group_2 [data-delete=group]').click(); + + assert.rulesMatch( + $b.queryBuilder('getRules'), + { + condition: 'AND', + rules: [{ + section: 'partner', + exists: 'EXISTS', + group: { + condition: 'AND', + rules: [{ + condition: 'AND', + rules: [{ + id: 'name', + operator: 'not_equal', + value: 'foo' + }, { + condition: 'OR', + rules: [{ + id: 'status', + operator: 'not_equal', + value: 'ac' + }] + }, { + condition: 'AND', + rules: [{ + id: 'name', + operator: 'not_equal', + value: 'bar' + }] + }] + }] + } + }] + }, + 'Should not delete group with readonly rule' + ); + }); + + /** + * Test groups limit within section + */ + QUnit.test('No groups allowed, other than the section group', function(assert) { + $b.queryBuilder({ + filters: basic_filters, + sections: basic_sections, + allow_groups: false + }); + + assert.ok( + $('#builder_group_0 [data-add=group]').length == 0, + 'Should not contain group add button' + ); + + assert.throws( + function(){ $b.queryBuilder('setRules', { + condition: 'AND', + rules: [{ + id: 'price', + field: 'price', + operator: 'less', + value: 10.25 + }, { + section: 'partner', + group: { + condition: 'AND', + rules: [{ + id: 'name', + field: 'name', + operator: 'begins_with', + value: 'Best' + }, { + condition: 'OR', + rules: [{ + id: 'status', + field: 'status', + operator: 'equal', + value: 'ac' + },{ + id: 'status', + field: 'status', + operator: 'not_equal', + value: 'iv' + }] + }] + } + }] + }); }, + /No more than 0 groups are allowed/, + 'Should throw "No more than 0 groups are allowed" error' + ); + }); + + /** + * Test filters ordering in sections + */ + QUnit.test('Sort filters in sections', function(assert) { + $b.queryBuilder({ + filters: basic_filters, + sections: [{ + id: 'partner', + label: 'Partner', + filters: [{ + id: '3', + label: { + fr: 'ccc', + en: 'Ccc' + } + }, { + id: '1', + label: 'AAA' + }, { + id: '5', + label: 'eee' + }, { + id: '2', + label: 'bbb' + }, { + id: '4', + label: { + fr: 'ddd', + en: 'Ddd' + } + }] + }], + sort_filters: true, + lang_code: 'fr' + }); + + $('#builder_group_0>.rules-group-header>.group-actions [data-add=section]').trigger('click'); + $('[name=builder_section_0_section_type]').val('partner').trigger('change'); + + var options = []; + $('[name=builder_rule_1_filter]>*').each(function() { + options.push($(this).val()); + }); + + assert.deepEqual( + options, + ['-1', '1', '2', '3', '4', '5'], + 'Filters should be sorted by alphabetical order' + ); + + $b.queryBuilder('destroy'); + + $b.queryBuilder({ + filters: basic_filters, + sections: [{ + id: 'partner', + label: 'Partner', + filters: [{ + id: '3', + label: 'ccc' + }, { + id: '1', + label: 'AAA' + }, { + id: '5', + label: 'eee' + }, { + id: '2', + label: 'bbb' + }, { + id: '4', + label: 'ddd' + }] + }], + sort_filters: function(a, b) { + return parseInt(b.id) - parseInt(a.id); + } + }); + + $('#builder_group_0>.rules-group-header>.group-actions [data-add=section]').trigger('click'); + $('[name=builder_section_0_section_type]').val('partner').trigger('change'); + + options = []; + $('[name=builder_rule_1_filter]>*').each(function() { + options.push($(this).val()); + }); + + assert.deepEqual( + options, + ['-1', '5', '4', '3', '2', '1'], + 'Filters should be sorted by custom order' + ); + }); }); From 360fece7ce107e2a7af04fac625c387a39607371 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Tue, 15 Nov 2016 12:18:16 -0500 Subject: [PATCH 29/44] Removed allow_sections from the example --- examples/index.html | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/index.html b/examples/index.html index c35aca98..2af400d6 100644 --- a/examples/index.html +++ b/examples/index.html @@ -120,7 +120,6 @@

    Output

    var options = { allow_empty: true, - allow_sections: true, //default_filter: 'name', sort_filters: true, From eb4f8ea8b6367942ede447a0837f3d07b970af11 Mon Sep 17 00:00:00 2001 From: Reha Sterbin Date: Fri, 18 Nov 2016 14:30:57 -0500 Subject: [PATCH 30/44] Sortable plugin now restricts moving rules/groups in and out of sections --- src/core.js | 34 +++++++++++++++-- src/defaults.js | 1 + src/model.js | 5 +-- src/plugins/sortable/plugin.js | 68 ++++++++++++++++++++++++++-------- src/template.js | 19 ++++++---- 5 files changed, 98 insertions(+), 29 deletions(-) diff --git a/src/core.js b/src/core.js index f0f16da5..8e965059 100644 --- a/src/core.js +++ b/src/core.js @@ -402,6 +402,10 @@ QueryBuilder.prototype.bindEvents = function() { case 'value': self.updateRuleValue(node); break; + + case 'section_type_id': + self.updateRuleSectionTypeId(node); + break; } } else if (node instanceof Group) { @@ -417,6 +421,10 @@ QueryBuilder.prototype.bindEvents = function() { case 'condition': self.updateGroupCondition(node); break; + + case 'section_type_id': + self.updateGroupSectionTypeId(node); + break; } } else if (node instanceof Section) { @@ -487,8 +495,9 @@ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) { var group_id = this.nextGroupId(); var section_root = parent instanceof Section; - var in_section = section_root || parent.section_type_id; - var $group = $(this.getGroupTemplate(group_id, level, in_section, section_root)); + 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); @@ -646,9 +655,28 @@ QueryBuilder.prototype.deleteSection = function(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} @@ -727,7 +755,7 @@ QueryBuilder.prototype.addRule = function(parent, data, flags) { } var rule_id = this.nextRuleId(); - var $rule = $(this.getRuleTemplate(rule_id)); + var $rule = $(this.getRuleTemplate(rule_id, parent.section_type_id)); var model = parent.addRule($rule); if (data !== undefined) { diff --git a/src/defaults.js b/src/defaults.js index 59c244dc..b73d5ca6 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -47,6 +47,7 @@ var Selectors = QueryBuilder.selectors = { value_container: '.rule-value-container', error_container: '.error-container', condition_container: '.rules-group-header .group-conditions', + exists_container: '.rules-section-header .section-exists-options', section_header: '.rules-section-header', group_header: '.rules-group-header', diff --git a/src/model.js b/src/model.js index 0a325e40..afccbeb4 100644 --- a/src/model.js +++ b/src/model.js @@ -232,7 +232,7 @@ var Group = function(parent, $el) { Node.call(this, parent, $el); this.rules = []; - this.section_type_id = null; + this.__.section_type_id = null; this.__.condition = null; }; @@ -440,8 +440,7 @@ var Rule = function(parent, $el) { Node.call(this, parent, $el); - this.section_type_id = null; - + this.__.section_type_id = null; this.__.filter = null; this.__.operator = null; this.__.flags = {}; diff --git a/src/plugins/sortable/plugin.js b/src/plugins/sortable/plugin.js index 6b2818ea..20ac43ac 100644 --- a/src/plugins/sortable/plugin.js +++ b/src/plugins/sortable/plugin.js @@ -3,7 +3,6 @@ * Enables drag & drop sort of rules. */ -Selectors.rule_and_group_containers = Selectors.rule_container + ', ' + Selectors.group_container; Selectors.drag_handle = '.drag-handle'; QueryBuilder.define('sortable', function(options) { @@ -21,10 +20,24 @@ QueryBuilder.define('sortable', function(options) { var ghost; var src; + var acceptable = function(node) { + if (node instanceof Section) { + return Selectors.rule_container + '[data-stype="' + node.type_id + '"], ' + + Selectors.group_container + '[data-stype="' + node.type_id + '"]'; + } else if (node.section_type_id != undefined) { + return Selectors.rule_container + '[data-stype="' + node.section_type_id + '"], ' + + Selectors.group_container + '[data-stype="' + node.section_type_id + '"]'; + } else { + return Selectors.rule_container + '[data-stype=""], ' + + Selectors.group_container + '[data-stype=""], ' + + Selectors.section_container; + } + }; + /** * Init drag and drop */ - this.on('afterAddRule afterAddGroup', function(e, node) { + this.on('afterAddRule afterAddGroup afterAddSection', function(e, node) { if (node == placeholder) { return; } @@ -76,16 +89,18 @@ QueryBuilder.define('sortable', function(options) { /** * Configure drop on groups and rules */ - interact(node.$el[0]) - .dropzone({ - accept: Selectors.rule_and_group_containers, - ondragenter: function(event) { - moveSortableToTarget(placeholder, $(event.target)); - }, - ondrop: function(event) { - moveSortableToTarget(src, $(event.target)); - } - }); + if (!(node instanceof Section)) { + interact(node.$el[0]) + .dropzone({ + accept: acceptable(node), + ondragenter: function(event) { + moveSortableToTarget(placeholder, $(event.target)); + }, + ondrop: function(event) { + moveSortableToTarget(src, $(event.target)); + } + }); + } /** * Configure drop on group headers @@ -93,7 +108,7 @@ QueryBuilder.define('sortable', function(options) { if (node instanceof Group) { interact(node.$el.find(Selectors.group_header)[0]) .dropzone({ - accept: Selectors.rule_and_group_containers, + accept: acceptable(node), ondragenter: function(event) { moveSortableToTarget(placeholder, $(event.target)); }, @@ -107,7 +122,7 @@ QueryBuilder.define('sortable', function(options) { /** * Detach interactables */ - this.on('beforeDeleteRule beforeDeleteGroup', function(e, node) { + this.on('beforeDeleteRule beforeDeleteGroup beforeDeleteSection', function(e, node) { if (!e.isDefaultPrevented()) { interact(node.$el[0]).unset(); @@ -147,11 +162,26 @@ QueryBuilder.define('sortable', function(options) { } }); + /** + * Remove drag handle from non-sortable sections + */ + this.on('parseSectionFlags.filter', function(flags) { + if (flags.value.no_sortable === undefined) { + flags.value.no_sortable = options.default_no_sortable; + } + }); + + this.on('afterApplySectionFlags', function(e, group) { + if (group.flags.no_sortable) { + group.$el.find('.drag-handle').remove(); + } + }); + /** * Modify templates */ - this.on('getGroupTemplate.filter', function(h, level) { - if (level > 1) { + this.on('getGroupTemplate.filter', function(h, level, in_section, section_root) { + if (level > 1 && !section_root) { var $h = $(h.value); $h.find(Selectors.condition_container).after('
    '); h.value = $h.prop('outerHTML'); @@ -163,6 +193,12 @@ QueryBuilder.define('sortable', function(options) { $h.find(Selectors.rule_header).after('
    '); h.value = $h.prop('outerHTML'); }); + + this.on('getSectionTemplate.filter', function(h) { + var $h = $(h.value); + $h.find(Selectors.exists_container).after('
    '); + h.value = $h.prop('outerHTML'); + }); }, { default_no_sortable: false, icon: 'glyphicon glyphicon-sort' diff --git a/src/template.js b/src/template.js index f4e4ceb7..68f314a6 100644 --- a/src/template.js +++ b/src/template.js @@ -1,5 +1,5 @@ QueryBuilder.templates.group = '\ -
    \ +
    \
    \
    \
    '; QueryBuilder.templates.section = '\ -
    \ +
    \
    \
    \