/*!
* jQuery.extendext 1.0.0
*
* Copyright 2014-2019 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 if (typeof module === 'object' && module.exports) {
module.exports = factory(require('jquery'));
} 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();
if (
arrayMode !== 'concat' &&
arrayMode !== 'replace' &&
arrayMode !== 'extend'
) {
arrayMode = 'default';
}
// 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) {
copy = options[name];
// Prevent never-ending loop
if (name === '__proto__' || target === copy) {
continue;
}
// Recurse if we're merging plain objects or arrays
if (
deep &&
copy &&
($.isPlainObject(copy) || (copyIsArray = $.isArray(copy)))
) {
src = target[name];
// Ensure proper type for the source value
if (copyIsArray && !Array.isArray(src)) {
clone = [];
} else if (!copyIsArray && !$.isPlainObject(src)) {
clone = {};
} else {
clone = src;
}
copyIsArray = false;
// 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 = {
name: 'doT',
version: '1.1.1',
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
log: true,
},
_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');
})();
/* istanbul ignore else */
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) {
/* istanbul ignore else */
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.6.2
* Copyright 2014-2021 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
* Licensed under MIT (https://opensource.org/licenses/MIT)
*/
(function (root, factory) {
if (typeof define == 'function' && define.amd) {
define('query-builder', ['jquery', 'dot/doT', 'jquery-extendext'], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory(
require('jquery'),
require('dot/doT'),
require('jquery-extendext')
);
} else {
factory(root.jQuery, root.doT);
}
})(this, function ($, doT) {
'use strict';
/**
* @typedef {object} Filter
* @memberof QueryBuilder
* @description See {@link http://querybuilder.js.org/index.html#filters}
*/
/**
* @typedef {object} Operator
* @memberof QueryBuilder
* @description See {@link http://querybuilder.js.org/index.html#operators}
*/
/**
* @param {jQuery} $el
* @param {object} options - see {@link http://querybuilder.js.org/#options}
* @constructor
*/
var QueryBuilder = function ($el, options) {
$el[0].queryBuilder = this;
/**
* Element container
* @member {jQuery}
* @readonly
*/
this.$el = $el;
/**
* Configuration object
* @member {object}
* @readonly
*/
this.settings = $.extendext(
true,
'replace',
{},
QueryBuilder.DEFAULTS,
options
);
/**
* Internal model
* @member {Model}
* @readonly
*/
this.model = new Model();
/**
* Internal status
* @member {object}
* @property {string} id - id of the container
* @property {boolean} generated_id - if the container id has been generated
* @property {int} group_id - current group id
* @property {int} rule_id - current rule id
* @property {boolean} has_optgroup - if filters have optgroups
* @property {boolean} has_operator_optgroup - if operators have optgroups
* @readonly
* @private
*/
this.status = {
id: null,
generated_id: false,
group_id: 0,
rule_id: 0,
has_optgroup: false,
has_operator_optgroup: false,
};
/**
* List of filters
* @member {QueryBuilder.Filter[]}
* @readonly
*/
this.filters = this.settings.filters;
/**
* List of icons
* @member {object.<string, string>}
* @readonly
*/
this.icons = this.settings.icons;
/**
* List of operators
* @member {QueryBuilder.Operator[]}
* @readonly
*/
this.operators = this.settings.operators;
/**
* List of templates
* @member {object.<string, function>}
* @readonly
*/
this.templates = this.settings.templates;
/**
* Plugins configuration
* @member {object.<string, object>}
* @readonly
*/
this.plugins = this.settings.plugins;
/**
* Translations object
* @member {object}
* @readonly
*/
this.lang = null;
// 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
);
// "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;
}
// 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();
};
$.extend(
QueryBuilder.prototype,
/** @lends QueryBuilder.prototype */ {
/**
* Triggers an event on the builder container
* @param {string} type
* @returns {$.Event}
*/
trigger: function (type) {
var event = new $.Event(this._tojQueryEvent(type), {
builder: this,
});
this.$el.triggerHandler(
event,
Array.prototype.slice.call(arguments, 1)
);
return event;
},
/**
* Triggers an event on the builder container and returns the modified value
* @param {string} type
* @param {*} value
* @returns {*}
*/
change: function (type, value) {
var event = new $.Event(this._tojQueryEvent(type, true), {
builder: this,
value: value,
});
this.$el.triggerHandler(
event,
Array.prototype.slice.call(arguments, 2)
);
return event.value;
},
/**
* Attaches an event listener on the builder container
* @param {string} type
* @param {function} cb
* @returns {QueryBuilder}
*/
on: function (type, cb) {
this.$el.on(this._tojQueryEvent(type), cb);
return this;
},
/**
* Removes an event listener from the builder container
* @param {string} type
* @param {function} [cb]
* @returns {QueryBuilder}
*/
off: function (type, cb) {
this.$el.off(this._tojQueryEvent(type), cb);
return this;
},
/**
* Attaches an event listener called once on the builder container
* @param {string} type
* @param {function} cb
* @returns {QueryBuilder}
*/
once: function (type, cb) {
this.$el.one(this._tojQueryEvent(type), cb);
return this;
},
/**
* Appends `.queryBuilder` and optionally `.filter` to the events names
* @param {string} name
* @param {boolean} [filter=false]
* @returns {string}
* @private
*/
_tojQueryEvent: function (name, filter) {
return name
.split(' ')
.map(function (type) {
return type + '.queryBuilder' + (filter ? '.filter' : '');
})
.join(' ');
},
}
);
/**
* Allowed types and their internal representation
* @type {object.<string, string>}
* @readonly
* @private
*/
QueryBuilder.types = {
string: 'string',
integer: 'number',
double: 'number',
date: 'datetime',
time: 'datetime',
datetime: 'datetime',
boolean: 'boolean',
};
/**
* Allowed inputs
* @type {string[]}
* @readonly
* @private
*/
QueryBuilder.inputs = [
'text',
'number',
'textarea',
'radio',
'checkbox',
'select',
];
/**
* Runtime modifiable options with `setOptions` method
* @type {string[]}
* @readonly
* @private
*/
QueryBuilder.modifiable_options = [
'display_errors',
'allow_groups',
'allow_empty',
'default_condition',
'default_filter',
];
/**
* CSS selectors for common components
* @type {object.<string, string>}
* @readonly
*/
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)
* @type {object.<string, string>}
* @readonly
*/
QueryBuilder.templates = {};
/**
* Localized strings (see i18n/)
* @type {object.<string, object>}
* @readonly
*/
QueryBuilder.regional = {};
/**
* Default operators
* @type {object.<string, object>}
* @readonly
*/
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
* @type {object}
* @readonly
*/
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: false,
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_add_rule: false,
no_add_group: false,
no_delete: false,
},
templates: {
group: null,
rule: null,
filterSelect: null,
operatorSelect: null,
ruleValueSelect: 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',
},
};
/**
* @module plugins
*/
/**
* Definition of available plugins
* @type {object.<String, object>}
*/
QueryBuilder.plugins = {};
/**
* Gets or extends the default configuration
* @param {object} [options] - new configuration
* @returns {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);
}
};
/**
* Registers a new plugin
* @param {string} name
* @param {function} fct - init function
* @param {object} [def] - default options
*/
QueryBuilder.define = function (name, fct, def) {
QueryBuilder.plugins[name] = {
fct: fct,
def: def || {},
};
};
/**
* Adds new methods to QueryBuilder prototype
* @param {object.<string, function>} methods
*/
QueryBuilder.extend = function (methods) {
$.extend(QueryBuilder.prototype, methods);
};
/**
* Initializes plugins for an instance
* @throws ConfigError
* @private
*/
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);
};
/**
* Returns the config of a plugin, if the plugin is not loaded, returns the default config.
* @param {string} name
* @param {string} [property]
* @throws ConfigError
* @returns {*}
*/
QueryBuilder.prototype.getPluginOptions = function (name, property) {
var plugin;
if (this.plugins && this.plugins[name]) {
plugin = this.plugins[name];
} else if (QueryBuilder.plugins[name]) {
plugin = QueryBuilder.plugins[name].def;
}
if (plugin) {
if (property) {
return plugin[property];
} else {
return plugin;
}
} else {
Utils.error('Config', 'Unable to find plugin "{0}"', name);
}
};
/**
* Final initialisation of the builder
* @param {object} [rules]
* @fires QueryBuilder.afterInit
* @private
*/
QueryBuilder.prototype.init = function (rules) {
/**
* When the initilization is done, just before creating the root group
* @event afterInit
* @memberof QueryBuilder
*/
this.trigger('afterInit');
if (rules) {
this.setRules(rules);
delete this.settings.rules;
} else {
this.setRoot(true);
}
};
/**
* Checks the configuration of each filter
* @param {QueryBuilder.Filter[]} filters
* @returns {QueryBuilder.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 =
QueryBuilder.types[filter.type] === 'number' ? 'number' : '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':
var cleanValues = [];
filter.has_optgroup = false;
Utils.iterateOptions(
filter.values,
function (value, label, optgroup) {
cleanValues.push({
value: value,
label: label,
optgroup: optgroup || null,
});
if (optgroup) {
filter.has_optgroup = true;
// register optgroup if needed
if (!this.settings.optgroups[optgroup]) {
this.settings.optgroups[optgroup] = optgroup;
}
}
}.bind(this)
);
if (filter.has_optgroup) {
filter.values = Utils.groupSort(cleanValues, 'optgroup');
} else {
filter.values = cleanValues;
}
if (filter.placeholder) {
if (filter.placeholder_value === undefined) {
filter.placeholder_value = -1;
}
filter.values.forEach(function (entry) {
if (entry.value == 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.translate(a.label).localeCompare(self.translate(b.label));
});
}
}
if (this.status.has_optgroup) {
filters = Utils.groupSort(filters, 'optgroup');
}
return filters;
};
/**
* Checks the configuration of each operator
* @param {QueryBuilder.Operator[]} operators
* @returns {QueryBuilder.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;
};
/**
* Adds all events listeners to the builder
* @private
*/
QueryBuilder.prototype.bindEvents = function () {
var self = this;
var Selectors = QueryBuilder.selectors;
// group condition change
this.$el.on('change.queryBuilder', Selectors.group_condition, function () {
if ($(this).is(':checked')) {
var $group = $(this).closest(Selectors.group_container);
self.getModel($group).condition = $(this).val();
}
});
// rule filter change
this.$el.on('change.queryBuilder', Selectors.rule_filter, function () {
var $rule = $(this).closest(Selectors.rule_container);
self.getModel($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);
self.getModel($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(self.getModel($group));
});
// delete rule button
this.$el.on('click.queryBuilder', Selectors.delete_rule, function () {
var $rule = $(this).closest(Selectors.rule_container);
self.deleteRule(self.getModel($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(self.getModel($group));
});
// delete group button
this.$el.on('click.queryBuilder', Selectors.delete_group, function () {
var $group = $(this).closest(Selectors.group_container);
self.deleteGroup(self.getModel($group));
});
}
// model events
this.model.on({
drop: function (e, node) {
node.$el.remove();
self.refreshGroupsConditions();
},
add: function (e, parent, node, index) {
if (index === 0) {
node.$el.prependTo(
parent.$el.find('>' + QueryBuilder.selectors.rules_list)
);
} else {
node.$el.insertAfter(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('>' + QueryBuilder.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.updateError(node);
break;
case 'flags':
self.applyRuleFlags(node);
break;
case 'filter':
self.updateRuleFilter(node, oldValue);
break;
case 'operator':
self.updateRuleOperator(node, oldValue);
break;
case 'value':
self.updateRuleValue(node, oldValue);
break;
}
} else {
switch (field) {
case 'error':
self.updateError(node);
break;
case 'flags':
self.applyGroupFlags(node);
break;
case 'condition':
self.updateGroupCondition(node, oldValue);
break;
}
}
},
});
};
/**
* Creates the root group
* @param {boolean} [addRule=true] - adds a default empty rule
* @param {object} [data] - group custom data
* @param {object} [flags] - flags to apply to the group
* @returns {Group} root group
* @fires QueryBuilder.afterAddGroup
*/
QueryBuilder.prototype.setRoot = function (addRule, data, flags) {
addRule = addRule === undefined || addRule === true;
var group_id = this.nextGroupId();
var $group = $($.parseHTML(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.model.root.condition = this.settings.default_condition;
this.trigger('afterAddGroup', this.model.root);
if (addRule) {
this.addRule(this.model.root);
}
return this.model.root;
};
/**
* Adds a new group
* @param {Group} parent
* @param {boolean} [addRule=true] - adds a default empty rule
* @param {object} [data] - group custom data
* @param {object} [flags] - flags to apply to the group
* @returns {Group}
* @fires QueryBuilder.beforeAddGroup
* @fires QueryBuilder.afterAddGroup
*/
QueryBuilder.prototype.addGroup = function (parent, addRule, data, flags) {
addRule = addRule === undefined || addRule === true;
var level = parent.level + 1;
/**
* Just before adding a group, can be prevented.
* @event beforeAddGroup
* @memberof QueryBuilder
* @param {Group} parent
* @param {boolean} addRule - if an empty rule will be added in the group
* @param {int} level - nesting level of the group, 1 is the root group
*/
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);
model.condition = this.settings.default_condition;
/**
* Just after adding a group
* @event afterAddGroup
* @memberof QueryBuilder
* @param {Group} group
*/
this.trigger('afterAddGroup', model);
/**
* After any change in the rules
* @event rulesChanged
* @memberof QueryBuilder
*/
this.trigger('rulesChanged');
if (addRule) {
this.addRule(model);
}
return model;
};
/**
* Tries to delete a group. The group is not deleted if at least one rule is flagged `no_delete`.
* @param {Group} group
* @returns {boolean} if the group has been deleted
* @fires QueryBuilder.beforeDeleteGroup
* @fires QueryBuilder.afterDeleteGroup
*/
QueryBuilder.prototype.deleteGroup = function (group) {
if (group.isRoot()) {
return false;
}
/**
* Just before deleting a group, can be prevented
* @event beforeDeleteGroup
* @memberof QueryBuilder
* @param {Group} parent
*/
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();
/**
* Just after deleting a group
* @event afterDeleteGroup
* @memberof QueryBuilder
*/
this.trigger('afterDeleteGroup');
this.trigger('rulesChanged');
}
return del;
};
/**
* Performs actions when a group's condition changes
* @param {Group} group
* @param {object} previousCondition
* @fires QueryBuilder.afterUpdateGroupCondition
* @private
*/
QueryBuilder.prototype.updateGroupCondition = function (
group,
previousCondition
) {
group.$el
.find('>' + QueryBuilder.selectors.group_condition)
.each(function () {
var $this = $(this);
$this.prop('checked', $this.val() === group.condition);
$this.parent().toggleClass('active', $this.val() === group.condition);
});
/**
* After the group condition has been modified
* @event afterUpdateGroupCondition
* @memberof QueryBuilder
* @param {Group} group
* @param {object} previousCondition
*/
this.trigger('afterUpdateGroupCondition', group, previousCondition);
this.trigger('rulesChanged');
};
/**
* Updates the visibility of conditions based on number of rules inside each group
* @private
*/
QueryBuilder.prototype.refreshGroupsConditions = function () {
(function walk(group) {
if (!group.flags || (group.flags && !group.flags.condition_readonly)) {
group.$el
.find('>' + QueryBuilder.selectors.group_condition)
.prop('disabled', group.rules.length <= 1)
.parent()
.toggleClass('disabled', group.rules.length <= 1);
}
group.each(
null,
function (group) {
walk(group);
},
this
);
})(this.model.root);
};
/**
* Adds a new rule
* @param {Group} parent
* @param {object} [data] - rule custom data
* @param {object} [flags] - flags to apply to the rule
* @returns {Rule}
* @fires QueryBuilder.beforeAddRule
* @fires QueryBuilder.afterAddRule
* @fires QueryBuilder.changer:getDefaultFilter
*/
QueryBuilder.prototype.addRule = function (parent, data, flags) {
/**
* Just before adding a rule, can be prevented
* @event beforeAddRule
* @memberof QueryBuilder
* @param {Group} parent
*/
var e = this.trigger('beforeAddRule', parent);
if (e.isDefaultPrevented()) {
return null;
}
var rule_id = this.nextRuleId();
var $rule = $($.parseHTML(this.getRuleTemplate(rule_id)));
var model = parent.addRule($rule);
model.data = data;
model.flags = $.extend({}, this.settings.default_rule_flags, flags);
/**
* Just after adding a rule
* @event afterAddRule
* @memberof QueryBuilder
* @param {Rule} rule
*/
this.trigger('afterAddRule', model);
this.trigger('rulesChanged');
this.createRuleFilters(model);
if (this.settings.default_filter || !this.settings.display_empty_filter) {
/**
* Modifies the default filter for a rule
* @event changer:getDefaultFilter
* @memberof QueryBuilder
* @param {QueryBuilder.Filter} filter
* @param {Rule} rule
* @returns {QueryBuilder.Filter}
*/
model.filter = this.change(
'getDefaultFilter',
this.getFilterById(this.settings.default_filter || this.filters[0].id),
model
);
}
return model;
};
/**
* Tries to delete a rule
* @param {Rule} rule
* @returns {boolean} if the rule has been deleted
* @fires QueryBuilder.beforeDeleteRule
* @fires QueryBuilder.afterDeleteRule
*/
QueryBuilder.prototype.deleteRule = function (rule) {
if (rule.flags.no_delete) {
return false;
}
/**
* Just before deleting a rule, can be prevented
* @event beforeDeleteRule
* @memberof QueryBuilder
* @param {Rule} rule
*/
var e = this.trigger('beforeDeleteRule', rule);
if (e.isDefaultPrevented()) {
return false;
}
rule.drop();
/**
* Just after deleting a rule
* @event afterDeleteRule
* @memberof QueryBuilder
*/
this.trigger('afterDeleteRule');
this.trigger('rulesChanged');
return true;
};
/**
* Creates the filters for a rule
* @param {Rule} rule
* @fires QueryBuilder.changer:getRuleFilters
* @fires QueryBuilder.afterCreateRuleFilters
* @private
*/
QueryBuilder.prototype.createRuleFilters = function (rule) {
/**
* Modifies the list a filters available for a rule
* @event changer:getRuleFilters
* @memberof QueryBuilder
* @param {QueryBuilder.Filter[]} filters
* @param {Rule} rule
* @returns {QueryBuilder.Filter[]}
*/
var filters = this.change('getRuleFilters', this.filters, rule);
var $filterSelect = $($.parseHTML(this.getRuleFilterSelect(rule, filters)));
rule.$el.find(QueryBuilder.selectors.filter_container).html($filterSelect);
/**
* After creating the dropdown for filters
* @event afterCreateRuleFilters
* @memberof QueryBuilder
* @param {Rule} rule
*/
this.trigger('afterCreateRuleFilters', rule);
this.applyRuleFlags(rule);
};
/**
* Creates the operators for a rule and init the rule operator
* @param {Rule} rule
* @fires QueryBuilder.afterCreateRuleOperators
* @private
*/
QueryBuilder.prototype.createRuleOperators = function (rule) {
var $operatorContainer = rule.$el
.find(QueryBuilder.selectors.operator_container)
.empty();
if (!rule.filter) {
return;
}
var operators = this.getOperators(rule.filter);
var $operatorSelect = $(
$.parseHTML(this.getRuleOperatorSelect(rule, operators))
);
$operatorContainer.html($operatorSelect);
// set the operator without triggering update event
if (rule.filter.default_operator) {
rule.__.operator = this.getOperatorByType(rule.filter.default_operator);
} else {
rule.__.operator = operators[0];
}
rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
/**
* After creating the dropdown for operators
* @event afterCreateRuleOperators
* @memberof QueryBuilder
* @param {Rule} rule
* @param {QueryBuilder.Operator[]} operators - allowed operators for this rule
*/
this.trigger('afterCreateRuleOperators', rule, operators);
this.applyRuleFlags(rule);
};
/**
* Creates the main input for a rule
* @param {Rule} rule
* @fires QueryBuilder.afterCreateRuleInput
* @private
*/
QueryBuilder.prototype.createRuleInput = function (rule) {
var $valueContainer = rule.$el
.find(QueryBuilder.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 = $($.parseHTML($.trim(this.getRuleInput(rule, i))));
if (i > 0) $valueContainer.append(this.settings.inputs_separator);
$valueContainer.append($ruleInput);
$inputs = $inputs.add($ruleInput);
}
$valueContainer.css('display', '');
$inputs.on('change ' + (filter.input_event || ''), function () {
if (!rule._updating_input) {
rule._updating_value = true;
rule.value = self.getRuleInputValue(rule);
rule._updating_value = false;
}
});
if (filter.plugin) {
$inputs[filter.plugin](filter.plugin_config || {});
}
/**
* After creating the input for a rule and initializing optional plugin
* @event afterCreateRuleInput
* @memberof QueryBuilder
* @param {Rule} rule
*/
this.trigger('afterCreateRuleInput', rule);
if (filter.default_value !== undefined) {
rule.value = filter.default_value;
} else {
rule._updating_value = true;
rule.value = self.getRuleInputValue(rule);
rule._updating_value = false;
}
this.applyRuleFlags(rule);
};
/**
* Performs action when a rule's filter changes
* @param {Rule} rule
* @param {object} previousFilter
* @fires QueryBuilder.afterUpdateRuleFilter
* @private
*/
QueryBuilder.prototype.updateRuleFilter = function (rule, previousFilter) {
this.createRuleOperators(rule);
this.createRuleInput(rule);
rule.$el
.find(QueryBuilder.selectors.rule_filter)
.val(rule.filter ? rule.filter.id : '-1');
// clear rule data if the filter changed
if (previousFilter && rule.filter && previousFilter.id !== rule.filter.id) {
rule.data = undefined;
}
/**
* After the filter has been updated and the operators and input re-created
* @event afterUpdateRuleFilter
* @memberof QueryBuilder
* @param {Rule} rule
* @param {object} previousFilter
*/
this.trigger('afterUpdateRuleFilter', rule, previousFilter);
this.trigger('rulesChanged');
};
/**
* Performs actions when a rule's operator changes
* @param {Rule} rule
* @param {object} previousOperator
* @fires QueryBuilder.afterUpdateRuleOperator
* @private
*/
QueryBuilder.prototype.updateRuleOperator = function (
rule,
previousOperator
) {
var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container);
if (!rule.operator || rule.operator.nb_inputs === 0) {
$valueContainer.hide();
rule.__.value = undefined;
} else {
$valueContainer.css('display', '');
if (
$valueContainer.is(':empty') ||
!previousOperator ||
rule.operator.nb_inputs !== previousOperator.nb_inputs ||
rule.operator.optgroup !== previousOperator.optgroup
) {
this.createRuleInput(rule);
}
}
if (rule.operator) {
rule.$el
.find(QueryBuilder.selectors.rule_operator)
.val(rule.operator.type);
// refresh value if the format changed for this operator
rule.__.value = this.getRuleInputValue(rule);
}
/**
* After the operator has been updated and the input optionally re-created
* @event afterUpdateRuleOperator
* @memberof QueryBuilder
* @param {Rule} rule
* @param {object} previousOperator
*/
this.trigger('afterUpdateRuleOperator', rule, previousOperator);
this.trigger('rulesChanged');
};
/**
* Performs actions when rule's value changes
* @param {Rule} rule
* @param {object} previousValue
* @fires QueryBuilder.afterUpdateRuleValue
* @private
*/
QueryBuilder.prototype.updateRuleValue = function (rule, previousValue) {
if (!rule._updating_value) {
this.setRuleInputValue(rule, rule.value);
}
/**
* After the rule value has been modified
* @event afterUpdateRuleValue
* @memberof QueryBuilder
* @param {Rule} rule
* @param {*} previousValue
*/
this.trigger('afterUpdateRuleValue', rule, previousValue);
this.trigger('rulesChanged');
};
/**
* Changes a rule's properties depending on its flags
* @param {Rule} rule
* @fires QueryBuilder.afterApplyRuleFlags
* @private
*/
QueryBuilder.prototype.applyRuleFlags = function (rule) {
var flags = rule.flags;
var Selectors = QueryBuilder.selectors;
rule.$el
.find(Selectors.rule_filter)
.prop('disabled', flags.filter_readonly);
rule.$el
.find(Selectors.rule_operator)
.prop('disabled', flags.operator_readonly);
rule.$el.find(Selectors.rule_value).prop('disabled', flags.value_readonly);
if (flags.no_delete) {
rule.$el.find(Selectors.delete_rule).remove();
}
/**
* After rule's flags has been applied
* @event afterApplyRuleFlags
* @memberof QueryBuilder
* @param {Rule} rule
*/
this.trigger('afterApplyRuleFlags', rule);
};
/**
* Changes group's properties depending on its flags
* @param {Group} group
* @fires QueryBuilder.afterApplyGroupFlags
* @private
*/
QueryBuilder.prototype.applyGroupFlags = function (group) {
var flags = group.flags;
var Selectors = QueryBuilder.selectors;
group.$el
.find('>' + Selectors.group_condition)
.prop('disabled', flags.condition_readonly)
.parent()
.toggleClass('readonly', flags.condition_readonly);
if (flags.no_add_rule) {
group.$el.find(Selectors.add_rule).remove();
}
if (flags.no_add_group) {
group.$el.find(Selectors.add_group).remove();
}
if (flags.no_delete) {
group.$el.find(Selectors.delete_group).remove();
}
/**
* After group's flags has been applied
* @event afterApplyGroupFlags
* @memberof QueryBuilder
* @param {Group} group
*/
this.trigger('afterApplyGroupFlags', group);
};
/**
* Clears all errors markers
* @param {Node} [node] 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
);
}
};
/**
* Adds/Removes error on a Rule or Group
* @param {Node} node
* @fires QueryBuilder.changer:displayError
* @private
*/
QueryBuilder.prototype.updateError = function (node) {
if (this.settings.display_errors) {
if (node.error === null) {
node.$el.removeClass('has-error');
} else {
var errorMessage = this.translate('errors', node.error[0]);
errorMessage = Utils.fmt(errorMessage, node.error.slice(1));
/**
* Modifies an error message before display
* @event changer:displayError
* @memberof QueryBuilder
* @param {string} errorMessage - the error message (translated and formatted)
* @param {array} error - the raw error array (error code and optional arguments)
* @param {Node} node
* @returns {string}
*/
errorMessage = this.change(
'displayError',
errorMessage,
node.error,
node
);
node.$el
.addClass('has-error')
.find(QueryBuilder.selectors.error_container)
.eq(0)
.attr('title', errorMessage);
}
}
};
/**
* Triggers a validation error event
* @param {Node} node
* @param {string|array} error
* @param {*} value
* @fires QueryBuilder.validationError
* @private
*/
QueryBuilder.prototype.triggerValidationError = function (
node,
error,
value
) {
if (!$.isArray(error)) {
error = [error];
}
/**
* Fired when a validation error occurred, can be prevented
* @event validationError
* @memberof QueryBuilder
* @param {Node} node
* @param {string} error
* @param {*} value
*/
var e = this.trigger('validationError', node, error, value);
if (!e.isDefaultPrevented()) {
node.error = error;
}
};
/**
* Destroys the builder
* @fires QueryBuilder.beforeDestroy
*/
QueryBuilder.prototype.destroy = function () {
/**
* Before the {@link QueryBuilder#destroy} method
* @event beforeDestroy
* @memberof QueryBuilder
*/
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;
};
/**
* Clear all rules and resets the root group
* @fires QueryBuilder.beforeReset
* @fires QueryBuilder.afterReset
*/
QueryBuilder.prototype.reset = function () {
/**
* Before the {@link QueryBuilder#reset} method, can be prevented
* @event beforeReset
* @memberof QueryBuilder
*/
var e = this.trigger('beforeReset');
if (e.isDefaultPrevented()) {
return;
}
this.status.group_id = 1;
this.status.rule_id = 0;
this.model.root.empty();
this.model.root.data = undefined;
this.model.root.flags = $.extend({}, this.settings.default_group_flags);
this.model.root.condition = this.settings.default_condition;
this.addRule(this.model.root);
/**
* After the {@link QueryBuilder#reset} method
* @event afterReset
* @memberof QueryBuilder
*/
this.trigger('afterReset');
this.trigger('rulesChanged');
};
/**
* Clears all rules and removes the root group
* @fires QueryBuilder.beforeClear
* @fires QueryBuilder.afterClear
*/
QueryBuilder.prototype.clear = function () {
/**
* Before the {@link QueryBuilder#clear} method, can be prevented
* @event beforeClear
* @memberof QueryBuilder
*/
var e = this.trigger('beforeClear');
if (e.isDefaultPrevented()) {
return;
}
this.status.group_id = 0;
this.status.rule_id = 0;
if (this.model.root) {
this.model.root.drop();
this.model.root = null;
}
/**
* After the {@link QueryBuilder#clear} method
* @event afterClear
* @memberof QueryBuilder
*/
this.trigger('afterClear');
this.trigger('rulesChanged');
};
/**
* Modifies the builder configuration.<br>
* Only options defined in QueryBuilder.modifiable_options are modifiable
* @param {object} options
*/
QueryBuilder.prototype.setOptions = function (options) {
$.each(
options,
function (opt, value) {
if (QueryBuilder.modifiable_options.indexOf(opt) !== -1) {
this.settings[opt] = value;
}
}.bind(this)
);
};
/**
* Returns the model associated to a DOM object, or the root model
* @param {jQuery} [target]
* @returns {Node}
*/
QueryBuilder.prototype.getModel = function (target) {
if (!target) {
return this.model.root;
} else if (target instanceof Node) {
return target;
} else {
return $(target).data('queryBuilderModel');
}
};
/**
* Validates the whole builder
* @param {object} [options]
* @param {boolean} [options.skip_empty=false] - skips validating rules that have no filter selected
* @returns {boolean}
* @fires QueryBuilder.changer:validate
*/
QueryBuilder.prototype.validate = function (options) {
options = $.extend(
{
skip_empty: false,
},
options
);
this.clearErrors();
var self = this;
var valid = (function parse(group) {
var done = 0;
var errors = 0;
group.each(
function (rule) {
if (!rule.filter && options.skip_empty) {
return;
}
if (!rule.filter) {
self.triggerValidationError(rule, 'no_filter', null);
errors++;
return;
}
if (!rule.operator) {
self.triggerValidationError(rule, 'no_operator', 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) {
var res = parse(group);
if (res === true) {
done++;
} else if (res === false) {
errors++;
}
}
);
if (errors > 0) {
return false;
} else if (done === 0 && !group.isRoot() && options.skip_empty) {
return null;
} else if (
done === 0 &&
(!self.settings.allow_empty || !group.isRoot())
) {
self.triggerValidationError(group, 'empty_group', null);
return false;
}
return true;
})(this.model.root);
/**
* Modifies the result of the {@link QueryBuilder#validate} method
* @event changer:validate
* @memberof QueryBuilder
* @param {boolean} valid
* @returns {boolean}
*/
return this.change('validate', valid);
};
/**
* Gets an object representing current rules
* @param {object} [options]
* @param {boolean|string} [options.get_flags=false] - export flags, true: only changes from default flags or 'all'
* @param {boolean} [options.allow_invalid=false] - returns rules even if they are invalid
* @param {boolean} [options.skip_empty=false] - remove rules that have no filter selected
* @returns {object}
* @fires QueryBuilder.changer:ruleToJson
* @fires QueryBuilder.changer:groupToJson
* @fires QueryBuilder.changer:getRules
*/
QueryBuilder.prototype.getRules = function (options) {
options = $.extend(
{
get_flags: false,
allow_invalid: false,
skip_empty: false,
},
options
);
var valid = this.validate(options);
if (!valid && !options.allow_invalid) {
return null;
}
var self = this;
var out = (function parse(group) {
var groupData = {
condition: group.condition,
rules: [],
};
if (group.data) {
groupData.data = $.extendext(true, 'replace', {}, group.data);
}
if (options.get_flags) {
var flags = self.getGroupFlags(
group.flags,
options.get_flags === 'all'
);
if (!$.isEmptyObject(flags)) {
groupData.flags = flags;
}
}
group.each(
function (rule) {
if (!rule.filter && options.skip_empty) {
return;
}
var value = null;
if (!rule.operator || rule.operator.nb_inputs !== 0) {
value = rule.value;
}
var ruleData = {
id: rule.filter ? rule.filter.id : null,
field: rule.filter ? rule.filter.field : null,
type: rule.filter ? rule.filter.type : null,
input: rule.filter ? rule.filter.input : null,
operator: rule.operator ? rule.operator.type : null,
value: value,
};
if ((rule.filter && rule.filter.data) || rule.data) {
ruleData.data = $.extendext(
true,
'replace',
{},
rule.filter ? rule.filter.data : {},
rule.data
);
}
if (options.get_flags) {
var flags = self.getRuleFlags(
rule.flags,
options.get_flags === 'all'
);
if (!$.isEmptyObject(flags)) {
ruleData.flags = flags;
}
}
/**
* Modifies the JSON generated from a Rule object
* @event changer:ruleToJson
* @memberof QueryBuilder
* @param {object} json
* @param {Rule} rule
* @returns {object}
*/
groupData.rules.push(self.change('ruleToJson', ruleData, rule));
},
function (model) {
var data = parse(model);
if (data.rules.length !== 0 || !options.skip_empty) {
groupData.rules.push(data);
}
},
this
);
/**
* Modifies the JSON generated from a Group object
* @event changer:groupToJson
* @memberof QueryBuilder
* @param {object} json
* @param {Group} group
* @returns {object}
*/
return self.change('groupToJson', groupData, group);
})(this.model.root);
out.valid = valid;
/**
* Modifies the result of the {@link QueryBuilder#getRules} method
* @event changer:getRules
* @memberof QueryBuilder
* @param {object} json
* @returns {object}
*/
return this.change('getRules', out);
};
/**
* Sets rules from object
* @param {object} data
* @param {object} [options]
* @param {boolean} [options.allow_invalid=false] - silent-fail if the data are invalid
* @throws RulesError, UndefinedConditionError
* @fires QueryBuilder.changer:setRules
* @fires QueryBuilder.changer:jsonToRule
* @fires QueryBuilder.changer:jsonToGroup
* @fires QueryBuilder.afterSetRules
*/
QueryBuilder.prototype.setRules = function (data, options) {
options = $.extend(
{
allow_invalid: false,
},
options
);
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));
/**
* Modifies data before the {@link QueryBuilder#setRules} method
* @event changer:setRules
* @memberof QueryBuilder
* @param {object} json
* @param {object} options
* @returns {object}
*/
data = this.change('setRules', data, options);
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(
!options.allow_invalid,
'UndefinedCondition',
'Invalid condition "{0}"',
data.condition
);
data.condition = self.settings.default_condition;
}
group.condition = data.condition;
data.rules.forEach(function (item) {
var model;
if (item.rules !== undefined) {
if (
self.settings.allow_groups !== -1 &&
self.settings.allow_groups < group.level
) {
Utils.error(
!options.allow_invalid,
'RulesParse',
'No more than {0} groups are allowed',
self.settings.allow_groups
);
self.reset();
} else {
model = self.addGroup(
group,
false,
item.data,
self.parseGroupFlags(item)
);
if (model === null) {
return;
}
add(item, model);
}
} else {
if (!item.empty) {
if (item.id === undefined) {
Utils.error(
!options.allow_invalid,
'RulesParse',
'Missing rule field id'
);
item.empty = true;
}
if (item.operator === undefined) {
item.operator = 'equal';
}
}
model = self.addRule(group, item.data, self.parseRuleFlags(item));
if (model === null) {
return;
}
if (!item.empty) {
model.filter = self.getFilterById(item.id, !options.allow_invalid);
}
if (model.filter) {
model.operator = self.getOperatorByType(
item.operator,
!options.allow_invalid
);
if (!model.operator) {
model.operator = self.getOperators(model.filter)[0];
}
}
if (model.operator && model.operator.nb_inputs !== 0) {
if (item.value !== undefined) {
model.value = item.value;
} else if (model.filter.default_value !== undefined) {
model.value = model.filter.default_value;
}
}
/**
* Modifies the Rule object generated from the JSON
* @event changer:jsonToRule
* @memberof QueryBuilder
* @param {Rule} rule
* @param {object} json
* @returns {Rule} the same rule
*/
if (self.change('jsonToRule', model, item) != model) {
Utils.error('RulesParse', 'Plugin tried to change rule reference');
}
}
});
/**
* Modifies the Group object generated from the JSON
* @event changer:jsonToGroup
* @memberof QueryBuilder
* @param {Group} group
* @param {object} json
* @returns {Group} the same group
*/
if (self.change('jsonToGroup', group, data) != group) {
Utils.error('RulesParse', 'Plugin tried to change group reference');
}
})(data, this.model.root);
/**
* After the {@link QueryBuilder#setRules} method
* @event afterSetRules
* @memberof QueryBuilder
*/
this.trigger('afterSetRules');
};
/**
* Performs value validation
* @param {Rule} rule
* @param {string|string[]} value
* @returns {array|boolean} true or error array
* @fires QueryBuilder.changer:validateValue
*/
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._validateValue(rule, value);
}
/**
* Modifies the result of the rule validation method
* @event changer:validateValue
* @memberof QueryBuilder
* @param {array|boolean} result - true or an error array
* @param {*} value
* @param {Rule} rule
* @returns {array|boolean}
*/
return this.change('validateValue', result, value, rule);
};
/**
* Default validation function
* @param {Rule} rule
* @param {string|string[]} value
* @returns {array|boolean} true or error array
* @throws ConfigError
* @private
*/
QueryBuilder.prototype._validateValue = function (rule, value) {
var filter = rule.filter;
var operator = rule.operator;
var validation = filter.validation || {};
var result = true;
var tmp, tempValue;
if (rule.operator.nb_inputs === 1) {
value = [value];
}
for (var i = 0; i < operator.nb_inputs; i++) {
if (!operator.multiple && $.isArray(value[i]) && value[i].length > 1) {
result = [
'operator_not_multiple',
operator.type,
this.translate('operators', operator.type),
];
break;
}
switch (filter.input) {
case 'radio':
if (value[i] === undefined || value[i].length === 0) {
if (!validation.allow_empty_value) {
result = ['radio_empty'];
}
break;
}
break;
case 'checkbox':
if (value[i] === undefined || value[i].length === 0) {
if (!validation.allow_empty_value) {
result = ['checkbox_empty'];
}
break;
}
break;
case 'select':
if (
value[i] === undefined ||
value[i].length === 0 ||
(filter.placeholder && value[i] == filter.placeholder_value)
) {
if (!validation.allow_empty_value) {
result = ['select_empty'];
}
break;
}
break;
default:
tempValue = $.isArray(value[i]) ? value[i] : [value[i]];
for (var j = 0; j < tempValue.length; j++) {
switch (QueryBuilder.types[filter.type]) {
case 'string':
if (tempValue[j] === undefined || tempValue[j].length === 0) {
if (!validation.allow_empty_value) {
result = ['string_empty'];
}
break;
}
if (validation.min !== undefined) {
if (tempValue[j].length < parseInt(validation.min)) {
result = [
this.getValidationMessage(
validation,
'min',
'string_exceed_min_length'
),
validation.min,
];
break;
}
}
if (validation.max !== undefined) {
if (tempValue[j].length > parseInt(validation.max)) {
result = [
this.getValidationMessage(
validation,
'max',
'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(tempValue[j])) {
result = [
this.getValidationMessage(
validation,
'format',
'string_invalid_format'
),
validation.format,
];
break;
}
}
break;
case 'number':
if (tempValue[j] === undefined || tempValue[j].length === 0) {
if (!validation.allow_empty_value) {
result = ['number_nan'];
}
break;
}
if (isNaN(tempValue[j])) {
result = ['number_nan'];
break;
}
if (filter.type == 'integer') {
if (parseInt(tempValue[j]) != tempValue[j]) {
result = ['number_not_integer'];
break;
}
} else {
if (parseFloat(tempValue[j]) != tempValue[j]) {
result = ['number_not_double'];
break;
}
}
if (validation.min !== undefined) {
if (tempValue[j] < parseFloat(validation.min)) {
result = [
this.getValidationMessage(
validation,
'min',
'number_exceed_min'
),
validation.min,
];
break;
}
}
if (validation.max !== undefined) {
if (tempValue[j] > parseFloat(validation.max)) {
result = [
this.getValidationMessage(
validation,
'max',
'number_exceed_max'
),
validation.max,
];
break;
}
}
if (
validation.step !== undefined &&
validation.step !== 'any'
) {
var v = (tempValue[j] / validation.step).toPrecision(14);
if (parseInt(v) != v) {
result = [
this.getValidationMessage(
validation,
'step',
'number_wrong_step'
),
validation.step,
];
break;
}
}
break;
case 'datetime':
if (tempValue[j] === undefined || tempValue[j].length === 0) {
if (!validation.allow_empty_value) {
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(tempValue[j], validation.format);
if (!datetime.isValid()) {
result = [
this.getValidationMessage(
validation,
'format',
'datetime_invalid'
),
validation.format,
];
break;
} else {
if (validation.min) {
if (
datetime < moment(validation.min, validation.format)
) {
result = [
this.getValidationMessage(
validation,
'min',
'datetime_exceed_min'
),
validation.min,
];
break;
}
}
if (validation.max) {
if (
datetime > moment(validation.max, validation.format)
) {
result = [
this.getValidationMessage(
validation,
'max',
'datetime_exceed_max'
),
validation.max,
];
break;
}
}
}
}
break;
case 'boolean':
if (tempValue[j] === undefined || tempValue[j].length === 0) {
if (!validation.allow_empty_value) {
result = ['boolean_not_valid'];
}
break;
}
tmp = ('' + tempValue[j]).trim().toLowerCase();
if (
tmp !== 'true' &&
tmp !== 'false' &&
tmp !== '1' &&
tmp !== '0' &&
tempValue[j] !== 1 &&
tempValue[j] !== 0
) {
result = ['boolean_not_valid'];
break;
}
}
if (result !== true) {
break;
}
}
}
if (result !== true) {
break;
}
}
if (
(rule.operator.type === 'between' ||
rule.operator.type === 'not_between') &&
value.length === 2
) {
switch (QueryBuilder.types[filter.type]) {
case 'number':
if (value[0] > value[1]) {
result = ['number_between_invalid', value[0], value[1]];
}
break;
case 'datetime':
// 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'
);
}
if (
moment(value[0], validation.format).isAfter(
moment(value[1], validation.format)
)
) {
result = ['datetime_between_invalid', value[0], value[1]];
}
}
break;
}
}
return result;
};
/**
* Returns an incremented group ID
* @returns {string}
* @private
*/
QueryBuilder.prototype.nextGroupId = function () {
return this.status.id + '_group_' + this.status.group_id++;
};
/**
* Returns an incremented rule ID
* @returns {string}
* @private
*/
QueryBuilder.prototype.nextRuleId = function () {
return this.status.id + '_rule_' + this.status.rule_id++;
};
/**
* Returns the operators for a filter
* @param {string|object} filter - filter id or filter object
* @returns {object[]}
* @fires QueryBuilder.changer:getOperators
*/
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)
);
});
}
/**
* Modifies the operators available for a filter
* @event changer:getOperators
* @memberof QueryBuilder
* @param {QueryBuilder.Operator[]} operators
* @param {QueryBuilder.Filter} filter
* @returns {QueryBuilder.Operator[]}
*/
return this.change('getOperators', result, filter);
};
/**
* Returns a particular filter by its id
* @param {string} id
* @param {boolean} [doThrow=true]
* @returns {object|null}
* @throws UndefinedFilterError
*/
QueryBuilder.prototype.getFilterById = function (id, doThrow) {
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(
doThrow !== false,
'UndefinedFilter',
'Undefined filter "{0}"',
id
);
return null;
};
/**
* Returns a particular operator by its type
* @param {string} type
* @param {boolean} [doThrow=true]
* @returns {object|null}
* @throws UndefinedOperatorError
*/
QueryBuilder.prototype.getOperatorByType = function (type, doThrow) {
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(
doThrow !== false,
'UndefinedOperator',
'Undefined operator "{0}"',
type
);
return null;
};
/**
* Returns rule's current input value
* @param {Rule} rule
* @returns {*}
* @fires QueryBuilder.changer:getRuleValue
* @private
*/
QueryBuilder.prototype.getRuleInputValue = 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(QueryBuilder.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 = [];
// jshint loopfunc:true
$value.find('[name=' + name + ']:checked').each(function () {
tmp.push($(this).val());
});
// jshint loopfunc:false
value.push(tmp);
break;
case 'select':
if (filter.multiple) {
tmp = [];
// jshint loopfunc:true
$value
.find('[name=' + name + '] option:selected')
.each(function () {
tmp.push($(this).val());
});
// jshint loopfunc:false
value.push(tmp);
} else {
value.push(
$value.find('[name=' + name + '] option:selected').val()
);
}
break;
default:
value.push($value.find('[name=' + name + ']').val());
}
}
value = value.map(function (val) {
if (
operator.multiple &&
filter.value_separator &&
typeof val == 'string'
) {
val = val.split(filter.value_separator);
}
if ($.isArray(val)) {
return val.map(function (subval) {
return Utils.changeType(subval, filter.type);
});
} else {
return Utils.changeType(val, filter.type);
}
});
if (operator.nb_inputs === 1) {
value = value[0];
}
// @deprecated
if (filter.valueParser) {
value = filter.valueParser.call(this, rule, value);
}
}
/**
* Modifies the rule's value grabbed from the DOM
* @event changer:getRuleValue
* @memberof QueryBuilder
* @param {*} value
* @param {Rule} rule
* @returns {*}
*/
return this.change('getRuleValue', value, rule);
};
/**
* Sets the value of a rule's input
* @param {Rule} rule
* @param {*} value
* @private
*/
QueryBuilder.prototype.setRuleInputValue = function (rule, value) {
var filter = rule.filter;
var operator = rule.operator;
if (!filter || !operator) {
return;
}
rule._updating_input = true;
if (filter.valueSetter) {
filter.valueSetter.call(this, rule, value);
} else {
var $value = rule.$el.find(QueryBuilder.selectors.value_container);
if (operator.nb_inputs == 1) {
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]];
}
// jshint loopfunc:true
value[i].forEach(function (value) {
$value
.find('[name=' + name + '][value="' + value + '"]')
.prop('checked', true)
.trigger('change');
});
// jshint loopfunc:false
break;
default:
if (
operator.multiple &&
filter.value_separator &&
$.isArray(value[i])
) {
value[i] = value[i].join(filter.value_separator);
}
$value
.find('[name=' + name + ']')
.val(value[i])
.trigger('change');
break;
}
}
}
rule._updating_input = false;
};
/**
* Parses rule flags
* @param {object} rule
* @returns {object}
* @fires QueryBuilder.changer:parseRuleFlags
* @private
*/
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);
}
/**
* Modifies the consolidated rule's flags
* @event changer:parseRuleFlags
* @memberof QueryBuilder
* @param {object} flags
* @param {object} rule - <b>not</b> a Rule object
* @returns {object}
*/
return this.change('parseRuleFlags', flags, rule);
};
/**
* Gets a copy of flags of a rule
* @param {object} flags
* @param {boolean} [all=false] - return all flags or only changes from default flags
* @returns {object}
* @private
*/
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;
}
};
/**
* Parses group flags
* @param {object} group
* @returns {object}
* @fires QueryBuilder.changer:parseGroupFlags
* @private
*/
QueryBuilder.prototype.parseGroupFlags = function (group) {
var flags = $.extend({}, this.settings.default_group_flags);
if (group.readonly) {
$.extend(flags, {
condition_readonly: true,
no_add_rule: true,
no_add_group: true,
no_delete: true,
});
}
if (group.flags) {
$.extend(flags, group.flags);
}
/**
* Modifies the consolidated group's flags
* @event changer:parseGroupFlags
* @memberof QueryBuilder
* @param {object} flags
* @param {object} group - <b>not</b> a Group object
* @returns {object}
*/
return this.change('parseGroupFlags', flags, group);
};
/**
* Gets a copy of flags of a group
* @param {object} flags
* @param {boolean} [all=false] - return all flags or only changes from default flags
* @returns {object}
* @private
*/
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 either by looking in the `lang` object or in itself if it's an object where keys are language codes
* @param {string} [category]
* @param {string|object} key
* @returns {string}
* @fires QueryBuilder.changer:translate
*/
QueryBuilder.prototype.translate = function (category, key) {
if (!key) {
key = category;
category = undefined;
}
var translation;
if (typeof key === 'object') {
translation = key[this.settings.lang_code] || key['en'];
} else {
translation = (category ? this.lang[category] : this.lang)[key] || key;
}
/**
* Modifies the translated label
* @event changer:translate
* @memberof QueryBuilder
* @param {string} translation
* @param {string|object} key
* @param {string} [category]
* @returns {string}
*/
return this.change('translate', translation, key, category);
};
/**
* Returns a validation message
* @param {object} validation
* @param {string} type
* @param {string} def
* @returns {string}
* @private
*/
QueryBuilder.prototype.getValidationMessage = function (
validation,
type,
def
) {
return (validation.messages && validation.messages[type]) || def;
};
QueryBuilder.templates.group =
'\
<div id="{{= it.group_id }}" class="rules-group-container"> \
<div 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.translate("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.translate("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.translate("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.translate("conditions", condition) }} \
</label> \
{{~}} \
</div> \
{{? it.settings.display_errors }} \
<div class="error-container"><i class="{{= it.icons.error }}"></i></div> \
{{?}} \
</div> \
<div class=rules-group-body> \
<div class=rules-list></div> \
</div> \
</div>';
QueryBuilder.templates.rule =
'\
<div 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.translate("delete_rule") }} \
</button> \
</div> \
</div> \
{{? it.settings.display_errors }} \
<div class="error-container">111<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> \
</div>';
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 }}" {{? filter.icon}}data-icon="{{= filter.icon}}"{{?}}>{{= it.translate(filter.label) }}</option> \
{{~}} \
{{? optgroup !== null }}</optgroup>{{?}} \
</select>';
QueryBuilder.templates.operatorSelect =
'\
{{? it.operators.length === 1 }} \
<span> \
{{= it.translate("operators", it.operators[0].type) }} \
</span> \
{{?}} \
{{ var optgroup = null; }} \
<select class="form-control {{? it.operators.length === 1 }}hide{{?}}" 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 }}" {{? operator.icon}}data-icon="{{= operator.icon}}"{{?}}>{{= it.translate("operators", operator.type) }}</option> \
{{~}} \
{{? optgroup !== null }}</optgroup>{{?}} \
</select>';
QueryBuilder.templates.ruleValueSelect =
'\
{{ var optgroup = null; }} \
<select class="form-control" name="{{= it.name }}" {{? it.rule.filter.multiple }}multiple{{?}}> \
{{? it.rule.filter.placeholder }} \
<option value="{{= it.rule.filter.placeholder_value }}" disabled selected>{{= it.rule.filter.placeholder }}</option> \
{{?}} \
{{~ it.rule.filter.values: entry }} \
{{? optgroup !== entry.optgroup }} \
{{? optgroup !== null }}</optgroup>{{?}} \
{{? (optgroup = entry.optgroup) !== null }} \
<optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
{{?}} \
{{?}} \
<option value="{{= entry.value }}">{{= entry.label }}</option> \
{{~}} \
{{? optgroup !== null }}</optgroup>{{?}} \
</select>';
/**
* Returns group's HTML
* @param {string} group_id
* @param {int} level
* @returns {string}
* @fires QueryBuilder.changer:getGroupTemplate
* @private
*/
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,
settings: this.settings,
translate: this.translate.bind(this),
});
/**
* Modifies the raw HTML of a group
* @event changer:getGroupTemplate
* @memberof QueryBuilder
* @param {string} html
* @param {int} level
* @returns {string}
*/
return this.change('getGroupTemplate', h, level);
};
/**
* Returns rule's HTML
* @param {string} rule_id
* @returns {string}
* @fires QueryBuilder.changer:getRuleTemplate
* @private
*/
QueryBuilder.prototype.getRuleTemplate = function (rule_id) {
var h = this.templates.rule({
builder: this,
rule_id: rule_id,
icons: this.icons,
settings: this.settings,
translate: this.translate.bind(this),
});
/**
* Modifies the raw HTML of a rule
* @event changer:getRuleTemplate
* @memberof QueryBuilder
* @param {string} html
* @returns {string}
*/
return this.change('getRuleTemplate', h);
};
/**
* Returns rule's filter HTML
* @param {Rule} rule
* @param {object[]} filters
* @returns {string}
* @fires QueryBuilder.changer:getRuleFilterTemplate
* @private
*/
QueryBuilder.prototype.getRuleFilterSelect = function (rule, filters) {
var h = this.templates.filterSelect({
builder: this,
rule: rule,
filters: filters,
icons: this.icons,
settings: this.settings,
translate: this.translate.bind(this),
});
/**
* Modifies the raw HTML of the rule's filter dropdown
* @event changer:getRuleFilterSelect
* @memberof QueryBuilder
* @param {string} html
* @param {Rule} rule
* @param {QueryBuilder.Filter[]} filters
* @returns {string}
*/
return this.change('getRuleFilterSelect', h, rule, filters);
};
/**
* Returns rule's operator HTML
* @param {Rule} rule
* @param {object[]} operators
* @returns {string}
* @fires QueryBuilder.changer:getRuleOperatorTemplate
* @private
*/
QueryBuilder.prototype.getRuleOperatorSelect = function (rule, operators) {
var h = this.templates.operatorSelect({
builder: this,
rule: rule,
operators: operators,
icons: this.icons,
settings: this.settings,
translate: this.translate.bind(this),
});
/**
* Modifies the raw HTML of the rule's operator dropdown
* @event changer:getRuleOperatorSelect
* @memberof QueryBuilder
* @param {string} html
* @param {Rule} rule
* @param {QueryBuilder.Operator[]} operators
* @returns {string}
*/
return this.change('getRuleOperatorSelect', h, rule, operators);
};
/**
* Returns the rule's value select HTML
* @param {string} name
* @param {Rule} rule
* @returns {string}
* @fires QueryBuilder.changer:getRuleValueSelect
* @private
*/
QueryBuilder.prototype.getRuleValueSelect = function (name, rule) {
var h = this.templates.ruleValueSelect({
builder: this,
name: name,
rule: rule,
icons: this.icons,
settings: this.settings,
translate: this.translate.bind(this),
});
/**
* Modifies the raw HTML of the rule's value dropdown (in case of a "select filter)
* @event changer:getRuleValueSelect
* @memberof QueryBuilder
* @param {string} html
* @param [string} name
* @param {Rule} rule
* @returns {string}
*/
return this.change('getRuleValueSelect', h, name, rule);
};
/**
* Returns the rule's value HTML
* @param {Rule} rule
* @param {int} value_id
* @returns {string}
* @fires QueryBuilder.changer:getRuleInput
* @private
*/
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 = '';
var placeholder = Array.isArray(filter.placeholder)
? filter.placeholder[value_id]
: filter.placeholder;
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 = this.getRuleValueSelect(name, rule);
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 (placeholder) h += ' placeholder="' + placeholder + '"';
h += '></textarea>';
break;
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 (placeholder) h += ' placeholder="' + placeholder + '"';
if (filter.size) h += ' size="' + filter.size + '"';
h += '>';
break;
default:
h += '<input class="form-control" type="text" name="' + name + '"';
if (placeholder) h += ' placeholder="' + 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 + '"';
h += '>';
}
}
/**
* Modifies the raw HTML of the rule's input
* @event changer:getRuleInput
* @memberof QueryBuilder
* @param {string} html
* @param {Rule} rule
* @param {string} name - the name that the input must have
* @returns {string}
*/
return this.change('getRuleInput', h, rule, name);
};
/**
* @namespace
*/
var Utils = {};
/**
* @member {object}
* @memberof QueryBuilder
* @see Utils
*/
QueryBuilder.utils = Utils;
/**
* @callback Utils#OptionsIteratee
* @param {string} key
* @param {string} value
* @param {string} [optgroup]
*/
/**
* Iterates over radio/checkbox/selection options, it accept four formats
*
* @example
* // array of values
* options = ['one', 'two', 'three']
* @example
* // simple key-value map
* options = {1: 'one', 2: 'two', 3: 'three'}
* @example
* // array of 1-element maps
* options = [{1: 'one'}, {2: 'two'}, {3: 'three'}]
* @example
* // array of elements
* options = [{value: 1, label: 'one', optgroup: 'group'}, {value: 2, label: 'two'}]
*
* @param {object|array} options
* @param {Utils#OptionsIteratee} tpl
*/
Utils.iterateOptions = function (options, tpl) {
if (options) {
if ($.isArray(options)) {
options.forEach(function (entry) {
if ($.isPlainObject(entry)) {
// array of elements
if ('value' in entry) {
tpl(entry.value, entry.label || entry.value, entry.optgroup);
}
// array of one-element maps
else {
$.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 {string} str
* @param {...*} args
* @returns {string}
*/
Utils.fmt = function (str, args) {
if (!Array.isArray(args)) {
args = Array.prototype.slice.call(arguments, 1);
}
return str.replace(/{([0-9]+)}/g, function (m, i) {
return args[parseInt(i)];
});
};
/**
* Throws an Error object with custom name or logs an error
* @param {boolean} [doThrow=true]
* @param {string} type
* @param {string} message
* @param {...*} args
*/
Utils.error = function () {
var i = 0;
var doThrow = typeof arguments[i] === 'boolean' ? arguments[i++] : true;
var type = arguments[i++];
var message = arguments[i++];
var args = Array.isArray(arguments[i])
? arguments[i]
: Array.prototype.slice.call(arguments, i);
if (doThrow) {
var err = new Error(Utils.fmt(message, args));
err.name = type + 'Error';
err.args = args;
throw err;
} else {
console.error(type + 'Error: ' + Utils.fmt(message, args));
}
};
/**
* Changes the type of a value to int, float or bool
* @param {*} value
* @param {string} type - 'integer', 'double', 'boolean' or anything else (passthrough)
* @returns {*}
*/
Utils.changeType = function (value, type) {
if (value === '' || value === undefined) {
return undefined;
}
switch (type) {
// @formatter:off
case 'integer':
if (typeof value === 'string' && !/^-?\d+$/.test(value)) {
return value;
}
return parseInt(value);
case 'double':
if (typeof value === 'string' && !/^-?\d+\.?\d*$/.test(value)) {
return value;
}
return parseFloat(value);
case 'boolean':
if (
typeof value === 'string' &&
!/^(0|1|true|false){1}$/i.test(value)
) {
return value;
}
return (
value === true ||
value === 1 ||
value.toLowerCase() === 'true' ||
value === '1'
);
default:
return value;
// @formatter:on
}
};
/**
* Escapes a string like PHP's mysql_real_escape_string does
* @param {string} value
* @param {string} [additionalEscape] additionnal chars to escape
* @returns {string}
*/
Utils.escapeString = function (value, additionalEscape) {
if (typeof value != 'string') {
return value;
}
var escaped = value
.replace(/[\0\n\r\b\\\'\"]/g, function (s) {
switch (s) {
// @formatter:off
case '\0':
return '\\0';
case '\n':
return '\\n';
case '\r':
return '\\r';
case '\b':
return '\\b';
case "'":
return "''";
default:
return '\\' + s;
// @formatter:off
}
})
// uglify compliant
.replace(/\t/g, '\\t')
.replace(/\x1a/g, '\\Z');
if (additionalEscape) {
escaped = escaped.replace(
new RegExp('[' + additionalEscape + ']', 'g'),
function (s) {
return '\\' + s;
}
);
}
return escaped;
};
/**
* Escapes a string for use in regex
* @param {string} str
* @returns {string}
*/
Utils.escapeRegExp = function (str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
};
/**
* Escapes a string for use in HTML element id
* @param {string} str
* @returns {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;
};
/**
* Sorts 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;
};
/**
* Defines properties on an Node prototype with getter and setter.<br>
* Update events are emitted in the setter through root Model (if any).<br>
* The object must have a `__` object, non enumerable property to store values.
* @param {function} obj
* @param {string[]} fields
*/
Utils.defineModelProperties = function (obj, fields) {
fields.forEach(function (field) {
Object.defineProperty(obj.prototype, field, {
enumerable: true,
get: function () {
return this.__[field];
},
set: function (value) {
var previousValue =
this.__[field] !== null && typeof this.__[field] == 'object'
? $.extend({}, this.__[field])
: this.__[field];
this.__[field] = value;
if (this.model !== null) {
/**
* After a value of the model changed
* @event model:update
* @memberof Model
* @param {Node} node
* @param {string} field
* @param {*} value
* @param {*} previousValue
*/
this.model.trigger('update', this, field, value, previousValue);
}
},
});
});
};
/**
* Main object storing data model and emitting model events
* @constructor
*/
function Model() {
/**
* @member {Group}
* @readonly
*/
this.root = null;
/**
* Base for event emitting
* @member {jQuery}
* @readonly
* @private
*/
this.$ = $(this);
}
$.extend(
Model.prototype,
/** @lends Model.prototype */ {
/**
* Triggers an event on the model
* @param {string} type
* @returns {$.Event}
*/
trigger: function (type) {
var event = new $.Event(type);
this.$.triggerHandler(event, Array.prototype.slice.call(arguments, 1));
return event;
},
/**
* Attaches an event listener on the model
* @param {string} type
* @param {function} cb
* @returns {Model}
*/
on: function () {
this.$.on.apply(this.$, Array.prototype.slice.call(arguments));
return this;
},
/**
* Removes an event listener from the model
* @param {string} type
* @param {function} [cb]
* @returns {Model}
*/
off: function () {
this.$.off.apply(this.$, Array.prototype.slice.call(arguments));
return this;
},
/**
* Attaches an event listener called once on the model
* @param {string} type
* @param {function} cb
* @returns {Model}
*/
once: function () {
this.$.one.apply(this.$, Array.prototype.slice.call(arguments));
return this;
},
}
);
/**
* Root abstract object
* @constructor
* @param {Node} [parent]
* @param {jQuery} $el
*/
var Node = function (parent, $el) {
if (!(this instanceof Node)) {
return new Node(parent, $el);
}
Object.defineProperty(this, '__', { value: {} });
$el.data('queryBuilderModel', this);
/**
* @name level
* @member {int}
* @memberof Node
* @instance
* @readonly
*/
this.__.level = 1;
/**
* @name error
* @member {string}
* @memberof Node
* @instance
*/
this.__.error = null;
/**
* @name flags
* @member {object}
* @memberof Node
* @instance
* @readonly
*/
this.__.flags = {};
/**
* @name data
* @member {object}
* @memberof Node
* @instance
*/
this.__.data = undefined;
/**
* @member {jQuery}
* @readonly
*/
this.$el = $el;
/**
* @member {string}
* @readonly
*/
this.id = $el[0].id;
/**
* @member {Model}
* @readonly
*/
this.model = null;
/**
* @member {Group}
* @readonly
*/
this.parent = parent;
};
Utils.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;
},
});
/**
* Checks if this Node is the root
* @returns {boolean}
*/
Node.prototype.isRoot = function () {
return this.level === 1;
};
/**
* Returns the node position inside its parent
* @returns {int}
*/
Node.prototype.getPos = function () {
if (this.isRoot()) {
return -1;
} else {
return this.parent.getNodePos(this);
}
};
/**
* Deletes self
* @fires Model.model:drop
*/
Node.prototype.drop = function () {
var model = this.model;
if (!!this.parent) {
this.parent.removeNode(this);
}
this.$el.removeData('queryBuilderModel');
if (model !== null) {
/**
* After a node of the model has been removed
* @event model:drop
* @memberof Model
* @param {Node} node
*/
model.trigger('drop', this);
}
};
/**
* Moves itself after another Node
* @param {Node} target
* @fires Model.model:move
*/
Node.prototype.moveAfter = function (target) {
if (!this.isRoot()) {
this.move(target.parent, target.getPos() + 1);
}
};
/**
* Moves itself at the beginning of parent or another Group
* @param {Group} [target]
* @fires Model.model:move
*/
Node.prototype.moveAtBegin = function (target) {
if (!this.isRoot()) {
if (target === undefined) {
target = this.parent;
}
this.move(target, 0);
}
};
/**
* Moves itself at the end of parent or another Group
* @param {Group} [target]
* @fires Model.model:move
*/
Node.prototype.moveAtEnd = function (target) {
if (!this.isRoot()) {
if (target === undefined) {
target = this.parent;
}
this.move(target, target.length() === 0 ? 0 : target.length() - 1);
}
};
/**
* Moves itself at specific position of Group
* @param {Group} target
* @param {int} index
* @fires Model.model:move
*/
Node.prototype.move = function (target, index) {
if (!this.isRoot()) {
if (typeof target === 'number') {
index = target;
target = this.parent;
}
this.parent.removeNode(this);
target.insertNode(this, index, false);
if (this.model !== null) {
/**
* After a node of the model has been moved
* @event model:move
* @memberof Model
* @param {Node} node
* @param {Node} target
* @param {int} index
*/
this.model.trigger('move', this, target, index);
}
}
};
/**
* Group object
* @constructor
* @extends Node
* @param {Group} [parent]
* @param {jQuery} $el
*/
var Group = function (parent, $el) {
if (!(this instanceof Group)) {
return new Group(parent, $el);
}
Node.call(this, parent, $el);
/**
* @member {object[]}
* @readonly
*/
this.rules = [];
/**
* @name condition
* @member {string}
* @memberof Group
* @instance
*/
this.__.condition = null;
};
Group.prototype = Object.create(Node.prototype);
Group.prototype.constructor = Group;
Utils.defineModelProperties(Group, ['condition']);
/**
* Removes group's content
*/
Group.prototype.empty = function () {
this.each(
'reverse',
function (rule) {
rule.drop();
},
function (group) {
group.drop();
}
);
};
/**
* Deletes self
*/
Group.prototype.drop = function () {
this.empty();
Node.prototype.drop.call(this);
};
/**
* Returns the number of children
* @returns {int}
*/
Group.prototype.length = function () {
return this.rules.length;
};
/**
* Adds a Node at specified index
* @param {Node} node
* @param {int} [index=end]
* @param {boolean} [trigger=false] - fire 'add' event
* @returns {Node} the inserted node
* @fires Model.model:add
*/
Group.prototype.insertNode = function (node, index, trigger) {
if (index === undefined) {
index = this.length();
}
this.rules.splice(index, 0, node);
node.parent = this;
if (trigger && this.model !== null) {
/**
* After a node of the model has been added
* @event model:add
* @memberof Model
* @param {Node} parent
* @param {Node} node
* @param {int} index
*/
this.model.trigger('add', this, node, index);
}
return node;
};
/**
* Adds a new Group at specified index
* @param {jQuery} $el
* @param {int} [index=end]
* @returns {Group}
* @fires Model.model:add
*/
Group.prototype.addGroup = function ($el, index) {
return this.insertNode(new Group(this, $el), index, true);
};
/**
* Adds a new Rule at specified index
* @param {jQuery} $el
* @param {int} [index=end]
* @returns {Rule}
* @fires Model.model:add
*/
Group.prototype.addRule = function ($el, index) {
return this.insertNode(new Rule(this, $el), index, true);
};
/**
* Deletes a specific Node
* @param {Node} node
*/
Group.prototype.removeNode = function (node) {
var index = this.getNodePos(node);
if (index !== -1) {
node.parent = null;
this.rules.splice(index, 1);
}
};
/**
* Returns the position of a child Node
* @param {Node} node
* @returns {int}
*/
Group.prototype.getNodePos = function (node) {
return this.rules.indexOf(node);
};
/**
* @callback Model#GroupIteratee
* @param {Node} node
* @returns {boolean} stop the iteration
*/
/**
* Iterate over all Nodes
* @param {boolean} [reverse=false] - iterate in reverse order, required if you delete nodes
* @param {Model#GroupIteratee} cbRule - callback for Rules (can be `null` but not omitted)
* @param {Model#GroupIteratee} [cbGroup] - callback for Groups
* @param {object} [context] - context for callbacks
* @returns {boolean} if the iteration has been stopped by a callback
*/
Group.prototype.each = function (reverse, cbRule, cbGroup, context) {
if (typeof reverse !== 'boolean' && typeof reverse !== 'string') {
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) {
stop = cbGroup.call(context, this.rules[i]) === false;
}
} else if (!!cbRule) {
stop = cbRule.call(context, this.rules[i]) === false;
}
if (stop) {
break;
}
}
return !stop;
};
/**
* Checks if the group contains a particular Node
* @param {Node} node
* @param {boolean} [recursive=false]
* @returns {boolean}
*/
Group.prototype.contains = function (node, recursive) {
if (this.getNodePos(node) !== -1) {
return true;
} else if (!recursive) {
return false;
} else {
// the loop will return with false as soon as the Node is found
return !this.each(
function () {
return true;
},
function (group) {
return !group.contains(node, true);
}
);
}
};
/**
* Rule object
* @constructor
* @extends Node
* @param {Group} parent
* @param {jQuery} $el
*/
var Rule = function (parent, $el) {
if (!(this instanceof Rule)) {
return new Rule(parent, $el);
}
Node.call(this, parent, $el);
this._updating_value = false;
this._updating_input = false;
/**
* @name filter
* @member {QueryBuilder.Filter}
* @memberof Rule
* @instance
*/
this.__.filter = null;
/**
* @name operator
* @member {QueryBuilder.Operator}
* @memberof Rule
* @instance
*/
this.__.operator = null;
/**
* @name value
* @member {*}
* @memberof Rule
* @instance
*/
this.__.value = undefined;
};
Rule.prototype = Object.create(Node.prototype);
Rule.prototype.constructor = Rule;
Utils.defineModelProperties(Rule, ['filter', 'operator', 'value']);
/**
* Checks if this Node is the root
* @returns {boolean} always false
*/
Rule.prototype.isRoot = function () {
return false;
};
/**
* @member {function}
* @memberof QueryBuilder
* @see Group
*/
QueryBuilder.Group = Group;
/**
* @member {function}
* @memberof QueryBuilder
* @see Rule
*/
QueryBuilder.Rule = Rule;
/**
* The {@link http://learn.jquery.com/plugins/|jQuery Plugins} namespace
* @external "jQuery.fn"
*/
/**
* Instanciates or accesses the {@link QueryBuilder} on an element
* @function
* @memberof external:"jQuery.fn"
* @param {*} option - initial configuration or method name
* @param {...*} args - method arguments
*
* @example
* $('#builder').queryBuilder({ /** configuration object *\/ });
* @example
* $('#builder').queryBuilder('methodName', methodParam1, methodParam2);
*/
$.fn.queryBuilder = function (option) {
if (this.length === 0) {
Utils.error('Config', 'No target defined');
}
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) {
var builder = new QueryBuilder(this, options);
this.data('queryBuilder', builder);
builder.init(options.rules);
}
if (typeof option == 'string') {
return data[option].apply(data, Array.prototype.slice.call(arguments, 1));
}
return this;
};
/**
* @function
* @memberof external:"jQuery.fn"
* @see QueryBuilder
*/
$.fn.queryBuilder.constructor = QueryBuilder;
/**
* @function
* @memberof external:"jQuery.fn"
* @see QueryBuilder.defaults
*/
$.fn.queryBuilder.defaults = QueryBuilder.defaults;
/**
* @function
* @memberof external:"jQuery.fn"
* @see QueryBuilder.defaults
*/
$.fn.queryBuilder.extend = QueryBuilder.extend;
/**
* @function
* @memberof external:"jQuery.fn"
* @see QueryBuilder.define
*/
$.fn.queryBuilder.define = QueryBuilder.define;
/**
* @function
* @memberof external:"jQuery.fn"
* @see QueryBuilder.regional
*/
$.fn.queryBuilder.regional = QueryBuilder.regional;
/**
* @class BtCheckbox
* @memberof module:plugins
* @description Applies Awesome Bootstrap Checkbox for checkbox and radio inputs.
* @param {object} [options]
* @param {string} [options.font='glyphicons']
* @param {string} [options.color='default']
*/
QueryBuilder.define(
'bt-checkbox',
function (options) {
if (options.font == 'glyphicons') {
this.$el.addClass('bt-checkbox-glyphicons');
}
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',
}
);
/**
* @class BtSelectpicker
* @memberof module:plugins
* @descriptioon Applies Bootstrap Select on filters and operators combo-boxes.
* @param {object} [options]
* @param {string} [options.container='body']
* @param {string} [options.style='btn-inverse btn-xs']
* @param {int|string} [options.width='auto']
* @param {boolean} [options.showIcon=false]
* @throws MissingLibraryError
*/
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'
);
}
var Selectors = QueryBuilder.selectors;
// 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');
});
this.on('beforeDeleteRule', function (e, rule) {
rule.$el.find(Selectors.rule_filter).selectpicker('destroy');
rule.$el.find(Selectors.rule_operator).selectpicker('destroy');
});
},
{
container: 'body',
style: 'btn-inverse btn-xs',
width: 'auto',
showIcon: false,
}
);
/**
* @class BtTooltipErrors
* @memberof module:plugins
* @description Applies Bootstrap Tooltips on validation error messages.
* @param {object} [options]
* @param {string} [options.placement='right']
* @throws MissingLibraryError
*/
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 = $($.parseHTML(h.value));
$h.find(QueryBuilder.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(QueryBuilder.selectors.error_container)
.eq(0)
.tooltip(options)
.tooltip('hide')
.tooltip('fixTitle');
}
});
},
{
placement: 'right',
}
);
/**
* @class ChangeFilters
* @memberof module:plugins
* @description Allows to change available filters after plugin initialization.
*/
QueryBuilder.extend(
/** @lends module:plugins.ChangeFilters.prototype */ {
/**
* Change the filters of the builder
* @param {boolean} [deleteOrphans=false] - delete rules using old filters
* @param {QueryBuilder[]} filters
* @fires module:plugins.ChangeFilters.changer:setFilters
* @fires module:plugins.ChangeFilters.afterSetFilters
* @throws ChangeFilterError
*/
setFilters: function (deleteOrphans, filters) {
var self = this;
if (filters === undefined) {
filters = deleteOrphans;
deleteOrphans = false;
}
filters = this.checkFilters(filters);
/**
* Modifies the filters before {@link module:plugins.ChangeFilters.setFilters} method
* @event changer:setFilters
* @memberof module:plugins.ChangeFilters
* @param {QueryBuilder.Filter[]} filters
* @returns {QueryBuilder.Filter[]}
*/
filters = this.change('setFilters', filters);
var filtersIds = filters.map(function (filter) {
return filter.id;
});
// check for orphans
if (!deleteOrphans) {
(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();
self.trigger('rulesChanged');
} else {
self.createRuleFilters(rule);
rule.$el
.find(QueryBuilder.selectors.rule_filter)
.val(rule.filter ? rule.filter.id : '-1');
self.trigger('afterUpdateRuleFilter', rule);
}
},
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(QueryBuilder.selectors.rule_filter)
.selectpicker('render');
}
// select2 if 구문 추가
if (this.settings.plugins['select2']) {
this.$el
.find(QueryBuilder.selectors.rule_filter)
.trigger('change.select2');
}
}
// 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;
}
}
/**
* After {@link module:plugins.ChangeFilters.setFilters} method
* @event afterSetFilters
* @memberof module:plugins.ChangeFilters
* @param {QueryBuilder.Filter[]} filters
*/
this.trigger('afterSetFilters', filters);
},
/**
* Adds a new filter to the builder
* @param {QueryBuilder.Filter|Filter[]} newFilters
* @param {int|string} [position=#end] - index or '#start' or '#end'
* @fires module:plugins.ChangeFilters.changer:setFilters
* @fires module:plugins.ChangeFilters.afterSetFilters
* @throws ChangeFilterError
*/
addFilter: function (newFilters, position) {
if (position === undefined || position == '#end') {
position = this.filters.length;
} else if (position == '#start') {
position = 0;
}
if (!$.isArray(newFilters)) {
newFilters = [newFilters];
}
var filters = $.extend(true, [], this.filters);
// numeric position
if (parseInt(position) == position) {
Array.prototype.splice.apply(
filters,
[position, 0].concat(newFilters)
);
} 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(newFilters)
);
}
// defaults to end of list
else {
Array.prototype.push.apply(filters, newFilters);
}
}
this.setFilters(filters);
},
/**
* Removes a filter from the builder
* @param {string|string[]} filterIds
* @param {boolean} [deleteOrphans=false] delete rules using old filters
* @fires module:plugins.ChangeFilters.changer:setFilters
* @fires module:plugins.ChangeFilters.afterSetFilters
* @throws ChangeFilterError
*/
removeFilter: function (filterIds, deleteOrphans) {
var filters = $.extend(true, [], this.filters);
if (typeof filterIds === 'string') {
filterIds = [filterIds];
}
filters = filters.filter(function (filter) {
return filterIds.indexOf(filter.id) === -1;
});
this.setFilters(deleteOrphans, filters);
},
}
);
/**
* select2 플러그인 선언
* @class Select2
* @memberof module:plugins
* @descriptioon Applies Select2 on filters and operators combo-boxes.
* @param {object} [options]
* @throws MissingLibraryError
*/
QueryBuilder.define(
'select2',
function (options) {
if (!$.fn.select2) {
Utils.error(
'MissingLibrary',
'Select2 is required to use "select2" plugin.'
);
}
if (this.settings.plugins['select2']) {
Utils.error(
'Conflict',
'select2 is already selected as the dropdown plugin. Please remove select2 from the plugin list'
);
}
var Selectors = QueryBuilder.selectors;
// init select2
this.on('afterCreateRuleFilters', function (e, rule) {
rule.$el
.find(Selectors.rule_filter)
.removeClass('form-control')
.select2(options);
});
this.on('afterCreateRuleOperators', function (e, rule) {
rule.$el
.find(Selectors.rule_operator)
.removeClass('form-control')
.select2(options);
});
// update selectpicker on change
this.on('afterUpdateRuleFilter', function (e, rule) {
rule.$el.find(Selectors.rule_filter).trigger('change.select2');
});
this.on('afterUpdateRuleOperator', function (e, rule) {
rule.$el.find(Selectors.rule_operator).trigger('change.select2');
});
this.on('beforeDeleteRule', function (e, rule) {
rule.$el.find(Selectors.rule_filter).select2('destroy');
rule.$el.find(Selectors.rule_operator).select2('destroy');
});
}
);
/**
* @class ChosenSelectpicker
* @memberof module:plugins
* @descriptioon Applies chosen-js Select on filters and operators combo-boxes.
* @param {object} [options] Supports all the options for chosen
* @throws MissingLibraryError
*/
QueryBuilder.define('chosen-selectpicker', function (options) {
if (!$.fn.chosen) {
Utils.error(
'MissingLibrary',
'chosen is required to use "chosen-selectpicker" plugin. Get it here: https://github.com/harvesthq/chosen'
);
}
if (this.settings.plugins['bt-selectpicker']) {
Utils.error(
'Conflict',
'bt-selectpicker is already selected as the dropdown plugin. Please remove chosen-selectpicker from the plugin list'
);
}
var Selectors = QueryBuilder.selectors;
// init selectpicker
this.on('afterCreateRuleFilters', function (e, rule) {
rule.$el
.find(Selectors.rule_filter)
.removeClass('form-control')
.chosen(options);
});
this.on('afterCreateRuleOperators', function (e, rule) {
if (e.builder.getOperators(rule.filter).length > 1) {
rule.$el
.find(Selectors.rule_operator)
.removeClass('form-control')
.chosen(options);
}
});
// update selectpicker on change
this.on('afterUpdateRuleFilter', function (e, rule) {
rule.$el.find(Selectors.rule_filter).trigger('chosen:updated');
});
this.on('afterUpdateRuleOperator', function (e, rule) {
rule.$el.find(Selectors.rule_operator).trigger('chosen:updated');
});
this.on('beforeDeleteRule', function (e, rule) {
rule.$el.find(Selectors.rule_filter).chosen('destroy');
rule.$el.find(Selectors.rule_operator).chosen('destroy');
});
});
/**
* @class FilterDescription
* @memberof module:plugins
* @description Provides three ways to display a description about a filter: inline, Bootsrap Popover or Bootbox.
* @param {object} [options]
* @param {string} [options.icon='glyphicon glyphicon-info-sign']
* @param {string} [options.mode='popover'] - inline, popover or bootbox
* @throws ConfigError
*/
QueryBuilder.define(
'filter-description',
function (options) {
// INLINE
if (options.mode === 'inline') {
this.on(
'afterUpdateRuleFilter afterUpdateRuleOperator',
function (e, rule) {
var $p = rule.$el.find('p.filter-description');
var description = e.builder.getFilterDescription(rule.filter, rule);
if (!description) {
$p.hide();
} else {
if ($p.length === 0) {
$p = $($.parseHTML('<p class="filter-description"></p>'));
$p.appendTo(rule.$el);
} else {
$p.css('display', '');
}
$p.html('<i class="' + options.icon + '"></i> ' + 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 afterUpdateRuleOperator',
function (e, rule) {
var $b = rule.$el.find('button.filter-description');
var description = e.builder.getFilterDescription(rule.filter, rule);
if (!description) {
$b.hide();
if ($b.data('bs.popover')) {
$b.popover('hide');
}
} else {
if ($b.length === 0) {
$b = $(
$.parseHTML(
'<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(QueryBuilder.selectors.rule_actions)
);
$b.popover({
placement: 'left',
container: 'body',
html: true,
});
$b.on('mouseout', function () {
$b.popover('hide');
});
} else {
$b.css('display', '');
}
$b.data('bs.popover').options.content = 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 afterUpdateRuleOperator',
function (e, rule) {
var $b = rule.$el.find('button.filter-description');
var description = e.builder.getFilterDescription(rule.filter, rule);
if (!description) {
$b.hide();
} else {
if ($b.length === 0) {
$b = $(
$.parseHTML(
'<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(QueryBuilder.selectors.rule_actions)
);
$b.on('click', function () {
bootbox.alert($b.data('description'));
});
} else {
$b.css('display', '');
}
$b.data('description', description);
}
}
);
}
},
{
icon: 'glyphicon glyphicon-info-sign',
mode: 'popover',
}
);
QueryBuilder.extend(
/** @lends module:plugins.FilterDescription.prototype */ {
/**
* Returns the description of a filter for a particular rule (if present)
* @param {object} filter
* @param {Rule} [rule]
* @returns {string}
* @private
*/
getFilterDescription: function (filter, rule) {
if (!filter) {
return undefined;
} else if (typeof filter.description == 'function') {
return filter.description.call(this, rule);
} else {
return filter.description;
}
},
}
);
/**
* @class Invert
* @memberof module:plugins
* @description Allows to invert a rule operator, a group condition or the entire builder.
* @param {object} [options]
* @param {string} [options.icon='glyphicon glyphicon-random']
* @param {boolean} [options.recursive=true]
* @param {boolean} [options.invert_rules=true]
* @param {boolean} [options.display_rules_button=false]
* @param {boolean} [options.silent_fail=false]
*/
QueryBuilder.define(
'invert',
function (options) {
var self = this;
var Selectors = QueryBuilder.selectors;
// Bind events
this.on('afterInit', function () {
self.$el.on('click.queryBuilder', '[data-invert=group]', function () {
var $group = $(this).closest(Selectors.group_container);
self.invert(self.getModel($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(self.getModel($rule), options);
});
}
});
// Modify templates
if (!options.disable_template) {
this.on('getGroupTemplate.filter', function (h) {
var $h = $($.parseHTML(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.translate('invert') +
'</button>'
);
h.value = $h.prop('outerHTML');
});
if (options.display_rules_button && options.invert_rules) {
this.on('getRuleTemplate.filter', function (h) {
var $h = $($.parseHTML(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.translate('invert') +
'</button>'
);
h.value = $h.prop('outerHTML');
});
}
}
},
{
icon: 'glyphicon glyphicon-random',
recursive: true,
invert_rules: true,
display_rules_button: false,
silent_fail: false,
disable_template: false,
}
);
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.extend(
/** @lends module:plugins.Invert.prototype */ {
/**
* Invert a Group, a Rule or the whole builder
* @param {Node} [node]
* @param {object} [options] {@link module:plugins.Invert}
* @fires module:plugins.Invert.afterInvert
* @throws InvertConditionError, InvertOperatorError
*/
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) {
/**
* After {@link module:plugins.Invert.invert} method
* @event afterInvert
* @memberof module:plugins.Invert
* @param {Node} node - the main group or rule that has been modified
* @param {object} options
*/
this.trigger('afterInvert', node, options);
this.trigger('rulesChanged');
}
},
}
);
/**
* @class MongoDbSupport
* @memberof module:plugins
* @description Allows to export rules as a MongoDB find object as well as populating the builder from a MongoDB object.
*/
QueryBuilder.defaults({
mongoOperators: {
// @formatter:off
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 };
},
// @formatter:on
},
mongoRuleOperators: {
$eq: function (v) {
return {
val: v,
op: v === null ? 'is_null' : v === '' ? 'is_empty' : 'equal',
};
},
$ne: function (v) {
v = v.$ne;
return {
val: v,
op:
v === null
? 'is_not_null'
: v === ''
? 'is_not_empty'
: 'not_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' };
},
},
});
QueryBuilder.extend(
/** @lends module:plugins.MongoDbSupport.prototype */ {
/**
* Returns rules as a MongoDB query
* @param {object} [data] - current rules by default
* @returns {object}
* @fires module:plugins.MongoDbSupport.changer:getMongoDBField
* @fires module:plugins.MongoDbSupport.changer:ruleToMongo
* @fires module:plugins.MongoDbSupport.changer:groupToMongo
* @throws UndefinedMongoConditionError, UndefinedMongoOperatorError
*/
getMongo: function (data) {
data = data === undefined ? this.getRules() : data;
if (!data) {
return null;
}
var self = this;
return (function parse(group) {
if (!group.condition) {
group.condition = self.settings.default_condition;
}
if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) {
Utils.error(
'UndefinedMongoCondition',
'Unable to build MongoDB query with condition "{0}"',
group.condition
);
}
if (!group.rules) {
return {};
}
var parts = [];
group.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);
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];
}
}
/**
* Modifies the MongoDB field used by a rule
* @event changer:getMongoDBField
* @memberof module:plugins.MongoDbSupport
* @param {string} field
* @param {Rule} rule
* @returns {string}
*/
var field = self.change('getMongoDBField', rule.field, rule);
var ruleExpression = {};
ruleExpression[field] = mdb.call(self, rule.value);
/**
* Modifies the MongoDB expression generated for a rul
* @event changer:ruleToMongo
* @memberof module:plugins.MongoDbSupport
* @param {object} expression
* @param {Rule} rule
* @param {*} value
* @param {function} valueWrapper - function that takes the value and adds the operator
* @returns {object}
*/
parts.push(
self.change(
'ruleToMongo',
ruleExpression,
rule,
rule.value,
mdb
)
);
}
});
var groupExpression = {};
groupExpression['$' + group.condition.toLowerCase()] = parts;
/**
* Modifies the MongoDB expression generated for a group
* @event changer:groupToMongo
* @memberof module:plugins.MongoDbSupport
* @param {object} expression
* @param {Group} group
* @returns {object}
*/
return self.change('groupToMongo', groupExpression, group);
})(data);
},
/**
* Converts a MongoDB query to rules
* @param {object} query
* @returns {object}
* @fires module:plugins.MongoDbSupport.changer:parseMongoNode
* @fires module:plugins.MongoDbSupport.changer:getMongoDBFieldID
* @fires module:plugins.MongoDbSupport.changer:mongoToRule
* @fires module:plugins.MongoDbSupport.changer:mongoToGroup
* @throws MongoParseError, UndefinedMongoConditionError, UndefinedMongoOperatorError
*/
getRulesFromMongo: function (query) {
if (query === undefined || query === null) {
return null;
}
var self = this;
/**
* Custom parsing of a MongoDB expression, you can return a sub-part of the expression, or a well formed group or rule JSON
* @event changer:parseMongoNode
* @memberof module:plugins.MongoDbSupport
* @param {object} expression
* @returns {object} expression, rule or group
*/
query = self.change('parseMongoNode', query);
// a plugin returned a group
if ('rules' in query && 'condition' in query) {
return query;
}
// a plugin returned a rule
if ('id' in query && 'operator' in query && 'value' in query) {
return {
condition: this.settings.default_condition,
rules: [query],
};
}
var key = self.getMongoCondition(query);
if (!key) {
Utils.error('MongoParse', 'Invalid MongoDB query format');
}
return (function parse(data, topKey) {
var rules = data[topKey];
var parts = [];
rules.forEach(function (data) {
// allow plugins to manually parse or handle special cases
data = self.change('parseMongoNode', data);
// a plugin returned a group
if ('rules' in data && 'condition' in data) {
parts.push(data);
return;
}
// a plugin returned a rule
if ('id' in data && 'operator' in data && 'value' in data) {
parts.push(data);
return;
}
var key = self.getMongoCondition(data);
if (key) {
parts.push(parse(data, key));
} else {
var field = Object.keys(data)[0];
var value = data[field];
var operator = self.getMongoOperator(value);
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);
var id = self.getMongoDBFieldID(field, value);
/**
* Modifies the rule generated from the MongoDB expression
* @event changer:mongoToRule
* @memberof module:plugins.MongoDbSupport
* @param {object} rule
* @param {object} expression
* @returns {object}
*/
var rule = self.change(
'mongoToRule',
{
id: id,
field: field,
operator: opVal.op,
value: opVal.val,
},
data
);
parts.push(rule);
}
});
/**
* Modifies the group generated from the MongoDB expression
* @event changer:mongoToGroup
* @memberof module:plugins.MongoDbSupport
* @param {object} group
* @param {object} expression
* @returns {object}
*/
return self.change(
'mongoToGroup',
{
condition: topKey.replace('$', '').toUpperCase(),
rules: parts,
},
data
);
})(query, key);
},
/**
* Sets rules a from MongoDB query
* @see module:plugins.MongoDbSupport.getRulesFromMongo
*/
setRulesFromMongo: function (query) {
this.setRules(this.getRulesFromMongo(query));
},
/**
* Returns a filter identifier from the MongoDB field.
* Automatically use the only one filter with a matching field, fires a changer otherwise.
* @param {string} field
* @param {*} value
* @fires module:plugins.MongoDbSupport:changer:getMongoDBFieldID
* @returns {string}
* @private
*/
getMongoDBFieldID: function (field, value) {
var matchingFilters = this.filters.filter(function (filter) {
return filter.field === field;
});
var id;
if (matchingFilters.length === 1) {
id = matchingFilters[0].id;
} else {
/**
* Returns a filter identifier from the MongoDB field
* @event changer:getMongoDBFieldID
* @memberof module:plugins.MongoDbSupport
* @param {string} field
* @param {*} value
* @returns {string}
*/
id = this.change('getMongoDBFieldID', field, value);
}
return id;
},
/**
* Finds which operator is used in a MongoDB sub-object
* @param {*} data
* @returns {string|undefined}
* @private
*/
getMongoOperator: function (data) {
if (data !== null && typeof data === 'object') {
if (data.$gte !== undefined && data.$lte !== undefined) {
return 'between';
}
if (data.$lt !== undefined && data.$gt !== undefined) {
return 'not_between';
}
var knownKeys = Object.keys(data).filter(
function (key) {
return !!this.settings.mongoRuleOperators[key];
}.bind(this)
);
if (knownKeys.length === 1) {
return knownKeys[0];
}
} else {
return '$eq';
}
},
/**
* Returns the key corresponding to "$or" or "$and"
* @param {object} data
* @returns {string|undefined}
* @private
*/
getMongoCondition: function (data) {
var keys = Object.keys(data);
for (var i = 0, l = keys.length; i < l; i++) {
if (
keys[i].toLowerCase() === '$or' ||
keys[i].toLowerCase() === '$and'
) {
return keys[i];
}
}
},
}
);
/**
* @class NotGroup
* @memberof module:plugins
* @description Adds a "Not" checkbox in front of group conditions.
* @param {object} [options]
* @param {string} [options.icon_checked='glyphicon glyphicon-checked']
* @param {string} [options.icon_unchecked='glyphicon glyphicon-unchecked']
*/
QueryBuilder.define(
'not-group',
function (options) {
var self = this;
// Bind events
this.on('afterInit', function () {
self.$el.on('click.queryBuilder', '[data-not=group]', function () {
var $group = $(this).closest(QueryBuilder.selectors.group_container);
var group = self.getModel($group);
group.not = !group.not;
});
self.model.on('update', function (e, node, field) {
if (node instanceof Group && field === 'not') {
self.updateGroupNot(node);
}
});
});
// Init "not" property
this.on('afterAddGroup', function (e, group) {
group.__.not = false;
});
// Modify templates
if (!options.disable_template) {
this.on('getGroupTemplate.filter', function (h) {
var $h = $($.parseHTML(h.value));
$h.find(QueryBuilder.selectors.condition_container).prepend(
'<button type="button" class="btn btn-xs btn-default" data-not="group">' +
'<i class="' +
options.icon_unchecked +
'"></i> ' +
self.translate('NOT') +
'</button>'
);
h.value = $h.prop('outerHTML');
});
}
// Export "not" to JSON
this.on('groupToJson.filter', function (e, group) {
e.value.not = group.not;
});
// Read "not" from JSON
this.on('jsonToGroup.filter', function (e, json) {
e.value.not = !!json.not;
});
// Export "not" to SQL
this.on('groupToSQL.filter', function (e, group) {
if (group.not) {
e.value = 'NOT ( ' + e.value + ' )';
}
});
// Parse "NOT" function from sqlparser
this.on('parseSQLNode.filter', function (e) {
if (e.value.name && e.value.name.toUpperCase() == 'NOT') {
e.value = e.value.arguments.value[0];
// if the there is no sub-group, create one
if (['AND', 'OR'].indexOf(e.value.operation.toUpperCase()) === -1) {
e.value = new SQLParser.nodes.Op(
self.settings.default_condition,
e.value,
null
);
}
e.value.not = true;
}
});
// Request to create sub-group if the "not" flag is set
this.on('sqlGroupsDistinct.filter', function (e, group, data, i) {
if (data.not && i > 0) {
e.value = true;
}
});
// Read "not" from parsed SQL
this.on('sqlToGroup.filter', function (e, data) {
e.value.not = !!data.not;
});
// Export "not" to Mongo
this.on('groupToMongo.filter', function (e, group) {
var key = '$' + group.condition.toLowerCase();
if (group.not && e.value[key]) {
e.value = { $nor: [e.value] };
}
});
// Parse "$nor" operator from Mongo
this.on('parseMongoNode.filter', function (e) {
var keys = Object.keys(e.value);
if (keys[0] == '$nor') {
e.value = e.value[keys[0]][0];
e.value.not = true;
}
});
// Read "not" from parsed Mongo
this.on('mongoToGroup.filter', function (e, data) {
e.value.not = !!data.not;
});
},
{
icon_unchecked: 'glyphicon glyphicon-unchecked',
icon_checked: 'glyphicon glyphicon-check',
disable_template: false,
}
);
/**
* From {@link module:plugins.NotGroup}
* @name not
* @member {boolean}
* @memberof Group
* @instance
*/
Utils.defineModelProperties(Group, ['not']);
QueryBuilder.selectors.group_not =
QueryBuilder.selectors.group_header + ' [data-not=group]';
QueryBuilder.extend(
/** @lends module:plugins.NotGroup.prototype */ {
/**
* Performs actions when a group's not changes
* @param {Group} group
* @fires module:plugins.NotGroup.afterUpdateGroupNot
* @private
*/
updateGroupNot: function (group) {
var options = this.plugins['not-group'];
group.$el
.find('>' + QueryBuilder.selectors.group_not)
.toggleClass('active', group.not)
.find('i')
.attr(
'class',
group.not ? options.icon_checked : options.icon_unchecked
);
/**
* After the group's not flag has been modified
* @event afterUpdateGroupNot
* @memberof module:plugins.NotGroup
* @param {Group} group
*/
this.trigger('afterUpdateGroupNot', group);
this.trigger('rulesChanged');
},
}
);
/**
* @class Sortable
* @memberof module:plugins
* @description Enables drag & drop sort of rules.
* @param {object} [options]
* @param {boolean} [options.inherit_no_drop=true]
* @param {boolean} [options.inherit_no_sortable=true]
* @param {string} [options.icon='glyphicon glyphicon-sort']
* @throws MissingLibraryError, ConfigError
*/
QueryBuilder.define(
'sortable',
function (options) {
if (!('interact' in window)) {
Utils.error(
'MissingLibrary',
'interact.js is required to use "sortable" plugin. Get it here: http://interactjs.io'
);
}
if (options.default_no_sortable !== undefined) {
Utils.error(
false,
'Config',
'Sortable plugin : "default_no_sortable" options is deprecated, use standard "default_rule_flags" and "default_group_flags" instead'
);
this.settings.default_rule_flags.no_sortable =
this.settings.default_group_flags.no_sortable =
options.default_no_sortable;
}
// recompute drop-zones during drag (when a rule is hidden)
interact.dynamicDrop(true);
// set move threshold to 10px
interact.pointerMoveTolerance(10);
var placeholder;
var ghost;
var src;
var moved;
// Init drag and drop
this.on('afterAddRule afterAddGroup', function (e, node) {
if (node == placeholder) {
return;
}
var self = e.builder;
// Inherit flags
if (
options.inherit_no_sortable &&
node.parent &&
node.parent.flags.no_sortable
) {
node.flags.no_sortable = true;
}
if (
options.inherit_no_drop &&
node.parent &&
node.parent.flags.no_drop
) {
node.flags.no_drop = true;
}
// Configure drag
if (!node.flags.no_sortable) {
interact(node.$el[0]).draggable({
allowFrom: QueryBuilder.selectors.drag_handle,
onstart: function (event) {
moved = false;
// get model of dragged element
src = self.getModel(event.target);
// create ghost
ghost = src.$el
.clone()
.appendTo(src.$el.parent())
.width(src.$el.outerWidth())
.addClass('dragging');
// create drop placeholder
var ph = $(
$.parseHTML('<div class="rule-placeholder"> </div>')
).height(src.$el.outerHeight());
placeholder = src.parent.addRule(ph, src.getPos());
// hide dragged element
src.$el.hide();
},
onmove: function (event) {
// make the ghost follow the cursor
ghost[0].style.top = event.clientY - 15 + 'px';
ghost[0].style.left = event.clientX - 15 + 'px';
},
onend: function (event) {
// starting from Interact 1.3.3, onend is called before ondrop
if (event.dropzone) {
moveSortableToTarget(src, $(event.relatedTarget), self);
moved = true;
}
// remove ghost
ghost.remove();
ghost = undefined;
// remove placeholder
placeholder.drop();
placeholder = undefined;
// show element
src.$el.css('display', '');
/**
* After a node has been moved with {@link module:plugins.Sortable}
* @event afterMove
* @memberof module:plugins.Sortable
* @param {Node} node
*/
self.trigger('afterMove', src);
self.trigger('rulesChanged');
},
});
}
if (!node.flags.no_drop) {
// Configure drop on groups and rules
interact(node.$el[0]).dropzone({
accept: QueryBuilder.selectors.rule_and_group_containers,
ondragenter: function (event) {
moveSortableToTarget(placeholder, $(event.target), self);
},
ondrop: function (event) {
if (!moved) {
moveSortableToTarget(src, $(event.target), self);
}
},
});
// Configure drop on group headers
if (node instanceof Group) {
interact(
node.$el.find(QueryBuilder.selectors.group_header)[0]
).dropzone({
accept: QueryBuilder.selectors.rule_and_group_containers,
ondragenter: function (event) {
moveSortableToTarget(placeholder, $(event.target), self);
},
ondrop: function (event) {
if (!moved) {
moveSortableToTarget(src, $(event.target), self);
}
},
});
}
}
});
// Detach interactables
this.on('beforeDeleteRule beforeDeleteGroup', function (e, node) {
if (!e.isDefaultPrevented()) {
interact(node.$el[0]).unset();
if (node instanceof Group) {
interact(
node.$el.find(QueryBuilder.selectors.group_header)[0]
).unset();
}
}
});
// Remove drag handle from non-sortable items
this.on('afterApplyRuleFlags afterApplyGroupFlags', function (e, node) {
if (node.flags.no_sortable) {
node.$el.find('.drag-handle').remove();
}
});
// Modify templates
if (!options.disable_template) {
this.on('getGroupTemplate.filter', function (h, level) {
if (level > 1) {
var $h = $($.parseHTML(h.value));
$h.find(QueryBuilder.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 = $($.parseHTML(h.value));
$h.find(QueryBuilder.selectors.rule_header).after(
'<div class="drag-handle"><i class="' +
options.icon +
'"></i></div>'
);
h.value = $h.prop('outerHTML');
});
}
},
{
inherit_no_sortable: true,
inherit_no_drop: true,
icon: 'glyphicon glyphicon-sort',
disable_template: false,
}
);
QueryBuilder.selectors.rule_and_group_containers =
QueryBuilder.selectors.rule_container +
', ' +
QueryBuilder.selectors.group_container;
QueryBuilder.selectors.drag_handle = '.drag-handle';
QueryBuilder.defaults({
default_rule_flags: {
no_sortable: false,
no_drop: false,
},
default_group_flags: {
no_sortable: false,
no_drop: false,
},
});
/**
* Moves an element (placeholder or actual object) depending on active target
* @memberof module:plugins.Sortable
* @param {Node} node
* @param {jQuery} target
* @param {QueryBuilder} [builder]
* @private
*/
function moveSortableToTarget(node, target, builder) {
var parent, method;
var Selectors = QueryBuilder.selectors;
// on rule
parent = target.closest(Selectors.rule_container);
if (parent.length) {
method = 'moveAfter';
}
// on group header
if (!method) {
parent = target.closest(Selectors.group_header);
if (parent.length) {
parent = target.closest(Selectors.group_container);
method = 'moveAtBegin';
}
}
// on group
if (!method) {
parent = target.closest(Selectors.group_container);
if (parent.length) {
method = 'moveAtEnd';
}
}
if (method) {
node[method](builder.getModel(parent));
// refresh radio value
if (builder && node instanceof Rule) {
builder.setRuleInputValue(node, node.value);
}
}
}
/**
* @class SqlSupport
* @memberof module:plugins
* @description Allows to export rules as a SQL WHERE statement as well as populating the builder from an SQL query.
* @param {object} [options]
* @param {boolean} [options.boolean_as_integer=true] - `true` to convert boolean values to integer in the SQL output
*/
QueryBuilder.define('sql-support', function (options) {}, {
boolean_as_integer: true,
});
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}%', escape: '%_' },
not_begins_with: { op: 'NOT LIKE ?', mod: '{0}%', escape: '%_' },
contains: { op: 'LIKE ?', mod: '%{0}%', escape: '%_' },
not_contains: { op: 'NOT LIKE ?', mod: '%{0}%', escape: '%_' },
ends_with: { op: 'LIKE ?', mod: '%{0}', escape: '%_' },
not_ends_with: { op: 'NOT LIKE ?', mod: '%{0}', escape: '%_' },
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);
}
},
'NOT LIKE': function (v) {
if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
return {
val: v.slice(1, -1),
op: 'not_contains',
};
} else if (v.slice(0, 1) == '%') {
return {
val: v.slice(1),
op: 'not_ends_with',
};
} else if (v.slice(-1) == '%') {
return {
val: v.slice(0, -1),
op: 'not_begins_with',
};
} else {
Utils.error(
'SQLParse',
'Invalid value for NOT 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('|') + ')\\b',
'g'
);
return {
parse: function (v) {
return regex1.test(v) ? values[v.slice(1)] : v;
},
esc: function (sql) {
return sql.replace(
regex2,
"'" + (char == '$' ? '$$' : char) + "$1'"
);
},
};
},
},
});
/**
* @typedef {object} SqlQuery
* @memberof module:plugins.SqlSupport
* @property {string} sql
* @property {object} params
*/
QueryBuilder.extend(
/** @lends module:plugins.SqlSupport.prototype */ {
/**
* Returns rules as a SQL query
* @param {boolean|string} [stmt] - use prepared statements: false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)'
* @param {boolean} [nl=false] output with new lines
* @param {object} [data] - current rules by default
* @returns {module:plugins.SqlSupport.SqlQuery}
* @fires module:plugins.SqlSupport.changer:getSQLField
* @fires module:plugins.SqlSupport.changer:ruleToSQL
* @fires module:plugins.SqlSupport.changer:groupToSQL
* @throws UndefinedSQLConditionError, UndefinedSQLOperatorError
*/
getSQL: function (stmt, nl, data) {
data = data === undefined ? this.getRules() : data;
if (!data) {
return null;
}
nl = !!nl ? '\n' : ' ';
var boolean_as_integer = this.getPluginOptions(
'sql-support',
'boolean_as_integer'
);
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(group) {
if (!group.condition) {
group.condition = self.settings.default_condition;
}
if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) {
Utils.error(
'UndefinedSQLCondition',
'Unable to build SQL query with condition "{0}"',
group.condition
);
}
if (!group.rules) {
return '';
}
var parts = [];
group.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 == 'boolean' && boolean_as_integer) {
v = v ? 1 : 0;
} else if (
!stmt &&
rule.type !== 'integer' &&
rule.type !== 'double' &&
rule.type !== 'boolean'
) {
v = Utils.escapeString(v, sql.escape);
}
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;
}
});
}
var sqlFn = function (v) {
return sql.op.replace('?', function () {
return v;
});
};
/**
* Modifies the SQL field used by a rule
* @event changer:getSQLField
* @memberof module:plugins.SqlSupport
* @param {string} field
* @param {Rule} rule
* @returns {string}
*/
var field = self.change('getSQLField', rule.field, rule);
var ruleExpression = field + ' ' + sqlFn(value);
/**
* Modifies the SQL generated for a rule
* @event changer:ruleToSQL
* @memberof module:plugins.SqlSupport
* @param {string} expression
* @param {Rule} rule
* @param {*} value
* @param {function} valueWrapper - function that takes the value and adds the operator
* @returns {string}
*/
parts.push(
self.change('ruleToSQL', ruleExpression, rule, value, sqlFn)
);
}
});
var groupExpression = parts.join(' ' + group.condition + nl);
/**
* Modifies the SQL generated for a group
* @event changer:groupToSQL
* @memberof module:plugins.SqlSupport
* @param {string} expression
* @param {Group} group
* @returns {string}
*/
return self.change('groupToSQL', groupExpression, group);
})(data);
if (stmt) {
return {
sql: sql,
params: stmt.run(),
};
} else {
return {
sql: sql,
};
}
},
/**
* Convert a SQL query to rules
* @param {string|module:plugins.SqlSupport.SqlQuery} query
* @param {boolean|string} stmt
* @returns {object}
* @fires module:plugins.SqlSupport.changer:parseSQLNode
* @fires module:plugins.SqlSupport.changer:getSQLFieldID
* @fires module:plugins.SqlSupport.changer:sqlToRule
* @fires module:plugins.SqlSupport.changer:sqlToGroup
* @throws MissingLibraryError, SQLParseError, UndefinedSQLOperatorError
*/
getRulesFromSQL: function (query, 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 query == 'string') {
query = { sql: query };
}
if (stmt === true) stmt = 'question_mark';
if (typeof stmt == 'string') {
var config = getStmtConfig(stmt);
stmt = this.settings.sqlRuleStatement[config[1]](
query.params,
config[2]
);
}
if (stmt) {
query.sql = stmt.esc(query.sql);
}
if (query.sql.toUpperCase().indexOf('SELECT') !== 0) {
query.sql = 'SELECT * FROM table WHERE ' + query.sql;
}
var parsed = SQLParser.parse(query.sql);
if (!parsed.where) {
Utils.error('SQLParse', 'No WHERE clause found');
}
/**
* Custom parsing of an AST node generated by SQLParser, you can return a sub-part of the tree, or a well formed group or rule JSON
* @event changer:parseSQLNode
* @memberof module:plugins.SqlSupport
* @param {object} AST node
* @returns {object} tree, rule or group
*/
var data = self.change('parseSQLNode', parsed.where.conditions);
// a plugin returned a group
if ('rules' in data && 'condition' in data) {
return data;
}
// a plugin returned a rule
if ('id' in data && 'operator' in data && 'value' in data) {
return {
condition: this.settings.default_condition,
rules: [data],
};
}
// create root group
var out = self.change(
'sqlToGroup',
{
condition: this.settings.default_condition,
rules: [],
},
data
);
// keep track of current group
var curr = out;
(function flatten(data, i) {
if (data === null) {
return;
}
// allow plugins to manually parse or handle special cases
data = self.change('parseSQLNode', data);
// a plugin returned a group
if ('rules' in data && 'condition' in data) {
curr.rules.push(data);
return;
}
// a plugin returned a rule
if ('id' in data && 'operator' in data && 'value' in data) {
curr.rules.push(data);
return;
}
// data must be a SQL parser node
if (
!('left' in data) ||
!('right' in data) ||
!('operation' in data)
) {
Utils.error('SQLParse', 'Unable to parse WHERE clause');
}
// 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
/**
* Given an existing group and an AST node, determines if a sub-group must be created
* @event changer:sqlGroupsDistinct
* @memberof module:plugins.SqlSupport
* @param {boolean} create - true by default if the group condition is different
* @param {object} group
* @param {object} AST
* @param {int} current group level
* @returns {boolean}
*/
var createGroup = self.change(
'sqlGroupsDistinct',
i > 0 && curr.condition != data.operation.toUpperCase(),
curr,
data,
i
);
if (createGroup) {
/**
* Modifies the group generated from the SQL expression (this is called before the group is filled with rules)
* @event changer:sqlToGroup
* @memberof module:plugins.SqlSupport
* @param {object} group
* @param {object} AST
* @returns {object}
*/
var group = self.change(
'sqlToGroup',
{
condition: self.settings.default_condition,
rules: [],
},
data
);
curr.rules.push(group);
curr = group;
}
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 ($.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 = self.settings.sqlRuleOperator[operator];
if (sqlrl === undefined) {
Utils.error(
'UndefinedSQLOperator',
'Invalid SQL operation "{0}".',
data.operation
);
}
var opVal = sqlrl.call(this, value, data.operation);
// find field name
var field;
if ('values' in data.left) {
field = data.left.values.join('.');
} else if ('value' in data.left) {
field = data.left.value;
} else {
Utils.error(
'SQLParse',
'Cannot find field name in {0}',
JSON.stringify(data.left)
);
}
// unescape chars declared by the operator
var finalValue = opVal.val;
var sql = self.settings.sqlOperators[opVal.op];
if (!stmt && sql && sql.escape) {
var searchChars = sql.escape
.split('')
.map(function (c) {
return '\\\\' + c;
})
.join('|');
finalValue = finalValue.replace(
new RegExp('(' + searchChars + ')', 'g'),
function (s) {
return s[1];
}
);
}
var id = self.getSQLFieldID(field, value);
/**
* Modifies the rule generated from the SQL expression
* @event changer:sqlToRule
* @memberof module:plugins.SqlSupport
* @param {object} rule
* @param {object} AST
* @returns {object}
*/
var rule = self.change(
'sqlToRule',
{
id: id,
field: field,
operator: opVal.op,
value: finalValue,
},
data
);
curr.rules.push(rule);
}
})(data, 0);
return out;
},
/**
* Sets the builder's rules from a SQL query
* @see module:plugins.SqlSupport.getRulesFromSQL
*/
setRulesFromSQL: function (query, stmt) {
this.setRules(this.getRulesFromSQL(query, stmt));
},
/**
* Returns a filter identifier from the SQL field.
* Automatically use the only one filter with a matching field, fires a changer otherwise.
* @param {string} field
* @param {*} value
* @fires module:plugins.SqlSupport:changer:getSQLFieldID
* @returns {string}
* @private
*/
getSQLFieldID: function (field, value) {
var matchingFilters = this.filters.filter(function (filter) {
return filter.field.toLowerCase() === field.toLowerCase();
});
var id;
if (matchingFilters.length === 1) {
id = matchingFilters[0].id;
} else {
/**
* Returns a filter identifier from the SQL field
* @event changer:getSQLFieldID
* @memberof module:plugins.SqlSupport
* @param {string} field
* @param {*} value
* @returns {string}
*/
id = this.change('getSQLFieldID', field, value);
}
return id;
},
}
);
/**
* Parses the statement configuration
* @memberof module:plugins.SqlSupport
* @param {string} stmt
* @returns {Array} null, mode, option
* @private
*/
function getStmtConfig(stmt) {
var config = stmt.match(/(question_mark|numbered|named)(?:\((.)\))?/);
if (!config) config = [null, 'question_mark', undefined];
return config;
}
/**
* @class UniqueFilter
* @memberof module:plugins
* @description 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);
// Ensure that the default filter is not already used if unique
this.on('getDefaultFilter.filter', function (e, model) {
var self = e.builder;
self.updateDisabledFilters();
if (e.value.id in self.status.used_filters) {
var found = self.filters.some(function (filter) {
if (
!(filter.id in self.status.used_filters) ||
(self.status.used_filters[filter.id].length > 0 &&
self.status.used_filters[filter.id].indexOf(model.parent) === -1)
) {
e.value = filter;
return true;
}
});
if (!found) {
Utils.error(
false,
'UniqueFilter',
'No more non-unique filters available'
);
e.value = undefined;
}
}
});
});
QueryBuilder.extend(
/** @lends module:plugins.UniqueFilter.prototype */ {
/**
* Updates the list of used filters
* @param {$.Event} [e]
* @private
*/
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);
},
/**
* Clear the list of used filters
* @param {$.Event} [e]
* @private
*/
clearDisabledFilters: function (e) {
var self = e ? e.builder : this;
self.status.used_filters = {};
self.applyDisabledFilters(e);
},
/**
* Disabled filters depending on the list of used ones
* @param {$.Event} [e]
* @private
*/
applyDisabledFilters: function (e) {
var self = e ? e.builder : this;
// re-enable everything
self.$el
.find(QueryBuilder.selectors.filter_container + ' option')
.prop('disabled', false);
// disable some
$.each(self.status.used_filters, function (filterId, groups) {
if (groups.length === 0) {
self.$el
.find(
QueryBuilder.selectors.filter_container +
' option[value="' +
filterId +
'"]:not(:selected)'
)
.prop('disabled', true);
} else {
groups.forEach(function (group) {
group.each(function (rule) {
rule.$el
.find(
QueryBuilder.selectors.filter_container +
' option[value="' +
filterId +
'"]:not(:selected)'
)
.prop('disabled', true);
});
});
}
});
// update Plugins
if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) {
self.$el
.find(QueryBuilder.selectors.rule_filter)
.selectpicker('render');
}
// select2 구문 추가
if (self.settings.plugins && self.settings.plugins['select2']) {
self.$el
.find(QueryBuilder.selectors.rule_filter)
.trigger('change.select2');
}
},
}
);
/*!
* jQuery QueryBuilder 2.6.2
* Locale: English (en)
* Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr
* Licensed under MIT (https://opensource.org/licenses/MIT)
*/
QueryBuilder.regional['en'] = {
__locale: 'English (en)',
__author: 'Damien "Mistic" Sorel, http://www.strangeplanet.fr',
add_rule: '하위필터',
add_group: '조건그룹',
delete_rule: 'Delete',
delete_group: 'Delete',
conditions: {
AND: 'AND',
OR: 'OR',
NOT: 'NOT',
},
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: '포함', // text display 설정
not_contains: '미포함', // text display 설정
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}',
number_between_invalid: 'Invalid values, {0} is greater than {1}',
datetime_empty: 'Empty value',
datetime_invalid: 'Invalid date format ({0})',
datetime_exceed_min: 'Must be after {0}',
datetime_exceed_max: 'Must be before {0}',
datetime_between_invalid: 'Invalid values, {0} is greater than {1}',
boolean_not_valid: 'Not a boolean',
operator_not_multiple: 'Operator "{1}" cannot accept multiple values',
},
invert: 'Invert',
NOT: 'NOT',
};
QueryBuilder.defaults({ lang_code: 'en' });
return QueryBuilder;
});
var rules_basic = {
condition: 'AND',
rules: [{
id: 'category',
operator: 'equal',
value: ""
}, {
condition: 'OR',
rules: [{
id: 'name',
operator: 'equal',
value: 2
}, {
id: 'category',
operator: 'equal',
value: 1
}]
}]
};
$('#builder').queryBuilder({
plugins: ['select2'],// 추가한 plugin 설정
filters: [{
id: 'name',
label: 'Name',
type: 'string',
operators: ['contains', 'not_contains']
}, {
id: 'category',
label: 'Category',
type: 'string',
input: 'select',
plugin: 'select2', // select2 플러그인 추가
values: {
1: 'Books',
2: 'Movies',
3: 'Music',
4: 'Tools',
5: 'Goodies',
6: 'Clothes'
},
operators: ['equal']
}],
rules: rules_basic
});