/*! * jQuery.extendext 0.1.1 * * Copyright 2014 Damien "Mistic" Sorel (http://www.strangeplanet.fr) * Licensed under MIT (http://opensource.org/licenses/MIT) * * Based on jQuery.extend by jQuery Foundation, Inc. and other contributors */ (function(root, factory) { if (typeof define === 'function' && define.amd) { define('jQuery.extendext', ['jquery'], factory); } else { factory(root.jQuery); } }(this, function($) { "use strict"; $.extendext = function() { var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, arrayMode = 'default'; // Handle a deep copy situation if ( typeof target === "boolean" ) { deep = target; // Skip the boolean and the target target = arguments[ i++ ] || {}; } // Handle array mode parameter if ( typeof target === "string" ) { arrayMode = $([target.toLowerCase(), 'default']).filter(['default','concat','replace','extend'])[0]; // Skip the string param target = arguments[ i++ ] || {}; } // Handle case when target is a string or something (possible in deep copy) if ( typeof target !== "object" && !$.isFunction(target) ) { target = {}; } // Extend jQuery itself if only one argument is passed if ( i === length ) { target = this; i--; } for ( ; i < length; i++ ) { // Only deal with non-null/undefined values if ( (options = arguments[ i ]) !== null ) { // Special operations for arrays if ($.isArray(options) && arrayMode !== 'default') { clone = target && $.isArray(target) ? target : []; switch (arrayMode) { case 'concat': target = clone.concat( $.extend( deep, [], options ) ); break; case 'replace': target = $.extend( deep, [], options ); break; case 'extend': options.forEach(function(e, i) { if (typeof e === 'object') { var type = $.isArray(e) ? [] : {}; clone[i] = $.extendext( deep, arrayMode, clone[i] || type, e ); } else if (clone.indexOf(e) === -1) { clone.push(e); } }); target = clone; break; } } else { // Extend the base object for ( name in options ) { src = target[ name ]; copy = options[ name ]; // Prevent never-ending loop if ( target === copy ) { continue; } // Recurse if we're merging plain objects or arrays if ( deep && copy && ( $.isPlainObject(copy) || (copyIsArray = $.isArray(copy)) ) ) { if ( copyIsArray ) { copyIsArray = false; clone = src && $.isArray(src) ? src : []; } else { clone = src && $.isPlainObject(src) ? src : {}; } // Never move original objects, clone them target[ name ] = $.extendext( deep, arrayMode, clone, copy ); // Don't bring in undefined values } else if ( copy !== undefined ) { target[ name ] = copy; } } } } } // Return the modified object return target; }; })); // doT.js // 2011-2014, Laura Doktorova, https://github.com/olado/doT // Licensed under the MIT license. (function() { "use strict"; var doT = { version: "1.0.3", templateSettings: { evaluate: /\{\{([\s\S]+?(\}?)+)\}\}/g, interpolate: /\{\{=([\s\S]+?)\}\}/g, encode: /\{\{!([\s\S]+?)\}\}/g, use: /\{\{#([\s\S]+?)\}\}/g, useParams: /(^|[^\w$])def(?:\.|\[[\'\"])([\w$\.]+)(?:[\'\"]\])?\s*\:\s*([\w$\.]+|\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})/g, define: /\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g, defineParams:/^\s*([\w$]+):([\s\S]+)/, conditional: /\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g, iterate: /\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/g, varname: "it", strip: true, append: true, selfcontained: false, doNotSkipEncoded: false }, template: undefined, //fn, compile template compile: undefined //fn, for express }, _globals; doT.encodeHTMLSource = function(doNotSkipEncoded) { var encodeHTMLRules = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "/": "/" }, matchHTML = doNotSkipEncoded ? /[&<>"'\/]/g : /&(?!#?\w+;)|<|>|"|'|\//g; return function(code) { return code ? code.toString().replace(matchHTML, function(m) {return encodeHTMLRules[m] || m;}) : ""; }; }; _globals = (function(){ return this || (0,eval)("this"); }()); if (typeof module !== "undefined" && module.exports) { module.exports = doT; } else if (typeof define === "function" && define.amd) { define('doT', function(){return doT;}); } else { _globals.doT = doT; } var startend = { append: { start: "'+(", end: ")+'", startencode: "'+encodeHTML(" }, split: { start: "';out+=(", end: ");out+='", startencode: "';out+=encodeHTML(" } }, skip = /$^/; function resolveDefs(c, block, def) { return ((typeof block === "string") ? block : block.toString()) .replace(c.define || skip, function(m, code, assign, value) { if (code.indexOf("def.") === 0) { code = code.substring(4); } if (!(code in def)) { if (assign === ":") { if (c.defineParams) value.replace(c.defineParams, function(m, param, v) { def[code] = {arg: param, text: v}; }); if (!(code in def)) def[code]= value; } else { new Function("def", "def['"+code+"']=" + value)(def); } } return ""; }) .replace(c.use || skip, function(m, code) { if (c.useParams) code = code.replace(c.useParams, function(m, s, d, param) { if (def[d] && def[d].arg && param) { var rw = (d+":"+param).replace(/'|\\/g, "_"); def.__exp = def.__exp || {}; def.__exp[rw] = def[d].text.replace(new RegExp("(^|[^\\w$])" + def[d].arg + "([^\\w$])", "g"), "$1" + param + "$2"); return s + "def.__exp['"+rw+"']"; } }); var v = new Function("def", "return " + code)(def); return v ? resolveDefs(c, v, def) : v; }); } function unescape(code) { return code.replace(/\\('|\\)/g, "$1").replace(/[\r\t\n]/g, " "); } doT.template = function(tmpl, c, def) { c = c || doT.templateSettings; var cse = c.append ? startend.append : startend.split, needhtmlencode, sid = 0, indv, str = (c.use || c.define) ? resolveDefs(c, tmpl, def || {}) : tmpl; str = ("var out='" + (c.strip ? str.replace(/(^|\r|\n)\t* +| +\t*(\r|\n|$)/g," ") .replace(/\r|\n|\t|\/\*[\s\S]*?\*\//g,""): str) .replace(/'|\\/g, "\\$&") .replace(c.interpolate || skip, function(m, code) { return cse.start + unescape(code) + cse.end; }) .replace(c.encode || skip, function(m, code) { needhtmlencode = true; return cse.startencode + unescape(code) + cse.end; }) .replace(c.conditional || skip, function(m, elsecase, code) { return elsecase ? (code ? "';}else if(" + unescape(code) + "){out+='" : "';}else{out+='") : (code ? "';if(" + unescape(code) + "){out+='" : "';}out+='"); }) .replace(c.iterate || skip, function(m, iterate, vname, iname) { if (!iterate) return "';} } out+='"; sid+=1; indv=iname || "i"+sid; iterate=unescape(iterate); return "';var arr"+sid+"="+iterate+";if(arr"+sid+"){var "+vname+","+indv+"=-1,l"+sid+"=arr"+sid+".length-1;while("+indv+"<l"+sid+"){" +vname+"=arr"+sid+"["+indv+"+=1];out+='"; }) .replace(c.evaluate || skip, function(m, code) { return "';" + unescape(code) + "out+='"; }) + "';return out;") .replace(/\n/g, "\\n").replace(/\t/g, '\\t').replace(/\r/g, "\\r") .replace(/(\s|;|\}|^|\{)out\+='';/g, '$1').replace(/\+''/g, ""); //.replace(/(\s|;|\}|^|\{)out\+=''\+/g,'$1out+='); if (needhtmlencode) { if (!c.selfcontained && _globals && !_globals._encodeHTML) _globals._encodeHTML = doT.encodeHTMLSource(c.doNotSkipEncoded); str = "var encodeHTML = typeof _encodeHTML !== 'undefined' ? _encodeHTML : (" + doT.encodeHTMLSource.toString() + "(" + (c.doNotSkipEncoded || '') + "));" + str; } try { return new Function(c.varname, str); } catch (e) { if (typeof console !== "undefined") console.log("Could not create a template function: " + str); throw e; } }; doT.compile = function(tmpl, def) { return doT.template(tmpl, null, def); }; }()); /*! * jQuery QueryBuilder 2.3.3 * Copyright 2014-2016 Damien "Mistic" Sorel (http://www.strangeplanet.fr) * Licensed under MIT (http://opensource.org/licenses/MIT) */ // Languages: en // Plugins: bt-checkbox, bt-selectpicker, bt-tooltip-errors, change-filters, filter-description, invert, mongodb-support, sortable, sql-support, unique-filter (function(root, factory) { if (typeof define == 'function' && define.amd) { define('query-builder', ['jquery', 'doT', 'jQuery.extendext'], factory); } else { factory(root.jQuery, root.doT); } }(this, function($, doT) { "use strict"; // CLASS DEFINITION // =============================== var QueryBuilder = function($el, options) { this.init($el, options); }; // EVENTS SYSTEM // =============================== $.extend(QueryBuilder.prototype, { change: function(type, value) { var event = new $.Event(type + '.queryBuilder.filter', { builder: this, value: value }); this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 2)); return event.value; }, trigger: function(type) { var event = new $.Event(type + '.queryBuilder', { builder: this }); this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 1)); return event; }, on: function(type, cb) { this.$el.on(type + '.queryBuilder', cb); return this; }, off: function(type, cb) { this.$el.off(type + '.queryBuilder', cb); return this; }, once: function(type, cb) { this.$el.one(type + '.queryBuilder', cb); return this; } }); // PLUGINS SYSTEM // =============================== QueryBuilder.plugins = {}; /** * Get or extend the default configuration * @param options {object,optional} new configuration, leave undefined to get the default config * @return {undefined|object} nothing or configuration object (copy) */ QueryBuilder.defaults = function(options) { if (typeof options == 'object') { $.extendext(true, 'replace', QueryBuilder.DEFAULTS, options); } else if (typeof options == 'string') { if (typeof QueryBuilder.DEFAULTS[options] == 'object') { return $.extend(true, {}, QueryBuilder.DEFAULTS[options]); } else { return QueryBuilder.DEFAULTS[options]; } } else { return $.extend(true, {}, QueryBuilder.DEFAULTS); } }; /** * Define a new plugin * @param {string} * @param {function} * @param {object,optional} default configuration */ QueryBuilder.define = function(name, fct, def) { QueryBuilder.plugins[name] = { fct: fct, def: def || {} }; }; /** * Add new methods * @param {object} */ QueryBuilder.extend = function(methods) { $.extend(QueryBuilder.prototype, methods); }; /** * Init plugins for an instance * @throws ConfigError */ QueryBuilder.prototype.initPlugins = function() { if (!this.plugins) { return; } if ($.isArray(this.plugins)) { var tmp = {}; this.plugins.forEach(function(plugin) { tmp[plugin] = null; }); this.plugins = tmp; } Object.keys(this.plugins).forEach(function(plugin) { if (plugin in QueryBuilder.plugins) { this.plugins[plugin] = $.extend(true, {}, QueryBuilder.plugins[plugin].def, this.plugins[plugin] || {} ); QueryBuilder.plugins[plugin].fct.call(this, this.plugins[plugin]); } else { Utils.error('Config', 'Unable to find plugin "{0}"', plugin); } }, this); }; /** * Allowed types and their internal representation */ QueryBuilder.types = { 'string': 'string', 'integer': 'number', 'double': 'number', 'date': 'datetime', 'time': 'datetime', 'datetime': 'datetime', 'boolean': 'boolean' }; /** * Allowed inputs */ QueryBuilder.inputs = [ 'text', 'textarea', 'radio', 'checkbox', 'select' ]; /** * Runtime modifiable options with `setOptions` method */ QueryBuilder.modifiable_options = [ 'display_errors', 'allow_groups', 'allow_empty', 'default_condition', 'default_filter' ]; /** * CSS selectors for common components */ var Selectors = QueryBuilder.selectors = { group_container: '.rules-group-container', rule_container: '.rule-container', filter_container: '.rule-filter-container', operator_container: '.rule-operator-container', value_container: '.rule-value-container', error_container: '.error-container', condition_container: '.rules-group-header .group-conditions', rule_header: '.rule-header', group_header: '.rules-group-header', group_actions: '.group-actions', rule_actions: '.rule-actions', rules_list: '.rules-group-body>.rules-list', group_condition: '.rules-group-header [name$=_cond]', rule_filter: '.rule-filter-container [name$=_filter]', rule_operator: '.rule-operator-container [name$=_operator]', rule_value: '.rule-value-container [name*=_value_]', add_rule: '[data-add=rule]', delete_rule: '[data-delete=rule]', add_group: '[data-add=group]', delete_group: '[data-delete=group]' }; /** * Template strings (see `template.js`) */ QueryBuilder.templates = {}; /** * Localized strings (see `i18n/`) */ QueryBuilder.regional = {}; /** * Default operators */ QueryBuilder.OPERATORS = { equal: { type: 'equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] }, not_equal: { type: 'not_equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] }, in: { type: 'in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime'] }, not_in: { type: 'not_in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime'] }, less: { type: 'less', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] }, less_or_equal: { type: 'less_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] }, greater: { type: 'greater', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] }, greater_or_equal: { type: 'greater_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] }, between: { type: 'between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] }, not_between: { type: 'not_between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] }, begins_with: { type: 'begins_with', nb_inputs: 1, multiple: false, apply_to: ['string'] }, not_begins_with: { type: 'not_begins_with', nb_inputs: 1, multiple: false, apply_to: ['string'] }, contains: { type: 'contains', nb_inputs: 1, multiple: false, apply_to: ['string'] }, not_contains: { type: 'not_contains', nb_inputs: 1, multiple: false, apply_to: ['string'] }, ends_with: { type: 'ends_with', nb_inputs: 1, multiple: false, apply_to: ['string'] }, not_ends_with: { type: 'not_ends_with', nb_inputs: 1, multiple: false, apply_to: ['string'] }, is_empty: { type: 'is_empty', nb_inputs: 0, multiple: false, apply_to: ['string'] }, is_not_empty: { type: 'is_not_empty', nb_inputs: 0, multiple: false, apply_to: ['string'] }, is_null: { type: 'is_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] }, is_not_null: { type: 'is_not_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] } }; /** * Default configuration */ QueryBuilder.DEFAULTS = { filters: [], plugins: [], sort_filters: false, display_errors: true, allow_groups: -1, allow_empty: false, conditions: ['AND', 'OR'], default_condition: 'AND', inputs_separator: ' , ', select_placeholder: '------', display_empty_filter: true, default_filter: null, optgroups: {}, default_rule_flags: { filter_readonly: false, operator_readonly: false, value_readonly: false, no_delete: false }, default_group_flags: { condition_readonly: false, no_delete: false }, templates: { group: null, rule: null, filterSelect: null, operatorSelect: null }, lang_code: 'en', lang: {}, operators: [ 'equal', 'not_equal', 'in', 'not_in', 'less', 'less_or_equal', 'greater', 'greater_or_equal', 'between', 'not_between', 'begins_with', 'not_begins_with', 'contains', 'not_contains', 'ends_with', 'not_ends_with', 'is_empty', 'is_not_empty', 'is_null', 'is_not_null' ], 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' } }; /** * Init the builder */ QueryBuilder.prototype.init = function($el, options) { $el[0].queryBuilder = this; this.$el = $el; // PROPERTIES this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options); this.model = new Model(); this.status = { group_id: 0, rule_id: 0, generated_id: false, has_optgroup: false, has_operator_oprgroup: false, id: null, updating_value: false }; // "allow_groups" can be boolean or int if (this.settings.allow_groups === false) { this.settings.allow_groups = 0; } else if (this.settings.allow_groups === true) { this.settings.allow_groups = -1; } // SETTINGS SHORTCUTS this.filters = this.settings.filters; this.icons = this.settings.icons; this.operators = this.settings.operators; this.templates = this.settings.templates; this.plugins = this.settings.plugins; // translations : english << 'lang_code' << custom if (QueryBuilder.regional['en'] === undefined) { Utils.error('Config', '"i18n/en.js" not loaded.'); } this.lang = $.extendext(true, 'replace', {}, QueryBuilder.regional['en'], QueryBuilder.regional[this.settings.lang_code], this.settings.lang); // init templates Object.keys(this.templates).forEach(function(tpl) { if (!this.templates[tpl]) { this.templates[tpl] = QueryBuilder.templates[tpl]; } if (typeof this.templates[tpl] == 'string') { this.templates[tpl] = doT.template(this.templates[tpl]); } }, this); // ensure we have a container id if (!this.$el.attr('id')) { this.$el.attr('id', 'qb_' + Math.floor(Math.random() * 99999)); this.status.generated_id = true; } this.status.id = this.$el.attr('id'); // INIT this.$el.addClass('query-builder form-inline'); this.filters = this.checkFilters(this.filters); this.operators = this.checkOperators(this.operators); this.bindEvents(); this.initPlugins(); this.trigger('afterInit'); if (options.rules) { this.setRules(options.rules); delete this.settings.rules; } else { this.setRoot(true); } }; /** * Checks the configuration of each filter * @throws ConfigError */ QueryBuilder.prototype.checkFilters = function(filters) { var definedFilters = []; if (!filters || filters.length === 0) { Utils.error('Config', 'Missing filters list'); } filters.forEach(function(filter, i) { if (!filter.id) { Utils.error('Config', 'Missing filter {0} id', i); } if (definedFilters.indexOf(filter.id) != -1) { Utils.error('Config', 'Filter "{0}" already defined', filter.id); } definedFilters.push(filter.id); if (!filter.type) { filter.type = 'string'; } else if (!QueryBuilder.types[filter.type]) { Utils.error('Config', 'Invalid type "{0}"', filter.type); } 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); } if (filter.operators) { filter.operators.forEach(function(operator) { if (typeof operator != 'string') { Utils.error('Config', 'Filter operators must be global operators types (string)'); } }); } if (!filter.field) { filter.field = filter.id; } if (!filter.label) { filter.label = filter.field; } if (!filter.optgroup) { filter.optgroup = null; } else { this.status.has_optgroup = true; // register optgroup if needed if (!this.settings.optgroups[filter.optgroup]) { this.settings.optgroups[filter.optgroup] = filter.optgroup; } } switch (filter.input) { case 'radio': case 'checkbox': if (!filter.values || filter.values.length < 1) { Utils.error('Config', 'Missing filter "{0}" values', filter.id); } break; case 'select': if (filter.placeholder) { if (filter.placeholder_value === undefined) { filter.placeholder_value = -1; } 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); } }); } break; } }, this); if (this.settings.sort_filters) { if (typeof this.settings.sort_filters == 'function') { filters.sort(this.settings.sort_filters); } else { var self = this; filters.sort(function(a, b) { return self.translateLabel(a.label).localeCompare(self.translateLabel(b.label)); }); } } if (this.status.has_optgroup) { filters = Utils.groupSort(filters, 'optgroup'); } return filters; }; /** * Checks the configuration of each operator * @throws ConfigError */ QueryBuilder.prototype.checkOperators = function(operators) { var definedOperators = []; operators.forEach(function(operator, i) { if (typeof operator == 'string') { if (!QueryBuilder.OPERATORS[operator]) { Utils.error('Config', 'Unknown operator "{0}"', operator); } operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator]); } else { if (!operator.type) { Utils.error('Config', 'Missing "type" for operator {0}', i); } if (QueryBuilder.OPERATORS[operator.type]) { operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator.type], operator); } if (operator.nb_inputs === undefined || operator.apply_to === undefined) { Utils.error('Config', 'Missing "nb_inputs" and/or "apply_to" for operator "{0}"', operator.type); } } if (definedOperators.indexOf(operator.type) != -1) { Utils.error('Config', 'Operator "{0}" already defined', operator.type); } definedOperators.push(operator.type); if (!operator.optgroup) { operator.optgroup = null; } else { this.status.has_operator_optgroup = true; // register optgroup if needed if (!this.settings.optgroups[operator.optgroup]) { this.settings.optgroups[operator.optgroup] = operator.optgroup; } } }, this); if (this.status.has_operator_optgroup) { operators = Utils.groupSort(operators, 'optgroup'); } return operators; }; /** * Add all events listeners */ QueryBuilder.prototype.bindEvents = function() { var self = this; // group condition change this.$el.on('change.queryBuilder', Selectors.group_condition, function() { if ($(this).is(':checked')) { var $group = $(this).closest(Selectors.group_container); Model($group).condition = $(this).val(); } }); // rule filter change this.$el.on('change.queryBuilder', Selectors.rule_filter, function() { var $rule = $(this).closest(Selectors.rule_container); Model($rule).filter = self.getFilterById($(this).val()); }); // rule operator change this.$el.on('change.queryBuilder', Selectors.rule_operator, function() { var $rule = $(this).closest(Selectors.rule_container); Model($rule).operator = self.getOperatorByType($(this).val()); }); // add rule button this.$el.on('click.queryBuilder', Selectors.add_rule, function() { var $group = $(this).closest(Selectors.group_container); self.addRule(Model($group)); }); // delete rule button this.$el.on('click.queryBuilder', Selectors.delete_rule, function() { var $rule = $(this).closest(Selectors.rule_container); self.deleteRule(Model($rule)); }); if (this.settings.allow_groups !== 0) { // add group button this.$el.on('click.queryBuilder', Selectors.add_group, function() { var $group = $(this).closest(Selectors.group_container); self.addGroup(Model($group)); }); // delete group button this.$el.on('click.queryBuilder', Selectors.delete_group, function() { var $group = $(this).closest(Selectors.group_container); self.deleteGroup(Model($group)); }); } // model events this.model.on({ 'drop': function(e, node) { node.$el.remove(); self.refreshGroupsConditions(); }, 'add': function(e, node, index) { if (index === 0) { node.$el.prependTo(node.parent.$el.find('>' + Selectors.rules_list)); } else { node.$el.insertAfter(node.parent.rules[index - 1].$el); } self.refreshGroupsConditions(); }, 'move': function(e, node, group, index) { node.$el.detach(); if (index === 0) { node.$el.prependTo(group.$el.find('>' + Selectors.rules_list)); } else { node.$el.insertAfter(group.rules[index - 1].$el); } self.refreshGroupsConditions(); }, 'update': function(e, node, field, value, oldValue) { if (node instanceof Rule) { switch (field) { case 'error': self.displayError(node); break; case 'flags': self.applyRuleFlags(node); break; case 'filter': self.updateRuleFilter(node); break; case 'operator': self.updateRuleOperator(node, oldValue); break; case 'value': self.updateRuleValue(node); break; } } else { switch (field) { case 'error': self.displayError(node); break; case 'flags': self.applyGroupFlags(node); break; case 'condition': self.updateGroupCondition(node); break; } } } }); }; /** * Create the root group * @param addRule {bool,optional} add a default empty rule * @param data {mixed,optional} group custom data * @param flags {object,optional} flags to apply to the group * @return group {Root} */ QueryBuilder.prototype.setRoot = function(addRule, data, flags) { addRule = (addRule === undefined || addRule === true); var group_id = this.nextGroupId(); var $group = $(this.getGroupTemplate(group_id, 1)); this.$el.append($group); this.model.root = new Group(null, $group); this.model.root.model = this.model; this.model.root.data = data; this.model.root.flags = $.extend({}, this.settings.default_group_flags, flags); this.trigger('afterAddGroup', this.model.root); this.model.root.condition = this.settings.default_condition; if (addRule) { this.addRule(this.model.root); } return this.model.root; }; /** * Add a new group * @param parent {Group} * @param addRule {bool,optional} add a default empty rule * @param data {mixed,optional} group custom data * @param flags {object,optional} flags to apply to the group * @return group {Group} */ QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) { addRule = (addRule === undefined || addRule === true); var level = parent.level + 1; var e = this.trigger('beforeAddGroup', parent, addRule, level); if (e.isDefaultPrevented()) { return null; } var group_id = this.nextGroupId(); var $group = $(this.getGroupTemplate(group_id, level)); var model = parent.addGroup($group); model.data = data; model.flags = $.extend({}, this.settings.default_group_flags, flags); this.trigger('afterAddGroup', model); model.condition = this.settings.default_condition; if (addRule) { this.addRule(model); } return model; }; /** * Tries to delete a group. The group is not deleted if at least one rule is no_delete. * @param group {Group} * @return {boolean} true if the group has been deleted */ QueryBuilder.prototype.deleteGroup = function(group) { if (group.isRoot()) { return false; } var e = this.trigger('beforeDeleteGroup', group); if (e.isDefaultPrevented()) { return false; } var del = true; group.each('reverse', function(rule) { del&= this.deleteRule(rule); }, function(group) { del&= this.deleteGroup(group); }, this); if (del) { group.drop(); this.trigger('afterDeleteGroup'); } return del; }; /** * Changes the condition of a group * @param group {Group} */ QueryBuilder.prototype.updateGroupCondition = function(group) { group.$el.find('>' + Selectors.group_condition).each(function() { var $this = $(this); $this.prop('checked', $this.val() === group.condition); $this.parent().toggleClass('active', $this.val() === group.condition); }); this.trigger('afterUpdateGroupCondition', group); }; /** * Update visibility of conditions based on number of rules inside each group */ QueryBuilder.prototype.refreshGroupsConditions = function() { (function walk(group) { if (!group.flags || (group.flags && !group.flags.condition_readonly)) { group.$el.find('>' + Selectors.group_condition).prop('disabled', group.rules.length <= 1) .parent().toggleClass('disabled', group.rules.length <= 1); } group.each(function(rule) {}, function(group) { walk(group); }, this); }(this.model.root)); }; /** * Add a new rule * @param parent {Group} * @param data {mixed,optional} rule custom data * @param flags {object,optional} flags to apply to the rule * @return rule {Rule} */ QueryBuilder.prototype.addRule = function(parent, data, flags) { var e = this.trigger('beforeAddRule', parent); if (e.isDefaultPrevented()) { return null; } var rule_id = this.nextRuleId(); var $rule = $(this.getRuleTemplate(rule_id)); var model = parent.addRule($rule); if (data !== undefined) { model.data = data; } model.flags = $.extend({}, this.settings.default_rule_flags, flags); this.trigger('afterAddRule', model); this.createRuleFilters(model); if (this.settings.default_filter || !this.settings.display_empty_filter) { model.filter = this.getFilterById(this.settings.default_filter || this.filters[0].id); } return model; }; /** * Delete a rule. * @param rule {Rule} * @return {boolean} true if the rule has been deleted */ QueryBuilder.prototype.deleteRule = function(rule) { if (rule.flags.no_delete) { return false; } var e = this.trigger('beforeDeleteRule', rule); if (e.isDefaultPrevented()) { return false; } rule.drop(); this.trigger('afterDeleteRule'); return true; }; /** * Create the filters <select> for a rule * @param rule {Rule} */ QueryBuilder.prototype.createRuleFilters = function(rule) { var filters = this.change('getRuleFilters', this.filters, rule); var $filterSelect = $(this.getRuleFilterSelect(rule, filters)); rule.$el.find(Selectors.filter_container).html($filterSelect); this.trigger('afterCreateRuleFilters', rule); }; /** * Create the operators <select> for a rule and init the rule operator * @param rule {Rule} */ QueryBuilder.prototype.createRuleOperators = function(rule) { var $operatorContainer = rule.$el.find(Selectors.operator_container).empty(); if (!rule.filter) { return; } var operators = this.getOperators(rule.filter); var $operatorSelect = $(this.getRuleOperatorSelect(rule, operators)); $operatorContainer.html($operatorSelect); // set the operator without triggering update event rule.__.operator = operators[0]; this.trigger('afterCreateRuleOperators', rule, operators); }; /** * Create the main input for a rule * @param rule {Rule} */ QueryBuilder.prototype.createRuleInput = function(rule) { var $valueContainer = rule.$el.find(Selectors.value_container).empty(); rule.__.value = undefined; if (!rule.filter || !rule.operator || rule.operator.nb_inputs === 0) { return; } var self = this; var $inputs = $(); var filter = rule.filter; for (var i = 0; i < rule.operator.nb_inputs; i++) { var $ruleInput = $(this.getRuleInput(rule, i)); if (i > 0) $valueContainer.append(this.settings.inputs_separator); $valueContainer.append($ruleInput); $inputs = $inputs.add($ruleInput); } $valueContainer.show(); $inputs.on('change ' + (filter.input_event || ''), function() { self.status.updating_value = true; rule.value = self.getRuleValue(rule); self.status.updating_value = false; }); if (filter.plugin) { $inputs[filter.plugin](filter.plugin_config || {}); } this.trigger('afterCreateRuleInput', rule); if (filter.default_value !== undefined) { rule.value = filter.default_value; } else { self.status.updating_value = true; rule.value = self.getRuleValue(rule); self.status.updating_value = false; } }; /** * Perform action when rule's filter is changed * @param rule {Rule} */ QueryBuilder.prototype.updateRuleFilter = function(rule) { this.createRuleOperators(rule); this.createRuleInput(rule); rule.$el.find(Selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1'); this.trigger('afterUpdateRuleFilter', rule); }; /** * Update main <input> visibility when rule operator changes * @param rule {Rule} * @param previousOperator {object} */ QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) { var $valueContainer = rule.$el.find(Selectors.value_container); if (!rule.operator || rule.operator.nb_inputs === 0) { $valueContainer.hide(); rule.__.value = undefined; } else { $valueContainer.show(); if ($valueContainer.is(':empty') || rule.operator.nb_inputs !== previousOperator.nb_inputs) { this.createRuleInput(rule); } } if (rule.operator) { rule.$el.find(Selectors.rule_operator).val(rule.operator.type); } this.trigger('afterUpdateRuleOperator', rule); }; /** * Perform action when rule's value is changed * @param rule {Rule} */ QueryBuilder.prototype.updateRuleValue = function(rule) { if (!this.status.updating_value) { this.setRuleValue(rule, rule.value); } this.trigger('afterUpdateRuleValue', rule); }; /** * Change rules properties depending on flags. * @param rule {Rule} */ QueryBuilder.prototype.applyRuleFlags = function(rule) { var flags = rule.flags; if (flags.filter_readonly) { rule.$el.find(Selectors.rule_filter).prop('disabled', true); } if (flags.operator_readonly) { rule.$el.find(Selectors.rule_operator).prop('disabled', true); } if (flags.value_readonly) { rule.$el.find(Selectors.rule_value).prop('disabled', true); } if (flags.no_delete) { rule.$el.find(Selectors.delete_rule).remove(); } this.trigger('afterApplyRuleFlags', rule); }; /** * Change group properties depending on flags. * @param group {Group} */ QueryBuilder.prototype.applyGroupFlags = function(group) { var flags = group.flags; if (flags.condition_readonly) { group.$el.find('>' + Selectors.group_condition).prop('disabled', true) .parent().addClass('readonly'); } if (flags.no_delete) { group.$el.find(Selectors.delete_group).remove(); } this.trigger('afterApplyGroupFlags', group); }; /** * Clear all errors markers * @param node {Node,optional} default is root Group */ QueryBuilder.prototype.clearErrors = function(node) { node = node || this.model.root; if (!node) { return; } node.error = null; if (node instanceof Group) { node.each(function(rule) { rule.error = null; }, function(group) { this.clearErrors(group); }, this); } }; /** * Add/Remove class .has-error and update error title * @param node {Node} */ QueryBuilder.prototype.displayError = function(node) { if (this.settings.display_errors) { if (node.error === null) { node.$el.removeClass('has-error'); } else { // translate the text without modifying event array var error = $.extend([], node.error, [ this.lang.errors[node.error[0]] || node.error[0] ]); node.$el.addClass('has-error') .find(Selectors.error_container).eq(0) .attr('title', Utils.fmt.apply(null, error)); } } }; /** * Trigger a validation error event * @param node {Node} * @param error {array} * @param value {mixed} */ QueryBuilder.prototype.triggerValidationError = function(node, error, value) { if (!$.isArray(error)) { error = [error]; } var e = this.trigger('validationError', node, error, value); if (!e.isDefaultPrevented()) { node.error = error; } }; /** * Destroy the plugin */ QueryBuilder.prototype.destroy = function() { this.trigger('beforeDestroy'); if (this.status.generated_id) { this.$el.removeAttr('id'); } this.clear(); this.model = null; this.$el .off('.queryBuilder') .removeClass('query-builder') .removeData('queryBuilder'); delete this.$el[0].queryBuilder; }; /** * Reset the plugin */ QueryBuilder.prototype.reset = function() { this.status.group_id = 1; this.status.rule_id = 0; this.model.root.empty(); this.addRule(this.model.root); this.trigger('afterReset'); }; /** * Clear the plugin */ QueryBuilder.prototype.clear = function() { this.status.group_id = 0; this.status.rule_id = 0; if (this.model.root) { this.model.root.drop(); this.model.root = null; } this.trigger('afterClear'); }; /** * Modify the builder configuration * Only options defined in QueryBuilder.modifiable_options are modifiable * @param {object} */ QueryBuilder.prototype.setOptions = function(options) { // use jQuery utils to filter options keys $.makeArray($(Object.keys(options)).filter(QueryBuilder.modifiable_options)) .forEach(function(opt) { this.settings[opt] = options[opt]; }, this); }; /** * Return the model associated to a DOM object, or root model * @param {jQuery,optional} * @return {Node} */ QueryBuilder.prototype.getModel = function(target) { return !target ? this.model.root : Model(target); }; /** * Validate the whole builder * @return {boolean} */ QueryBuilder.prototype.validate = function() { this.clearErrors(); var self = this; var valid = (function parse(group) { var done = 0; var errors = 0; group.each(function(rule) { if (!rule.filter) { self.triggerValidationError(rule, 'no_filter', null); errors++; return; } if (rule.operator.nb_inputs !== 0) { var valid = self.validateValue(rule, rule.value); if (valid !== true) { self.triggerValidationError(rule, valid, rule.value); errors++; return; } } done++; }, function(group) { if (parse(group)) { done++; } else { errors++; } }); if (errors > 0) { return false; } else if (done === 0 && (!self.settings.allow_empty || !group.isRoot())) { self.triggerValidationError(group, 'empty_group', null); return false; } return true; }(this.model.root)); return this.change('validate', valid); }; /** * Get an object representing current rules * @param {object} options * - get_flags: false[default] | true(only changes from default flags) | 'all' * @return {object} */ QueryBuilder.prototype.getRules = function(options) { options = $.extend({ get_flags: false }, options); if (!this.validate()) { return {}; } var self = this; var out = (function parse(group) { var data = { condition: group.condition, rules: [] }; if (group.data) { data.data = $.extendext(true, 'replace', {}, group.data); } if (options.get_flags) { var flags = self.getGroupFlags(group.flags, options.get_flags === 'all'); if (!$.isEmptyObject(flags)) { data.flags = flags; } } group.each(function(model) { var value = null; if (model.operator.nb_inputs !== 0) { value = model.value; } var rule = { id: model.filter.id, field: model.filter.field, type: model.filter.type, input: model.filter.input, operator: model.operator.type, value: value }; if (model.filter.data || model.data) { rule.data = $.extendext(true, 'replace', {}, model.filter.data, model.data); } if (options.get_flags) { var flags = self.getRuleFlags(model.flags, options.get_flags === 'all'); if (!$.isEmptyObject(flags)) { rule.flags = flags; } } data.rules.push(rule); }, function(model) { data.rules.push(parse(model)); }); return data; }(this.model.root)); return this.change('getRules', out); }; /** * Set rules from object * @throws RulesError, UndefinedConditionError * @param data {object} */ QueryBuilder.prototype.setRules = function(data) { if ($.isArray(data)) { data = { condition: this.settings.default_condition, rules: data }; } if (!data || !data.rules || (data.rules.length === 0 && !this.settings.allow_empty)) { Utils.error('RulesParse', 'Incorrect data object passed'); } this.clear(); this.setRoot(false, data.data, this.parseGroupFlags(data)); data = this.change('setRules', data); var self = this; (function add(data, group) { if (group === null) { return; } if (data.condition === undefined) { data.condition = self.settings.default_condition; } else if (self.settings.conditions.indexOf(data.condition) == -1) { Utils.error('UndefinedCondition', 'Invalid condition "{0}"', data.condition); } group.condition = data.condition; data.rules.forEach(function(item) { var model; if (item.rules && item.rules.length > 0) { 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.id === undefined) { Utils.error('RulesParse', 'Missing rule field id'); } if (item.operator === undefined) { item.operator = 'equal'; } model = self.addRule(group, item.data); if (model === null) { return; } model.filter = self.getFilterById(item.id); model.operator = self.getOperatorByType(item.operator); model.flags = self.parseRuleFlags(item); if (model.operator.nb_inputs !== 0 && item.value !== undefined) { model.value = item.value; } } }); }(data, this.model.root)); }; /** * Check if a value is correct for a filter * @param rule {Rule} * @param value {string|string[]|undefined} * @return {array|true} */ QueryBuilder.prototype.validateValue = function(rule, value) { var validation = rule.filter.validation || {}; var result = true; if (validation.callback) { result = validation.callback.call(this, value, rule); } else { result = this.validateValueInternal(rule, value); } return this.change('validateValue', result, value, rule); }; /** * Default validation function * @throws ConfigError * @param rule {Rule} * @param value {string|string[]|undefined} * @return {array|true} */ QueryBuilder.prototype.validateValueInternal = function(rule, value) { var filter = rule.filter; var operator = rule.operator; var validation = filter.validation || {}; var result = true; var tmp; if (rule.operator.nb_inputs === 1) { value = [value]; } else { value = value; } for (var i = 0; i < operator.nb_inputs; i++) { switch (filter.input) { case 'radio': if (value[i] === undefined) { result = ['radio_empty']; break; } break; case 'checkbox': if (value[i] === undefined || value[i].length === 0) { result = ['checkbox_empty']; break; } else if (!operator.multiple && value[i].length > 1) { result = ['operator_not_multiple', operator.type]; break; } break; case 'select': if (filter.multiple) { if (value[i] === undefined || value[i].length === 0 || (filter.placeholder && value[i] == filter.placeholder_value)) { result = ['select_empty']; break; } else if (!operator.multiple && value[i].length > 1) { result = ['operator_not_multiple', operator.type]; break; } } else { if (value[i] === undefined || (filter.placeholder && value[i] == filter.placeholder_value)) { result = ['select_empty']; break; } } break; default: switch (QueryBuilder.types[filter.type]) { case 'string': if (value[i] === undefined || value[i].length === 0) { result = ['string_empty']; break; } if (validation.min !== undefined) { if (value[i].length < parseInt(validation.min)) { result = ['string_exceed_min_length', validation.min]; break; } } if (validation.max !== undefined) { if (value[i].length > parseInt(validation.max)) { result = ['string_exceed_max_length', validation.max]; break; } } if (validation.format) { if (typeof validation.format == 'string') { validation.format = new RegExp(validation.format); } if (!validation.format.test(value[i])) { result = ['string_invalid_format', validation.format]; break; } } break; case 'number': if (value[i] === undefined || isNaN(value[i])) { result = ['number_nan']; break; } if (filter.type == 'integer') { if (parseInt(value[i]) != value[i]) { result = ['number_not_integer']; break; } } else { if (parseFloat(value[i]) != value[i]) { result = ['number_not_double']; break; } } if (validation.min !== undefined) { if (value[i] < parseFloat(validation.min)) { result = ['number_exceed_min', validation.min]; break; } } if (validation.max !== undefined) { if (value[i] > parseFloat(validation.max)) { result = ['number_exceed_max', validation.max]; break; } } if (validation.step !== undefined && validation.step !== 'any') { var v = (value[i] / validation.step).toPrecision(14); if (parseInt(v) != v) { result = ['number_wrong_step', validation.step]; break; } } break; case 'datetime': if (value[i] === undefined || value[i].length === 0) { result = ['datetime_empty']; break; } // we need MomentJS if (validation.format) { if (!('moment' in window)) { Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com'); } var datetime = moment(value[i], validation.format); if (!datetime.isValid()) { result = ['datetime_invalid', validation.format]; break; } else { if (validation.min) { if (datetime < moment(validation.min, validation.format)) { result = ['datetime_exceed_min', validation.min]; break; } } if (validation.max) { if (datetime > moment(validation.max, validation.format)) { result = ['datetime_exceed_max', validation.max]; break; } } } } break; case 'boolean': tmp = value[i].trim().toLowerCase(); if (tmp !== 'true' && tmp !== 'false' && tmp !== '1' && tmp !== '0' && value[i] !== 1 && value[i] !== 0) { result = ['boolean_not_valid']; break; } } } if (result !== true) { break; } } return result; }; /** * Returns an incremented group ID * @return {string} */ QueryBuilder.prototype.nextGroupId = function() { return this.status.id + '_group_' + (this.status.group_id++); }; /** * Returns an incremented rule ID * @return {string} */ QueryBuilder.prototype.nextRuleId = function() { return this.status.id + '_rule_' + (this.status.rule_id++); }; /** * Returns the operators for a filter * @param filter {string|object} (filter id name or filter object) * @return {object[]} */ QueryBuilder.prototype.getOperators = function(filter) { if (typeof filter == 'string') { filter = this.getFilterById(filter); } var result = []; for (var i = 0, l = this.operators.length; i < l; i++) { // filter operators check if (filter.operators) { if (filter.operators.indexOf(this.operators[i].type) == -1) { continue; } } // type check else if (this.operators[i].apply_to.indexOf(QueryBuilder.types[filter.type]) == -1) { continue; } result.push(this.operators[i]); } // keep sort order defined for the filter if (filter.operators) { result.sort(function(a, b) { return filter.operators.indexOf(a.type) - filter.operators.indexOf(b.type); }); } return this.change('getOperators', result, filter); }; /** * Returns a particular filter by its id * @throws UndefinedFilterError * @param filterId {string} * @return {object|null} */ QueryBuilder.prototype.getFilterById = function(id) { 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]; } } Utils.error('UndefinedFilter', 'Undefined filter "{0}"', id); }; /** * Return a particular operator by its type * @throws UndefinedOperatorError * @param type {string} * @return {object|null} */ QueryBuilder.prototype.getOperatorByType = function(type) { if (type == '-1') { return null; } for (var i = 0, l = this.operators.length; i < l; i++) { if (this.operators[i].type == type) { return this.operators[i]; } } Utils.error('UndefinedOperator', 'Undefined operator "{0}"', type); }; /** * Returns rule value * @param rule {Rule} * @return {mixed} */ QueryBuilder.prototype.getRuleValue = function(rule) { var filter = rule.filter; var operator = rule.operator; var value = []; if (filter.valueGetter) { value = filter.valueGetter.call(this, rule); } else { var $value = rule.$el.find(Selectors.value_container); for (var i = 0; i < operator.nb_inputs; i++) { var name = Utils.escapeElementId(rule.id + '_value_' + i); var tmp; switch (filter.input) { case 'radio': value.push($value.find('[name=' + name + ']:checked').val()); break; case 'checkbox': tmp = []; $value.find('[name=' + name + ']:checked').each(function() { tmp.push($(this).val()); }); value.push(tmp); break; case 'select': if (filter.multiple) { tmp = []; $value.find('[name=' + name + '] option:selected').each(function() { tmp.push($(this).val()); }); value.push(tmp); } else { value.push($value.find('[name=' + name + '] option:selected').val()); } break; default: value.push($value.find('[name=' + name + ']').val()); } } if (operator.nb_inputs === 1) { value = value[0]; } // @deprecated if (filter.valueParser) { value = filter.valueParser.call(this, rule, value); } } return this.change('getRuleValue', value, rule); }; /** * Sets the value of a rule. * @param rule {Rule} * @param value {mixed} */ QueryBuilder.prototype.setRuleValue = function(rule, value) { var filter = rule.filter; var operator = rule.operator; if (filter.valueSetter) { filter.valueSetter.call(this, rule, value); } else { var $value = rule.$el.find(Selectors.value_container); if (operator.nb_inputs == 1) { value = [value]; } else { value = value; } for (var i = 0; i < operator.nb_inputs; i++) { var name = Utils.escapeElementId(rule.id + '_value_' + i); switch (filter.input) { case 'radio': $value.find('[name=' + name + '][value="' + value[i] + '"]').prop('checked', true).trigger('change'); break; case 'checkbox': if (!$.isArray(value[i])) { value[i] = [value[i]]; } value[i].forEach(function(value) { $value.find('[name=' + name + '][value="' + value + '"]').prop('checked', true).trigger('change'); }); break; default: $value.find('[name=' + name + ']').val(value[i]).trigger('change'); break; } } } }; /** * Clean rule flags. * @param rule {object} * @return {object} */ QueryBuilder.prototype.parseRuleFlags = function(rule) { var flags = $.extend({}, this.settings.default_rule_flags); if (rule.readonly) { $.extend(flags, { filter_readonly: true, operator_readonly: true, value_readonly: true, no_delete: true }); } if (rule.flags) { $.extend(flags, rule.flags); } return this.change('parseRuleFlags', flags, rule); }; /** * Get a copy of flags of a rule. * @param {object} flags * @param {boolean} all - true to return all flags, false to return only changes from default * @returns {object} */ QueryBuilder.prototype.getRuleFlags = function(flags, all) { if (all) { return $.extend({}, flags); } else { var ret = {}; $.each(this.settings.default_rule_flags, function(key, value) { if (flags[key] !== value) { ret[key] = flags[key]; } }); return ret; } }; /** * Clean group flags. * @param group {object} * @return {object} */ QueryBuilder.prototype.parseGroupFlags = function(group) { var flags = $.extend({}, this.settings.default_group_flags); if (group.readonly) { $.extend(flags, { condition_readonly: true, no_delete: true }); } if (group.flags) { $.extend(flags, group.flags); } return this.change('parseGroupFlags', flags, group); }; /** * Get a copy of flags of a group. * @param {object} flags * @param {boolean} all - true to return all flags, false to return only changes from default * @returns {object} */ QueryBuilder.prototype.getGroupFlags = function(flags, all) { if (all) { return $.extend({}, flags); } else { var ret = {}; $.each(this.settings.default_group_flags, function(key, value) { if (flags[key] !== value) { ret[key] = flags[key]; } }); return ret; } }; /** * Translate a label * @param label {string|object} * @return string */ QueryBuilder.prototype.translateLabel = function(label) { return typeof label == 'object' ? (label[this.settings.lang_code] || label['en']) : label; }; QueryBuilder.templates.group = '\ <dl id="{{= it.group_id }}" class="rules-group-container"> \ <dt class="rules-group-header"> \ <div class="btn-group pull-right group-actions"> \ <button type="button" class="btn btn-xs btn-success" data-add="rule"> \ <i class="{{= it.icons.add_rule }}"></i> {{= it.lang.add_rule }} \ </button> \ {{? it.settings.allow_groups===-1 || it.settings.allow_groups>=it.level }} \ <button type="button" class="btn btn-xs btn-success" data-add="group"> \ <i class="{{= it.icons.add_group }}"></i> {{= it.lang.add_group }} \ </button> \ {{?}} \ {{? it.level>1 }} \ <button type="button" class="btn btn-xs btn-danger" data-delete="group"> \ <i class="{{= it.icons.remove_group }}"></i> {{= it.lang.delete_group }} \ </button> \ {{?}} \ </div> \ <div class="btn-group group-conditions"> \ {{~ it.conditions: condition }} \ <label class="btn btn-xs btn-primary"> \ <input type="radio" name="{{= it.group_id }}_cond" value="{{= condition }}"> {{= it.lang.conditions[condition] || condition }} \ </label> \ {{~}} \ </div> \ {{? it.settings.display_errors }} \ <div class="error-container"><i class="{{= it.icons.error }}"></i></div> \ {{?}} \ </dt> \ <dd class=rules-group-body> \ <ul class=rules-list></ul> \ </dd> \ </dl>'; QueryBuilder.templates.rule = '\ <li id="{{= it.rule_id }}" class="rule-container"> \ <div class="rule-header"> \ <div class="btn-group pull-right rule-actions"> \ <button type="button" class="btn btn-xs btn-danger" data-delete="rule"> \ <i class="{{= it.icons.remove_rule }}"></i> {{= it.lang.delete_rule }} \ </button> \ </div> \ </div> \ {{? it.settings.display_errors }} \ <div class="error-container"><i class="{{= it.icons.error }}"></i></div> \ {{?}} \ <div class="rule-filter-container"></div> \ <div class="rule-operator-container"></div> \ <div class="rule-value-container"></div> \ </li>'; QueryBuilder.templates.filterSelect = '\ {{ var optgroup = null; }} \ <select class="form-control" name="{{= it.rule.id }}_filter"> \ {{? it.settings.display_empty_filter }} \ <option value="-1">{{= it.settings.select_placeholder }}</option> \ {{?}} \ {{~ it.filters: filter }} \ {{? optgroup !== filter.optgroup }} \ {{? optgroup !== null }}</optgroup>{{?}} \ {{? (optgroup = filter.optgroup) !== null }} \ <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \ {{?}} \ {{?}} \ <option value="{{= filter.id }}">{{= it.translate(filter.label) }}</option> \ {{~}} \ {{? optgroup !== null }}</optgroup>{{?}} \ </select>'; QueryBuilder.templates.operatorSelect = '\ {{ var optgroup = null; }} \ <select class="form-control" name="{{= it.rule.id }}_operator"> \ {{~ it.operators: operator }} \ {{? optgroup !== operator.optgroup }} \ {{? optgroup !== null }}</optgroup>{{?}} \ {{? (optgroup = operator.optgroup) !== null }} \ <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \ {{?}} \ {{?}} \ <option value="{{= operator.type }}">{{= it.lang.operators[operator.type] || operator.type }}</option> \ {{~}} \ {{? optgroup !== null }}</optgroup>{{?}} \ </select>'; /** * Returns group HTML * @param group_id {string} * @param level {int} * @return {string} */ QueryBuilder.prototype.getGroupTemplate = function(group_id, level) { var h = this.templates.group({ builder: this, group_id: group_id, level: level, conditions: this.settings.conditions, icons: this.icons, lang: this.lang, settings: this.settings }); return this.change('getGroupTemplate', h, level); }; /** * Returns rule HTML * @param rule_id {string} * @return {string} */ QueryBuilder.prototype.getRuleTemplate = function(rule_id) { var h = this.templates.rule({ builder: this, rule_id: rule_id, icons: this.icons, lang: this.lang, settings: this.settings }); return this.change('getRuleTemplate', h); }; /** * Returns rule filter <select> HTML * @param rule {Rule} * @param filters {array} * @return {string} */ QueryBuilder.prototype.getRuleFilterSelect = function(rule, filters) { var h = this.templates.filterSelect({ builder: this, rule: rule, filters: filters, icons: this.icons, lang: this.lang, settings: this.settings, translate: this.translateLabel }); return this.change('getRuleFilterSelect', h, rule); }; /** * Returns rule operator <select> HTML * @param rule {Rule} * @param operators {object} * @return {string} */ QueryBuilder.prototype.getRuleOperatorSelect = function(rule, operators) { var h = this.templates.operatorSelect({ builder: this, rule: rule, operators: operators, icons: this.icons, lang: this.lang, settings: this.settings, translate: this.translateLabel }); return this.change('getRuleOperatorSelect', h, rule); }; /** * Return the rule value HTML * @param rule {Rule} * @param filter {object} * @param value_id {int} * @return {string} */ QueryBuilder.prototype.getRuleInput = function(rule, value_id) { var filter = rule.filter; var validation = rule.filter.validation || {}; var name = rule.id + '_value_' + value_id; var c = filter.vertical ? ' class=block' : ''; var h = ''; if (typeof filter.input == 'function') { h = filter.input.call(this, rule, name); } else { switch (filter.input) { case 'radio': case 'checkbox': Utils.iterateOptions(filter.values, function(key, val) { h+= '<label' + c + '><input type="' + filter.input + '" name="' + name + '" value="' + key + '"> ' + val + '</label> '; }); break; case 'select': h+= '<select class="form-control" name="' + name + '"' + (filter.multiple ? ' multiple' : '') + '>'; if (filter.placeholder) { h+= '<option value="' + filter.placeholder_value + '" disabled selected>' + filter.placeholder + '</option>'; } Utils.iterateOptions(filter.values, function(key, val) { h+= '<option value="' + key + '">' + val + '</option> '; }); h+= '</select>'; break; case 'textarea': h+= '<textarea class="form-control" name="' + name + '"'; if (filter.size) h+= ' cols="' + filter.size + '"'; if (filter.rows) h+= ' rows="' + filter.rows + '"'; if (validation.min !== undefined) h+= ' minlength="' + validation.min + '"'; if (validation.max !== undefined) h+= ' maxlength="' + validation.max + '"'; if (filter.placeholder) h+= ' placeholder="' + filter.placeholder + '"'; h+= '></textarea>'; break; default: switch (QueryBuilder.types[filter.type]) { case 'number': h+= '<input class="form-control" type="number" name="' + name + '"'; if (validation.step !== undefined) h+= ' step="' + validation.step + '"'; if (validation.min !== undefined) h+= ' min="' + validation.min + '"'; if (validation.max !== undefined) h+= ' max="' + validation.max + '"'; if (filter.placeholder) h+= ' placeholder="' + filter.placeholder + '"'; if (filter.size) h+= ' size="' + filter.size + '"'; h+= '>'; break; default: h+= '<input class="form-control" type="text" name="' + name + '"'; if (filter.placeholder) h+= ' placeholder="' + filter.placeholder + '"'; if (filter.type === 'string' && validation.min !== undefined) h+= ' minlength="' + validation.min + '"'; if (filter.type === 'string' && validation.max !== undefined) h+= ' maxlength="' + validation.max + '"'; if (filter.size) h+= ' size="' + filter.size + '"'; if(filter.type!='date'){ h+= ' onkeyup="setcon(this,event)">'; if(filter.validcode!="null"){ h+=' <a href="javascript:sysHelp('+"'"+name+"'"+','+"'"+filter.validcode+"'"+')"><img src="../IMAGES/BUTTON/MAIN/SEARCH20.gif" alt="Help" name="helpimage6" id="helpimage6" width="20" height="20" border="0" align="absmiddle"></a>'; }else{ h+=' <button type="button" class="btn btn-primary" id="conditionsingle" value="'+filter.help+','+name+','+filter.linkhelp+','+"FALSE"+'">Search</button>'; h+=' <button type="button" class="btn btn-primary" id="conditionmulti" value="'+filter.help+','+name+','+filter.linkhelp+','+"TRUE"+'">Multi-Search</button>'; } }else{ h+= ' onkeyup="formatDatereport(this);setcon(this,event)" onkeypress="chkInteger(event);" onblur="chkInputDatereport(this);">'; h+=' <a href="#" onmouseout="MM_swapImgRestore()" onclick="viewCalSTD('+"'"+name+"'"+')"><img src="../IMAGES/BUTTON/MAIN/CALENDAR20.gif" alt="Calendar" name="Image1101" width="20" height="20" border="0" align="absmiddle" id="Image1101"></a>'; } } } } return this.change('getRuleInput', h, rule, name); }; // Model CLASS // =============================== /** * Main object storing data model and emitting events * --------- * Access Node object stored in jQuery objects * @param el {jQuery|Node} * @return {Node} */ function Model(el) { if (!(this instanceof Model)) { return Model.getModel(el); } this.root = null; this.$ = $(this); } $.extend(Model.prototype, { trigger: function(type) { this.$.triggerHandler(type, Array.prototype.slice.call(arguments, 1)); return this; }, on: function() { this.$.on.apply(this.$, Array.prototype.slice.call(arguments)); return this; }, off: function() { this.$.off.apply(this.$, Array.prototype.slice.call(arguments)); return this; }, once: function() { this.$.one.apply(this.$, Array.prototype.slice.call(arguments)); return this; } }); /** * Access Node object stored in jQuery objects * @param el {jQuery|Node} * @return {Node} */ Model.getModel = function(el) { if (!el) { return null; } else if (el instanceof Node) { return el; } else { return $(el).data('queryBuilderModel'); } }; /* * Define Node properties with getter and setter * Update events are emitted in the setter through root Model (if any) */ function defineModelProperties(obj, fields) { fields.forEach(function(field) { Object.defineProperty(obj.prototype, field, { enumerable: true, get: function() { return this.__[field]; }, set: function(value) { var oldValue = (this.__[field] !== null && typeof this.__[field] == 'object') ? $.extend({}, this.__[field]) : this.__[field]; this.__[field] = value; if (this.model !== null) { this.model.trigger('update', this, field, value, oldValue); } } }); }); } // Node abstract CLASS // =============================== /** * @param {Node} * @param {jQuery} */ var Node = function(parent, $el) { if (!(this instanceof Node)) { return new Node(); } Object.defineProperty(this, '__', { value: {} }); $el.data('queryBuilderModel', this); this.__.level = 1; this.__.error = null; this.__.data = undefined; this.$el = $el; this.id = $el[0].id; this.model = null; this.parent = parent; }; defineModelProperties(Node, ['level', 'error', 'data', 'flags']); Object.defineProperty(Node.prototype, 'parent', { enumerable: true, get: function() { return this.__.parent; }, set: function(value) { this.__.parent = value; this.level = value === null ? 1 : value.level + 1; this.model = value === null ? null : value.model; } }); /** * Check if this Node is the root * @return {boolean} */ Node.prototype.isRoot = function() { return (this.level === 1); }; /** * Return node position inside parent * @return {int} */ Node.prototype.getPos = function() { if (this.isRoot()) { return -1; } else { return this.parent.getNodePos(this); } }; /** * Delete self */ Node.prototype.drop = function() { var model = this.model; if (!this.isRoot()) { this.parent._removeNode(this); } if (model !== null) { model.trigger('drop', this); } }; /** * Move itself after another Node * @param {Node} * @return {Node} self */ Node.prototype.moveAfter = function(node) { if (this.isRoot()) return; this._move(node.parent, node.getPos() + 1); return this; }; /** * Move itself at the beginning of parent or another Group * @param {Group,optional} * @return {Node} self */ Node.prototype.moveAtBegin = function(target) { if (this.isRoot()) return; if (target === undefined) { target = this.parent; } this._move(target, 0); return this; }; /** * Move itself at the end of parent or another Group * @param {Group,optional} * @return {Node} self */ Node.prototype.moveAtEnd = function(target) { if (this.isRoot()) return; if (target === undefined) { target = this.parent; } this._move(target, target.length() - 1); return this; }; /** * Move itself at specific position of Group * @param {Group} * @param {int} */ Node.prototype._move = function(group, index) { this.parent._removeNode(this); group._appendNode(this, index, false); if (this.model !== null) { this.model.trigger('move', this, group, index); } }; // GROUP CLASS // =============================== /** * @param {Group} * @param {jQuery} */ var Group = function(parent, $el) { if (!(this instanceof Group)) { return new Group(parent, $el); } Node.call(this, parent, $el); this.rules = []; this.__.condition = null; }; Group.prototype = Object.create(Node.prototype); Group.prototype.constructor = Group; defineModelProperties(Group, ['condition']); /** * Empty the Group */ Group.prototype.empty = function() { this.each('reverse', function(rule) { rule.drop(); }, function(group) { group.drop(); }); }; /** * Delete self */ Group.prototype.drop = function() { this.empty(); Node.prototype.drop.call(this); }; /** * Return the number of children * @return {int} */ Group.prototype.length = function() { return this.rules.length; }; /** * Add a Node at specified index * @param {Node} * @param {int,optional} * @param {boolean,optional} * @return {Node} the inserted node */ Group.prototype._appendNode = function(node, index, trigger) { if (index === undefined) { index = this.length(); } this.rules.splice(index, 0, node); node.parent = this; if (trigger && this.model !== null) { this.model.trigger('add', node, index); } return node; }; /** * Add a Group by jQuery element at specified index * @param {jQuery} * @param {int,optional} * @return {Group} the inserted group */ Group.prototype.addGroup = function($el, index) { return this._appendNode(new Group(this, $el), index, true); }; /** * Add a Rule by jQuery element at specified index * @param {jQuery} * @param {int,optional} * @return {Rule} the inserted rule */ Group.prototype.addRule = function($el, index) { return this._appendNode(new Rule(this, $el), index, true); }; /** * Delete a specific Node * @param {Node} * @return {Group} self */ Group.prototype._removeNode = function(node) { var index = this.getNodePos(node); if (index !== -1) { node.parent = null; this.rules.splice(index, 1); } return this; }; /** * Return position of a child Node * @param {Node} * @return {int} */ Group.prototype.getNodePos = function(node) { return this.rules.indexOf(node); }; /** * Iterate over all Nodes * @param {boolean,optional} iterate in reverse order, required if you delete nodes * @param {function} callback for Rules * @param {function,optional} callback for Groups * @return {boolean} */ Group.prototype.each = function(reverse, cbRule, cbGroup, context) { if (typeof reverse == 'function') { context = cbGroup; cbGroup = cbRule; cbRule = reverse; reverse = false; } context = context === undefined ? null : context; var i = reverse ? this.rules.length - 1 : 0; var l = reverse ? 0 : this.rules.length - 1; var c = reverse ? -1 : 1; var next = function() { return reverse ? i >= l : i <= l; }; var stop = false; for (; next(); i+= c) { if (this.rules[i] instanceof Group) { if (cbGroup !== undefined) { stop = cbGroup.call(context, this.rules[i]) === false; } } else { stop = cbRule.call(context, this.rules[i]) === false; } if (stop) { break; } } return !stop; }; /** * Return true if the group contains a particular Node * @param {Node} * @param {boolean,optional} recursive search * @return {boolean} */ Group.prototype.contains = function(node, deep) { if (this.getNodePos(node) !== -1) { return true; } else if (!deep) { return false; } else { // the loop will return with false as soon as the Node is found return !this.each(function(rule) { return true; }, function(group) { return !group.contains(node, true); }); } }; // RULE CLASS // =============================== /** * @param {Group} * @param {jQuery} */ var Rule = function(parent, $el) { if (!(this instanceof Rule)) { return new Rule(parent, $el); } Node.call(this, parent, $el); this.__.filter = null; this.__.operator = null; this.__.flags = {}; this.__.value = undefined; }; Rule.prototype = Object.create(Node.prototype); Rule.prototype.constructor = Rule; defineModelProperties(Rule, ['filter', 'operator', 'value']); // EXPORT // =============================== QueryBuilder.Group = Group; QueryBuilder.Rule = Rule; var Utils = QueryBuilder.utils = {}; /** * Utility to iterate over radio/checkbox/selection options. * it accept three formats: array of values, map, array of 1-element maps * * @param options {object|array} * @param tpl {callable} (takes key and text) */ Utils.iterateOptions = function(options, tpl) { if (options) { if ($.isArray(options)) { options.forEach(function(entry) { // array of one-element maps if ($.isPlainObject(entry)) { $.each(entry, function(key, val) { tpl(key, val); return false; // break after first entry }); } // array of values else { tpl(entry, entry); } }); } // unordered map else { $.each(options, function(key, val) { tpl(key, val); }); } } }; /** * Replaces {0}, {1}, ... in a string * @param str {string} * @param args,... {mixed} * @return {string} */ Utils.fmt = function(str/*, args*/) { var args = Array.prototype.slice.call(arguments, 1); return str.replace(/{([0-9]+)}/g, function(m, i) { return args[parseInt(i)]; }); }; /** * Throw an Error object with custom name * @param type {string} * @param message {string} * @param args,... {mixed} */ Utils.error = function(type, message/*, args*/) { var err = new Error(Utils.fmt.apply(null, Array.prototype.slice.call(arguments, 1))); err.name = type + 'Error'; err.args = Array.prototype.slice.call(arguments, 2); throw err; }; /** * Change type of a value to int or float * @param value {mixed} * @param type {string} 'integer', 'double' or anything else * @param boolAsInt {boolean} return 0 or 1 for booleans * @return {mixed} */ Utils.changeType = function(value, type, boolAsInt) { switch (type) { case 'integer': return parseInt(value); case 'double': return parseFloat(value); case 'boolean': var bool = value.trim().toLowerCase() === 'true' || value.trim() === '1' || value === 1; return boolAsInt ? (bool ? 1 : 0) : bool; default: return value; } }; /** * Escape string like mysql_real_escape_string * @param value {string} * @return {string} */ Utils.escapeString = function(value) { if (typeof value != 'string') { return value; } return value .replace(/[\0\n\r\b\\\'\"]/g, function(s) { switch (s) { case '\0': return '\\0'; case '\n': return '\\n'; case '\r': return '\\r'; case '\b': return '\\b'; default: return '\\' + s; } }) // uglify compliant .replace(/\t/g, '\\t') .replace(/\x1a/g, '\\Z'); }; /** * Escape value for use in regex * @param value {string} * @return {string} */ Utils.escapeRegExp = function(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); }; /** * Escape HTML element id * @param value {string} * @return {string} */ Utils.escapeElementId = function(str) { // Regex based on that suggested by: // https://learn.jquery.com/using-jquery-core/faq/how-do-i-select-an-element-by-an-id-that-has-characters-used-in-css-notation/ // - escapes : . [ ] , // - avoids escaping already escaped values return (str) ? str.replace(/(\\)?([:.\[\],])/g, function( $0, $1, $2 ) { return $1 ? $0 : '\\' + $2; }) : str; }; /** * Sort objects by grouping them by {key}, preserving initial order when possible * @param {object[]} items * @param {string} key * @returns {object[]} */ Utils.groupSort = function(items, key) { var optgroups = []; var newItems = []; items.forEach(function(item) { var idx; if (item[key]) { idx = optgroups.lastIndexOf(item[key]); if (idx == -1) { idx = optgroups.length; } else { idx++; } } else { idx = optgroups.length; } optgroups.splice(idx, 0, item[key]); newItems.splice(idx, 0, item); }); return newItems; }; $.fn.queryBuilder = function(option) { if (this.length > 1) { Utils.error('Config', 'Unable to initialize on multiple target'); } var data = this.data('queryBuilder'); var options = (typeof option == 'object' && option) || {}; if (!data && option == 'destroy') { return this; } if (!data) { this.data('queryBuilder', new QueryBuilder(this, options)); } if (typeof option == 'string') { return data[option].apply(data, Array.prototype.slice.call(arguments, 1)); } return this; }; $.fn.queryBuilder.constructor = QueryBuilder; $.fn.queryBuilder.defaults = QueryBuilder.defaults; $.fn.queryBuilder.extend = QueryBuilder.extend; $.fn.queryBuilder.define = QueryBuilder.define; $.fn.queryBuilder.regional = QueryBuilder.regional; /*! * jQuery QueryBuilder Awesome Bootstrap Checkbox * Applies Awesome Bootstrap Checkbox for checkbox and radio inputs. */ QueryBuilder.define('bt-checkbox', function(options) { if (options.font == 'glyphicons') { var injectCSS = document.createElement('style'); injectCSS.innerHTML = '\ .checkbox input[type=checkbox]:checked + label:after { \ font-family: "Glyphicons Halflings"; \ content: "\\e013"; \ } \ .checkbox label:after { \ padding-left: 4px; \ padding-top: 2px; \ font-size: 9px; \ }'; document.body.appendChild(injectCSS); } this.on('getRuleInput.filter', function(h, rule, name) { var filter = rule.filter; if ((filter.input === 'radio' || filter.input === 'checkbox') && !filter.plugin) { h.value = ''; if (!filter.colors) { filter.colors = {}; } if (filter.color) { filter.colors._def_ = filter.color; } var style = filter.vertical ? ' style="display:block"' : ''; var i = 0; Utils.iterateOptions(filter.values, function(key, val) { var color = filter.colors[key] || filter.colors._def_ || options.color; var id = name + '_' + (i++); h.value+= '\ <div' + style + ' class="' + filter.input + ' ' + filter.input + '-' + color + '"> \ <input type="' + filter.input + '" name="' + name + '" id="' + id + '" value="' + key + '"> \ <label for="' + id + '">' + val + '</label> \ </div>'; }); } }); }, { font: 'glyphicons', color: 'default' }); /*! * jQuery QueryBuilder Bootstrap Selectpicker * Applies Bootstrap Select on filters and operators combo-boxes. */ /** * @throws ConfigError */ QueryBuilder.define('bt-selectpicker', function(options) { if (!$.fn.selectpicker || !$.fn.selectpicker.Constructor) { Utils.error('MissingLibrary', 'Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select'); } // init selectpicker this.on('afterCreateRuleFilters', function(e, rule) { rule.$el.find(Selectors.rule_filter).removeClass('form-control').selectpicker(options); }); this.on('afterCreateRuleOperators', function(e, rule) { rule.$el.find(Selectors.rule_operator).removeClass('form-control').selectpicker(options); }); // update selectpicker on change this.on('afterUpdateRuleFilter', function(e, rule) { rule.$el.find(Selectors.rule_filter).selectpicker('render'); }); this.on('afterUpdateRuleOperator', function(e, rule) { rule.$el.find(Selectors.rule_operator).selectpicker('render'); }); }, { container: 'body', style: 'btn-inverse btn-xs', width: 'auto', showIcon: false }); /*! * jQuery QueryBuilder Bootstrap Tooltip errors * Applies Bootstrap Tooltips on validation error messages. */ /** * @throws ConfigError */ QueryBuilder.define('bt-tooltip-errors', function(options) { if (!$.fn.tooltip || !$.fn.tooltip.Constructor || !$.fn.tooltip.Constructor.prototype.fixTitle) { Utils.error('MissingLibrary', 'Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com'); } var self = this; // add BT Tooltip data this.on('getRuleTemplate.filter getGroupTemplate.filter', function(h) { var $h = $(h.value); $h.find(Selectors.error_container).attr('data-toggle', 'tooltip'); h.value = $h.prop('outerHTML'); }); // init/refresh tooltip when title changes this.model.on('update', function(e, node, field) { if (field == 'error' && self.settings.display_errors) { node.$el.find(Selectors.error_container).eq(0) .tooltip(options) .tooltip('hide') .tooltip('fixTitle'); } }); }, { placement: 'right' }); /*! * jQuery QueryBuilder Change Filters * Allows to change available filters after plugin initialization. */ QueryBuilder.extend({ /** * Change the filters of the builder * @throws ChangeFilterError * @param {boolean,optional} delete rules using old filters * @param {object[]} new filters */ setFilters: function(delete_orphans, filters) { var self = this; if (filters === undefined) { filters = delete_orphans; delete_orphans = false; } filters = this.checkFilters(filters); filters = this.change('setFilters', filters); var filtersIds = filters.map(function(filter) { return filter.id; }); // check for orphans if (!delete_orphans) { (function checkOrphans(node) { node.each( function(rule) { if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) { Utils.error('ChangeFilter', 'A rule is using filter "{0}"', rule.filter.id); } }, checkOrphans ); }(this.model.root)); } // replace filters this.filters = filters; // apply on existing DOM (function updateBuilder(node) { node.each(true, function(rule) { if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) { rule.drop(); } else { self.createRuleFilters(rule); rule.$el.find(Selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1'); } }, updateBuilder ); }(this.model.root)); // update plugins if (this.settings.plugins) { if (this.settings.plugins['unique-filter']) { this.updateDisabledFilters(); } if (this.settings.plugins['bt-selectpicker']) { this.$el.find(Selectors.rule_filter).selectpicker('render'); } } // reset the default_filter if does not exist anymore if (this.settings.default_filter) { try { this.getFilterById(this.settings.default_filter); } catch (e) { this.settings.default_filter = null; } } this.trigger('afterSetFilters', filters); }, /** * Adds a new filter to the builder * @param {object|object[]} the new filter * @param {mixed,optional} numeric index or '#start' or '#end' */ addFilter: function(new_filters, position) { if (position === undefined || position == '#end') { position = this.filters.length; } else if (position == '#start') { position = 0; } if (!$.isArray(new_filters)) { new_filters = [new_filters]; } var filters = $.extend(true, [], this.filters); // numeric position if (parseInt(position) == position) { Array.prototype.splice.apply(filters, [position, 0].concat(new_filters)); } else { // after filter by its id if (this.filters.some(function(filter, index) { if (filter.id == position) { position = index + 1; return true; } })) { Array.prototype.splice.apply(filters, [position, 0].concat(new_filters)); } // defaults to end of list else { Array.prototype.push.apply(filters, new_filters); } } this.setFilters(filters); }, /** * Removes a filter from the builder * @param {string|string[]} the filter id * @param {boolean,optional} delete rules using old filters */ removeFilter: function(filter_ids, delete_orphans) { var filters = $.extend(true, [], this.filters); if (typeof filter_ids === 'string') { filter_ids = [filter_ids]; } filters = filters.filter(function(filter) { return filter_ids.indexOf(filter.id) === -1; }); this.setFilters(delete_orphans, filters); } }); /*! * jQuery QueryBuilder Filter Description * Provides three ways to display a description about a filter: inline, Bootsrap Popover or Bootbox. */ /** * @throws ConfigError */ QueryBuilder.define('filter-description', function(options) { /** * INLINE */ if (options.mode === 'inline') { this.on('afterUpdateRuleFilter', function(e, rule) { var $p = rule.$el.find('p.filter-description'); if (!rule.filter || !rule.filter.description) { $p.hide(); } else { if ($p.length === 0) { $p = $('<p class="filter-description"></p>'); $p.appendTo(rule.$el); } else { $p.show(); } $p.html('<i class="' + options.icon + '"></i> ' + rule.filter.description); } }); } /** * POPOVER */ else if (options.mode === 'popover') { if (!$.fn.popover || !$.fn.popover.Constructor || !$.fn.popover.Constructor.prototype.fixTitle) { Utils.error('MissingLibrary', 'Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com'); } this.on('afterUpdateRuleFilter', function(e, rule) { var $b = rule.$el.find('button.filter-description'); if (!rule.filter || !rule.filter.description) { $b.hide(); if ($b.data('bs.popover')) { $b.popover('hide'); } } else { if ($b.length === 0) { $b = $('<button type="button" class="btn btn-xs btn-info filter-description" data-toggle="popover"><i class="' + options.icon + '"></i></button>'); $b.prependTo(rule.$el.find(Selectors.rule_actions)); $b.popover({ placement: 'left', container: 'body', html: true }); $b.on('mouseout', function() { $b.popover('hide'); }); } else { $b.show(); } $b.data('bs.popover').options.content = rule.filter.description; if ($b.attr('aria-describedby')) { $b.popover('show'); } } }); } /** * BOOTBOX */ else if (options.mode === 'bootbox') { if (!('bootbox' in window)) { Utils.error('MissingLibrary', 'Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com'); } this.on('afterUpdateRuleFilter', function(e, rule) { var $b = rule.$el.find('button.filter-description'); if (!rule.filter || !rule.filter.description) { $b.hide(); } else { if ($b.length === 0) { $b = $('<button type="button" class="btn btn-xs btn-info filter-description" data-toggle="bootbox"><i class="' + options.icon + '"></i></button>'); $b.prependTo(rule.$el.find(Selectors.rule_actions)); $b.on('click', function() { bootbox.alert($b.data('description')); }); } $b.data('description', rule.filter.description); } }); } }, { icon: 'glyphicon glyphicon-info-sign', mode: 'popover' }); /*! * jQuery QueryBuilder Invert * Allows to invert a rule operator, a group condition or the entire builder. */ QueryBuilder.defaults({ operatorOpposites: { 'equal': 'not_equal', 'not_equal': 'equal', 'in': 'not_in', 'not_in': 'in', 'less': 'greater_or_equal', 'less_or_equal': 'greater', 'greater': 'less_or_equal', 'greater_or_equal': 'less', 'between': 'not_between', 'not_between': 'between', 'begins_with': 'not_begins_with', 'not_begins_with': 'begins_with', 'contains': 'not_contains', 'not_contains': 'contains', 'ends_with': 'not_ends_with', 'not_ends_with': 'ends_with', 'is_empty': 'is_not_empty', 'is_not_empty': 'is_empty', 'is_null': 'is_not_null', 'is_not_null': 'is_null' }, conditionOpposites: { 'AND': 'OR', 'OR': 'AND' } }); QueryBuilder.define('invert', function(options) { var self = this; /** * Bind events */ this.on('afterInit', function() { self.$el.on('click.queryBuilder', '[data-invert=group]', function() { var $group = $(this).closest(Selectors.group_container); self.invert(Model($group), options); }); if (options.display_rules_button && options.invert_rules) { self.$el.on('click.queryBuilder', '[data-invert=rule]', function() { var $rule = $(this).closest(Selectors.rule_container); self.invert(Model($rule), options); }); } }); /** * Modify templates */ this.on('getGroupTemplate.filter', function(h, level) { var $h = $(h.value); $h.find(Selectors.condition_container).after('<button type="button" class="btn btn-xs btn-default" data-invert="group"><i class="' + options.icon + '"></i> ' + self.lang.invert + '</button>'); h.value = $h.prop('outerHTML'); }); if (options.display_rules_button && options.invert_rules) { this.on('getRuleTemplate.filter', function(h) { var $h = $(h.value); $h.find(Selectors.rule_actions).prepend('<button type="button" class="btn btn-xs btn-default" data-invert="rule"><i class="' + options.icon + '"></i> ' + self.lang.invert + '</button>'); h.value = $h.prop('outerHTML'); }); } }, { icon: 'glyphicon glyphicon-random', recursive: true, invert_rules: true, display_rules_button: false, silent_fail: false }); QueryBuilder.extend({ /** * Invert a Group, a Rule or the whole builder * @throws InvertConditionError, InvertOperatorError * @param {Node,optional} * @param {object,optional} */ invert: function(node, options) { if (!(node instanceof Node)) { if (!this.model.root) return; options = node; node = this.model.root; } if (typeof options != 'object') options = {}; if (options.recursive === undefined) options.recursive = true; if (options.invert_rules === undefined) options.invert_rules = true; if (options.silent_fail === undefined) options.silent_fail = false; if (options.trigger === undefined) options.trigger = true; if (node instanceof Group) { // invert group condition if (this.settings.conditionOpposites[node.condition]) { node.condition = this.settings.conditionOpposites[node.condition]; } else if (!options.silent_fail) { Utils.error('InvertCondition', 'Unknown inverse of condition "{0}"', node.condition); } // recursive call if (options.recursive) { var tempOpts = $.extend({}, options, { trigger: false }); node.each(function(rule) { if (options.invert_rules) { this.invert(rule, tempOpts); } }, function(group) { this.invert(group, tempOpts); }, this); } } else if (node instanceof Rule) { if (node.operator && !node.filter.no_invert) { // invert rule operator if (this.settings.operatorOpposites[node.operator.type]) { var invert = this.settings.operatorOpposites[node.operator.type]; // check if the invert is "authorized" if (!node.filter.operators || node.filter.operators.indexOf(invert) != -1) { node.operator = this.getOperatorByType(invert); } } else if (!options.silent_fail) { Utils.error('InvertOperator', 'Unknown inverse of operator "{0}"', node.operator.type); } } } if (options.trigger) { this.trigger('afterInvert', node, options); } } }); /*! * jQuery QueryBuilder MongoDB Support * Allows to export rules as a MongoDB find object as well as populating the builder from a MongoDB object. */ // DEFAULT CONFIG // =============================== QueryBuilder.defaults({ mongoOperators: { equal: function(v) { return v[0]; }, not_equal: function(v) { return { '$ne': v[0] }; }, in: function(v) { return { '$in': v }; }, not_in: function(v) { return { '$nin': v }; }, less: function(v) { return { '$lt': v[0] }; }, less_or_equal: function(v) { return { '$lte': v[0] }; }, greater: function(v) { return { '$gt': v[0] }; }, greater_or_equal: function(v) { return { '$gte': v[0] }; }, between: function(v) { return { '$gte': v[0], '$lte': v[1] }; }, not_between: function(v) { return { '$lt': v[0], '$gt': v[1] }; }, begins_with: function(v) { return { '$regex': '^' + Utils.escapeRegExp(v[0]) }; }, not_begins_with: function(v) { return { '$regex': '^(?!' + Utils.escapeRegExp(v[0]) + ')' }; }, contains: function(v) { return { '$regex': Utils.escapeRegExp(v[0]) }; }, not_contains: function(v) { return { '$regex': '^((?!' + Utils.escapeRegExp(v[0]) + ').)*$', '$options': 's' }; }, ends_with: function(v) { return { '$regex': Utils.escapeRegExp(v[0]) + '$' }; }, not_ends_with: function(v) { return { '$regex': '(?<!' + Utils.escapeRegExp(v[0]) + ')$' }; }, is_empty: function(v) { return ''; }, is_not_empty: function(v) { return { '$ne': '' }; }, is_null: function(v) { return null; }, is_not_null: function(v) { return { '$ne': null }; } }, mongoRuleOperators: { $ne: function(v) { v = v.$ne; return { 'val': v, 'op': v === null ? 'is_not_null' : (v === '' ? 'is_not_empty' : 'not_equal') }; }, eq: function(v) { return { 'val': v, 'op': v === null ? 'is_null' : (v === '' ? 'is_empty' : 'equal') }; }, $regex: function(v) { v = v.$regex; if (v.slice(0, 4) == '^(?!' && v.slice(-1) == ')') { return { 'val': v.slice(4, -1), 'op': 'not_begins_with' }; } else if (v.slice(0, 5) == '^((?!' && v.slice(-5) == ').)*$') { return { 'val': v.slice(5, -5), 'op': 'not_contains' }; } else if (v.slice(0, 4) == '(?<!' && v.slice(-2) == ')$') { return { 'val': v.slice(4, -2), 'op': 'not_ends_with' }; } else if (v.slice(-1) == '$') { return { 'val': v.slice(0, -1), 'op': 'ends_with' }; } else if (v.slice(0, 1) == '^') { return { 'val': v.slice(1), 'op': 'begins_with' }; } else { return { 'val': v, 'op': 'contains' }; } }, between: function(v) { return { 'val': [v.$gte, v.$lte], 'op': 'between' }; }, not_between: function(v) { return { 'val': [v.$lt, v.$gt], 'op': 'not_between' }; }, $in: function(v) { return { 'val': v.$in, 'op': 'in' }; }, $nin: function(v) { return { 'val': v.$nin, 'op': 'not_in' }; }, $lt: function(v) { return { 'val': v.$lt, 'op': 'less' }; }, $lte: function(v) { return { 'val': v.$lte, 'op': 'less_or_equal' }; }, $gt: function(v) { return { 'val': v.$gt, 'op': 'greater' }; }, $gte: function(v) { return { 'val': v.$gte, 'op': 'greater_or_equal' }; } } }); // PUBLIC METHODS // =============================== QueryBuilder.extend({ /** * Get rules as MongoDB query * @throws UndefinedMongoConditionError, UndefinedMongoOperatorError * @param data {object} (optional) rules * @return {object} */ getMongo: function(data) { data = (data === undefined) ? this.getRules() : data; var self = this; return (function parse(data) { if (!data.condition) { data.condition = self.settings.default_condition; } if (['AND', 'OR'].indexOf(data.condition.toUpperCase()) === -1) { Utils.error('UndefinedMongoCondition', 'Unable to build MongoDB query with condition "{0}"', data.condition); } if (!data.rules) { return {}; } var parts = []; data.rules.forEach(function(rule) { if (rule.rules && rule.rules.length > 0) { parts.push(parse(rule)); } else { var mdb = self.settings.mongoOperators[rule.operator]; var ope = self.getOperatorByType(rule.operator); var values = []; if (mdb === undefined) { Utils.error('UndefinedMongoOperator', 'Unknown MongoDB operation for operator "{0}"', rule.operator); } if (ope.nb_inputs !== 0) { if (!(rule.value instanceof Array)) { rule.value = [rule.value]; } rule.value.forEach(function(v) { values.push(Utils.changeType(v, rule.type, false)); }); } var part = {}; part[rule.field] = mdb.call(self, values); parts.push(part); } }); var res = {}; if (parts.length > 0) { res['$' + data.condition.toLowerCase()] = parts; } return res; }(data)); }, /** * Convert MongoDB object to rules * @throws MongoParseError, UndefinedMongoConditionError, UndefinedMongoOperatorError * @param data {object} query object * @return {object} */ getRulesFromMongo: function(data) { if (data === undefined || data === null) { return null; } var self = this; var conditions = { '$and': 'AND', '$or': 'OR' }; return (function parse(data) { var topKeys = Object.keys(data); if (topKeys.length > 1) { Utils.error('MongoParse', 'Invalid MongoDB query format'); } if (!conditions[topKeys[0].toLowerCase()]) { Utils.error('UndefinedMongoCondition', 'Unable to build MongoDB query with condition "{0}"', topKeys[0]); } var rules = data[topKeys[0]]; var parts = []; rules.forEach(function(rule) { var keys = Object.keys(rule); if (conditions[keys[0].toLowerCase()]) { parts.push(parse(rule)); } else { var field = keys[0]; var value = rule[field]; var operator = determineMongoOperator(value, field); if (operator === undefined) { Utils.error('MongoParse', 'Invalid MongoDB query format'); } var mdbrl = self.settings.mongoRuleOperators[operator]; if (mdbrl === undefined) { Utils.error('UndefinedMongoOperator', 'JSON Rule operation unknown for operator "{0}"', operator); } var opVal = mdbrl.call(self, value); parts.push({ id: self.change('getMongoDBFieldID', field, value), field: field, operator: opVal.op, value: opVal.val }); } }); var res = {}; if (parts.length > 0) { res.condition = conditions[topKeys[0].toLowerCase()]; res.rules = parts; } return res; }(data)); }, /** * Set rules from MongoDB object * @param data {object} */ setRulesFromMongo: function(data) { this.setRules(this.getRulesFromMongo(data)); } }); /** * Find which operator is used in a MongoDB sub-object * @param {mixed} value * @param {string} field * @return {string|undefined} */ function determineMongoOperator(value, field) { if (value !== null && typeof value == 'object') { var subkeys = Object.keys(value); if (subkeys.length === 1) { return subkeys[0]; } else { if (value.$gte !== undefined && value.$lte !== undefined) { return 'between'; } if (value.$lt !== undefined && value.$gt !== undefined) { return 'not_between'; } else if (value.$regex !== undefined) { // optional $options return '$regex'; } else { return; } } } else { return 'eq'; } } /*! * jQuery QueryBuilder Sortable * Enables drag & drop sort of rules. */ Selectors.rule_and_group_containers = Selectors.rule_container + ', ' + Selectors.group_container; QueryBuilder.define('sortable', function(options) { /** * Init HTML5 drag and drop */ this.on('afterInit', function(e) { // configure jQuery to use dataTransfer $.event.props.push('dataTransfer'); var placeholder; var src; var self = e.builder; // only add "draggable" attribute when hovering drag handle // preventing text select bug in Firefox self.$el.on('mouseover.queryBuilder', '.drag-handle', function() { self.$el.find(Selectors.rule_and_group_containers).attr('draggable', true); }); self.$el.on('mouseout.queryBuilder', '.drag-handle', function() { self.$el.find(Selectors.rule_and_group_containers).removeAttr('draggable'); }); // dragstart: create placeholder and hide current element self.$el.on('dragstart.queryBuilder', '[draggable]', function(e) { e.stopPropagation(); // notify drag and drop (only dummy text) e.dataTransfer.setData('text', 'drag'); src = Model(e.target); // Chrome glitchs // - helper invisible if hidden immediately // - "dragend" is called immediately if we modify the DOM directly setTimeout(function() { var ph = $('<div class="rule-placeholder"> </div>'); ph.css('min-height', src.$el.height()); placeholder = src.parent.addRule(ph, src.getPos()); src.$el.hide(); }, 0); }); // dragenter: move the placeholder self.$el.on('dragenter.queryBuilder', '[draggable]', function(e) { e.preventDefault(); e.stopPropagation(); if (placeholder) { moveSortableToTarget(placeholder, $(e.target)); } }); // dragover: prevent glitches self.$el.on('dragover.queryBuilder', '[draggable]', function(e) { e.preventDefault(); e.stopPropagation(); }); // drop: move current element self.$el.on('drop.queryBuilder', function(e) { e.preventDefault(); e.stopPropagation(); moveSortableToTarget(src, $(e.target)); }); // dragend: show current element and delete placeholder self.$el.on('dragend.queryBuilder', '[draggable]', function(e) { e.preventDefault(); e.stopPropagation(); src.$el.show(); placeholder.drop(); self.$el.find(Selectors.rule_and_group_containers).removeAttr('draggable'); self.trigger('afterMove', src); src = placeholder = null; }); }); /** * Remove drag handle from non-sortable rules */ this.on('parseRuleFlags.filter', function(flags) { if (flags.value.no_sortable === undefined) { flags.value.no_sortable = options.default_no_sortable; } }); this.on('afterApplyRuleFlags', function(e, rule) { if (rule.flags.no_sortable) { rule.$el.find('.drag-handle').remove(); } }); /** * Remove drag handle from non-sortable groups */ this.on('parseGroupFlags.filter', function(flags) { if (flags.value.no_sortable === undefined) { flags.value.no_sortable = options.default_no_sortable; } }); this.on('afterApplyGroupFlags', 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) { var $h = $(h.value); $h.find(Selectors.condition_container).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>'); h.value = $h.prop('outerHTML'); } }); this.on('getRuleTemplate.filter', function(h) { var $h = $(h.value); $h.find(Selectors.rule_header).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>'); h.value = $h.prop('outerHTML'); }); }, { default_no_sortable: false, icon: 'glyphicon glyphicon-sort' }); /** * Move an element (placeholder or actual object) depending on active target * @param {Node} * @param {jQuery} */ function moveSortableToTarget(element, target) { var parent; // on rule parent = target.closest(Selectors.rule_container); if (parent.length) { element.moveAfter(Model(parent)); return; } // on group header parent = target.closest(Selectors.group_header); if (parent.length) { parent = target.closest(Selectors.group_container); element.moveAtBegin(Model(parent)); return; } // on group parent = target.closest(Selectors.group_container); if (parent.length) { element.moveAtEnd(Model(parent)); return; } } /*! * jQuery QueryBuilder SQL Support * Allows to export rules as a SQL WHERE statement as well as populating the builder from an SQL query. */ // DEFAULT CONFIG // =============================== QueryBuilder.defaults({ /* operators for internal -> SQL conversion */ sqlOperators: { equal: { op: '= ?' }, not_equal: { op: '!= ?' }, in: { op: 'IN(?)', sep: ', ' }, not_in: { op: 'NOT IN(?)', sep: ', ' }, less: { op: '< ?' }, less_or_equal: { op: '<= ?' }, greater: { op: '> ?' }, greater_or_equal: { op: '>= ?' }, between: { op: 'BETWEEN ?', sep: ' AND ' }, not_between: { op: 'NOT BETWEEN ?', sep: ' AND ' }, begins_with: { op: 'LIKE(?)', mod: '{0}%' }, not_begins_with: { op: 'NOT LIKE(?)', mod: '{0}%' }, contains: { op: 'LIKE(?)', mod: '%{0}%' }, not_contains: { op: 'NOT LIKE(?)', mod: '%{0}%' }, ends_with: { op: 'LIKE(?)', mod: '%{0}' }, not_ends_with: { op: 'NOT LIKE(?)', mod: '%{0}' }, is_empty: { op: '= \'\'' }, is_not_empty: { op: '!= \'\'' }, is_null: { op: 'IS NULL' }, is_not_null: { op: 'IS NOT NULL' } }, /* operators for SQL -> internal conversion */ sqlRuleOperator: { '=': function(v) { return { val: v, op: v === '' ? 'is_empty' : 'equal' }; }, '!=': function(v) { return { val: v, op: v === '' ? 'is_not_empty' : 'not_equal' }; }, 'LIKE': function(v) { if (v.slice(0, 1) == '%' && v.slice(-1) == '%') { return { val: v.slice(1, -1), op: 'contains' }; } else if (v.slice(0, 1) == '%') { return { val: v.slice(1), op: 'ends_with' }; } else if (v.slice(-1) == '%') { return { val: v.slice(0, -1), op: 'begins_with' }; } else { Utils.error('SQLParse', 'Invalid value for LIKE operator "{0}"', v); } }, 'IN': function(v) { return { val: v, op: 'in' }; }, 'NOT IN': function(v) { return { val: v, op: 'not_in' }; }, '<': function(v) { return { val: v, op: 'less' }; }, '<=': function(v) { return { val: v, op: 'less_or_equal' }; }, '>': function(v) { return { val: v, op: 'greater' }; }, '>=': function(v) { return { val: v, op: 'greater_or_equal' }; }, 'BETWEEN': function(v) { return { val: v, op: 'between' }; }, 'NOT BETWEEN': function(v) { return { val: v, op: 'not_between' }; }, 'IS': function(v) { if (v !== null) { Utils.error('SQLParse', 'Invalid value for IS operator'); } return { val: null, op: 'is_null' }; }, 'IS NOT': function(v) { if (v !== null) { Utils.error('SQLParse', 'Invalid value for IS operator'); } return { val: null, op: 'is_not_null' }; } }, /* statements for internal -> SQL conversion */ sqlStatements: { 'question_mark': function() { var params = []; return { add: function(rule, value) { params.push(value); return '?'; }, run: function() { return params; } }; }, 'numbered': function(char) { if (!char || char.length > 1) char = '$'; var index = 0; var params = []; return { add: function(rule, value) { params.push(value); index++; return char + index; }, run: function() { return params; } }; }, 'named': function(char) { if (!char || char.length > 1) char = ':'; var indexes = {}; var params = {}; return { add: function(rule, value) { if (!indexes[rule.field]) indexes[rule.field] = 1; var key = rule.field + '_' + (indexes[rule.field]++); params[key] = value; return char + key; }, run: function() { return params; } }; } }, /* statements for SQL -> internal conversion */ sqlRuleStatement: { 'question_mark': function(values) { var index = 0; return { parse: function(v) { return v == '?' ? values[index++] : v; }, esc: function(sql) { return sql.replace(/\?/g, '\'?\''); } }; }, 'numbered': function(values, char) { if (!char || char.length > 1) char = '$'; var regex1 = new RegExp('^\\' + char + '[0-9]+$'); var regex2 = new RegExp('\\' + char + '([0-9]+)', 'g'); return { parse: function(v) { return regex1.test(v) ? values[v.slice(1) - 1] : v; }, esc: function(sql) { return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\''); } }; }, 'named': function(values, char) { if (!char || char.length > 1) char = ':'; var regex1 = new RegExp('^\\' + char); var regex2 = new RegExp('\\' + char + '(' + Object.keys(values).join('|') + ')', 'g'); return { parse: function(v) { return regex1.test(v) ? values[v.slice(1)] : v; }, esc: function(sql) { return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\''); } }; } } }); // PUBLIC METHODS // =============================== QueryBuilder.extend({ /** * Get rules as SQL query * @throws UndefinedSQLConditionError, UndefinedSQLOperatorError * @param stmt {boolean|string} use prepared statements - false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)' * @param nl {bool} output with new lines * @param data {object} (optional) rules * @return {object} */ getSQL: function(stmt, nl, data) { data = (data === undefined) ? this.getRules() : data; nl = (nl === true) ? '\n' : ' '; if (stmt === true) stmt = 'question_mark'; if (typeof stmt == 'string') { var config = getStmtConfig(stmt); stmt = this.settings.sqlStatements[config[1]](config[2]); } var self = this; var sql = (function parse(data) { if (!data.condition) { data.condition = self.settings.default_condition; } if (['AND', 'OR'].indexOf(data.condition.toUpperCase()) === -1) { Utils.error('UndefinedSQLCondition', 'Unable to build SQL query with condition "{0}"', data.condition); } if (!data.rules) { return ''; } var parts = []; data.rules.forEach(function(rule) { if (rule.rules && rule.rules.length > 0) { parts.push('(' + nl + parse(rule) + nl + ')' + nl); } else { var sql = self.settings.sqlOperators[rule.operator]; var ope = self.getOperatorByType(rule.operator); var value = ''; if (sql === undefined) { Utils.error('UndefinedSQLOperator', 'Unknown SQL operation for operator "{0}"', rule.operator); } if (ope.nb_inputs !== 0) { if (!(rule.value instanceof Array)) { rule.value = [rule.value]; } rule.value.forEach(function(v, i) { if (i > 0) { value+= sql.sep; } if (rule.type == 'integer' || rule.type == 'double' || rule.type == 'boolean') { v = Utils.changeType(v, rule.type, true); } else if (!stmt) { v = Utils.escapeString(v); } if (sql.mod) { v = Utils.fmt(sql.mod, v); } if (stmt) { value+= stmt.add(rule, v); } else { if (typeof v == 'string') { v = '\'' + v + '\''; } value+= v; } }); } parts.push(rule.field + ' ' + sql.op.replace(/\?/, value)); } }); return parts.join(' ' + data.condition + nl); }(data)); if (stmt) { return { sql: sql, params: stmt.run() }; } else { return { sql: sql }; } }, /** * Convert SQL to rules * @throws ConfigError, SQLParseError, UndefinedSQLOperatorError * @param data {object} query object * @param stmt {boolean|string} use prepared statements - false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)' * @return {object} */ getRulesFromSQL: function(data, stmt) { if (!('SQLParser' in window)) { Utils.error('MissingLibrary', 'SQLParser is required to parse SQL queries. Get it here https://github.com/mistic100/sql-parser'); } var self = this; if (typeof data == 'string') { data = { sql: data }; } if (stmt === true) stmt = 'question_mark'; if (typeof stmt == 'string') { var config = getStmtConfig(stmt); stmt = this.settings.sqlRuleStatement[config[1]](data.params, config[2]); } if (stmt) { data.sql = stmt.esc(data.sql); } if (data.sql.toUpperCase().indexOf('SELECT') !== 0) { data.sql = 'SELECT * FROM table WHERE ' + data.sql; } var parsed = SQLParser.parse(data.sql); if (!parsed.where) { Utils.error('SQLParse', 'No WHERE clause found'); } var out = { condition: this.settings.default_condition, rules: [] }; var curr = out; (function flatten(data, i) { // it's a node if (['AND', 'OR'].indexOf(data.operation.toUpperCase()) !== -1) { // create a sub-group if the condition is not the same and it's not the first level if (i > 0 && curr.condition != data.operation.toUpperCase()) { curr.rules.push({ condition: self.settings.default_condition, rules: [] }); curr = curr.rules[curr.rules.length - 1]; } curr.condition = data.operation.toUpperCase(); i++; // some magic ! var next = curr; flatten(data.left, i); curr = next; flatten(data.right, i); } // it's a leaf else { if (data.left.value === undefined || data.right.value === undefined) { Utils.error('SQLParse', 'Missing field and/or value'); } if ($.isPlainObject(data.right.value)) { Utils.error('SQLParse', 'Value format not supported for {0}.', data.left.value); } // convert array var value; if ($.isArray(data.right.value)) { value = data.right.value.map(function(v) { return v.value; }); } else { value = data.right.value; } // get actual values if (stmt) { if ($.isArray(value)) { value = value.map(stmt.parse); } else { value = stmt.parse(value); } } // convert operator var operator = data.operation.toUpperCase(); if (operator == '<>') operator = '!='; var sqlrl; if (operator == 'NOT LIKE') { sqlrl = self.settings.sqlRuleOperator['LIKE']; } else { sqlrl = self.settings.sqlRuleOperator[operator]; } if (sqlrl === undefined) { Utils.error('UndefinedSQLOperator', 'Invalid SQL operation "{0}".', data.operation); } var opVal = sqlrl.call(this, value, data.operation); if (operator == 'NOT LIKE') opVal.op = 'not_' + opVal.op; var left_value = data.left.values.join('.'); curr.rules.push({ id: self.change('getSQLFieldID', left_value, value), field: left_value, operator: opVal.op, value: opVal.val }); } }(parsed.where.conditions, 0)); return out; }, /** * Set rules from SQL * @param data {object} */ setRulesFromSQL: function(data, stmt) { this.setRules(this.getRulesFromSQL(data, stmt)); } }); function getStmtConfig(stmt) { var config = stmt.match(/(question_mark|numbered|named)(?:\((.)\))?/); if (!config) config = [null, 'question_mark', undefined]; return config; } /*! * jQuery QueryBuilder Unique Filter * Allows to define some filters as "unique": ie which can be used for only one rule, globally or in the same group. */ QueryBuilder.define('unique-filter', function() { this.status.used_filters = {}; this.on('afterUpdateRuleFilter', this.updateDisabledFilters); this.on('afterDeleteRule', this.updateDisabledFilters); this.on('afterCreateRuleFilters', this.applyDisabledFilters); this.on('afterReset', this.clearDisabledFilters); this.on('afterClear', this.clearDisabledFilters); }); QueryBuilder.extend({ updateDisabledFilters: function(e) { var self = e ? e.builder : this; self.status.used_filters = {}; if (!self.model) { return; } // get used filters (function walk(group) { group.each(function(rule) { if (rule.filter && rule.filter.unique) { if (!self.status.used_filters[rule.filter.id]) { self.status.used_filters[rule.filter.id] = []; } if (rule.filter.unique == 'group') { self.status.used_filters[rule.filter.id].push(rule.parent); } } }, function(group) { walk(group); }); }(self.model.root)); self.applyDisabledFilters(e); }, clearDisabledFilters: function(e) { var self = e ? e.builder : this; self.status.used_filters = {}; self.applyDisabledFilters(e); }, applyDisabledFilters: function(e) { var self = e ? e.builder : this; // re-enable everything self.$el.find(Selectors.filter_container + ' option').prop('disabled', false); // disable some $.each(self.status.used_filters, function(filterId, groups) { if (groups.length === 0) { self.$el.find(Selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true); } else { groups.forEach(function(group) { group.each(function(rule) { rule.$el.find(Selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true); }); }); } }); // update Selectpicker if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) { self.$el.find(Selectors.rule_filter).selectpicker('render'); } } }); /*! * jQuery QueryBuilder 2.3.3 * Locale: English (en) * Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr * Licensed under MIT (http://opensource.org/licenses/MIT) */ QueryBuilder.regional['en'] = { "__locale": "English (en)", "__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr", "add_rule": "Add rule", "add_group": "Add group", "delete_rule": "Delete", "delete_group": "Delete", "conditions": { "AND": "AND", "OR": "OR" }, "operators": { "equal": "equal", "not_equal": "not equal", "in": "in", "not_in": "not in", "less": "less", "less_or_equal": "less or equal", "greater": "greater", "greater_or_equal": "greater or equal", "between": "between", "not_between": "not between", "begins_with": "begins with", "not_begins_with": "doesn't begin with", "contains": "contains", "not_contains": "doesn't contain", "ends_with": "ends with", "not_ends_with": "doesn't end with", "is_empty": "is empty", "is_not_empty": "is not empty", "is_null": "is null", "is_not_null": "is not null" }, "errors": { "no_filter": "No filter selected", "empty_group": "The group is empty", "radio_empty": "No value selected", "checkbox_empty": "No value selected", "select_empty": "No value selected", "string_empty": "Empty value", "string_exceed_min_length": "Must contain at least {0} characters", "string_exceed_max_length": "Must not contain more than {0} characters", "string_invalid_format": "Invalid format ({0})", "number_nan": "Not a number", "number_not_integer": "Not an integer", "number_not_double": "Not a real number", "number_exceed_min": "Must be greater than {0}", "number_exceed_max": "Must be lower than {0}", "number_wrong_step": "Must be a multiple of {0}", "datetime_empty": "Empty value", "datetime_invalid": "Invalid date format ({0})", "datetime_exceed_min": "Must be after {0}", "datetime_exceed_max": "Must be before {0}", "boolean_not_valid": "Not a boolean", "operator_not_multiple": "Operator {0} cannot accept multiple values" }, "invert": "Invert" }; QueryBuilder.regional['th'] = { "__locale": "English (en)", "__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr", "add_rule": "Add rule", "add_group": "Add group", "delete_rule": "Delete", "delete_group": "Delete", "conditions": { "AND": "AND", "OR": "OR" }, "operators": { "equal": "เท่ากับ", "not_equal": "ไม่เท่ากับ", "in": "ในค่า", "not_in": "ไม่เท่ากับในค่า", "less": "น้อยกว่า", "less_or_equal": "น้อยกว่าหรือเท่ากับ", "greater": "มากกว่า", "greater_or_equal": "มากกว่าหรือเท่ากับ", "between": "between", "not_between": "not between", "begins_with": "begins with", "not_begins_with": "doesn't begin with", "contains": "เหมือนกับ", "not_contains": "doesn't contain", "ends_with": "ends with", "not_ends_with": "doesn't end with", "is_empty": "is empty", "is_not_empty": "is not empty", "is_null": "ค่าว่าง", "is_not_null": "ไม่ใช่ค่าว่าง" }, "errors": { "no_filter": "No filter selected", "empty_group": "The group is empty", "radio_empty": "No value selected", "checkbox_empty": "No value selected", "select_empty": "No value selected", "string_empty": "Empty value", "string_exceed_min_length": "Must contain at least {0} characters", "string_exceed_max_length": "Must not contain more than {0} characters", "string_invalid_format": "Invalid format ({0})", "number_nan": "Not a number", "number_not_integer": "Not an integer", "number_not_double": "Not a real number", "number_exceed_min": "Must be greater than {0}", "number_exceed_max": "Must be lower than {0}", "number_wrong_step": "Must be a multiple of {0}", "datetime_empty": "Empty value", "datetime_invalid": "Invalid date format ({0})", "datetime_exceed_min": "Must be after {0}", "datetime_exceed_max": "Must be before {0}", "boolean_not_valid": "Not a boolean", "operator_not_multiple": "Operator {0} cannot accept multiple values" }, "invert": "Invert" }; QueryBuilder.defaults({ lang_code: 'en' }); }));