vendor: 11,369 bytes, lines 1-400
1/*! 2 * jQuery.extendext 1.0.0 3 * 4 * Copyright 2014-2019 Damien "Mistic" Sorel (http://www.strangeplanet.fr) 5 * Licensed under MIT (http://opensource.org/licenses/MIT) 6 * 7 * Based on jQuery.extend by jQuery Foundation, Inc. and other contributors 8 */ 9 10(function (root, factory) { 11 if (typeof define === 'function' && define.amd) { 12 define('jquery-extendext', ['jquery'], factory); 13 } 14 else if (typeof module === 'object' && module.exports) { 15 module.exports = factory(require('jquery')); 16 } 17 else { 18 factory(root.jQuery); 19 } 20}(this, function ($) { 21 "use strict"; 22 23 $.extendext = function () { 24 var options, name, src, copy, copyIsArray, clone, 25 target = arguments[0] || {}, 26 i = 1, 27 length = arguments.length, 28 deep = false, 29 arrayMode = 'default'; 30 31 // Handle a deep copy situation 32 if (typeof target === "boolean") { 33 deep = target; 34 35 // Skip the boolean and the target 36 target = arguments[i++] || {}; 37 } 38 39 // Handle array mode parameter 40 if (typeof target === "string") { 41 arrayMode = target.toLowerCase(); 42 if (arrayMode !== 'concat' && arrayMode !== 'replace' && arrayMode !== 'extend') { 43 arrayMode = 'default'; 44 } 45 46 // Skip the string param 47 target = arguments[i++] || {}; 48 } 49 50 // Handle case when target is a string or something (possible in deep copy) 51 if (typeof target !== "object" && !$.isFunction(target)) { 52 target = {}; 53 } 54 55 // Extend jQuery itself if only one argument is passed 56 if (i === length) { 57 target = this; 58 i--; 59 } 60 61 for (; i < length; i++) { 62 // Only deal with non-null/undefined values 63 if ((options = arguments[i]) !== null) { 64 // Special operations for arrays 65 if ($.isArray(options) && arrayMode !== 'default') { 66 clone = target && $.isArray(target) ? target : []; 67 68 switch (arrayMode) { 69 case 'concat': 70 target = clone.concat($.extend(deep, [], options)); 71 break; 72 73 case 'replace': 74 target = $.extend(deep, [], options); 75 break; 76 77 case 'extend': 78 options.forEach(function (e, i) { 79 if (typeof e === 'object') { 80 var type = $.isArray(e) ? [] : {}; 81 clone[i] = $.extendext(deep, arrayMode, clone[i] || type, e); 82 83 } else if (clone.indexOf(e) === -1) { 84 clone.push(e); 85 } 86 }); 87 88 target = clone; 89 break; 90 } 91 92 } else { 93 // Extend the base object 94 for (name in options) { 95 copy = options[name]; 96 97 // Prevent never-ending loop 98 if (name === '__proto__' || target === copy) { 99 continue; 100 } 101 102 // Recurse if we're merging plain objects or arrays 103 if (deep && copy && ( $.isPlainObject(copy) || 104 (copyIsArray = $.isArray(copy)) )) { 105 src = target[name]; 106 107 // Ensure proper type for the source value 108 if ( copyIsArray && !Array.isArray( src ) ) { 109 clone = []; 110 } else if ( !copyIsArray && !$.isPlainObject( src ) ) { 111 clone = {}; 112 } else { 113 clone = src; 114 } 115 copyIsArray = false; 116 117 // Never move original objects, clone them 118 target[name] = $.extendext(deep, arrayMode, clone, copy); 119 120 // Don't bring in undefined values 121 } else if (copy !== undefined) { 122 target[name] = copy; 123 } 124 } 125 } 126 } 127 } 128 129 // Return the modified object 130 return target; 131 }; 132})); 133 134 135/*! 136 * jQuery QueryBuilder 2.7.0 137 * Copyright 2014-2023 Damien "Mistic" Sorel (http://www.strangeplanet.fr) 138 * Licensed under MIT (https://opensource.org/licenses/MIT) 139 */ 140 141(function(root, factory) { 142 if (typeof define == 'function' && define.amd) { 143 define('query-builder', ['jquery', 'jquery-extendext'], factory); 144 } 145 else if (typeof module === 'object' && module.exports) { 146 module.exports = factory(require('jquery'), require('jquery-extendext')); 147 } 148 else { 149 factory(root.jQuery); 150 } 151}(this, function($) { 152"use strict"; 153 154/** 155 * @typedef {object} Filter 156 * @memberof QueryBuilder 157 * @description See {@link http://querybuilder.js.org/index.html#filters} 158 */ 159 160/** 161 * @typedef {object} Operator 162 * @memberof QueryBuilder 163 * @description See {@link http://querybuilder.js.org/index.html#operators} 164 */ 165 166/** 167 * @param {jQuery} $el 168 * @param {object} options - see {@link http://querybuilder.js.org/#options} 169 * @constructor 170 */ 171var QueryBuilder = function($el, options) { 172 $el[0].queryBuilder = this; 173 174 /** 175 * Element container 176 * @member {jQuery} 177 * @readonly 178 */ 179 this.$el = $el; 180 181 /** 182 * Configuration object 183 * @member {object} 184 * @readonly 185 */ 186 this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options); 187 188 /** 189 * Internal model 190 * @member {Model} 191 * @readonly 192 */ 193 this.model = new Model(); 194 195 /** 196 * Internal status 197 * @member {object} 198 * @property {string} id - id of the container 199 * @property {boolean} generated_id - if the container id has been generated 200 * @property {int} group_id - current group id 201 * @property {int} rule_id - current rule id 202 * @property {boolean} has_optgroup - if filters have optgroups 203 * @property {boolean} has_operator_optgroup - if operators have optgroups 204 * @readonly 205 * @private 206 */ 207 this.status = { 208 id: null, 209 generated_id: false, 210 group_id: 0, 211 rule_id: 0, 212 has_optgroup: false, 213 has_operator_optgroup: false 214 }; 215 216 /** 217 * List of filters 218 * @member {QueryBuilder.Filter[]} 219 * @readonly 220 */ 221 this.filters = this.settings.filters; 222 223 /** 224 * List of icons 225 * @member {object.<string, string>} 226 * @readonly 227 */ 228 this.icons = this.settings.icons; 229 230 /** 231 * List of operators 232 * @member {QueryBuilder.Operator[]} 233 * @readonly 234 */ 235 this.operators = this.settings.operators; 236 237 /** 238 * List of templates 239 * @member {object.<string, function>} 240 * @readonly 241 */ 242 this.templates = this.settings.templates; 243 244 /** 245 * Plugins configuration 246 * @member {object.<string, object>} 247 * @readonly 248 */ 249 this.plugins = this.settings.plugins; 250 251 /** 252 * Translations object 253 * @member {object} 254 * @readonly 255 */ 256 this.lang = null; 257 258 // translations : english << 'lang_code' << custom 259 if (QueryBuilder.regional['en'] === undefined) { 260 Utils.error('Config', '"i18n/en.js" not loaded.'); 261 } 262 this.lang = $.extendext(true, 'replace', {}, QueryBuilder.regional['en'], QueryBuilder.regional[this.settings.lang_code], this.settings.lang); 263 264 // "allow_groups" can be boolean or int 265 if (this.settings.allow_groups === false) { 266 this.settings.allow_groups = 0; 267 } 268 else if (this.settings.allow_groups === true) { 269 this.settings.allow_groups = -1; 270 } 271 272 // init templates 273 Object.keys(this.templates).forEach(function(tpl) { 274 if (!this.templates[tpl]) { 275 this.templates[tpl] = QueryBuilder.templates[tpl]; 276 } 277 if (typeof this.templates[tpl] !== 'function') { 278 throw new Error(`Template ${tpl} must be a function`); 279 } 280 }, this); 281 282 // ensure we have a container id 283 if (!this.$el.attr('id')) { 284 this.$el.attr('id', 'qb_' + Math.floor(Math.random() * 99999)); 285 this.status.generated_id = true; 286 } 287 this.status.id = this.$el.attr('id'); 288 289 // INIT 290 this.$el.addClass('query-builder form-inline'); 291 292 this.filters = this.checkFilters(this.filters); 293 this.operators = this.checkOperators(this.operators); 294 this.bindEvents(); 295 this.initPlugins(); 296}; 297 298$.extend(QueryBuilder.prototype, /** @lends QueryBuilder.prototype */ { 299 /** 300 * Triggers an event on the builder container 301 * @param {string} type 302 * @returns {$.Event} 303 */ 304 trigger: function(type) { 305 var event = new $.Event(this._tojQueryEvent(type), { 306 builder: this 307 }); 308 309 this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 1)); 310 311 return event; 312 }, 313 314 /** 315 * Triggers an event on the builder container and returns the modified value 316 * @param {string} type 317 * @param {*} value 318 * @returns {*} 319 */ 320 change: function(type, value) { 321 var event = new $.Event(this._tojQueryEvent(type, true), { 322 builder: this, 323 value: value 324 }); 325 326 this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 2)); 327 328 return event.value; 329 }, 330 331 /** 332 * Attaches an event listener on the builder container 333 * @param {string} type 334 * @param {function} cb 335 * @returns {QueryBuilder} 336 */ 337 on: function(type, cb) { 338 this.$el.on(this._tojQueryEvent(type), cb); 339 return this; 340 }, 341 342 /** 343 * Removes an event listener from the builder container 344 * @param {string} type 345 * @param {function} [cb] 346 * @returns {QueryBuilder} 347 */ 348 off: function(type, cb) { 349 this.$el.off(this._tojQueryEvent(type), cb); 350 return this; 351 }, 352 353 /** 354 * Attaches an event listener called once on the builder container 355 * @param {string} type 356 * @param {function} cb 357 * @returns {QueryBuilder} 358 */ 359 once: function(type, cb) { 360 this.$el.one(this._tojQueryEvent(type), cb); 361 return this; 362 }, 363 364 /** 365 * Appends `.queryBuilder` and optionally `.filter` to the events names 366 * @param {string} name 367 * @param {boolean} [filter=false] 368 * @returns {string} 369 * @private 370 */ 371 _tojQueryEvent: function(name, filter) { 372 return name.split(' ').map(function(type) { 373 return type + '.queryBuilder' + (filter ? '.filter' : ''); 374 }).join(' '); 375 } 376}); 377 378 379/** 380 * Allowed types and their internal representation 381 * @type {object.<string, string>} 382 * @readonly 383 * @private 384 */ 385QueryBuilder.types = { 386 'string': 'string', 387 'integer': 'number', 388 'double': 'number', 389 'date': 'datetime', 390 'time': 'datetime', 391 'datetime': 'datetime', 392 'boolean': 'boolean' 393}; 394 395/** 396 * Allowed inputs 397 * @type {string[]} 398 * @readonly 399 * @private 400 */
401QueryBuilder.inputs = [ 402 'text', 403 'number', 404 'textarea', 405 'radio', 406 'checkbox', 407 'select' 408]; 409 410/** 411 * Runtime modifiable options with `setOptions` method 412 * @type {string[]} 413 * @readonly 414 * @private 415 */ 416QueryBuilder.modifiable_options = [ 417 'display_errors', 418 'allow_groups', 419 'allow_empty', 420 'default_condition', 421 'default_filter' 422]; 423 424/** 425 * CSS selectors for common components 426 * @type {object.<string, string>} 427 * @readonly 428 */ 429QueryBuilder.selectors = { 430 group_container: '.rules-group-container', 431 rule_container: '.rule-container', 432 filter_container: '.rule-filter-container', 433 operator_container: '.rule-operator-container', 434 value_container: '.rule-value-container', 435 error_container: '.error-container', 436 condition_container: '.rules-group-header .group-conditions', 437 438 rule_header: '.rule-header', 439 group_header: '.rules-group-header', 440 group_actions: '.group-actions', 441 rule_actions: '.rule-actions', 442 443 rules_list: '.rules-group-body>.rules-list', 444 445 group_condition: '.rules-group-header [name$=_cond]', 446 rule_filter: '.rule-filter-container [name$=_filter]', 447 rule_operator: '.rule-operator-container [name$=_operator]', 448 rule_value: '.rule-value-container [name*=_value_]', 449 450 add_rule: '[data-add=rule]', 451 delete_rule: '[data-delete=rule]', 452 add_group: '[data-add=group]', 453 delete_group: '[data-delete=group]' 454}; 455 456/** 457 * Template strings (see template.js) 458 * @type {object.<string, string>} 459 * @readonly 460 */ 461QueryBuilder.templates = {}; 462 463/** 464 * Localized strings (see i18n/) 465 * @type {object.<string, object>} 466 * @readonly 467 */ 468QueryBuilder.regional = {}; 469 470/** 471 * Default operators 472 * @type {object.<string, object>} 473 * @readonly 474 */ 475QueryBuilder.OPERATORS = { 476 equal: { type: 'equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] }, 477 not_equal: { type: 'not_equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] }, 478 in: { type: 'in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime'] }, 479 not_in: { type: 'not_in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime'] }, 480 less: { type: 'less', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] }, 481 less_or_equal: { type: 'less_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] }, 482 greater: { type: 'greater', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] }, 483 greater_or_equal: { type: 'greater_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] }, 484 between: { type: 'between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] }, 485 not_between: { type: 'not_between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] }, 486 begins_with: { type: 'begins_with', nb_inputs: 1, multiple: false, apply_to: ['string'] }, 487 not_begins_with: { type: 'not_begins_with', nb_inputs: 1, multiple: false, apply_to: ['string'] }, 488 contains: { type: 'contains', nb_inputs: 1, multiple: false, apply_to: ['string'] }, 489 not_contains: { type: 'not_contains', nb_inputs: 1, multiple: false, apply_to: ['string'] }, 490 ends_with: { type: 'ends_with', nb_inputs: 1, multiple: false, apply_to: ['string'] }, 491 not_ends_with: { type: 'not_ends_with', nb_inputs: 1, multiple: false, apply_to: ['string'] }, 492 is_empty: { type: 'is_empty', nb_inputs: 0, multiple: false, apply_to: ['string'] }, 493 is_not_empty: { type: 'is_not_empty', nb_inputs: 0, multiple: false, apply_to: ['string'] }, 494 is_null: { type: 'is_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] }, 495 is_not_null: { type: 'is_not_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] } 496}
vendor: 68,727 bytes, lines 496-2783
496; 497 498/** 499 * Default configuration 500 * @type {object} 501 * @readonly 502 */ 503QueryBuilder.DEFAULTS = { 504 filters: [], 505 plugins: [], 506 507 sort_filters: false, 508 display_errors: true, 509 allow_groups: -1, 510 allow_empty: false, 511 conditions: ['AND', 'OR'], 512 default_condition: 'AND', 513 inputs_separator: ' , ', 514 select_placeholder: '------', 515 display_empty_filter: true, 516 default_filter: null, 517 optgroups: {}, 518 519 default_rule_flags: { 520 filter_readonly: false, 521 operator_readonly: false, 522 value_readonly: false, 523 no_delete: false 524 }, 525 526 default_group_flags: { 527 condition_readonly: false, 528 no_add_rule: false, 529 no_add_group: false, 530 no_delete: false 531 }, 532 533 templates: { 534 group: null, 535 rule: null, 536 filterSelect: null, 537 operatorSelect: null, 538 ruleValueSelect: null 539 }, 540 541 lang_code: 'en', 542 lang: {}, 543 544 operators: [ 545 'equal', 546 'not_equal', 547 'in', 548 'not_in', 549 'less', 550 'less_or_equal', 551 'greater', 552 'greater_or_equal', 553 'between', 554 'not_between', 555 'begins_with', 556 'not_begins_with', 557 'contains', 558 'not_contains', 559 'ends_with', 560 'not_ends_with', 561 'is_empty', 562 'is_not_empty', 563 'is_null', 564 'is_not_null' 565 ], 566 567 icons: { 568 add_group: 'glyphicon glyphicon-plus-sign', 569 add_rule: 'glyphicon glyphicon-plus', 570 remove_group: 'glyphicon glyphicon-remove', 571 remove_rule: 'glyphicon glyphicon-remove', 572 error: 'glyphicon glyphicon-warning-sign' 573 } 574}; 575 576 577/** 578 * @module plugins 579 */ 580 581/** 582 * Definition of available plugins 583 * @type {object.<String, object>} 584 */ 585QueryBuilder.plugins = {}; 586 587/** 588 * Gets or extends the default configuration 589 * @param {object} [options] - new configuration 590 * @returns {undefined|object} nothing or configuration object (copy) 591 */ 592QueryBuilder.defaults = function(options) { 593 if (typeof options == 'object') { 594 $.extendext(true, 'replace', QueryBuilder.DEFAULTS, options); 595 } 596 else if (typeof options == 'string') { 597 if (typeof QueryBuilder.DEFAULTS[options] == 'object') { 598 return $.extend(true, {}, QueryBuilder.DEFAULTS[options]); 599 } 600 else { 601 return QueryBuilder.DEFAULTS[options]; 602 } 603 } 604 else { 605 return $.extend(true, {}, QueryBuilder.DEFAULTS); 606 } 607}; 608 609/** 610 * Registers a new plugin 611 * @param {string} name 612 * @param {function} fct - init function 613 * @param {object} [def] - default options 614 */ 615QueryBuilder.define = function(name, fct, def) { 616 QueryBuilder.plugins[name] = { 617 fct: fct, 618 def: def || {} 619 }; 620}; 621 622/** 623 * Adds new methods to QueryBuilder prototype 624 * @param {object.<string, function>} methods 625 */ 626QueryBuilder.extend = function(methods) { 627 $.extend(QueryBuilder.prototype, methods); 628}; 629 630/** 631 * Initializes plugins for an instance 632 * @throws ConfigError 633 * @private 634 */ 635QueryBuilder.prototype.initPlugins = function() { 636 if (!this.plugins) { 637 return; 638 } 639 640 if ($.isArray(this.plugins)) { 641 var tmp = {}; 642 this.plugins.forEach(function(plugin) { 643 tmp[plugin] = null; 644 }); 645 this.plugins = tmp; 646 } 647 648 Object.keys(this.plugins).forEach(function(plugin) { 649 if (plugin in QueryBuilder.plugins) { 650 this.plugins[plugin] = $.extend(true, {}, 651 QueryBuilder.plugins[plugin].def, 652 this.plugins[plugin] || {} 653 ); 654 655 QueryBuilder.plugins[plugin].fct.call(this, this.plugins[plugin]); 656 } 657 else { 658 Utils.error('Config', 'Unable to find plugin "{0}"', plugin); 659 } 660 }, this); 661}; 662 663/** 664 * Returns the config of a plugin, if the plugin is not loaded, returns the default config. 665 * @param {string} name 666 * @param {string} [property] 667 * @throws ConfigError 668 * @returns {*} 669 */ 670QueryBuilder.prototype.getPluginOptions = function(name, property) { 671 var plugin; 672 if (this.plugins && this.plugins[name]) { 673 plugin = this.plugins[name]; 674 } 675 else if (QueryBuilder.plugins[name]) { 676 plugin = QueryBuilder.plugins[name].def; 677 } 678 679 if (plugin) { 680 if (property) { 681 return plugin[property]; 682 } 683 else { 684 return plugin; 685 } 686 } 687 else { 688 Utils.error('Config', 'Unable to find plugin "{0}"', name); 689 } 690}; 691 692 693/** 694 * Final initialisation of the builder 695 * @param {object} [rules] 696 * @fires QueryBuilder.afterInit 697 * @private 698 */ 699QueryBuilder.prototype.init = function(rules) { 700 /** 701 * When the initilization is done, just before creating the root group 702 * @event afterInit 703 * @memberof QueryBuilder 704 */ 705 this.trigger('afterInit'); 706 707 if (rules) { 708 this.setRules(rules); 709 delete this.settings.rules; 710 } 711 else { 712 this.setRoot(true); 713 } 714}; 715 716/** 717 * Checks the configuration of each filter 718 * @param {QueryBuilder.Filter[]} filters 719 * @returns {QueryBuilder.Filter[]} 720 * @throws ConfigError 721 */ 722QueryBuilder.prototype.checkFilters = function(filters) { 723 var definedFilters = []; 724 725 if (!filters || filters.length === 0) { 726 Utils.error('Config', 'Missing filters list'); 727 } 728 729 filters.forEach(function(filter, i) { 730 if (!filter.id) { 731 Utils.error('Config', 'Missing filter {0} id', i); 732 } 733 if (definedFilters.indexOf(filter.id) != -1) { 734 Utils.error('Config', 'Filter "{0}" already defined', filter.id); 735 } 736 definedFilters.push(filter.id); 737 738 if (!filter.type) { 739 filter.type = 'string'; 740 } 741 else if (!QueryBuilder.types[filter.type]) { 742 Utils.error('Config', 'Invalid type "{0}"', filter.type); 743 } 744 745 if (!filter.input) { 746 filter.input = QueryBuilder.types[filter.type] === 'number' ? 'number' : 'text'; 747 } 748 else if (typeof filter.input != 'function' && QueryBuilder.inputs.indexOf(filter.input) == -1) { 749 Utils.error('Config', 'Invalid input "{0}"', filter.input); 750 } 751 752 if (filter.operators) { 753 filter.operators.forEach(function(operator) { 754 if (typeof operator != 'string') { 755 Utils.error('Config', 'Filter operators must be global operators types (string)'); 756 } 757 }); 758 } 759 760 if (!filter.field) { 761 filter.field = filter.id; 762 } 763 if (!filter.label) { 764 filter.label = filter.field; 765 } 766 767 if (!filter.optgroup) { 768 filter.optgroup = null; 769 } 770 else { 771 this.status.has_optgroup = true; 772 773 // register optgroup if needed 774 if (!this.settings.optgroups[filter.optgroup]) { 775 this.settings.optgroups[filter.optgroup] = filter.optgroup; 776 } 777 } 778 779 switch (filter.input) { 780 case 'radio': 781 case 'checkbox': 782 if (!filter.values || filter.values.length < 1) { 783 Utils.error('Config', 'Missing filter "{0}" values', filter.id); 784 } 785 break; 786 787 case 'select': 788 var cleanValues = []; 789 filter.has_optgroup = false; 790 791 Utils.iterateOptions(filter.values, function(value, label, optgroup) { 792 cleanValues.push({ 793 value: value, 794 label: label, 795 optgroup: optgroup || null 796 }); 797 798 if (optgroup) { 799 filter.has_optgroup = true; 800 801 // register optgroup if needed 802 if (!this.settings.optgroups[optgroup]) { 803 this.settings.optgroups[optgroup] = optgroup; 804 } 805 } 806 }.bind(this)); 807 808 if (filter.has_optgroup) { 809 filter.values = Utils.groupSort(cleanValues, 'optgroup'); 810 } 811 else { 812 filter.values = cleanValues; 813 } 814 815 if (filter.placeholder) { 816 if (filter.placeholder_value === undefined) { 817 filter.placeholder_value = -1; 818 } 819 820 filter.values.forEach(function(entry) { 821 if (entry.value == filter.placeholder_value) { 822 Utils.error('Config', 'Placeholder of filter "{0}" overlaps with one of its values', filter.id); 823 } 824 }); 825 } 826 break; 827 } 828 }, this); 829 830 if (this.settings.sort_filters) { 831 if (typeof this.settings.sort_filters == 'function') { 832 filters.sort(this.settings.sort_filters); 833 } 834 else { 835 var self = this; 836 filters.sort(function(a, b) { 837 return self.translate(a.label).localeCompare(self.translate(b.label)); 838 }); 839 } 840 } 841 842 if (this.status.has_optgroup) { 843 filters = Utils.groupSort(filters, 'optgroup'); 844 } 845 846 return filters; 847}; 848 849/** 850 * Checks the configuration of each operator 851 * @param {QueryBuilder.Operator[]} operators 852 * @returns {QueryBuilder.Operator[]} 853 * @throws ConfigError 854 */ 855QueryBuilder.prototype.checkOperators = function(operators) { 856 var definedOperators = []; 857 858 operators.forEach(function(operator, i) { 859 if (typeof operator == 'string') { 860 if (!QueryBuilder.OPERATORS[operator]) { 861 Utils.error('Config', 'Unknown operator "{0}"', operator); 862 } 863 864 operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator]); 865 } 866 else { 867 if (!operator.type) { 868 Utils.error('Config', 'Missing "type" for operator {0}', i); 869 } 870 871 if (QueryBuilder.OPERATORS[operator.type]) { 872 operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator.type], operator); 873 } 874 875 if (operator.nb_inputs === undefined || operator.apply_to === undefined) { 876 Utils.error('Config', 'Missing "nb_inputs" and/or "apply_to" for operator "{0}"', operator.type); 877 } 878 } 879 880 if (definedOperators.indexOf(operator.type) != -1) { 881 Utils.error('Config', 'Operator "{0}" already defined', operator.type); 882 } 883 definedOperators.push(operator.type); 884 885 if (!operator.optgroup) { 886 operator.optgroup = null; 887 } 888 else { 889 this.status.has_operator_optgroup = true; 890 891 // register optgroup if needed 892 if (!this.settings.optgroups[operator.optgroup]) { 893 this.settings.optgroups[operator.optgroup] = operator.optgroup; 894 } 895 } 896 }, this); 897 898 if (this.status.has_operator_optgroup) { 899 operators = Utils.groupSort(operators, 'optgroup'); 900 } 901 902 return operators; 903}; 904 905/** 906 * Adds all events listeners to the builder 907 * @private 908 */ 909QueryBuilder.prototype.bindEvents = function() { 910 var self = this; 911 var Selectors = QueryBuilder.selectors; 912 913 // group condition change 914 this.$el.on('change.queryBuilder', Selectors.group_condition, function() { 915 if ($(this).is(':checked')) { 916 var $group = $(this).closest(Selectors.group_container); 917 self.getModel($group).condition = $(this).val(); 918 } 919 }); 920 921 // rule filter change 922 this.$el.on('change.queryBuilder', Selectors.rule_filter, function() { 923 var $rule = $(this).closest(Selectors.rule_container); 924 self.getModel($rule).filter = self.getFilterById($(this).val()); 925 }); 926 927 // rule operator change 928 this.$el.on('change.queryBuilder', Selectors.rule_operator, function() { 929 var $rule = $(this).closest(Selectors.rule_container); 930 self.getModel($rule).operator = self.getOperatorByType($(this).val()); 931 }); 932 933 // add rule button 934 this.$el.on('click.queryBuilder', Selectors.add_rule, function() { 935 var $group = $(this).closest(Selectors.group_container); 936 self.addRule(self.getModel($group)); 937 }); 938 939 // delete rule button 940 this.$el.on('click.queryBuilder', Selectors.delete_rule, function() { 941 var $rule = $(this).closest(Selectors.rule_container); 942 self.deleteRule(self.getModel($rule)); 943 }); 944 945 if (this.settings.allow_groups !== 0) { 946 // add group button 947 this.$el.on('click.queryBuilder', Selectors.add_group, function() { 948 var $group = $(this).closest(Selectors.group_container); 949 self.addGroup(self.getModel($group)); 950 }); 951 952 // delete group button 953 this.$el.on('click.queryBuilder', Selectors.delete_group, function() { 954 var $group = $(this).closest(Selectors.group_container); 955 self.deleteGroup(self.getModel($group)); 956 }); 957 } 958 959 // model events 960 this.model.on({ 961 'drop': function(e, node) { 962 node.$el.remove(); 963 self.refreshGroupsConditions(); 964 }, 965 'add': function(e, parent, node, index) { 966 if (index === 0) { 967 node.$el.prependTo(parent.$el.find('>' + QueryBuilder.selectors.rules_list)); 968 } 969 else { 970 node.$el.insertAfter(parent.rules[index - 1].$el); 971 } 972 self.refreshGroupsConditions(); 973 }, 974 'move': function(e, node, group, index) { 975 node.$el.detach(); 976 977 if (index === 0) { 978 node.$el.prependTo(group.$el.find('>' + QueryBuilder.selectors.rules_list)); 979 } 980 else { 981 node.$el.insertAfter(group.rules[index - 1].$el); 982 } 983 self.refreshGroupsConditions(); 984 }, 985 'update': function(e, node, field, value, oldValue) { 986 if (node instanceof Rule) { 987 switch (field) { 988 case 'error': 989 self.updateError(node); 990 break; 991 992 case 'flags': 993 self.applyRuleFlags(node); 994 break; 995 996 case 'filter': 997 self.updateRuleFilter(node, oldValue); 998 break; 999 1000 case 'operator': 1001 self.updateRuleOperator(node, oldValue); 1002 break; 1003 1004 case 'value': 1005 self.updateRuleValue(node, oldValue); 1006 break; 1007 } 1008 } 1009 else { 1010 switch (field) { 1011 case 'error': 1012 self.updateError(node); 1013 break; 1014 1015 case 'flags': 1016 self.applyGroupFlags(node); 1017 break; 1018 1019 case 'condition': 1020 self.updateGroupCondition(node, oldValue); 1021 break; 1022 } 1023 } 1024 } 1025 }); 1026}; 1027 1028/** 1029 * Creates the root group 1030 * @param {boolean} [addRule=true] - adds a default empty rule 1031 * @param {object} [data] - group custom data 1032 * @param {object} [flags] - flags to apply to the group 1033 * @returns {Group} root group 1034 * @fires QueryBuilder.afterAddGroup 1035 */ 1036QueryBuilder.prototype.setRoot = function(addRule, data, flags) { 1037 addRule = (addRule === undefined || addRule === true); 1038 1039 var group_id = this.nextGroupId(); 1040 var $group = $($.parseHTML(this.getGroupTemplate(group_id, 1))); 1041 1042 this.$el.append($group); 1043 this.model.root = new Group(null, $group); 1044 this.model.root.model = this.model; 1045 1046 this.model.root.data = data; 1047 this.model.root.flags = $.extend({}, this.settings.default_group_flags, flags); 1048 this.model.root.condition = this.settings.default_condition; 1049 1050 this.trigger('afterAddGroup', this.model.root); 1051 1052 if (addRule) { 1053 this.addRule(this.model.root); 1054 } 1055 1056 return this.model.root; 1057}; 1058 1059/** 1060 * Adds a new group 1061 * @param {Group} parent 1062 * @param {boolean} [addRule=true] - adds a default empty rule 1063 * @param {object} [data] - group custom data 1064 * @param {object} [flags] - flags to apply to the group 1065 * @returns {Group} 1066 * @fires QueryBuilder.beforeAddGroup 1067 * @fires QueryBuilder.afterAddGroup 1068 */ 1069QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) { 1070 addRule = (addRule === undefined || addRule === true); 1071 1072 var level = parent.level + 1; 1073 1074 /** 1075 * Just before adding a group, can be prevented. 1076 * @event beforeAddGroup 1077 * @memberof QueryBuilder 1078 * @param {Group} parent 1079 * @param {boolean} addRule - if an empty rule will be added in the group 1080 * @param {int} level - nesting level of the group, 1 is the root group 1081 */ 1082 var e = this.trigger('beforeAddGroup', parent, addRule, level); 1083 if (e.isDefaultPrevented()) { 1084 return null; 1085 } 1086 1087 var group_id = this.nextGroupId(); 1088 var $group = $(this.getGroupTemplate(group_id, level)); 1089 var model = parent.addGroup($group); 1090 1091 model.data = data; 1092 model.flags = $.extend({}, this.settings.default_group_flags, flags); 1093 model.condition = this.settings.default_condition; 1094 1095 /** 1096 * Just after adding a group 1097 * @event afterAddGroup 1098 * @memberof QueryBuilder 1099 * @param {Group} group 1100 */ 1101 this.trigger('afterAddGroup', model); 1102 1103 /** 1104 * After any change in the rules 1105 * @event rulesChanged 1106 * @memberof QueryBuilder 1107 */ 1108 this.trigger('rulesChanged'); 1109 1110 if (addRule) { 1111 this.addRule(model); 1112 } 1113 1114 return model; 1115}; 1116 1117/** 1118 * Tries to delete a group. The group is not deleted if at least one rule is flagged `no_delete`. 1119 * @param {Group} group 1120 * @returns {boolean} if the group has been deleted 1121 * @fires QueryBuilder.beforeDeleteGroup 1122 * @fires QueryBuilder.afterDeleteGroup 1123 */ 1124QueryBuilder.prototype.deleteGroup = function(group) { 1125 if (group.isRoot()) { 1126 return false; 1127 } 1128 1129 /** 1130 * Just before deleting a group, can be prevented 1131 * @event beforeDeleteGroup 1132 * @memberof QueryBuilder 1133 * @param {Group} parent 1134 */ 1135 var e = this.trigger('beforeDeleteGroup', group); 1136 if (e.isDefaultPrevented()) { 1137 return false; 1138 } 1139 1140 var del = true; 1141 1142 group.each('reverse', function(rule) { 1143 del &= this.deleteRule(rule); 1144 }, function(group) { 1145 del &= this.deleteGroup(group); 1146 }, this); 1147 1148 if (del) { 1149 group.drop(); 1150 1151 /** 1152 * Just after deleting a group 1153 * @event afterDeleteGroup 1154 * @memberof QueryBuilder 1155 */ 1156 this.trigger('afterDeleteGroup'); 1157 1158 this.trigger('rulesChanged'); 1159 } 1160 1161 return del; 1162}; 1163 1164/** 1165 * Performs actions when a group's condition changes 1166 * @param {Group} group 1167 * @param {object} previousCondition 1168 * @fires QueryBuilder.afterUpdateGroupCondition 1169 * @private 1170 */ 1171QueryBuilder.prototype.updateGroupCondition = function(group, previousCondition) { 1172 group.$el.find('>' + QueryBuilder.selectors.group_condition).each(function() { 1173 var $this = $(this); 1174 $this.prop('checked', $this.val() === group.condition); 1175 $this.parent().toggleClass('active', $this.val() === group.condition); 1176 }); 1177 1178 /** 1179 * After the group condition has been modified 1180 * @event afterUpdateGroupCondition 1181 * @memberof QueryBuilder 1182 * @param {Group} group 1183 * @param {object} previousCondition 1184 */ 1185 this.trigger('afterUpdateGroupCondition', group, previousCondition); 1186 1187 this.trigger('rulesChanged'); 1188}; 1189 1190/** 1191 * Updates the visibility of conditions based on number of rules inside each group 1192 * @private 1193 */ 1194QueryBuilder.prototype.refreshGroupsConditions = function() { 1195 (function walk(group) { 1196 if (!group.flags || (group.flags && !group.flags.condition_readonly)) { 1197 group.$el.find('>' + QueryBuilder.selectors.group_condition).prop('disabled', group.rules.length <= 1) 1198 .parent().toggleClass('disabled', group.rules.length <= 1); 1199 } 1200 1201 group.each(null, function(group) { 1202 walk(group); 1203 }, this); 1204 }(this.model.root)); 1205}; 1206 1207/** 1208 * Adds a new rule 1209 * @param {Group} parent 1210 * @param {object} [data] - rule custom data 1211 * @param {object} [flags] - flags to apply to the rule 1212 * @returns {Rule} 1213 * @fires QueryBuilder.beforeAddRule 1214 * @fires QueryBuilder.afterAddRule 1215 * @fires QueryBuilder.changer:getDefaultFilter 1216 */ 1217QueryBuilder.prototype.addRule = function(parent, data, flags) { 1218 /** 1219 * Just before adding a rule, can be prevented 1220 * @event beforeAddRule 1221 * @memberof QueryBuilder 1222 * @param {Group} parent 1223 */ 1224 var e = this.trigger('beforeAddRule', parent); 1225 if (e.isDefaultPrevented()) { 1226 return null; 1227 } 1228 1229 var rule_id = this.nextRuleId(); 1230 var $rule = $($.parseHTML(this.getRuleTemplate(rule_id))); 1231 var model = parent.addRule($rule); 1232 1233 model.data = data; 1234 model.flags = $.extend({}, this.settings.default_rule_flags, flags); 1235 1236 /** 1237 * Just after adding a rule 1238 * @event afterAddRule 1239 * @memberof QueryBuilder 1240 * @param {Rule} rule 1241 */ 1242 this.trigger('afterAddRule', model); 1243 1244 this.trigger('rulesChanged'); 1245 1246 this.createRuleFilters(model); 1247 1248 if (this.settings.default_filter || !this.settings.display_empty_filter) { 1249 /** 1250 * Modifies the default filter for a rule 1251 * @event changer:getDefaultFilter 1252 * @memberof QueryBuilder 1253 * @param {QueryBuilder.Filter} filter 1254 * @param {Rule} rule 1255 * @returns {QueryBuilder.Filter} 1256 */ 1257 model.filter = this.change('getDefaultFilter', 1258 this.getFilterById(this.settings.default_filter || this.filters[0].id), 1259 model 1260 ); 1261 } 1262 1263 return model; 1264}; 1265 1266/** 1267 * Tries to delete a rule 1268 * @param {Rule} rule 1269 * @returns {boolean} if the rule has been deleted 1270 * @fires QueryBuilder.beforeDeleteRule 1271 * @fires QueryBuilder.afterDeleteRule 1272 */ 1273QueryBuilder.prototype.deleteRule = function(rule) { 1274 if (rule.flags.no_delete) { 1275 return false; 1276 } 1277 1278 /** 1279 * Just before deleting a rule, can be prevented 1280 * @event beforeDeleteRule 1281 * @memberof QueryBuilder 1282 * @param {Rule} rule 1283 */ 1284 var e = this.trigger('beforeDeleteRule', rule); 1285 if (e.isDefaultPrevented()) { 1286 return false; 1287 } 1288 1289 rule.drop(); 1290 1291 /** 1292 * Just after deleting a rule 1293 * @event afterDeleteRule 1294 * @memberof QueryBuilder 1295 */ 1296 this.trigger('afterDeleteRule'); 1297 1298 this.trigger('rulesChanged'); 1299 1300 return true; 1301}; 1302 1303/** 1304 * Creates the filters for a rule 1305 * @param {Rule} rule 1306 * @fires QueryBuilder.changer:getRuleFilters 1307 * @fires QueryBuilder.afterCreateRuleFilters 1308 * @private 1309 */ 1310QueryBuilder.prototype.createRuleFilters = function(rule) { 1311 /** 1312 * Modifies the list a filters available for a rule 1313 * @event changer:getRuleFilters 1314 * @memberof QueryBuilder 1315 * @param {QueryBuilder.Filter[]} filters 1316 * @param {Rule} rule 1317 * @returns {QueryBuilder.Filter[]} 1318 */ 1319 var filters = this.change('getRuleFilters', this.filters, rule); 1320 var $filterSelect = $($.parseHTML(this.getRuleFilterSelect(rule, filters))); 1321 1322 rule.$el.find(QueryBuilder.selectors.filter_container).html($filterSelect); 1323 1324 /** 1325 * After creating the dropdown for filters 1326 * @event afterCreateRuleFilters 1327 * @memberof QueryBuilder 1328 * @param {Rule} rule 1329 */ 1330 this.trigger('afterCreateRuleFilters', rule); 1331 1332 this.applyRuleFlags(rule); 1333}; 1334 1335/** 1336 * Creates the operators for a rule and init the rule operator 1337 * @param {Rule} rule 1338 * @fires QueryBuilder.afterCreateRuleOperators 1339 * @private 1340 */ 1341QueryBuilder.prototype.createRuleOperators = function(rule) { 1342 var $operatorContainer = rule.$el.find(QueryBuilder.selectors.operator_container).empty(); 1343 1344 if (!rule.filter) { 1345 return; 1346 } 1347 1348 var operators = this.getOperators(rule.filter); 1349 var $operatorSelect = $($.parseHTML(this.getRuleOperatorSelect(rule, operators))); 1350 1351 $operatorContainer.html($operatorSelect); 1352 1353 // set the operator without triggering update event 1354 if (rule.filter.default_operator) { 1355 rule.__.operator = this.getOperatorByType(rule.filter.default_operator); 1356 } 1357 else { 1358 rule.__.operator = operators[0]; 1359 } 1360 1361 rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type); 1362 1363 /** 1364 * After creating the dropdown for operators 1365 * @event afterCreateRuleOperators 1366 * @memberof QueryBuilder 1367 * @param {Rule} rule 1368 * @param {QueryBuilder.Operator[]} operators - allowed operators for this rule 1369 */ 1370 this.trigger('afterCreateRuleOperators', rule, operators); 1371 1372 this.applyRuleFlags(rule); 1373}; 1374 1375/** 1376 * Creates the main input for a rule 1377 * @param {Rule} rule 1378 * @fires QueryBuilder.afterCreateRuleInput 1379 * @private 1380 */ 1381QueryBuilder.prototype.createRuleInput = function(rule) { 1382 var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container).empty(); 1383 1384 rule.__.value = undefined; 1385 1386 if (!rule.filter || !rule.operator || rule.operator.nb_inputs === 0) { 1387 return; 1388 } 1389 1390 var self = this; 1391 var $inputs = $(); 1392 var filter = rule.filter; 1393 1394 for (var i = 0; i < rule.operator.nb_inputs; i++) { 1395 var $ruleInput = $($.parseHTML($.trim(this.getRuleInput(rule, i)))); 1396 if (i > 0) $valueContainer.append(this.settings.inputs_separator); 1397 $valueContainer.append($ruleInput); 1398 $inputs = $inputs.add($ruleInput); 1399 } 1400 1401 $valueContainer.css('display', ''); 1402 1403 $inputs.on('change ' + (filter.input_event || ''), function() { 1404 if (!rule._updating_input) { 1405 rule._updating_value = true; 1406 rule.value = self.getRuleInputValue(rule); 1407 rule._updating_value = false; 1408 } 1409 }); 1410 1411 if (filter.plugin) { 1412 $inputs[filter.plugin](filter.plugin_config || {}); 1413 } 1414 1415 /** 1416 * After creating the input for a rule and initializing optional plugin 1417 * @event afterCreateRuleInput 1418 * @memberof QueryBuilder 1419 * @param {Rule} rule 1420 */ 1421 this.trigger('afterCreateRuleInput', rule); 1422 1423 if (filter.default_value !== undefined) { 1424 rule.value = filter.default_value; 1425 } 1426 else { 1427 rule._updating_value = true; 1428 rule.value = self.getRuleInputValue(rule); 1429 rule._updating_value = false; 1430 } 1431 1432 this.applyRuleFlags(rule); 1433}; 1434 1435/** 1436 * Performs action when a rule's filter changes 1437 * @param {Rule} rule 1438 * @param {object} previousFilter 1439 * @fires QueryBuilder.afterUpdateRuleFilter 1440 * @private 1441 */ 1442QueryBuilder.prototype.updateRuleFilter = function(rule, previousFilter) { 1443 this.createRuleOperators(rule); 1444 this.createRuleInput(rule); 1445 1446 rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1'); 1447 1448 // clear rule data if the filter changed 1449 if (previousFilter && rule.filter && previousFilter.id !== rule.filter.id) { 1450 rule.data = undefined; 1451 } 1452 1453 /** 1454 * After the filter has been updated and the operators and input re-created 1455 * @event afterUpdateRuleFilter 1456 * @memberof QueryBuilder 1457 * @param {Rule} rule 1458 * @param {object} previousFilter 1459 */ 1460 this.trigger('afterUpdateRuleFilter', rule, previousFilter); 1461 1462 this.trigger('rulesChanged'); 1463}; 1464 1465/** 1466 * Performs actions when a rule's operator changes 1467 * @param {Rule} rule 1468 * @param {object} previousOperator 1469 * @fires QueryBuilder.afterUpdateRuleOperator 1470 * @private 1471 */ 1472QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) { 1473 var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container); 1474 1475 if (!rule.operator || rule.operator.nb_inputs === 0) { 1476 $valueContainer.hide(); 1477 1478 rule.__.value = undefined; 1479 } 1480 else { 1481 $valueContainer.css('display', ''); 1482 1483 if ($valueContainer.is(':empty') || !previousOperator || 1484 rule.operator.nb_inputs !== previousOperator.nb_inputs || 1485 rule.operator.optgroup !== previousOperator.optgroup 1486 ) { 1487 this.createRuleInput(rule); 1488 } 1489 } 1490 1491 if (rule.operator) { 1492 rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type); 1493 1494 // refresh value if the format changed for this operator 1495 rule.__.value = this.getRuleInputValue(rule); 1496 } 1497 1498 /** 1499 * After the operator has been updated and the input optionally re-created 1500 * @event afterUpdateRuleOperator 1501 * @memberof QueryBuilder 1502 * @param {Rule} rule 1503 * @param {object} previousOperator 1504 */ 1505 this.trigger('afterUpdateRuleOperator', rule, previousOperator); 1506 1507 this.trigger('rulesChanged'); 1508}; 1509 1510/** 1511 * Performs actions when rule's value changes 1512 * @param {Rule} rule 1513 * @param {object} previousValue 1514 * @fires QueryBuilder.afterUpdateRuleValue 1515 * @private 1516 */ 1517QueryBuilder.prototype.updateRuleValue = function(rule, previousValue) { 1518 if (!rule._updating_value) { 1519 this.setRuleInputValue(rule, rule.value); 1520 } 1521 1522 /** 1523 * After the rule value has been modified 1524 * @event afterUpdateRuleValue 1525 * @memberof QueryBuilder 1526 * @param {Rule} rule 1527 * @param {*} previousValue 1528 */ 1529 this.trigger('afterUpdateRuleValue', rule, previousValue); 1530 1531 this.trigger('rulesChanged'); 1532}; 1533 1534/** 1535 * Changes a rule's properties depending on its flags 1536 * @param {Rule} rule 1537 * @fires QueryBuilder.afterApplyRuleFlags 1538 * @private 1539 */ 1540QueryBuilder.prototype.applyRuleFlags = function(rule) { 1541 var flags = rule.flags; 1542 var Selectors = QueryBuilder.selectors; 1543 1544 rule.$el.find(Selectors.rule_filter).prop('disabled', flags.filter_readonly); 1545 rule.$el.find(Selectors.rule_operator).prop('disabled', flags.operator_readonly); 1546 rule.$el.find(Selectors.rule_value).prop('disabled', flags.value_readonly); 1547 1548 if (flags.no_delete) { 1549 rule.$el.find(Selectors.delete_rule).remove(); 1550 } 1551 1552 /** 1553 * After rule's flags has been applied 1554 * @event afterApplyRuleFlags 1555 * @memberof QueryBuilder 1556 * @param {Rule} rule 1557 */ 1558 this.trigger('afterApplyRuleFlags', rule); 1559}; 1560 1561/** 1562 * Changes group's properties depending on its flags 1563 * @param {Group} group 1564 * @fires QueryBuilder.afterApplyGroupFlags 1565 * @private 1566 */ 1567QueryBuilder.prototype.applyGroupFlags = function(group) { 1568 var flags = group.flags; 1569 var Selectors = QueryBuilder.selectors; 1570 1571 group.$el.find('>' + Selectors.group_condition).prop('disabled', flags.condition_readonly) 1572 .parent().toggleClass('readonly', flags.condition_readonly); 1573 1574 if (flags.no_add_rule) { 1575 group.$el.find(Selectors.add_rule).remove(); 1576 } 1577 if (flags.no_add_group) { 1578 group.$el.find(Selectors.add_group).remove(); 1579 } 1580 if (flags.no_delete) { 1581 group.$el.find(Selectors.delete_group).remove(); 1582 } 1583 1584 /** 1585 * After group's flags has been applied 1586 * @event afterApplyGroupFlags 1587 * @memberof QueryBuilder 1588 * @param {Group} group 1589 */ 1590 this.trigger('afterApplyGroupFlags', group); 1591}; 1592 1593/** 1594 * Clears all errors markers 1595 * @param {Node} [node] default is root Group 1596 */ 1597QueryBuilder.prototype.clearErrors = function(node) { 1598 node = node || this.model.root; 1599 1600 if (!node) { 1601 return; 1602 } 1603 1604 node.error = null; 1605 1606 if (node instanceof Group) { 1607 node.each(function(rule) { 1608 rule.error = null; 1609 }, function(group) { 1610 this.clearErrors(group); 1611 }, this); 1612 } 1613}; 1614 1615/** 1616 * Adds/Removes error on a Rule or Group 1617 * @param {Node} node 1618 * @fires QueryBuilder.changer:displayError 1619 * @private 1620 */ 1621QueryBuilder.prototype.updateError = function(node) { 1622 if (this.settings.display_errors) { 1623 if (node.error === null) { 1624 node.$el.removeClass('has-error'); 1625 } 1626 else { 1627 var errorMessage = this.translate('errors', node.error[0]); 1628 errorMessage = Utils.fmt(errorMessage, node.error.slice(1)); 1629 1630 /** 1631 * Modifies an error message before display 1632 * @event changer:displayError 1633 * @memberof QueryBuilder 1634 * @param {string} errorMessage - the error message (translated and formatted) 1635 * @param {array} error - the raw error array (error code and optional arguments) 1636 * @param {Node} node 1637 * @returns {string} 1638 */ 1639 errorMessage = this.change('displayError', errorMessage, node.error, node); 1640 1641 node.$el.addClass('has-error') 1642 .find(QueryBuilder.selectors.error_container).eq(0) 1643 .attr('title', errorMessage); 1644 } 1645 } 1646}; 1647 1648/** 1649 * Triggers a validation error event 1650 * @param {Node} node 1651 * @param {string|array} error 1652 * @param {*} value 1653 * @fires QueryBuilder.validationError 1654 * @private 1655 */ 1656QueryBuilder.prototype.triggerValidationError = function(node, error, value) { 1657 if (!$.isArray(error)) { 1658 error = [error]; 1659 } 1660 1661 /** 1662 * Fired when a validation error occurred, can be prevented 1663 * @event validationError 1664 * @memberof QueryBuilder 1665 * @param {Node} node 1666 * @param {string} error 1667 * @param {*} value 1668 */ 1669 var e = this.trigger('validationError', node, error, value); 1670 if (!e.isDefaultPrevented()) { 1671 node.error = error; 1672 } 1673}; 1674 1675 1676/** 1677 * Destroys the builder 1678 * @fires QueryBuilder.beforeDestroy 1679 */ 1680QueryBuilder.prototype.destroy = function() { 1681 /** 1682 * Before the {@link QueryBuilder#destroy} method 1683 * @event beforeDestroy 1684 * @memberof QueryBuilder 1685 */ 1686 this.trigger('beforeDestroy'); 1687 1688 if (this.status.generated_id) { 1689 this.$el.removeAttr('id'); 1690 } 1691 1692 this.clear(); 1693 this.model = null; 1694 1695 this.$el 1696 .off('.queryBuilder') 1697 .removeClass('query-builder') 1698 .removeData('queryBuilder'); 1699 1700 delete this.$el[0].queryBuilder; 1701}; 1702 1703/** 1704 * Clear all rules and resets the root group 1705 * @fires QueryBuilder.beforeReset 1706 * @fires QueryBuilder.afterReset 1707 */ 1708QueryBuilder.prototype.reset = function() { 1709 /** 1710 * Before the {@link QueryBuilder#reset} method, can be prevented 1711 * @event beforeReset 1712 * @memberof QueryBuilder 1713 */ 1714 var e = this.trigger('beforeReset'); 1715 if (e.isDefaultPrevented()) { 1716 return; 1717 } 1718 1719 this.status.group_id = 1; 1720 this.status.rule_id = 0; 1721 1722 this.model.root.empty(); 1723 1724 this.model.root.data = undefined; 1725 this.model.root.flags = $.extend({}, this.settings.default_group_flags); 1726 this.model.root.condition = this.settings.default_condition; 1727 1728 this.addRule(this.model.root); 1729 1730 /** 1731 * After the {@link QueryBuilder#reset} method 1732 * @event afterReset 1733 * @memberof QueryBuilder 1734 */ 1735 this.trigger('afterReset'); 1736 1737 this.trigger('rulesChanged'); 1738}; 1739 1740/** 1741 * Clears all rules and removes the root group 1742 * @fires QueryBuilder.beforeClear 1743 * @fires QueryBuilder.afterClear 1744 */ 1745QueryBuilder.prototype.clear = function() { 1746 /** 1747 * Before the {@link QueryBuilder#clear} method, can be prevented 1748 * @event beforeClear 1749 * @memberof QueryBuilder 1750 */ 1751 var e = this.trigger('beforeClear'); 1752 if (e.isDefaultPrevented()) { 1753 return; 1754 } 1755 1756 this.status.group_id = 0; 1757 this.status.rule_id = 0; 1758 1759 if (this.model.root) { 1760 this.model.root.drop(); 1761 this.model.root = null; 1762 } 1763 1764 /** 1765 * After the {@link QueryBuilder#clear} method 1766 * @event afterClear 1767 * @memberof QueryBuilder 1768 */ 1769 this.trigger('afterClear'); 1770 1771 this.trigger('rulesChanged'); 1772}; 1773 1774/** 1775 * Modifies the builder configuration.<br> 1776 * Only options defined in QueryBuilder.modifiable_options are modifiable 1777 * @param {object} options 1778 */ 1779QueryBuilder.prototype.setOptions = function(options) { 1780 $.each(options, function(opt, value) { 1781 if (QueryBuilder.modifiable_options.indexOf(opt) !== -1) { 1782 this.settings[opt] = value; 1783 } 1784 }.bind(this)); 1785}; 1786 1787/** 1788 * Returns the model associated to a DOM object, or the root model 1789 * @param {jQuery} [target] 1790 * @returns {Node} 1791 */ 1792QueryBuilder.prototype.getModel = function(target) { 1793 if (!target) { 1794 return this.model.root; 1795 } 1796 else if (target instanceof Node) { 1797 return target; 1798 } 1799 else { 1800 return $(target).data('queryBuilderModel'); 1801 } 1802}; 1803 1804/** 1805 * Validates the whole builder 1806 * @param {object} [options] 1807 * @param {boolean} [options.skip_empty=false] - skips validating rules that have no filter selected 1808 * @returns {boolean} 1809 * @fires QueryBuilder.changer:validate 1810 */ 1811QueryBuilder.prototype.validate = function(options) { 1812 options = $.extend({ 1813 skip_empty: false 1814 }, options); 1815 1816 this.clearErrors(); 1817 1818 var self = this; 1819 1820 var valid = (function parse(group) { 1821 var done = 0; 1822 var errors = 0; 1823 1824 group.each(function(rule) { 1825 if (!rule.filter && options.skip_empty) { 1826 return; 1827 } 1828 1829 if (!rule.filter) { 1830 self.triggerValidationError(rule, 'no_filter', null); 1831 errors++; 1832 return; 1833 } 1834 1835 if (!rule.operator) { 1836 self.triggerValidationError(rule, 'no_operator', null); 1837 errors++; 1838 return; 1839 } 1840 1841 if (rule.operator.nb_inputs !== 0) { 1842 var valid = self.validateValue(rule, rule.value); 1843 1844 if (valid !== true) { 1845 self.triggerValidationError(rule, valid, rule.value); 1846 errors++; 1847 return; 1848 } 1849 } 1850 1851 done++; 1852 1853 }, function(group) { 1854 var res = parse(group); 1855 if (res === true) { 1856 done++; 1857 } 1858 else if (res === false) { 1859 errors++; 1860 } 1861 }); 1862 1863 if (errors > 0) { 1864 return false; 1865 } 1866 else if (done === 0 && !group.isRoot() && options.skip_empty) { 1867 return null; 1868 } 1869 else if (done === 0 && (!self.settings.allow_empty || !group.isRoot())) { 1870 self.triggerValidationError(group, 'empty_group', null); 1871 return false; 1872 } 1873 1874 return true; 1875 1876 }(this.model.root)); 1877 1878 /** 1879 * Modifies the result of the {@link QueryBuilder#validate} method 1880 * @event changer:validate 1881 * @memberof QueryBuilder 1882 * @param {boolean} valid 1883 * @returns {boolean} 1884 */ 1885 return this.change('validate', valid); 1886}; 1887 1888/** 1889 * Gets an object representing current rules 1890 * @param {object} [options] 1891 * @param {boolean|string} [options.get_flags=false] - export flags, true: only changes from default flags or 'all' 1892 * @param {boolean} [options.allow_invalid=false] - returns rules even if they are invalid 1893 * @param {boolean} [options.skip_empty=false] - remove rules that have no filter selected 1894 * @returns {object} 1895 * @fires QueryBuilder.changer:ruleToJson 1896 * @fires QueryBuilder.changer:groupToJson 1897 * @fires QueryBuilder.changer:getRules 1898 */ 1899QueryBuilder.prototype.getRules = function(options) { 1900 options = $.extend({ 1901 get_flags: false, 1902 allow_invalid: false, 1903 skip_empty: false 1904 }, options); 1905 1906 var valid = this.validate(options); 1907 if (!valid && !options.allow_invalid) { 1908 return null; 1909 } 1910 1911 var self = this; 1912 1913 var out = (function parse(group) { 1914 var groupData = { 1915 condition: group.condition, 1916 rules: [] 1917 }; 1918 1919 if (group.data) { 1920 groupData.data = $.extendext(true, 'replace', {}, group.data); 1921 } 1922 1923 if (options.get_flags) { 1924 var flags = self.getGroupFlags(group.flags, options.get_flags === 'all'); 1925 if (!$.isEmptyObject(flags)) { 1926 groupData.flags = flags; 1927 } 1928 } 1929 1930 group.each(function(rule) { 1931 if (!rule.filter && options.skip_empty) { 1932 return; 1933 } 1934 1935 var value = null; 1936 if (!rule.operator || rule.operator.nb_inputs !== 0) { 1937 value = rule.value; 1938 } 1939 1940 var ruleData = { 1941 id: rule.filter ? rule.filter.id : null, 1942 field: rule.filter ? rule.filter.field : null, 1943 type: rule.filter ? rule.filter.type : null, 1944 input: rule.filter ? rule.filter.input : null, 1945 operator: rule.operator ? rule.operator.type : null, 1946 value: value 1947 }; 1948 1949 if (rule.filter && rule.filter.data || rule.data) { 1950 ruleData.data = $.extendext(true, 'replace', {}, rule.filter ? rule.filter.data : {}, rule.data); 1951 } 1952 1953 if (options.get_flags) { 1954 var flags = self.getRuleFlags(rule.flags, options.get_flags === 'all'); 1955 if (!$.isEmptyObject(flags)) { 1956 ruleData.flags = flags; 1957 } 1958 } 1959 1960 /** 1961 * Modifies the JSON generated from a Rule object 1962 * @event changer:ruleToJson 1963 * @memberof QueryBuilder 1964 * @param {object} json 1965 * @param {Rule} rule 1966 * @returns {object} 1967 */ 1968 groupData.rules.push(self.change('ruleToJson', ruleData, rule)); 1969 1970 }, function(model) { 1971 var data = parse(model); 1972 if (data.rules.length !== 0 || !options.skip_empty) { 1973 groupData.rules.push(data); 1974 } 1975 }, this); 1976 1977 /** 1978 * Modifies the JSON generated from a Group object 1979 * @event changer:groupToJson 1980 * @memberof QueryBuilder 1981 * @param {object} json 1982 * @param {Group} group 1983 * @returns {object} 1984 */ 1985 return self.change('groupToJson', groupData, group); 1986 1987 }(this.model.root)); 1988 1989 out.valid = valid; 1990 1991 /** 1992 * Modifies the result of the {@link QueryBuilder#getRules} method 1993 * @event changer:getRules 1994 * @memberof QueryBuilder 1995 * @param {object} json 1996 * @returns {object} 1997 */ 1998 return this.change('getRules', out); 1999}; 2000 2001/** 2002 * Sets rules from object 2003 * @param {object} data 2004 * @param {object} [options] 2005 * @param {boolean} [options.allow_invalid=false] - silent-fail if the data are invalid 2006 * @throws RulesError, UndefinedConditionError 2007 * @fires QueryBuilder.changer:setRules 2008 * @fires QueryBuilder.changer:jsonToRule 2009 * @fires QueryBuilder.changer:jsonToGroup 2010 * @fires QueryBuilder.afterSetRules 2011 */ 2012QueryBuilder.prototype.setRules = function(data, options) { 2013 options = $.extend({ 2014 allow_invalid: false 2015 }, options); 2016 2017 if ($.isArray(data)) { 2018 data = { 2019 condition: this.settings.default_condition, 2020 rules: data 2021 }; 2022 } 2023 2024 if (!data || !data.rules || (data.rules.length === 0 && !this.settings.allow_empty)) { 2025 Utils.error('RulesParse', 'Incorrect data object passed'); 2026 } 2027 2028 this.clear(); 2029 this.setRoot(false, data.data, this.parseGroupFlags(data)); 2030 2031 /** 2032 * Modifies data before the {@link QueryBuilder#setRules} method 2033 * @event changer:setRules 2034 * @memberof QueryBuilder 2035 * @param {object} json 2036 * @param {object} options 2037 * @returns {object} 2038 */ 2039 data = this.change('setRules', data, options); 2040 2041 var self = this; 2042 2043 (function add(data, group) { 2044 if (group === null) { 2045 return; 2046 } 2047 2048 if (data.condition === undefined) { 2049 data.condition = self.settings.default_condition; 2050 } 2051 else if (self.settings.conditions.indexOf(data.condition) == -1) { 2052 Utils.error(!options.allow_invalid, 'UndefinedCondition', 'Invalid condition "{0}"', data.condition); 2053 data.condition = self.settings.default_condition; 2054 } 2055 2056 group.condition = data.condition; 2057 2058 data.rules.forEach(function(item) { 2059 var model; 2060 2061 if (item.rules !== undefined) { 2062 if (self.settings.allow_groups !== -1 && self.settings.allow_groups < group.level) { 2063 Utils.error(!options.allow_invalid, 'RulesParse', 'No more than {0} groups are allowed', self.settings.allow_groups); 2064 self.reset(); 2065 } 2066 else { 2067 model = self.addGroup(group, false, item.data, self.parseGroupFlags(item)); 2068 if (model === null) { 2069 return; 2070 } 2071 2072 add(item, model); 2073 } 2074 } 2075 else { 2076 if (!item.empty) { 2077 if (item.id === undefined) { 2078 Utils.error(!options.allow_invalid, 'RulesParse', 'Missing rule field id'); 2079 item.empty = true; 2080 } 2081 if (item.operator === undefined) { 2082 item.operator = 'equal'; 2083 } 2084 } 2085 2086 model = self.addRule(group, item.data, self.parseRuleFlags(item)); 2087 if (model === null) { 2088 return; 2089 } 2090 2091 if (!item.empty) { 2092 model.filter = self.getFilterById(item.id, !options.allow_invalid); 2093 } 2094 2095 if (model.filter) { 2096 model.operator = self.getOperatorByType(item.operator, !options.allow_invalid); 2097 2098 if (!model.operator) { 2099 model.operator = self.getOperators(model.filter)[0]; 2100 } 2101 } 2102 2103 if (model.operator && model.operator.nb_inputs !== 0) { 2104 if (item.value !== undefined) { 2105 model.value = item.value; 2106 } 2107 else if (model.filter.default_value !== undefined) { 2108 model.value = model.filter.default_value; 2109 } 2110 } 2111 2112 /** 2113 * Modifies the Rule object generated from the JSON 2114 * @event changer:jsonToRule 2115 * @memberof QueryBuilder 2116 * @param {Rule} rule 2117 * @param {object} json 2118 * @returns {Rule} the same rule 2119 */ 2120 if (self.change('jsonToRule', model, item) != model) { 2121 Utils.error('RulesParse', 'Plugin tried to change rule reference'); 2122 } 2123 } 2124 }); 2125 2126 /** 2127 * Modifies the Group object generated from the JSON 2128 * @event changer:jsonToGroup 2129 * @memberof QueryBuilder 2130 * @param {Group} group 2131 * @param {object} json 2132 * @returns {Group} the same group 2133 */ 2134 if (self.change('jsonToGroup', group, data) != group) { 2135 Utils.error('RulesParse', 'Plugin tried to change group reference'); 2136 } 2137 2138 }(data, this.model.root)); 2139 2140 /** 2141 * After the {@link QueryBuilder#setRules} method 2142 * @event afterSetRules 2143 * @memberof QueryBuilder 2144 */ 2145 this.trigger('afterSetRules'); 2146}; 2147 2148 2149/** 2150 * Performs value validation 2151 * @param {Rule} rule 2152 * @param {string|string[]} value 2153 * @returns {array|boolean} true or error array 2154 * @fires QueryBuilder.changer:validateValue 2155 */ 2156QueryBuilder.prototype.validateValue = function(rule, value) { 2157 var validation = rule.filter.validation || {}; 2158 var result = true; 2159 2160 if (validation.callback) { 2161 result = validation.callback.call(this, value, rule); 2162 } 2163 else { 2164 result = this._validateValue(rule, value); 2165 } 2166 2167 /** 2168 * Modifies the result of the rule validation method 2169 * @event changer:validateValue 2170 * @memberof QueryBuilder 2171 * @param {array|boolean} result - true or an error array 2172 * @param {*} value 2173 * @param {Rule} rule 2174 * @returns {array|boolean} 2175 */ 2176 return this.change('validateValue', result, value, rule); 2177}; 2178 2179/** 2180 * Default validation function 2181 * @param {Rule} rule 2182 * @param {string|string[]} value 2183 * @returns {array|boolean} true or error array 2184 * @throws ConfigError 2185 * @private 2186 */ 2187QueryBuilder.prototype._validateValue = function(rule, value) { 2188 var filter = rule.filter; 2189 var operator = rule.operator; 2190 var validation = filter.validation || {}; 2191 var result = true; 2192 var tmp, tempValue; 2193 2194 if (rule.operator.nb_inputs === 1) { 2195 value = [value]; 2196 } 2197 2198 for (var i = 0; i < operator.nb_inputs; i++) { 2199 if (!operator.multiple && $.isArray(value[i]) && value[i].length > 1) { 2200 result = ['operator_not_multiple', operator.type, this.translate('operators', operator.type)]; 2201 break; 2202 } 2203 2204 switch (filter.input) { 2205 case 'radio': 2206 if (value[i] === undefined || value[i].length === 0) { 2207 if (!validation.allow_empty_value) { 2208 result = ['radio_empty']; 2209 } 2210 break; 2211 } 2212 break; 2213 2214 case 'checkbox': 2215 if (value[i] === undefined || value[i].length === 0) { 2216 if (!validation.allow_empty_value) { 2217 result = ['checkbox_empty']; 2218 } 2219 break; 2220 } 2221 break; 2222 2223 case 'select': 2224 if (value[i] === undefined || value[i].length === 0 || (filter.placeholder && value[i] == filter.placeholder_value)) { 2225 if (!validation.allow_empty_value) { 2226 result = ['select_empty']; 2227 } 2228 break; 2229 } 2230 break; 2231 2232 default: 2233 tempValue = $.isArray(value[i]) ? value[i] : [value[i]]; 2234 2235 for (var j = 0; j < tempValue.length; j++) { 2236 switch (QueryBuilder.types[filter.type]) { 2237 case 'string': 2238 if (tempValue[j] === undefined || tempValue[j].length === 0) { 2239 if (!validation.allow_empty_value) { 2240 result = ['string_empty']; 2241 } 2242 break; 2243 } 2244 if (validation.min !== undefined) { 2245 if (tempValue[j].length < parseInt(validation.min)) { 2246 result = [this.getValidationMessage(validation, 'min', 'string_exceed_min_length'), validation.min]; 2247 break; 2248 } 2249 } 2250 if (validation.max !== undefined) { 2251 if (tempValue[j].length > parseInt(validation.max)) { 2252 result = [this.getValidationMessage(validation, 'max', 'string_exceed_max_length'), validation.max]; 2253 break; 2254 } 2255 } 2256 if (validation.format) { 2257 if (typeof validation.format == 'string') { 2258 validation.format = new RegExp(validation.format); 2259 } 2260 if (!validation.format.test(tempValue[j])) { 2261 result = [this.getValidationMessage(validation, 'format', 'string_invalid_format'), validation.format]; 2262 break; 2263 } 2264 } 2265 break; 2266 2267 case 'number': 2268 if (tempValue[j] === undefined || tempValue[j].length === 0) { 2269 if (!validation.allow_empty_value) { 2270 result = ['number_nan']; 2271 } 2272 break; 2273 } 2274 if (isNaN(tempValue[j])) { 2275 result = ['number_nan']; 2276 break; 2277 } 2278 if (filter.type == 'integer') { 2279 if (parseInt(tempValue[j]) != tempValue[j]) { 2280 result = ['number_not_integer']; 2281 break; 2282 } 2283 } 2284 else { 2285 if (parseFloat(tempValue[j]) != tempValue[j]) { 2286 result = ['number_not_double']; 2287 break; 2288 } 2289 } 2290 if (validation.min !== undefined) { 2291 if (tempValue[j] < parseFloat(validation.min)) { 2292 result = [this.getValidationMessage(validation, 'min', 'number_exceed_min'), validation.min]; 2293 break; 2294 } 2295 } 2296 if (validation.max !== undefined) { 2297 if (tempValue[j] > parseFloat(validation.max)) { 2298 result = [this.getValidationMessage(validation, 'max', 'number_exceed_max'), validation.max]; 2299 break; 2300 } 2301 } 2302 if (validation.step !== undefined && validation.step !== 'any') { 2303 var v = (tempValue[j] / validation.step).toPrecision(14); 2304 if (parseInt(v) != v) { 2305 result = [this.getValidationMessage(validation, 'step', 'number_wrong_step'), validation.step]; 2306 break; 2307 } 2308 } 2309 break; 2310 2311 case 'datetime': 2312 if (tempValue[j] === undefined || tempValue[j].length === 0) { 2313 if (!validation.allow_empty_value) { 2314 result = ['datetime_empty']; 2315 } 2316 break; 2317 } 2318 2319 // we need MomentJS 2320 if (validation.format) { 2321 if (!('moment' in window)) { 2322 Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com'); 2323 } 2324 2325 var datetime = moment(tempValue[j], validation.format); 2326 if (!datetime.isValid()) { 2327 result = [this.getValidationMessage(validation, 'format', 'datetime_invalid'), validation.format]; 2328 break; 2329 } 2330 else { 2331 if (validation.min) { 2332 if (datetime < moment(validation.min, validation.format)) { 2333 result = [this.getValidationMessage(validation, 'min', 'datetime_exceed_min'), validation.min]; 2334 break; 2335 } 2336 } 2337 if (validation.max) { 2338 if (datetime > moment(validation.max, validation.format)) { 2339 result = [this.getValidationMessage(validation, 'max', 'datetime_exceed_max'), validation.max]; 2340 break; 2341 } 2342 } 2343 } 2344 } 2345 break; 2346 2347 case 'boolean': 2348 if (tempValue[j] === undefined || tempValue[j].length === 0) { 2349 if (!validation.allow_empty_value) { 2350 result = ['boolean_not_valid']; 2351 } 2352 break; 2353 } 2354 tmp = ('' + tempValue[j]).trim().toLowerCase(); 2355 if (tmp !== 'true' && tmp !== 'false' && tmp !== '1' && tmp !== '0' && tempValue[j] !== 1 && tempValue[j] !== 0) { 2356 result = ['boolean_not_valid']; 2357 break; 2358 } 2359 } 2360 2361 if (result !== true) { 2362 break; 2363 } 2364 } 2365 } 2366 2367 if (result !== true) { 2368 break; 2369 } 2370 } 2371 2372 if ((rule.operator.type === 'between' || rule.operator.type === 'not_between') && value.length === 2) { 2373 switch (QueryBuilder.types[filter.type]) { 2374 case 'number': 2375 if (value[0] > value[1]) { 2376 result = ['number_between_invalid', value[0], value[1]]; 2377 } 2378 break; 2379 2380 case 'datetime': 2381 // we need MomentJS 2382 if (validation.format) { 2383 if (!('moment' in window)) { 2384 Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com'); 2385 } 2386 2387 if (moment(value[0], validation.format).isAfter(moment(value[1], validation.format))) { 2388 result = ['datetime_between_invalid', value[0], value[1]]; 2389 } 2390 } 2391 break; 2392 } 2393 } 2394 2395 return result; 2396}; 2397 2398/** 2399 * Returns an incremented group ID 2400 * @returns {string} 2401 * @private 2402 */ 2403QueryBuilder.prototype.nextGroupId = function() { 2404 return this.status.id + '_group_' + (this.status.group_id++); 2405}; 2406 2407/** 2408 * Returns an incremented rule ID 2409 * @returns {string} 2410 * @private 2411 */ 2412QueryBuilder.prototype.nextRuleId = function() { 2413 return this.status.id + '_rule_' + (this.status.rule_id++); 2414}; 2415 2416/** 2417 * Returns the operators for a filter 2418 * @param {string|object} filter - filter id or filter object 2419 * @returns {object[]} 2420 * @fires QueryBuilder.changer:getOperators 2421 */ 2422QueryBuilder.prototype.getOperators = function(filter) { 2423 if (typeof filter == 'string') { 2424 filter = this.getFilterById(filter); 2425 } 2426 2427 var result = []; 2428 2429 for (var i = 0, l = this.operators.length; i < l; i++) { 2430 // filter operators check 2431 if (filter.operators) { 2432 if (filter.operators.indexOf(this.operators[i].type) == -1) { 2433 continue; 2434 } 2435 } 2436 // type check 2437 else if (this.operators[i].apply_to.indexOf(QueryBuilder.types[filter.type]) == -1) { 2438 continue; 2439 } 2440 2441 result.push(this.operators[i]); 2442 } 2443 2444 // keep sort order defined for the filter 2445 if (filter.operators) { 2446 result.sort(function(a, b) { 2447 return filter.operators.indexOf(a.type) - filter.operators.indexOf(b.type); 2448 }); 2449 } 2450 2451 /** 2452 * Modifies the operators available for a filter 2453 * @event changer:getOperators 2454 * @memberof QueryBuilder 2455 * @param {QueryBuilder.Operator[]} operators 2456 * @param {QueryBuilder.Filter} filter 2457 * @returns {QueryBuilder.Operator[]} 2458 */ 2459 return this.change('getOperators', result, filter); 2460}; 2461 2462/** 2463 * Returns a particular filter by its id 2464 * @param {string} id 2465 * @param {boolean} [doThrow=true] 2466 * @returns {object|null} 2467 * @throws UndefinedFilterError 2468 */ 2469QueryBuilder.prototype.getFilterById = function(id, doThrow) { 2470 if (id == '-1') { 2471 return null; 2472 } 2473 2474 for (var i = 0, l = this.filters.length; i < l; i++) { 2475 if (this.filters[i].id == id) { 2476 return this.filters[i]; 2477 } 2478 } 2479 2480 Utils.error(doThrow !== false, 'UndefinedFilter', 'Undefined filter "{0}"', id); 2481 2482 return null; 2483}; 2484 2485/** 2486 * Returns a particular operator by its type 2487 * @param {string} type 2488 * @param {boolean} [doThrow=true] 2489 * @returns {object|null} 2490 * @throws UndefinedOperatorError 2491 */ 2492QueryBuilder.prototype.getOperatorByType = function(type, doThrow) { 2493 if (type == '-1') { 2494 return null; 2495 } 2496 2497 for (var i = 0, l = this.operators.length; i < l; i++) { 2498 if (this.operators[i].type == type) { 2499 return this.operators[i]; 2500 } 2501 } 2502 2503 Utils.error(doThrow !== false, 'UndefinedOperator', 'Undefined operator "{0}"', type); 2504 2505 return null; 2506}; 2507 2508/** 2509 * Returns rule's current input value 2510 * @param {Rule} rule 2511 * @returns {*} 2512 * @fires QueryBuilder.changer:getRuleValue 2513 * @private 2514 */ 2515QueryBuilder.prototype.getRuleInputValue = function(rule) { 2516 var filter = rule.filter; 2517 var operator = rule.operator; 2518 var value = []; 2519 2520 if (filter.valueGetter) { 2521 value = filter.valueGetter.call(this, rule); 2522 } 2523 else { 2524 var $value = rule.$el.find(QueryBuilder.selectors.value_container); 2525 2526 for (var i = 0; i < operator.nb_inputs; i++) { 2527 var name = Utils.escapeElementId(rule.id + '_value_' + i); 2528 var tmp; 2529 2530 switch (filter.input) { 2531 case 'radio': 2532 value.push($value.find('[name=' + name + ']:checked').val()); 2533 break; 2534 2535 case 'checkbox': 2536 tmp = []; 2537 $value.find('[name=' + name + ']:checked').each(function() { 2538 tmp.push($(this).val()); 2539 }); 2540 value.push(tmp); 2541 break; 2542 2543 case 'select': 2544 if (filter.multiple) { 2545 tmp = []; 2546 $value.find('[name=' + name + '] option:selected').each(function() { 2547 tmp.push($(this).val()); 2548 }); 2549 value.push(tmp); 2550 } 2551 else { 2552 value.push($value.find('[name=' + name + '] option:selected').val()); 2553 } 2554 break; 2555 2556 default: 2557 value.push($value.find('[name=' + name + ']').val()); 2558 } 2559 } 2560 2561 value = value.map(function(val) { 2562 if (operator.multiple && filter.value_separator && typeof val == 'string') { 2563 val = val.split(filter.value_separator); 2564 } 2565 2566 if ($.isArray(val)) { 2567 return val.map(function(subval) { 2568 return Utils.changeType(subval, filter.type); 2569 }); 2570 } 2571 else { 2572 return Utils.changeType(val, filter.type); 2573 } 2574 }); 2575 2576 if (operator.nb_inputs === 1) { 2577 value = value[0]; 2578 } 2579 2580 // @deprecated 2581 if (filter.valueParser) { 2582 value = filter.valueParser.call(this, rule, value); 2583 } 2584 } 2585 2586 /** 2587 * Modifies the rule's value grabbed from the DOM 2588 * @event changer:getRuleValue 2589 * @memberof QueryBuilder 2590 * @param {*} value 2591 * @param {Rule} rule 2592 * @returns {*} 2593 */ 2594 return this.change('getRuleValue', value, rule); 2595}; 2596 2597/** 2598 * Sets the value of a rule's input 2599 * @param {Rule} rule 2600 * @param {*} value 2601 * @private 2602 */ 2603QueryBuilder.prototype.setRuleInputValue = function(rule, value) { 2604 var filter = rule.filter; 2605 var operator = rule.operator; 2606 2607 if (!filter || !operator) { 2608 return; 2609 } 2610 2611 rule._updating_input = true; 2612 2613 if (filter.valueSetter) { 2614 filter.valueSetter.call(this, rule, value); 2615 } 2616 else { 2617 var $value = rule.$el.find(QueryBuilder.selectors.value_container); 2618 2619 if (operator.nb_inputs == 1) { 2620 value = [value]; 2621 } 2622 2623 for (var i = 0; i < operator.nb_inputs; i++) { 2624 var name = Utils.escapeElementId(rule.id + '_value_' + i); 2625 2626 switch (filter.input) { 2627 case 'radio': 2628 $value.find('[name=' + name + '][value="' + value[i] + '"]').prop('checked', true).trigger('change'); 2629 break; 2630 2631 case 'checkbox': 2632 if (!$.isArray(value[i])) { 2633 value[i] = [value[i]]; 2634 } 2635 value[i].forEach(function(value) { 2636 $value.find('[name=' + name + '][value="' + value + '"]').prop('checked', true).trigger('change'); 2637 }); 2638 break; 2639 2640 default: 2641 if (operator.multiple && filter.value_separator && $.isArray(value[i])) { 2642 value[i] = value[i].join(filter.value_separator); 2643 } 2644 $value.find('[name=' + name + ']').val(value[i]).trigger('change'); 2645 break; 2646 } 2647 } 2648 } 2649 2650 rule._updating_input = false; 2651}; 2652 2653/** 2654 * Parses rule flags 2655 * @param {object} rule 2656 * @returns {object} 2657 * @fires QueryBuilder.changer:parseRuleFlags 2658 * @private 2659 */ 2660QueryBuilder.prototype.parseRuleFlags = function(rule) { 2661 var flags = $.extend({}, this.settings.default_rule_flags); 2662 2663 if (rule.readonly) { 2664 $.extend(flags, { 2665 filter_readonly: true, 2666 operator_readonly: true, 2667 value_readonly: true, 2668 no_delete: true 2669 }); 2670 } 2671 2672 if (rule.flags) { 2673 $.extend(flags, rule.flags); 2674 } 2675 2676 /** 2677 * Modifies the consolidated rule's flags 2678 * @event changer:parseRuleFlags 2679 * @memberof QueryBuilder 2680 * @param {object} flags 2681 * @param {object} rule - <b>not</b> a Rule object 2682 * @returns {object} 2683 */ 2684 return this.change('parseRuleFlags', flags, rule); 2685}; 2686 2687/** 2688 * Gets a copy of flags of a rule 2689 * @param {object} flags 2690 * @param {boolean} [all=false] - return all flags or only changes from default flags 2691 * @returns {object} 2692 * @private 2693 */ 2694QueryBuilder.prototype.getRuleFlags = function(flags, all) { 2695 if (all) { 2696 return $.extend({}, flags); 2697 } 2698 else { 2699 var ret = {}; 2700 $.each(this.settings.default_rule_flags, function(key, value) { 2701 if (flags[key] !== value) { 2702 ret[key] = flags[key]; 2703 } 2704 }); 2705 return ret; 2706 } 2707}; 2708 2709/** 2710 * Parses group flags 2711 * @param {object} group 2712 * @returns {object} 2713 * @fires QueryBuilder.changer:parseGroupFlags 2714 * @private 2715 */ 2716QueryBuilder.prototype.parseGroupFlags = function(group) { 2717 var flags = $.extend({}, this.settings.default_group_flags); 2718 2719 if (group.readonly) { 2720 $.extend(flags, { 2721 condition_readonly: true, 2722 no_add_rule: true, 2723 no_add_group: true, 2724 no_delete: true 2725 }); 2726 } 2727 2728 if (group.flags) { 2729 $.extend(flags, group.flags); 2730 } 2731 2732 /** 2733 * Modifies the consolidated group's flags 2734 * @event changer:parseGroupFlags 2735 * @memberof QueryBuilder 2736 * @param {object} flags 2737 * @param {object} group - <b>not</b> a Group object 2738 * @returns {object} 2739 */ 2740 return this.change('parseGroupFlags', flags, group); 2741}; 2742 2743/** 2744 * Gets a copy of flags of a group 2745 * @param {object} flags 2746 * @param {boolean} [all=false] - return all flags or only changes from default flags 2747 * @returns {object} 2748 * @private 2749 */ 2750QueryBuilder.prototype.getGroupFlags = function(flags, all) { 2751 if (all) { 2752 return $.extend({}, flags); 2753 } 2754 else { 2755 var ret = {}; 2756 $.each(this.settings.default_group_flags, function(key, value) { 2757 if (flags[key] !== value) { 2758 ret[key] = flags[key]; 2759 } 2760 }); 2761 return ret; 2762 } 2763}; 2764 2765/** 2766 * Translate a label either by looking in the `lang` object or in itself if it's an object where keys are language codes 2767 * @param {string} [category] 2768 * @param {string|object} key 2769 * @returns {string} 2770 * @fires QueryBuilder.changer:translate 2771 */ 2772QueryBuilder.prototype.translate = function(category, key) { 2773 if (!key) { 2774 key = category; 2775 category = undefined; 2776 } 2777 2778 var translation; 2779 if (typeof key === 'object') { 2780 translation = key[this.settings.lang_code] || key['en']; 2781 } 2782 else { 2783 translation = (category ? this.lang[category] : this.lang)[
2783key] || key; 2784 } 2785 2786 /** 2787 * Modifies the translated label 2788 * @event changer:translate 2789 * @memberof QueryBuilder 2790 * @param {string} translation 2791 * @param {string|object} key 2792 * @param {string} [category] 2793 * @returns {string} 2794 */ 2795 return this.change('translate', translation, key, category); 2796}; 2797 2798/** 2799 * Returns a validation message 2800 * @param {object} validation 2801 * @param {string} type 2802 * @param {string} def 2803 * @returns {string} 2804 * @private 2805 */ 2806QueryBuilder.prototype.getValidationMessage = function(validation, type, def) { 2807 return validation.messages && validation.messages[type] || def; 2808}; 2809 2810 2811QueryBuilder.templates.group = ({ group_id, level, conditions, icons, settings, translate, builder }) => { 2812 return ` 2813<div id="${group_id}" class="rules-group-container"> 2814 <div class="rules-group-header"> 2815 <div class="btn-group pull-right group-actions"> 2816 <button type="button" class="btn btn-xs btn-success" data-add="rule"> 2817 <i class="${icons}"></i> ${translate("add_rule")} 2818 </button> 2819 ${settings.allow_groups === -1 || settings.allow_groups >= level ? ` 2820 <button type="button" class="btn btn-xs btn-success" data-add="group"> 2821 <i class="${icons.add_group}"></i> ${translate("add_group")} 2822 </button> 2823 ` : ''} 2824 ${level > 1 ? ` 2825 <button type="button" class="btn btn-xs btn-danger" data-delete="group"> 2826 <i class="${icons.remove_group}"></i> ${translate("delete_group")} 2827 </button> 2828 ` : ''} 2829 </div> 2830 <div class="btn-group group-conditions"> 2831 ${conditions.map(condition => ` 2832 <label class="btn btn-xs btn-primary"> 2833 <input type="radio" name="${group_id}_cond" value="${condition}"> ${translate("conditions", condition)} 2834 </label> 2835 `).join('\n')} 2836 </div> 2837 ${settings.display_errors ? ` 2838 <div class="error-container"><i class="${icons.error}"></i></div> 2839 ` : ''} 2840 </div> 2841 <div class=rules-group-body> 2842 <div class=rules-list></div> 2843 </div> 2844</div>`; 2845}; 2846 2847QueryBuilder.templates.rule = ({ rule_id, icons, settings, translate, builder }) => { 2848 return ` 2849<div id="${rule_id}" class="rule-container"> 2850 <div class="rule-header"> 2851 <div class="btn-group pull-right rule-actions"> 2852 <button type="button" class="btn btn-xs btn-danger" data-delete="rule"> 2853 <i class="${icons.remove_rule}"></i> ${translate("delete_rule")} 2854 </button> 2855 </div> 2856 </div> 2857 ${settings.display_errors ? ` 2858 <div class="error-container"><i class="${icons.error}"></i></div> 2859 ` : ''} 2860 <div class="rule-filter-container"></div> 2861 <div class="rule-operator-container"></div> 2862 <div class="rule-value-container"></div> 2863</div>`; 2864}; 2865 2866QueryBuilder.templates.filterSelect = ({ rule, filters, icons, settings, translate, builder }) => { 2867 let optgroup = null; 2868 return ` 2869<select class="form-control" name="${rule.id}_filter"> 2870 ${settings.display_empty_filter ? ` 2871 <option value="-1">${settings.select_placeholder}</option> 2872 ` : ''} 2873 ${filters.map(filter => ` 2874 ${optgroup !== filter.optgroup ? ` 2875 ${optgroup !== null ? `</optgroup>` : ''} 2876 ${(optgroup = filter.optgroup) !== null ? ` 2877 <optgroup label="${translate(settings.optgroups[optgroup])}"> 2878 ` : ''} 2879 ` : ''} 2880 <option value="${filter.id}" ${filter.icon ? `data-icon="${filter.icon}"` : ''}>${translate(filter.label)}</option> 2881 `).join('')} 2882 ${optgroup !== null ? '</optgroup>' : ''} 2883</select>`; 2884}; 2885 2886QueryBuilder.templates.operatorSelect = ({ rule, operators, icons, settings, translate, builder }) => { 2887 let optgroup = null; 2888 return ` 2889${operators.length === 1 ? ` 2890<span> 2891${translate("operators", operators[0].type)} 2892</span> 2893` : ''} 2894<select class="form-control ${operators.length === 1 ? 'hide' : ''}" name="${rule.id}_operator"> 2895 ${operators.map(operator => ` 2896 ${optgroup !== operator.optgroup ? ` 2897 ${optgroup !== null ? `</optgroup>` : ''} 2898 ${(optgroup = operator.optgroup) !== null ? ` 2899 <optgroup label="${translate(settings.optgroups[optgroup])}"> 2900 ` : ''} 2901 ` : ''} 2902 <option value="${operator.type}" ${operator.icon ? `data-icon="${operator.icon}"` : ''}>${translate("operators", operator.type)}</option> 2903 `).join('')} 2904 ${optgroup !== null ? '</optgroup>' : ''} 2905</select>`; 2906}; 2907 2908QueryBuilder.templates.ruleValueSelect = ({ name, rule, icons, settings, translate, builder }) => { 2909 let optgroup = null; 2910 return ` 2911<select class="form-control" name="${name}" ${rule.filter.multiple ? 'multiple' : ''}> 2912 ${rule.filter.placeholder ? ` 2913 <option value="${rule.filter.placeholder_value}" disabled selected>${rule.filter.placeholder}</option> 2914 ` : ''}
2915 ${rule.filter.values.map(entry => ` 2916 ${optgroup !== entry.optgroup ? ` 2917 ${optgroup !== null ? `</optgroup>` : ''} 2918 ${(optgroup = entry.optgroup) !== null ? ` 2919 <optgroup label="${translate(settings.optgroups[optgroup])}"> 2920 ` : ''} 2921 ` : ''} 2922 <option value="${entry.value}">${entry.label}</option> 2923 `).join('')} 2924 ${optgroup !== null ? '</optgroup>' : ''} 2925</select>`; 2926}; 2927 2928/** 2929 * Returns group's HTML 2930 * @param {string} group_id 2931 * @param {int} level 2932 * @returns {string} 2933 * @fires QueryBuilder.changer:getGroupTemplate 2934 * @private 2935 */ 2936QueryBuilder.prototype.getGroupTemplate = function (group_id, level) { 2937 var h = this.templates.group({ 2938 builder: this, 2939 group_id: group_id, 2940 level: level, 2941 conditions: this.settings.conditions, 2942 icons: this.icons, 2943 settings: this.settings, 2944 translate: this.translate.bind(this) 2945 }).trim(); 2946 2947 /** 2948 * Modifies the raw HTML of a group 2949 * @event changer:getGroupTemplate 2950 * @memberof QueryBuilder 2951 * @param {string} html 2952 * @param {int} level 2953 * @returns {string} 2954 */ 2955 return this.change('getGroupTemplate', h, level); 2956}; 2957 2958/** 2959 * Returns rule's HTML 2960 * @param {string} rule_id 2961 * @returns {string} 2962 * @fires QueryBuilder.changer:getRuleTemplate 2963 * @private 2964 */ 2965QueryBuilder.prototype.getRuleTemplate = function (rule_id) {
2966 var h = this.templates.rule({ 2967 builder: this, 2968 rule_id: rule_id, 2969 icons: this.icons, 2970 settings: this.settings, 2971 translate: this.translate.bind(this) 2972 }).trim(); 2973 2974 /** 2975 * Modifies the raw HTML of a rule 2976 * @event changer:getRuleTemplate 2977 * @memberof QueryBuilder 2978 * @param {string} html 2979 * @returns {string} 2980 */ 2981 return this.change('getRuleTemplate', h); 2982}; 2983 2984/** 2985 * Returns rule's filter HTML 2986 * @param {Rule} rule 2987 * @param {object[]} filters 2988 * @returns {string} 2989 * @fires QueryBuilder.changer:getRuleFilterTemplate 2990 * @private 2991 */ 2992QueryBuilder.prototype.getRuleFilterSelect = function (rule, filters) { 2993 var h = this.templates.filterSelect({ 2994 builder: this, 2995 rule: rule, 2996 filters: filters, 2997 icons: this.icons, 2998 settings: this.settings, 2999 translate: this.translate.bind(this) 3000 }).trim(); 3001 3002 /** 3003 * Modifies the raw HTML of the rule's filter dropdown 3004 * @event changer:getRuleFilterSelect 3005 * @memberof QueryBuilder 3006 * @param {string} html 3007 * @param {Rule} rule 3008 * @param {QueryBuilder.Filter[]} filters 3009 * @returns {string} 3010 */ 3011 return this.change('getRuleFilterSelect', h, rule, filters); 3012}; 3013 3014/** 3015 * Returns rule's operator HTML 3016 * @param {Rule} rule 3017 * @param {object[]} operators 3018 * @returns {string} 3019 * @fires QueryBuilder.changer:getRuleOperatorTemplate 3020 * @private 3021 */ 3022QueryBuilder.prototype.getRuleOperatorSelect = function (rule, operators) {
vendor: 104,864 bytes, lines 3023-6363
3023 var h = this.templates.operatorSelect({ 3024 builder: this, 3025 rule: rule, 3026 operators: operators, 3027 icons: this.icons, 3028 settings: this.settings, 3029 translate: this.translate.bind(this) 3030 }).trim(); 3031 3032 /** 3033 * Modifies the raw HTML of the rule's operator dropdown 3034 * @event changer:getRuleOperatorSelect 3035 * @memberof QueryBuilder 3036 * @param {string} html 3037 * @param {Rule} rule 3038 * @param {QueryBuilder.Operator[]} operators 3039 * @returns {string} 3040 */ 3041 return this.change('getRuleOperatorSelect', h, rule, operators); 3042}; 3043 3044/** 3045 * Returns the rule's value select HTML 3046 * @param {string} name 3047 * @param {Rule} rule 3048 * @returns {string} 3049 * @fires QueryBuilder.changer:getRuleValueSelect 3050 * @private 3051 */ 3052QueryBuilder.prototype.getRuleValueSelect = function (name, rule) { 3053 var h = this.templates.ruleValueSelect({ 3054 builder: this, 3055 name: name, 3056 rule: rule, 3057 icons: this.icons, 3058 settings: this.settings, 3059 translate: this.translate.bind(this) 3060 }).trim(); 3061 3062 /** 3063 * Modifies the raw HTML of the rule's value dropdown (in case of a "select filter) 3064 * @event changer:getRuleValueSelect 3065 * @memberof QueryBuilder 3066 * @param {string} html 3067 * @param [string} name 3068 * @param {Rule} rule 3069 * @returns {string} 3070 */ 3071 return this.change('getRuleValueSelect', h, name, rule); 3072}; 3073 3074/** 3075 * Returns the rule's value HTML 3076 * @param {Rule} rule 3077 * @param {int} value_id 3078 * @returns {string} 3079 * @fires QueryBuilder.changer:getRuleInput 3080 * @private 3081 */ 3082QueryBuilder.prototype.getRuleInput = function (rule, value_id) { 3083 var filter = rule.filter; 3084 var validation = rule.filter.validation || {}; 3085 var name = rule.id + '_value_' + value_id; 3086 var c = filter.vertical ? ' class=block' : ''; 3087 var h = ''; 3088 var placeholder = Array.isArray(filter.placeholder) ? filter.placeholder[value_id] : filter.placeholder; 3089 3090 if (typeof filter.input == 'function') { 3091 h = filter.input.call(this, rule, name); 3092 } 3093 else { 3094 switch (filter.input) { 3095 case 'radio': 3096 case 'checkbox': 3097 Utils.iterateOptions(filter.values, function (key, val) { 3098 h += '<label' + c + '><input type="' + filter.input + '" name="' + name + '" value="' + key + '"> ' + val + '</label> '; 3099 }); 3100 break; 3101 3102 case 'select': 3103 h = this.getRuleValueSelect(name, rule); 3104 break; 3105 3106 case 'textarea': 3107 h += '<textarea class="form-control" name="' + name + '"'; 3108 if (filter.size) h += ' cols="' + filter.size + '"'; 3109 if (filter.rows) h += ' rows="' + filter.rows + '"'; 3110 if (validation.min !== undefined) h += ' minlength="' + validation.min + '"'; 3111 if (validation.max !== undefined) h += ' maxlength="' + validation.max + '"'; 3112 if (placeholder) h += ' placeholder="' + placeholder + '"'; 3113 h += '></textarea>'; 3114 break; 3115 3116 case 'number': 3117 h += '<input class="form-control" type="number" name="' + name + '"'; 3118 if (validation.step !== undefined) h += ' step="' + validation.step + '"'; 3119 if (validation.min !== undefined) h += ' min="' + validation.min + '"'; 3120 if (validation.max !== undefined) h += ' max="' + validation.max + '"'; 3121 if (placeholder) h += ' placeholder="' + placeholder + '"'; 3122 if (filter.size) h += ' size="' + filter.size + '"'; 3123 h += '>'; 3124 break; 3125 3126 default: 3127 h += '<input class="form-control" type="text" name="' + name + '"'; 3128 if (placeholder) h += ' placeholder="' + placeholder + '"'; 3129 if (filter.type === 'string' && validation.min !== undefined) h += ' minlength="' + validation.min + '"'; 3130 if (filter.type === 'string' && validation.max !== undefined) h += ' maxlength="' + validation.max + '"'; 3131 if (filter.size) h += ' size="' + filter.size + '"'; 3132 h += '>'; 3133 } 3134 } 3135 3136 /** 3137 * Modifies the raw HTML of the rule's input 3138 * @event changer:getRuleInput 3139 * @memberof QueryBuilder 3140 * @param {string} html 3141 * @param {Rule} rule 3142 * @param {string} name - the name that the input must have 3143 * @returns {string} 3144 */ 3145 return this.change('getRuleInput', h, rule, name); 3146}; 3147 3148 3149/** 3150 * @namespace 3151 */ 3152var Utils = {}; 3153 3154/** 3155 * @member {object} 3156 * @memberof QueryBuilder 3157 * @see Utils 3158 */ 3159QueryBuilder.utils = Utils; 3160 3161/** 3162 * @callback Utils#OptionsIteratee 3163 * @param {string} key 3164 * @param {string} value 3165 * @param {string} [optgroup] 3166 */ 3167 3168/** 3169 * Iterates over radio/checkbox/selection options, it accept four formats 3170 * 3171 * @example 3172 * // array of values 3173 * options = ['one', 'two', 'three'] 3174 * @example 3175 * // simple key-value map 3176 * options = {1: 'one', 2: 'two', 3: 'three'} 3177 * @example 3178 * // array of 1-element maps 3179 * options = [{1: 'one'}, {2: 'two'}, {3: 'three'}] 3180 * @example 3181 * // array of elements 3182 * options = [{value: 1, label: 'one', optgroup: 'group'}, {value: 2, label: 'two'}] 3183 * 3184 * @param {object|array} options 3185 * @param {Utils#OptionsIteratee} tpl 3186 */ 3187Utils.iterateOptions = function(options, tpl) { 3188 if (options) { 3189 if ($.isArray(options)) { 3190 options.forEach(function(entry) { 3191 if ($.isPlainObject(entry)) { 3192 // array of elements 3193 if ('value' in entry) { 3194 tpl(entry.value, entry.label || entry.value, entry.optgroup); 3195 } 3196 // array of one-element maps 3197 else { 3198 $.each(entry, function(key, val) { 3199 tpl(key, val); 3200 return false; // break after first entry 3201 }); 3202 } 3203 } 3204 // array of values 3205 else { 3206 tpl(entry, entry); 3207 } 3208 }); 3209 } 3210 // unordered map 3211 else { 3212 $.each(options, function(key, val) { 3213 tpl(key, val); 3214 }); 3215 } 3216 } 3217}; 3218 3219/** 3220 * Replaces {0}, {1}, ... in a string 3221 * @param {string} str 3222 * @param {...*} args 3223 * @returns {string} 3224 */ 3225Utils.fmt = function(str, args) { 3226 if (!Array.isArray(args)) { 3227 args = Array.prototype.slice.call(arguments, 1); 3228 } 3229 3230 return str.replace(/{([0-9]+)}/g, function(m, i) { 3231 return args[parseInt(i)]; 3232 }); 3233}; 3234 3235/** 3236 * Throws an Error object with custom name or logs an error 3237 * @param {boolean} [doThrow=true] 3238 * @param {string} type 3239 * @param {string} message 3240 * @param {...*} args 3241 */ 3242Utils.error = function() { 3243 var i = 0; 3244 var doThrow = typeof arguments[i] === 'boolean' ? arguments[i++] : true; 3245 var type = arguments[i++]; 3246 var message = arguments[i++]; 3247 var args = Array.isArray(arguments[i]) ? arguments[i] : Array.prototype.slice.call(arguments, i); 3248 3249 if (doThrow) { 3250 var err = new Error(Utils.fmt(message, args)); 3251 err.name = type + 'Error'; 3252 err.args = args; 3253 throw err; 3254 } 3255 else { 3256 console.error(type + 'Error: ' + Utils.fmt(message, args)); 3257 } 3258}; 3259 3260/** 3261 * Changes the type of a value to int, float or bool 3262 * @param {*} value 3263 * @param {string} type - 'integer', 'double', 'boolean' or anything else (passthrough) 3264 * @returns {*} 3265 */ 3266Utils.changeType = function(value, type) { 3267 if (value === '' || value === undefined) { 3268 return undefined; 3269 } 3270 3271 switch (type) { 3272 // @formatter:off 3273 case 'integer': 3274 if (typeof value === 'string' && !/^-?\d+$/.test(value)) { 3275 return value; 3276 } 3277 return parseInt(value); 3278 case 'double': 3279 if (typeof value === 'string' && !/^-?\d+\.?\d*$/.test(value)) { 3280 return value; 3281 } 3282 return parseFloat(value); 3283 case 'boolean': 3284 if (typeof value === 'string' && !/^(0|1|true|false){1}$/i.test(value)) { 3285 return value; 3286 } 3287 return value === true || value === 1 || value.toLowerCase() === 'true' || value === '1'; 3288 default: return value; 3289 // @formatter:on 3290 } 3291}; 3292 3293/** 3294 * Escapes a string like PHP's mysql_real_escape_string does 3295 * @param {string} value 3296 * @param {string} [additionalEscape] additionnal chars to escape 3297 * @returns {string} 3298 */ 3299Utils.escapeString = function(value, additionalEscape) { 3300 if (typeof value != 'string') { 3301 return value; 3302 } 3303 3304 var escaped = value 3305 .replace(/[\0\n\r\b\\\'\"]/g, function(s) { 3306 switch (s) { 3307 // @formatter:off 3308 case '\0': return '\\0'; 3309 case '\n': return '\\n'; 3310 case '\r': return '\\r'; 3311 case '\b': return '\\b'; 3312 case '\'': return '\'\''; 3313 default: return '\\' + s; 3314 // @formatter:off 3315 } 3316 }) 3317 // uglify compliant 3318 .replace(/\t/g, '\\t') 3319 .replace(/\x1a/g, '\\Z'); 3320 3321 if (additionalEscape) { 3322 escaped = escaped 3323 .replace(new RegExp('[' + additionalEscape + ']', 'g'), function(s) { 3324 return '\\' + s; 3325 }); 3326 } 3327 3328 return escaped; 3329}; 3330 3331/** 3332 * Escapes a string for use in regex 3333 * @param {string} str 3334 * @returns {string} 3335 */ 3336Utils.escapeRegExp = function(str) { 3337 return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); 3338}; 3339 3340/** 3341 * Escapes a string for use in HTML element id 3342 * @param {string} str 3343 * @returns {string} 3344 */ 3345Utils.escapeElementId = function(str) { 3346 // Regex based on that suggested by: 3347 // https://learn.jquery.com/using-jquery-core/faq/how-do-i-select-an-element-by-an-id-that-has-characters-used-in-css-notation/ 3348 // - escapes : . [ ] , 3349 // - avoids escaping already escaped values 3350 return (str) ? str.replace(/(\\)?([:.\[\],])/g, 3351 function( $0, $1, $2 ) { return $1 ? $0 : '\\' + $2; }) : str; 3352}; 3353 3354/** 3355 * Sorts objects by grouping them by `key`, preserving initial order when possible 3356 * @param {object[]} items 3357 * @param {string} key 3358 * @returns {object[]} 3359 */ 3360Utils.groupSort = function(items, key) { 3361 var optgroups = []; 3362 var newItems = []; 3363 3364 items.forEach(function(item) { 3365 var idx; 3366 3367 if (item[key]) { 3368 idx = optgroups.lastIndexOf(item[key]); 3369 3370 if (idx == -1) { 3371 idx = optgroups.length; 3372 } 3373 else { 3374 idx++; 3375 } 3376 } 3377 else { 3378 idx = optgroups.length; 3379 } 3380 3381 optgroups.splice(idx, 0, item[key]); 3382 newItems.splice(idx, 0, item); 3383 }); 3384 3385 return newItems; 3386}; 3387 3388/** 3389 * Defines properties on an Node prototype with getter and setter.<br> 3390 * Update events are emitted in the setter through root Model (if any).<br> 3391 * The object must have a `__` object, non enumerable property to store values. 3392 * @param {function} obj 3393 * @param {string[]} fields 3394 */ 3395Utils.defineModelProperties = function(obj, fields) { 3396 fields.forEach(function(field) { 3397 Object.defineProperty(obj.prototype, field, { 3398 enumerable: true, 3399 get: function() { 3400 return this.__[field]; 3401 }, 3402 set: function(value) { 3403 var previousValue = (this.__[field] !== null && typeof this.__[field] == 'object') ? 3404 $.extend({}, this.__[field]) : 3405 this.__[field]; 3406 3407 this.__[field] = value; 3408 3409 if (this.model !== null) { 3410 /** 3411 * After a value of the model changed 3412 * @event model:update 3413 * @memberof Model 3414 * @param {Node} node 3415 * @param {string} field 3416 * @param {*} value 3417 * @param {*} previousValue 3418 */ 3419 this.model.trigger('update', this, field, value, previousValue); 3420 } 3421 } 3422 }); 3423 }); 3424}; 3425 3426 3427/** 3428 * Main object storing data model and emitting model events 3429 * @constructor 3430 */ 3431function Model() { 3432 /** 3433 * @member {Group} 3434 * @readonly 3435 */ 3436 this.root = null; 3437 3438 /** 3439 * Base for event emitting 3440 * @member {jQuery} 3441 * @readonly 3442 * @private 3443 */ 3444 this.$ = $(this); 3445} 3446 3447$.extend(Model.prototype, /** @lends Model.prototype */ { 3448 /** 3449 * Triggers an event on the model 3450 * @param {string} type 3451 * @returns {$.Event} 3452 */ 3453 trigger: function(type) { 3454 var event = new $.Event(type); 3455 this.$.triggerHandler(event, Array.prototype.slice.call(arguments, 1)); 3456 return event; 3457 }, 3458 3459 /** 3460 * Attaches an event listener on the model 3461 * @param {string} type 3462 * @param {function} cb 3463 * @returns {Model} 3464 */ 3465 on: function() { 3466 this.$.on.apply(this.$, Array.prototype.slice.call(arguments)); 3467 return this; 3468 }, 3469 3470 /** 3471 * Removes an event listener from the model 3472 * @param {string} type 3473 * @param {function} [cb] 3474 * @returns {Model} 3475 */ 3476 off: function() { 3477 this.$.off.apply(this.$, Array.prototype.slice.call(arguments)); 3478 return this; 3479 }, 3480 3481 /** 3482 * Attaches an event listener called once on the model 3483 * @param {string} type 3484 * @param {function} cb 3485 * @returns {Model} 3486 */ 3487 once: function() { 3488 this.$.one.apply(this.$, Array.prototype.slice.call(arguments)); 3489 return this; 3490 } 3491}); 3492 3493 3494/** 3495 * Root abstract object 3496 * @constructor 3497 * @param {Node} [parent] 3498 * @param {jQuery} $el 3499 */ 3500var Node = function(parent, $el) { 3501 if (!(this instanceof Node)) { 3502 return new Node(parent, $el); 3503 } 3504 3505 Object.defineProperty(this, '__', { value: {} }); 3506 3507 $el.data('queryBuilderModel', this); 3508 3509 /** 3510 * @name level 3511 * @member {int} 3512 * @memberof Node 3513 * @instance 3514 * @readonly 3515 */ 3516 this.__.level = 1; 3517 3518 /** 3519 * @name error 3520 * @member {string} 3521 * @memberof Node 3522 * @instance 3523 */ 3524 this.__.error = null; 3525 3526 /** 3527 * @name flags 3528 * @member {object} 3529 * @memberof Node 3530 * @instance 3531 * @readonly 3532 */ 3533 this.__.flags = {}; 3534 3535 /** 3536 * @name data 3537 * @member {object} 3538 * @memberof Node 3539 * @instance 3540 */ 3541 this.__.data = undefined; 3542 3543 /** 3544 * @member {jQuery} 3545 * @readonly 3546 */ 3547 this.$el = $el; 3548 3549 /** 3550 * @member {string} 3551 * @readonly 3552 */ 3553 this.id = $el[0].id; 3554 3555 /** 3556 * @member {Model} 3557 * @readonly 3558 */ 3559 this.model = null; 3560 3561 /** 3562 * @member {Group} 3563 * @readonly 3564 */ 3565 this.parent = parent; 3566}; 3567 3568Utils.defineModelProperties(Node, ['level', 'error', 'data', 'flags']); 3569 3570Object.defineProperty(Node.prototype, 'parent', { 3571 enumerable: true, 3572 get: function() { 3573 return this.__.parent; 3574 }, 3575 set: function(value) { 3576 this.__.parent = value; 3577 this.level = value === null ? 1 : value.level + 1; 3578 this.model = value === null ? null : value.model; 3579 } 3580}); 3581 3582/** 3583 * Checks if this Node is the root 3584 * @returns {boolean} 3585 */ 3586Node.prototype.isRoot = function() { 3587 return (this.level === 1); 3588}; 3589 3590/** 3591 * Returns the node position inside its parent 3592 * @returns {int} 3593 */ 3594Node.prototype.getPos = function() { 3595 if (this.isRoot()) { 3596 return -1; 3597 } 3598 else { 3599 return this.parent.getNodePos(this); 3600 } 3601}; 3602 3603/** 3604 * Deletes self 3605 * @fires Model.model:drop 3606 */ 3607Node.prototype.drop = function() { 3608 var model = this.model; 3609 3610 if (!!this.parent) { 3611 this.parent.removeNode(this); 3612 } 3613 3614 this.$el.removeData('queryBuilderModel'); 3615 3616 if (model !== null) { 3617 /** 3618 * After a node of the model has been removed 3619 * @event model:drop 3620 * @memberof Model 3621 * @param {Node} node 3622 */ 3623 model.trigger('drop', this); 3624 } 3625}; 3626 3627/** 3628 * Moves itself after another Node 3629 * @param {Node} target 3630 * @fires Model.model:move 3631 */ 3632Node.prototype.moveAfter = function(target) { 3633 if (!this.isRoot()) { 3634 this.move(target.parent, target.getPos() + 1); 3635 } 3636}; 3637 3638/** 3639 * Moves itself at the beginning of parent or another Group 3640 * @param {Group} [target] 3641 * @fires Model.model:move 3642 */ 3643Node.prototype.moveAtBegin = function(target) { 3644 if (!this.isRoot()) { 3645 if (target === undefined) { 3646 target = this.parent; 3647 } 3648 3649 this.move(target, 0); 3650 } 3651}; 3652 3653/** 3654 * Moves itself at the end of parent or another Group 3655 * @param {Group} [target] 3656 * @fires Model.model:move 3657 */ 3658Node.prototype.moveAtEnd = function(target) { 3659 if (!this.isRoot()) { 3660 if (target === undefined) { 3661 target = this.parent; 3662 } 3663 3664 this.move(target, target.length() === 0 ? 0 : target.length() - 1); 3665 } 3666}; 3667 3668/** 3669 * Moves itself at specific position of Group 3670 * @param {Group} target 3671 * @param {int} index 3672 * @fires Model.model:move 3673 */ 3674Node.prototype.move = function(target, index) { 3675 if (!this.isRoot()) { 3676 if (typeof target === 'number') { 3677 index = target; 3678 target = this.parent; 3679 } 3680 3681 this.parent.removeNode(this); 3682 target.insertNode(this, index, false); 3683 3684 if (this.model !== null) { 3685 /** 3686 * After a node of the model has been moved 3687 * @event model:move 3688 * @memberof Model 3689 * @param {Node} node 3690 * @param {Node} target 3691 * @param {int} index 3692 */ 3693 this.model.trigger('move', this, target, index); 3694 } 3695 } 3696}; 3697 3698 3699/** 3700 * Group object 3701 * @constructor 3702 * @extends Node 3703 * @param {Group} [parent] 3704 * @param {jQuery} $el 3705 */ 3706var Group = function(parent, $el) { 3707 if (!(this instanceof Group)) { 3708 return new Group(parent, $el); 3709 } 3710 3711 Node.call(this, parent, $el); 3712 3713 /** 3714 * @member {object[]} 3715 * @readonly 3716 */ 3717 this.rules = []; 3718 3719 /** 3720 * @name condition 3721 * @member {string} 3722 * @memberof Group 3723 * @instance 3724 */ 3725 this.__.condition = null; 3726}; 3727 3728Group.prototype = Object.create(Node.prototype); 3729Group.prototype.constructor = Group; 3730 3731Utils.defineModelProperties(Group, ['condition']); 3732 3733/** 3734 * Removes group's content 3735 */ 3736Group.prototype.empty = function() { 3737 this.each('reverse', function(rule) { 3738 rule.drop(); 3739 }, function(group) { 3740 group.drop(); 3741 }); 3742}; 3743 3744/** 3745 * Deletes self 3746 */ 3747Group.prototype.drop = function() { 3748 this.empty(); 3749 Node.prototype.drop.call(this); 3750}; 3751 3752/** 3753 * Returns the number of children 3754 * @returns {int} 3755 */ 3756Group.prototype.length = function() { 3757 return this.rules.length; 3758}; 3759 3760/** 3761 * Adds a Node at specified index 3762 * @param {Node} node 3763 * @param {int} [index=end] 3764 * @param {boolean} [trigger=false] - fire 'add' event 3765 * @returns {Node} the inserted node 3766 * @fires Model.model:add 3767 */ 3768Group.prototype.insertNode = function(node, index, trigger) { 3769 if (index === undefined) { 3770 index = this.length(); 3771 } 3772 3773 this.rules.splice(index, 0, node); 3774 node.parent = this; 3775 3776 if (trigger && this.model !== null) { 3777 /** 3778 * After a node of the model has been added 3779 * @event model:add 3780 * @memberof Model 3781 * @param {Node} parent 3782 * @param {Node} node 3783 * @param {int} index 3784 */ 3785 this.model.trigger('add', this, node, index); 3786 } 3787 3788 return node; 3789}; 3790 3791/** 3792 * Adds a new Group at specified index 3793 * @param {jQuery} $el 3794 * @param {int} [index=end] 3795 * @returns {Group} 3796 * @fires Model.model:add 3797 */ 3798Group.prototype.addGroup = function($el, index) { 3799 return this.insertNode(new Group(this, $el), index, true); 3800}; 3801 3802/** 3803 * Adds a new Rule at specified index 3804 * @param {jQuery} $el 3805 * @param {int} [index=end] 3806 * @returns {Rule} 3807 * @fires Model.model:add 3808 */ 3809Group.prototype.addRule = function($el, index) { 3810 return this.insertNode(new Rule(this, $el), index, true); 3811}; 3812 3813/** 3814 * Deletes a specific Node 3815 * @param {Node} node 3816 */ 3817Group.prototype.removeNode = function(node) { 3818 var index = this.getNodePos(node); 3819 if (index !== -1) { 3820 node.parent = null; 3821 this.rules.splice(index, 1); 3822 } 3823}; 3824 3825/** 3826 * Returns the position of a child Node 3827 * @param {Node} node 3828 * @returns {int} 3829 */ 3830Group.prototype.getNodePos = function(node) { 3831 return this.rules.indexOf(node); 3832}; 3833 3834/** 3835 * @callback Model#GroupIteratee 3836 * @param {Node} node 3837 * @returns {boolean} stop the iteration 3838 */ 3839 3840/** 3841 * Iterate over all Nodes 3842 * @param {boolean} [reverse=false] - iterate in reverse order, required if you delete nodes 3843 * @param {Model#GroupIteratee} cbRule - callback for Rules (can be `null` but not omitted) 3844 * @param {Model#GroupIteratee} [cbGroup] - callback for Groups 3845 * @param {object} [context] - context for callbacks 3846 * @returns {boolean} if the iteration has been stopped by a callback 3847 */ 3848Group.prototype.each = function(reverse, cbRule, cbGroup, context) { 3849 if (typeof reverse !== 'boolean' && typeof reverse !== 'string') { 3850 context = cbGroup; 3851 cbGroup = cbRule; 3852 cbRule = reverse; 3853 reverse = false; 3854 } 3855 context = context === undefined ? null : context; 3856 3857 var i = reverse ? this.rules.length - 1 : 0; 3858 var l = reverse ? 0 : this.rules.length - 1; 3859 var c = reverse ? -1 : 1; 3860 var next = function() { 3861 return reverse ? i >= l : i <= l; 3862 }; 3863 var stop = false; 3864 3865 for (; next(); i += c) { 3866 if (this.rules[i] instanceof Group) { 3867 if (!!cbGroup) { 3868 stop = cbGroup.call(context, this.rules[i]) === false; 3869 } 3870 } 3871 else if (!!cbRule) { 3872 stop = cbRule.call(context, this.rules[i]) === false; 3873 } 3874 3875 if (stop) { 3876 break; 3877 } 3878 } 3879 3880 return !stop; 3881}; 3882 3883/** 3884 * Checks if the group contains a particular Node 3885 * @param {Node} node 3886 * @param {boolean} [recursive=false] 3887 * @returns {boolean} 3888 */ 3889Group.prototype.contains = function(node, recursive) { 3890 if (this.getNodePos(node) !== -1) { 3891 return true; 3892 } 3893 else if (!recursive) { 3894 return false; 3895 } 3896 else { 3897 // the loop will return with false as soon as the Node is found 3898 return !this.each(function() { 3899 return true; 3900 }, function(group) { 3901 return !group.contains(node, true); 3902 }); 3903 } 3904}; 3905 3906 3907/** 3908 * Rule object 3909 * @constructor 3910 * @extends Node 3911 * @param {Group} parent 3912 * @param {jQuery} $el 3913 */ 3914var Rule = function(parent, $el) { 3915 if (!(this instanceof Rule)) { 3916 return new Rule(parent, $el); 3917 } 3918 3919 Node.call(this, parent, $el); 3920 3921 this._updating_value = false; 3922 this._updating_input = false; 3923 3924 /** 3925 * @name filter 3926 * @member {QueryBuilder.Filter} 3927 * @memberof Rule 3928 * @instance 3929 */ 3930 this.__.filter = null; 3931 3932 /** 3933 * @name operator 3934 * @member {QueryBuilder.Operator} 3935 * @memberof Rule 3936 * @instance 3937 */ 3938 this.__.operator = null; 3939 3940 /** 3941 * @name value 3942 * @member {*} 3943 * @memberof Rule 3944 * @instance 3945 */ 3946 this.__.value = undefined; 3947}; 3948 3949Rule.prototype = Object.create(Node.prototype); 3950Rule.prototype.constructor = Rule; 3951 3952Utils.defineModelProperties(Rule, ['filter', 'operator', 'value']); 3953 3954/** 3955 * Checks if this Node is the root 3956 * @returns {boolean} always false 3957 */ 3958Rule.prototype.isRoot = function() { 3959 return false; 3960}; 3961 3962 3963/** 3964 * @member {function} 3965 * @memberof QueryBuilder 3966 * @see Group 3967 */ 3968QueryBuilder.Group = Group; 3969 3970/** 3971 * @member {function} 3972 * @memberof QueryBuilder 3973 * @see Rule 3974 */ 3975QueryBuilder.Rule = Rule; 3976 3977 3978/** 3979 * The {@link http://learn.jquery.com/plugins/|jQuery Plugins} namespace 3980 * @external "jQuery.fn" 3981 */ 3982 3983/** 3984 * Instanciates or accesses the {@link QueryBuilder} on an element 3985 * @function 3986 * @memberof external:"jQuery.fn" 3987 * @param {*} option - initial configuration or method name 3988 * @param {...*} args - method arguments 3989 * 3990 * @example 3991 * $('#builder').queryBuilder({ /** configuration object *\/ }); 3992 * @example 3993 * $('#builder').queryBuilder('methodName', methodParam1, methodParam2); 3994 */ 3995$.fn.queryBuilder = function(option) { 3996 if (this.length === 0) { 3997 Utils.error('Config', 'No target defined'); 3998 } 3999 if (this.length > 1) { 4000 Utils.error('Config', 'Unable to initialize on multiple target'); 4001 } 4002 4003 var data = this.data('queryBuilder'); 4004 var options = (typeof option == 'object' && option) || {}; 4005 4006 if (!data && option == 'destroy') { 4007 return this; 4008 } 4009 if (!data) { 4010 var builder = new QueryBuilder(this, options); 4011 this.data('queryBuilder', builder); 4012 builder.init(options.rules); 4013 } 4014 if (typeof option == 'string') { 4015 return data[option].apply(data, Array.prototype.slice.call(arguments, 1)); 4016 } 4017 4018 return this; 4019}; 4020 4021/** 4022 * @function 4023 * @memberof external:"jQuery.fn" 4024 * @see QueryBuilder 4025 */ 4026$.fn.queryBuilder.constructor = QueryBuilder; 4027 4028/** 4029 * @function 4030 * @memberof external:"jQuery.fn" 4031 * @see QueryBuilder.defaults 4032 */ 4033$.fn.queryBuilder.defaults = QueryBuilder.defaults; 4034 4035/** 4036 * @function 4037 * @memberof external:"jQuery.fn" 4038 * @see QueryBuilder.defaults 4039 */ 4040$.fn.queryBuilder.extend = QueryBuilder.extend; 4041 4042/** 4043 * @function 4044 * @memberof external:"jQuery.fn" 4045 * @see QueryBuilder.define 4046 */ 4047$.fn.queryBuilder.define = QueryBuilder.define; 4048 4049/** 4050 * @function 4051 * @memberof external:"jQuery.fn" 4052 * @see QueryBuilder.regional 4053 */ 4054$.fn.queryBuilder.regional = QueryBuilder.regional; 4055 4056 4057/** 4058 * @class BtCheckbox 4059 * @memberof module:plugins 4060 * @description Applies Awesome Bootstrap Checkbox for checkbox and radio inputs. 4061 * @param {object} [options] 4062 * @param {string} [options.font='glyphicons'] 4063 * @param {string} [options.color='default'] 4064 */ 4065QueryBuilder.define('bt-checkbox', function(options) { 4066 if (options.font == 'glyphicons') { 4067 this.$el.addClass('bt-checkbox-glyphicons'); 4068 } 4069 4070 this.on('getRuleInput.filter', function(h, rule, name) { 4071 var filter = rule.filter; 4072 4073 if ((filter.input === 'radio' || filter.input === 'checkbox') && !filter.plugin) { 4074 h.value = ''; 4075 4076 if (!filter.colors) { 4077 filter.colors = {}; 4078 } 4079 if (filter.color) { 4080 filter.colors._def_ = filter.color; 4081 } 4082 4083 var style = filter.vertical ? ' style="display:block"' : ''; 4084 var i = 0; 4085 4086 Utils.iterateOptions(filter.values, function(key, val) { 4087 var color = filter.colors[key] || filter.colors._def_ || options.color; 4088 var id = name + '_' + (i++); 4089 4090 h.value+= '\ 4091<div' + style + ' class="' + filter.input + ' ' + filter.input + '-' + color + '"> \ 4092 <input type="' + filter.input + '" name="' + name + '" id="' + id + '" value="' + key + '"> \ 4093 <label for="' + id + '">' + val + '</label> \ 4094</div>'; 4095 }); 4096 } 4097 }); 4098}, { 4099 font: 'glyphicons', 4100 color: 'default' 4101}); 4102 4103 4104/** 4105 * @class BtSelectpicker 4106 * @memberof module:plugins 4107 * @descriptioon Applies Bootstrap Select on filters and operators combo-boxes. 4108 * @param {object} [options] 4109 * @param {string} [options.container='body'] 4110 * @param {string} [options.style='btn-inverse btn-xs'] 4111 * @param {int|string} [options.width='auto'] 4112 * @param {boolean} [options.showIcon=false] 4113 * @throws MissingLibraryError 4114 */ 4115QueryBuilder.define('bt-selectpicker', function(options) { 4116 if (!$.fn.selectpicker || !$.fn.selectpicker.Constructor) { 4117 Utils.error('MissingLibrary', 'Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select'); 4118 } 4119 4120 var Selectors = QueryBuilder.selectors; 4121 4122 // init selectpicker 4123 this.on('afterCreateRuleFilters', function(e, rule) { 4124 rule.$el.find(Selectors.rule_filter).removeClass('form-control').selectpicker(options); 4125 }); 4126 4127 this.on('afterCreateRuleOperators', function(e, rule) { 4128 rule.$el.find(Selectors.rule_operator).removeClass('form-control').selectpicker(options); 4129 }); 4130 4131 // update selectpicker on change 4132 this.on('afterUpdateRuleFilter', function(e, rule) { 4133 rule.$el.find(Selectors.rule_filter).selectpicker('render'); 4134 }); 4135 4136 this.on('afterUpdateRuleOperator', function(e, rule) { 4137 rule.$el.find(Selectors.rule_operator).selectpicker('render'); 4138 }); 4139 4140 this.on('beforeDeleteRule', function(e, rule) { 4141 rule.$el.find(Selectors.rule_filter).selectpicker('destroy'); 4142 rule.$el.find(Selectors.rule_operator).selectpicker('destroy'); 4143 }); 4144}, { 4145 container: 'body', 4146 style: 'btn-inverse btn-xs', 4147 width: 'auto', 4148 showIcon: false 4149}); 4150 4151 4152/** 4153 * @class BtTooltipErrors 4154 * @memberof module:plugins 4155 * @description Applies Bootstrap Tooltips on validation error messages. 4156 * @param {object} [options] 4157 * @param {string} [options.placement='right'] 4158 * @throws MissingLibraryError 4159 */ 4160QueryBuilder.define('bt-tooltip-errors', function(options) { 4161 if (!$.fn.tooltip || !$.fn.tooltip.Constructor || !$.fn.tooltip.Constructor.prototype.fixTitle) { 4162 Utils.error('MissingLibrary', 'Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com'); 4163 } 4164 4165 var self = this; 4166 4167 // add BT Tooltip data 4168 this.on('getRuleTemplate.filter getGroupTemplate.filter', function(h) { 4169 var $h = $($.parseHTML(h.value)); 4170 $h.find(QueryBuilder.selectors.error_container).attr('data-toggle', 'tooltip'); 4171 h.value = $h.prop('outerHTML'); 4172 }); 4173 4174 // init/refresh tooltip when title changes 4175 this.model.on('update', function(e, node, field) { 4176 if (field == 'error' && self.settings.display_errors) { 4177 node.$el.find(QueryBuilder.selectors.error_container).eq(0) 4178 .tooltip(options) 4179 .tooltip('hide') 4180 .tooltip('fixTitle'); 4181 } 4182 }); 4183}, { 4184 placement: 'right' 4185}); 4186 4187 4188/** 4189 * @class ChangeFilters 4190 * @memberof module:plugins 4191 * @description Allows to change available filters after plugin initialization. 4192 */ 4193 4194QueryBuilder.extend(/** @lends module:plugins.ChangeFilters.prototype */ { 4195 /** 4196 * Change the filters of the builder 4197 * @param {boolean} [deleteOrphans=false] - delete rules using old filters 4198 * @param {QueryBuilder[]} filters 4199 * @fires module:plugins.ChangeFilters.changer:setFilters 4200 * @fires module:plugins.ChangeFilters.afterSetFilters 4201 * @throws ChangeFilterError 4202 */ 4203 setFilters: function(deleteOrphans, filters) { 4204 var self = this; 4205 4206 if (filters === undefined) { 4207 filters = deleteOrphans; 4208 deleteOrphans = false; 4209 } 4210 4211 filters = this.checkFilters(filters); 4212 4213 /** 4214 * Modifies the filters before {@link module:plugins.ChangeFilters.setFilters} method 4215 * @event changer:setFilters 4216 * @memberof module:plugins.ChangeFilters 4217 * @param {QueryBuilder.Filter[]} filters 4218 * @returns {QueryBuilder.Filter[]} 4219 */ 4220 filters = this.change('setFilters', filters); 4221 4222 var filtersIds = filters.map(function(filter) { 4223 return filter.id; 4224 }); 4225 4226 // check for orphans 4227 if (!deleteOrphans) { 4228 (function checkOrphans(node) { 4229 node.each( 4230 function(rule) { 4231 if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) { 4232 Utils.error('ChangeFilter', 'A rule is using filter "{0}"', rule.filter.id); 4233 } 4234 }, 4235 checkOrphans 4236 ); 4237 }(this.model.root)); 4238 } 4239 4240 // replace filters 4241 this.filters = filters; 4242 4243 // apply on existing DOM 4244 (function updateBuilder(node) { 4245 node.each(true, 4246 function(rule) { 4247 if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) { 4248 rule.drop(); 4249 4250 self.trigger('rulesChanged'); 4251 } 4252 else { 4253 self.createRuleFilters(rule); 4254 4255 rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1'); 4256 self.trigger('afterUpdateRuleFilter', rule); 4257 } 4258 }, 4259 updateBuilder 4260 ); 4261 }(this.model.root)); 4262 4263 // update plugins 4264 if (this.settings.plugins) { 4265 if (this.settings.plugins['unique-filter']) { 4266 this.updateDisabledFilters(); 4267 } 4268 if (this.settings.plugins['bt-selectpicker']) { 4269 this.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render'); 4270 } 4271 } 4272 4273 // reset the default_filter if does not exist anymore 4274 if (this.settings.default_filter) { 4275 try { 4276 this.getFilterById(this.settings.default_filter); 4277 } 4278 catch (e) { 4279 this.settings.default_filter = null; 4280 } 4281 } 4282 4283 /** 4284 * After {@link module:plugins.ChangeFilters.setFilters} method 4285 * @event afterSetFilters 4286 * @memberof module:plugins.ChangeFilters 4287 * @param {QueryBuilder.Filter[]} filters 4288 */ 4289 this.trigger('afterSetFilters', filters); 4290 }, 4291 4292 /** 4293 * Adds a new filter to the builder 4294 * @param {QueryBuilder.Filter|Filter[]} newFilters 4295 * @param {int|string} [position=#end] - index or '#start' or '#end' 4296 * @fires module:plugins.ChangeFilters.changer:setFilters 4297 * @fires module:plugins.ChangeFilters.afterSetFilters 4298 * @throws ChangeFilterError 4299 */ 4300 addFilter: function(newFilters, position) { 4301 if (position === undefined || position == '#end') { 4302 position = this.filters.length; 4303 } 4304 else if (position == '#start') { 4305 position = 0; 4306 } 4307 4308 if (!$.isArray(newFilters)) { 4309 newFilters = [newFilters]; 4310 } 4311 4312 var filters = $.extend(true, [], this.filters); 4313 4314 // numeric position 4315 if (parseInt(position) == position) { 4316 Array.prototype.splice.apply(filters, [position, 0].concat(newFilters)); 4317 } 4318 else { 4319 // after filter by its id 4320 if (this.filters.some(function(filter, index) { 4321 if (filter.id == position) { 4322 position = index + 1; 4323 return true; 4324 } 4325 }) 4326 ) { 4327 Array.prototype.splice.apply(filters, [position, 0].concat(newFilters)); 4328 } 4329 // defaults to end of list 4330 else { 4331 Array.prototype.push.apply(filters, newFilters); 4332 } 4333 } 4334 4335 this.setFilters(filters); 4336 }, 4337 4338 /** 4339 * Removes a filter from the builder 4340 * @param {string|string[]} filterIds 4341 * @param {boolean} [deleteOrphans=false] delete rules using old filters 4342 * @fires module:plugins.ChangeFilters.changer:setFilters 4343 * @fires module:plugins.ChangeFilters.afterSetFilters 4344 * @throws ChangeFilterError 4345 */ 4346 removeFilter: function(filterIds, deleteOrphans) { 4347 var filters = $.extend(true, [], this.filters); 4348 if (typeof filterIds === 'string') { 4349 filterIds = [filterIds]; 4350 } 4351 4352 filters = filters.filter(function(filter) { 4353 return filterIds.indexOf(filter.id) === -1; 4354 }); 4355 4356 this.setFilters(deleteOrphans, filters); 4357 } 4358}); 4359 4360 4361/** 4362 * @class ChosenSelectpicker 4363 * @memberof module:plugins 4364 * @descriptioon Applies chosen-js Select on filters and operators combo-boxes. 4365 * @param {object} [options] Supports all the options for chosen 4366 * @throws MissingLibraryError 4367 */ 4368QueryBuilder.define('chosen-selectpicker', function(options) { 4369 4370 if (!$.fn.chosen) { 4371 Utils.error('MissingLibrary', 'chosen is required to use "chosen-selectpicker" plugin. Get it here: https://github.com/harvesthq/chosen'); 4372 } 4373 4374 if (this.settings.plugins['bt-selectpicker']) { 4375 Utils.error('Conflict', 'bt-selectpicker is already selected as the dropdown plugin. Please remove chosen-selectpicker from the plugin list'); 4376 } 4377 4378 var Selectors = QueryBuilder.selectors; 4379 4380 // init selectpicker 4381 this.on('afterCreateRuleFilters', function(e, rule) { 4382 rule.$el.find(Selectors.rule_filter).removeClass('form-control').chosen(options); 4383 }); 4384 4385 this.on('afterCreateRuleOperators', function(e, rule) { 4386 if (e.builder.getOperators(rule.filter).length > 1) { 4387 rule.$el.find(Selectors.rule_operator).removeClass('form-control').chosen(options); 4388 } 4389 }); 4390 4391 // update selectpicker on change 4392 this.on('afterUpdateRuleFilter', function(e, rule) { 4393 rule.$el.find(Selectors.rule_filter).trigger('chosen:updated'); 4394 }); 4395 4396 this.on('afterUpdateRuleOperator', function(e, rule) { 4397 rule.$el.find(Selectors.rule_operator).trigger('chosen:updated'); 4398 }); 4399 4400 this.on('beforeDeleteRule', function(e, rule) { 4401 rule.$el.find(Selectors.rule_filter).chosen('destroy'); 4402 rule.$el.find(Selectors.rule_operator).chosen('destroy'); 4403 }); 4404}); 4405 4406 4407/** 4408 * @class FilterDescription 4409 * @memberof module:plugins 4410 * @description Provides three ways to display a description about a filter: inline, Bootsrap Popover or Bootbox. 4411 * @param {object} [options] 4412 * @param {string} [options.icon='glyphicon glyphicon-info-sign'] 4413 * @param {string} [options.mode='popover'] - inline, popover or bootbox 4414 * @throws ConfigError 4415 */ 4416QueryBuilder.define('filter-description', function(options) { 4417 // INLINE 4418 if (options.mode === 'inline') { 4419 this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) { 4420 var $p = rule.$el.find('p.filter-description'); 4421 var description = e.builder.getFilterDescription(rule.filter, rule); 4422 4423 if (!description) { 4424 $p.hide(); 4425 } 4426 else { 4427 if ($p.length === 0) { 4428 $p = $($.parseHTML('<p class="filter-description"></p>')); 4429 $p.appendTo(rule.$el); 4430 } 4431 else { 4432 $p.css('display', ''); 4433 } 4434 4435 $p.html('<i class="' + options.icon + '"></i> ' + description); 4436 } 4437 }); 4438 } 4439 // POPOVER 4440 else if (options.mode === 'popover') { 4441 if (!$.fn.popover || !$.fn.popover.Constructor || !$.fn.popover.Constructor.prototype.fixTitle) { 4442 Utils.error('MissingLibrary', 'Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com'); 4443 } 4444 4445 this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) { 4446 var $b = rule.$el.find('button.filter-description'); 4447 var description = e.builder.getFilterDescription(rule.filter, rule); 4448 4449 if (!description) { 4450 $b.hide(); 4451 4452 if ($b.data('bs.popover')) { 4453 $b.popover('hide'); 4454 } 4455 } 4456 else { 4457 if ($b.length === 0) { 4458 $b = $($.parseHTML('<button type="button" class="btn btn-xs btn-info filter-description" data-toggle="popover"><i class="' + options.icon + '"></i></button>')); 4459 $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions)); 4460 4461 $b.popover({ 4462 placement: 'left', 4463 container: 'body', 4464 html: true 4465 }); 4466 4467 $b.on('mouseout', function() { 4468 $b.popover('hide'); 4469 }); 4470 } 4471 else { 4472 $b.css('display', ''); 4473 } 4474 4475 $b.data('bs.popover').options.content = description; 4476 4477 if ($b.attr('aria-describedby')) { 4478 $b.popover('show'); 4479 } 4480 } 4481 }); 4482 } 4483 // BOOTBOX 4484 else if (options.mode === 'bootbox') { 4485 if (!('bootbox' in window)) { 4486 Utils.error('MissingLibrary', 'Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com'); 4487 } 4488 4489 this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) { 4490 var $b = rule.$el.find('button.filter-description'); 4491 var description = e.builder.getFilterDescription(rule.filter, rule); 4492 4493 if (!description) { 4494 $b.hide(); 4495 } 4496 else { 4497 if ($b.length === 0) { 4498 $b = $($.parseHTML('<button type="button" class="btn btn-xs btn-info filter-description" data-toggle="bootbox"><i class="' + options.icon + '"></i></button>')); 4499 $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions)); 4500 4501 $b.on('click', function() { 4502 bootbox.alert($b.data('description')); 4503 }); 4504 } 4505 else { 4506 $b.css('display', ''); 4507 } 4508 4509 $b.data('description', description); 4510 } 4511 }); 4512 } 4513}, { 4514 icon: 'glyphicon glyphicon-info-sign', 4515 mode: 'popover' 4516}); 4517 4518QueryBuilder.extend(/** @lends module:plugins.FilterDescription.prototype */ { 4519 /** 4520 * Returns the description of a filter for a particular rule (if present) 4521 * @param {object} filter 4522 * @param {Rule} [rule] 4523 * @returns {string} 4524 * @private 4525 */ 4526 getFilterDescription: function(filter, rule) { 4527 if (!filter) { 4528 return undefined; 4529 } 4530 else if (typeof filter.description == 'function') { 4531 return filter.description.call(this, rule); 4532 } 4533 else { 4534 return filter.description; 4535 } 4536 } 4537}); 4538 4539 4540/** 4541 * @class Invert 4542 * @memberof module:plugins 4543 * @description Allows to invert a rule operator, a group condition or the entire builder. 4544 * @param {object} [options] 4545 * @param {string} [options.icon='glyphicon glyphicon-random'] 4546 * @param {boolean} [options.recursive=true] 4547 * @param {boolean} [options.invert_rules=true] 4548 * @param {boolean} [options.display_rules_button=false] 4549 * @param {boolean} [options.silent_fail=false] 4550 */ 4551QueryBuilder.define('invert', function(options) { 4552 var self = this; 4553 var Selectors = QueryBuilder.selectors; 4554 4555 // Bind events 4556 this.on('afterInit', function() { 4557 self.$el.on('click.queryBuilder', '[data-invert=group]', function() { 4558 var $group = $(this).closest(Selectors.group_container); 4559 self.invert(self.getModel($group), options); 4560 }); 4561 4562 if (options.display_rules_button && options.invert_rules) { 4563 self.$el.on('click.queryBuilder', '[data-invert=rule]', function() { 4564 var $rule = $(this).closest(Selectors.rule_container); 4565 self.invert(self.getModel($rule), options); 4566 }); 4567 } 4568 }); 4569 4570 // Modify templates 4571 if (!options.disable_template) { 4572 this.on('getGroupTemplate.filter', function(h) { 4573 var $h = $($.parseHTML(h.value)); 4574 $h.find(Selectors.condition_container).after( 4575 '<button type="button" class="btn btn-xs btn-default" data-invert="group">' + 4576 '<i class="' + options.icon + '"></i> ' + self.translate('invert') + 4577 '</button>' 4578 ); 4579 h.value = $h.prop('outerHTML'); 4580 }); 4581 4582 if (options.display_rules_button && options.invert_rules) { 4583 this.on('getRuleTemplate.filter', function(h) { 4584 var $h = $($.parseHTML(h.value)); 4585 $h.find(Selectors.rule_actions).prepend( 4586 '<button type="button" class="btn btn-xs btn-default" data-invert="rule">' + 4587 '<i class="' + options.icon + '"></i> ' + self.translate('invert') + 4588 '</button>' 4589 ); 4590 h.value = $h.prop('outerHTML'); 4591 }); 4592 } 4593 } 4594}, { 4595 icon: 'glyphicon glyphicon-random', 4596 recursive: true, 4597 invert_rules: true, 4598 display_rules_button: false, 4599 silent_fail: false, 4600 disable_template: false 4601}); 4602 4603QueryBuilder.defaults({ 4604 operatorOpposites: { 4605 'equal': 'not_equal', 4606 'not_equal': 'equal', 4607 'in': 'not_in', 4608 'not_in': 'in', 4609 'less': 'greater_or_equal', 4610 'less_or_equal': 'greater', 4611 'greater': 'less_or_equal', 4612 'greater_or_equal': 'less', 4613 'between': 'not_between', 4614 'not_between': 'between', 4615 'begins_with': 'not_begins_with', 4616 'not_begins_with': 'begins_with', 4617 'contains': 'not_contains', 4618 'not_contains': 'contains', 4619 'ends_with': 'not_ends_with', 4620 'not_ends_with': 'ends_with', 4621 'is_empty': 'is_not_empty', 4622 'is_not_empty': 'is_empty', 4623 'is_null': 'is_not_null', 4624 'is_not_null': 'is_null' 4625 }, 4626 4627 conditionOpposites: { 4628 'AND': 'OR', 4629 'OR': 'AND' 4630 } 4631}); 4632 4633QueryBuilder.extend(/** @lends module:plugins.Invert.prototype */ { 4634 /** 4635 * Invert a Group, a Rule or the whole builder 4636 * @param {Node} [node] 4637 * @param {object} [options] {@link module:plugins.Invert} 4638 * @fires module:plugins.Invert.afterInvert 4639 * @throws InvertConditionError, InvertOperatorError 4640 */ 4641 invert: function(node, options) { 4642 if (!(node instanceof Node)) { 4643 if (!this.model.root) return; 4644 options = node; 4645 node = this.model.root; 4646 } 4647 4648 if (typeof options != 'object') options = {}; 4649 if (options.recursive === undefined) options.recursive = true; 4650 if (options.invert_rules === undefined) options.invert_rules = true; 4651 if (options.silent_fail === undefined) options.silent_fail = false; 4652 if (options.trigger === undefined) options.trigger = true; 4653 4654 if (node instanceof Group) { 4655 // invert group condition 4656 if (this.settings.conditionOpposites[node.condition]) { 4657 node.condition = this.settings.conditionOpposites[node.condition]; 4658 } 4659 else if (!options.silent_fail) { 4660 Utils.error('InvertCondition', 'Unknown inverse of condition "{0}"', node.condition); 4661 } 4662 4663 // recursive call 4664 if (options.recursive) { 4665 var tempOpts = $.extend({}, options, { trigger: false }); 4666 node.each(function(rule) { 4667 if (options.invert_rules) { 4668 this.invert(rule, tempOpts); 4669 } 4670 }, function(group) { 4671 this.invert(group, tempOpts); 4672 }, this); 4673 } 4674 } 4675 else if (node instanceof Rule) { 4676 if (node.operator && !node.filter.no_invert) { 4677 // invert rule operator 4678 if (this.settings.operatorOpposites[node.operator.type]) { 4679 var invert = this.settings.operatorOpposites[node.operator.type]; 4680 // check if the invert is "authorized" 4681 if (!node.filter.operators || node.filter.operators.indexOf(invert) != -1) { 4682 node.operator = this.getOperatorByType(invert); 4683 } 4684 } 4685 else if (!options.silent_fail) { 4686 Utils.error('InvertOperator', 'Unknown inverse of operator "{0}"', node.operator.type); 4687 } 4688 } 4689 } 4690 4691 if (options.trigger) { 4692 /** 4693 * After {@link module:plugins.Invert.invert} method 4694 * @event afterInvert 4695 * @memberof module:plugins.Invert 4696 * @param {Node} node - the main group or rule that has been modified 4697 * @param {object} options 4698 */ 4699 this.trigger('afterInvert', node, options); 4700 4701 this.trigger('rulesChanged'); 4702 } 4703 } 4704}); 4705 4706 4707/** 4708 * @class MongoDbSupport 4709 * @memberof module:plugins 4710 * @description Allows to export rules as a MongoDB find object as well as populating the builder from a MongoDB object. 4711 */ 4712 4713QueryBuilder.defaults({ 4714 mongoOperators: { 4715 // @formatter:off 4716 equal: function(v) { return v[0]; }, 4717 not_equal: function(v) { return { '$ne': v[0] }; }, 4718 in: function(v) { return { '$in': v }; }, 4719 not_in: function(v) { return { '$nin': v }; }, 4720 less: function(v) { return { '$lt': v[0] }; }, 4721 less_or_equal: function(v) { return { '$lte': v[0] }; }, 4722 greater: function(v) { return { '$gt': v[0] }; }, 4723 greater_or_equal: function(v) { return { '$gte': v[0] }; }, 4724 between: function(v) { return { '$gte': v[0], '$lte': v[1] }; }, 4725 not_between: function(v) { return { '$lt': v[0], '$gt': v[1] }; }, 4726 begins_with: function(v) { return { '$regex': '^' + Utils.escapeRegExp(v[0]) }; }, 4727 not_begins_with: function(v) { return { '$regex': '^(?!' + Utils.escapeRegExp(v[0]) + ')' }; }, 4728 contains: function(v) { return { '$regex': Utils.escapeRegExp(v[0]) }; }, 4729 not_contains: function(v) { return { '$regex': '^((?!' + Utils.escapeRegExp(v[0]) + ').)*$', '$options': 's' }; }, 4730 ends_with: function(v) { return { '$regex': Utils.escapeRegExp(v[0]) + '$' }; }, 4731 not_ends_with: function(v) { return { '$regex': '(?<!' + Utils.escapeRegExp(v[0]) + ')$' }; }, 4732 is_empty: function(v) { return ''; }, 4733 is_not_empty: function(v) { return { '$ne': '' }; }, 4734 is_null: function(v) { return null; }, 4735 is_not_null: function(v) { return { '$ne': null }; } 4736 // @formatter:on 4737 }, 4738 4739 mongoRuleOperators: { 4740 $eq: function(v) { 4741 return { 4742 'val': v, 4743 'op': v === null ? 'is_null' : (v === '' ? 'is_empty' : 'equal') 4744 }; 4745 }, 4746 $ne: function(v) { 4747 v = v.$ne; 4748 return { 4749 'val': v, 4750 'op': v === null ? 'is_not_null' : (v === '' ? 'is_not_empty' : 'not_equal') 4751 }; 4752 }, 4753 $regex: function(v) { 4754 v = v.$regex; 4755 if (v.slice(0, 4) == '^(?!' && v.slice(-1) == ')') { 4756 return { 'val': v.slice(4, -1), 'op': 'not_begins_with' }; 4757 } 4758 else if (v.slice(0, 5) == '^((?!' && v.slice(-5) == ').)*$') { 4759 return { 'val': v.slice(5, -5), 'op': 'not_contains' }; 4760 } 4761 else if (v.slice(0, 4) == '(?<!' && v.slice(-2) == ')$') { 4762 return { 'val': v.slice(4, -2), 'op': 'not_ends_with' }; 4763 } 4764 else if (v.slice(-1) == '$') { 4765 return { 'val': v.slice(0, -1), 'op': 'ends_with' }; 4766 } 4767 else if (v.slice(0, 1) == '^') { 4768 return { 'val': v.slice(1), 'op': 'begins_with' }; 4769 } 4770 else { 4771 return { 'val': v, 'op': 'contains' }; 4772 } 4773 }, 4774 between: function(v) { 4775 return { 'val': [v.$gte, v.$lte], 'op': 'between' }; 4776 }, 4777 not_between: function(v) { 4778 return { 'val': [v.$lt, v.$gt], 'op': 'not_between' }; 4779 }, 4780 $in: function(v) { 4781 return { 'val': v.$in, 'op': 'in' }; 4782 }, 4783 $nin: function(v) { 4784 return { 'val': v.$nin, 'op': 'not_in' }; 4785 }, 4786 $lt: function(v) { 4787 return { 'val': v.$lt, 'op': 'less' }; 4788 }, 4789 $lte: function(v) { 4790 return { 'val': v.$lte, 'op': 'less_or_equal' }; 4791 }, 4792 $gt: function(v) { 4793 return { 'val': v.$gt, 'op': 'greater' }; 4794 }, 4795 $gte: function(v) { 4796 return { 'val': v.$gte, 'op': 'greater_or_equal' }; 4797 } 4798 } 4799}); 4800 4801QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ { 4802 /** 4803 * Returns rules as a MongoDB query 4804 * @param {object} [data] - current rules by default 4805 * @returns {object} 4806 * @fires module:plugins.MongoDbSupport.changer:getMongoDBField 4807 * @fires module:plugins.MongoDbSupport.changer:ruleToMongo 4808 * @fires module:plugins.MongoDbSupport.changer:groupToMongo 4809 * @throws UndefinedMongoConditionError, UndefinedMongoOperatorError 4810 */ 4811 getMongo: function(data) { 4812 data = (data === undefined) ? this.getRules() : data; 4813 4814 if (!data) { 4815 return null; 4816 } 4817 4818 var self = this; 4819 4820 return (function parse(group) { 4821 if (!group.condition) { 4822 group.condition = self.settings.default_condition; 4823 } 4824 if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) { 4825 Utils.error('UndefinedMongoCondition', 'Unable to build MongoDB query with condition "{0}"', group.condition); 4826 } 4827 4828 if (!group.rules) { 4829 return {}; 4830 } 4831 4832 var parts = []; 4833 4834 group.rules.forEach(function(rule) { 4835 if (rule.rules && rule.rules.length > 0) { 4836 parts.push(parse(rule)); 4837 } 4838 else { 4839 var mdb = self.settings.mongoOperators[rule.operator]; 4840 var ope = self.getOperatorByType(rule.operator); 4841 4842 if (mdb === undefined) { 4843 Utils.error('UndefinedMongoOperator', 'Unknown MongoDB operation for operator "{0}"', rule.operator); 4844 } 4845 4846 if (ope.nb_inputs !== 0) { 4847 if (!(rule.value instanceof Array)) { 4848 rule.value = [rule.value]; 4849 } 4850 } 4851 4852 /** 4853 * Modifies the MongoDB field used by a rule 4854 * @event changer:getMongoDBField 4855 * @memberof module:plugins.MongoDbSupport 4856 * @param {string} field 4857 * @param {Rule} rule 4858 * @returns {string} 4859 */ 4860 var field = self.change('getMongoDBField', rule.field, rule); 4861 4862 var ruleExpression = {}; 4863 ruleExpression[field] = mdb.call(self, rule.value); 4864 4865 /** 4866 * Modifies the MongoDB expression generated for a rul 4867 * @event changer:ruleToMongo 4868 * @memberof module:plugins.MongoDbSupport 4869 * @param {object} expression 4870 * @param {Rule} rule 4871 * @param {*} value 4872 * @param {function} valueWrapper - function that takes the value and adds the operator 4873 * @returns {object} 4874 */ 4875 parts.push(self.change('ruleToMongo', ruleExpression, rule, rule.value, mdb)); 4876 } 4877 }); 4878 4879 var groupExpression = {}; 4880 groupExpression['$' + group.condition.toLowerCase()] = parts; 4881 4882 /** 4883 * Modifies the MongoDB expression generated for a group 4884 * @event changer:groupToMongo 4885 * @memberof module:plugins.MongoDbSupport 4886 * @param {object} expression 4887 * @param {Group} group 4888 * @returns {object} 4889 */ 4890 return self.change('groupToMongo', groupExpression, group); 4891 }(data)); 4892 }, 4893 4894 /** 4895 * Converts a MongoDB query to rules 4896 * @param {object} query 4897 * @returns {object} 4898 * @fires module:plugins.MongoDbSupport.changer:parseMongoNode 4899 * @fires module:plugins.MongoDbSupport.changer:getMongoDBFieldID 4900 * @fires module:plugins.MongoDbSupport.changer:mongoToRule 4901 * @fires module:plugins.MongoDbSupport.changer:mongoToGroup 4902 * @throws MongoParseError, UndefinedMongoConditionError, UndefinedMongoOperatorError 4903 */ 4904 getRulesFromMongo: function(query) { 4905 if (query === undefined || query === null) { 4906 return null; 4907 } 4908 4909 var self = this; 4910 4911 /** 4912 * Custom parsing of a MongoDB expression, you can return a sub-part of the expression, or a well formed group or rule JSON 4913 * @event changer:parseMongoNode 4914 * @memberof module:plugins.MongoDbSupport 4915 * @param {object} expression 4916 * @returns {object} expression, rule or group 4917 */ 4918 query = self.change('parseMongoNode', query); 4919 4920 // a plugin returned a group 4921 if ('rules' in query && 'condition' in query) { 4922 return query; 4923 } 4924 4925 // a plugin returned a rule 4926 if ('id' in query && 'operator' in query && 'value' in query) { 4927 return { 4928 condition: this.settings.default_condition, 4929 rules: [query] 4930 }; 4931 } 4932 4933 var key = self.getMongoCondition(query); 4934 if (!key) { 4935 Utils.error('MongoParse', 'Invalid MongoDB query format'); 4936 } 4937 4938 return (function parse(data, topKey) { 4939 var rules = data[topKey]; 4940 var parts = []; 4941 4942 rules.forEach(function(data) { 4943 // allow plugins to manually parse or handle special cases 4944 data = self.change('parseMongoNode', data); 4945 4946 // a plugin returned a group 4947 if ('rules' in data && 'condition' in data) { 4948 parts.push(data); 4949 return; 4950 } 4951 4952 // a plugin returned a rule 4953 if ('id' in data && 'operator' in data && 'value' in data) { 4954 parts.push(data); 4955 return; 4956 } 4957 4958 var key = self.getMongoCondition(data); 4959 if (key) { 4960 parts.push(parse(data, key)); 4961 } 4962 else { 4963 var field = Object.keys(data)[0]; 4964 var value = data[field]; 4965 4966 var operator = self.getMongoOperator(value); 4967 if (operator === undefined) { 4968 Utils.error('MongoParse', 'Invalid MongoDB query format'); 4969 } 4970 4971 var mdbrl = self.settings.mongoRuleOperators[operator]; 4972 if (mdbrl === undefined) { 4973 Utils.error('UndefinedMongoOperator', 'JSON Rule operation unknown for operator "{0}"', operator); 4974 } 4975 4976 var opVal = mdbrl.call(self, value); 4977 4978 var id = self.getMongoDBFieldID(field, value); 4979 4980 /** 4981 * Modifies the rule generated from the MongoDB expression 4982 * @event changer:mongoToRule 4983 * @memberof module:plugins.MongoDbSupport 4984 * @param {object} rule 4985 * @param {object} expression 4986 * @returns {object} 4987 */ 4988 var rule = self.change('mongoToRule', { 4989 id: id, 4990 field: field, 4991 operator: opVal.op, 4992 value: opVal.val 4993 }, data); 4994 4995 parts.push(rule); 4996 } 4997 }); 4998 4999 /** 5000 * Modifies the group generated from the MongoDB expression 5001 * @event changer:mongoToGroup 5002 * @memberof module:plugins.MongoDbSupport 5003 * @param {object} group 5004 * @param {object} expression 5005 * @returns {object} 5006 */ 5007 return self.change('mongoToGroup', { 5008 condition: topKey.replace('$', '').toUpperCase(), 5009 rules: parts 5010 }, data); 5011 }(query, key)); 5012 }, 5013 5014 /** 5015 * Sets rules a from MongoDB query 5016 * @see module:plugins.MongoDbSupport.getRulesFromMongo 5017 */ 5018 setRulesFromMongo: function(query) { 5019 this.setRules(this.getRulesFromMongo(query)); 5020 }, 5021 5022 /** 5023 * Returns a filter identifier from the MongoDB field. 5024 * Automatically use the only one filter with a matching field, fires a changer otherwise. 5025 * @param {string} field 5026 * @param {*} value 5027 * @fires module:plugins.MongoDbSupport:changer:getMongoDBFieldID 5028 * @returns {string} 5029 * @private 5030 */ 5031 getMongoDBFieldID: function(field, value) { 5032 var matchingFilters = this.filters.filter(function(filter) { 5033 return filter.field === field; 5034 }); 5035 5036 var id; 5037 if (matchingFilters.length === 1) { 5038 id = matchingFilters[0].id; 5039 } 5040 else { 5041 /** 5042 * Returns a filter identifier from the MongoDB field 5043 * @event changer:getMongoDBFieldID 5044 * @memberof module:plugins.MongoDbSupport 5045 * @param {string} field 5046 * @param {*} value 5047 * @returns {string} 5048 */ 5049 id = this.change('getMongoDBFieldID', field, value); 5050 } 5051 5052 return id; 5053 }, 5054 5055 /** 5056 * Finds which operator is used in a MongoDB sub-object 5057 * @param {*} data 5058 * @returns {string|undefined} 5059 * @private 5060 */ 5061 getMongoOperator: function(data) { 5062 if (data !== null && typeof data === 'object') { 5063 if (data.$gte !== undefined && data.$lte !== undefined) { 5064 return 'between'; 5065 } 5066 if (data.$lt !== undefined && data.$gt !== undefined) { 5067 return 'not_between'; 5068 } 5069 5070 var knownKeys = Object.keys(data).filter(function(key) { 5071 return !!this.settings.mongoRuleOperators[key]; 5072 }.bind(this)); 5073 5074 if (knownKeys.length === 1) { 5075 return knownKeys[0]; 5076 } 5077 } 5078 else { 5079 return '$eq'; 5080 } 5081 }, 5082 5083 5084 /** 5085 * Returns the key corresponding to "$or" or "$and" 5086 * @param {object} data 5087 * @returns {string|undefined} 5088 * @private 5089 */ 5090 getMongoCondition: function(data) { 5091 var keys = Object.keys(data); 5092 5093 for (var i = 0, l = keys.length; i < l; i++) { 5094 if (keys[i].toLowerCase() === '$or' || keys[i].toLowerCase() === '$and') { 5095 return keys[i]; 5096 } 5097 } 5098 } 5099}); 5100 5101 5102/** 5103 * @class NotGroup 5104 * @memberof module:plugins 5105 * @description Adds a "Not" checkbox in front of group conditions. 5106 * @param {object} [options] 5107 * @param {string} [options.icon_checked='glyphicon glyphicon-checked'] 5108 * @param {string} [options.icon_unchecked='glyphicon glyphicon-unchecked'] 5109 */ 5110QueryBuilder.define('not-group', function(options) { 5111 var self = this; 5112 5113 // Bind events 5114 this.on('afterInit', function() { 5115 self.$el.on('click.queryBuilder', '[data-not=group]', function() { 5116 var $group = $(this).closest(QueryBuilder.selectors.group_container); 5117 var group = self.getModel($group); 5118 group.not = !group.not; 5119 }); 5120 5121 self.model.on('update', function(e, node, field) { 5122 if (node instanceof Group && field === 'not') { 5123 self.updateGroupNot(node); 5124 } 5125 }); 5126 }); 5127 5128 // Init "not" property 5129 this.on('afterAddGroup', function(e, group) { 5130 group.__.not = false; 5131 }); 5132 5133 // Modify templates 5134 if (!options.disable_template) { 5135 this.on('getGroupTemplate.filter', function(h) { 5136 var $h = $($.parseHTML(h.value)); 5137 $h.find(QueryBuilder.selectors.condition_container).prepend( 5138 '<button type="button" class="btn btn-xs btn-default" data-not="group">' + 5139 '<i class="' + options.icon_unchecked + '"></i> ' + self.translate('NOT') + 5140 '</button>' 5141 ); 5142 h.value = $h.prop('outerHTML'); 5143 }); 5144 } 5145 5146 // Export "not" to JSON 5147 this.on('groupToJson.filter', function(e, group) { 5148 e.value.not = group.not; 5149 }); 5150 5151 // Read "not" from JSON 5152 this.on('jsonToGroup.filter', function(e, json) { 5153 e.value.not = !!json.not; 5154 }); 5155 5156 // Export "not" to SQL 5157 this.on('groupToSQL.filter', function(e, group) { 5158 if (group.not) { 5159 e.value = 'NOT ( ' + e.value + ' )'; 5160 } 5161 }); 5162 5163 // Parse "NOT" function from sqlparser 5164 this.on('parseSQLNode.filter', function(e) { 5165 if (e.value.name && e.value.name.toUpperCase() == 'NOT') { 5166 e.value = e.value.arguments.value[0]; 5167 5168 // if the there is no sub-group, create one 5169 if (['AND', 'OR'].indexOf(e.value.operation.toUpperCase()) === -1) { 5170 e.value = new SQLParser.nodes.Op( 5171 self.settings.default_condition, 5172 e.value, 5173 null 5174 ); 5175 } 5176 5177 e.value.not = true; 5178 } 5179 }); 5180 5181 // Request to create sub-group if the "not" flag is set 5182 this.on('sqlGroupsDistinct.filter', function(e, group, data, i) { 5183 if (data.not && i > 0) { 5184 e.value = true; 5185 } 5186 }); 5187 5188 // Read "not" from parsed SQL 5189 this.on('sqlToGroup.filter', function(e, data) { 5190 e.value.not = !!data.not; 5191 }); 5192 5193 // Export "not" to Mongo 5194 this.on('groupToMongo.filter', function(e, group) { 5195 var key = '$' + group.condition.toLowerCase(); 5196 if (group.not && e.value[key]) { 5197 e.value = { '$nor': [e.value] }; 5198 } 5199 }); 5200 5201 // Parse "$nor" operator from Mongo 5202 this.on('parseMongoNode.filter', function(e) { 5203 var keys = Object.keys(e.value); 5204 5205 if (keys[0] == '$nor') { 5206 e.value = e.value[keys[0]][0]; 5207 e.value.not = true; 5208 } 5209 }); 5210 5211 // Read "not" from parsed Mongo 5212 this.on('mongoToGroup.filter', function(e, data) { 5213 e.value.not = !!data.not; 5214 }); 5215}, { 5216 icon_unchecked: 'glyphicon glyphicon-unchecked', 5217 icon_checked: 'glyphicon glyphicon-check', 5218 disable_template: false 5219}); 5220 5221/** 5222 * From {@link module:plugins.NotGroup} 5223 * @name not 5224 * @member {boolean} 5225 * @memberof Group 5226 * @instance 5227 */ 5228Utils.defineModelProperties(Group, ['not']); 5229 5230QueryBuilder.selectors.group_not = QueryBuilder.selectors.group_header + ' [data-not=group]'; 5231 5232QueryBuilder.extend(/** @lends module:plugins.NotGroup.prototype */ { 5233 /** 5234 * Performs actions when a group's not changes 5235 * @param {Group} group 5236 * @fires module:plugins.NotGroup.afterUpdateGroupNot 5237 * @private 5238 */ 5239 updateGroupNot: function(group) { 5240 var options = this.plugins['not-group']; 5241 group.$el.find('>' + QueryBuilder.selectors.group_not) 5242 .toggleClass('active', group.not) 5243 .find('i').attr('class', group.not ? options.icon_checked : options.icon_unchecked); 5244 5245 /** 5246 * After the group's not flag has been modified 5247 * @event afterUpdateGroupNot 5248 * @memberof module:plugins.NotGroup 5249 * @param {Group} group 5250 */ 5251 this.trigger('afterUpdateGroupNot', group); 5252 5253 this.trigger('rulesChanged'); 5254 } 5255}); 5256 5257 5258/** 5259 * @class Sortable 5260 * @memberof module:plugins 5261 * @description Enables drag & drop sort of rules. 5262 * @param {object} [options] 5263 * @param {boolean} [options.inherit_no_drop=true] 5264 * @param {boolean} [options.inherit_no_sortable=true] 5265 * @param {string} [options.icon='glyphicon glyphicon-sort'] 5266 * @throws MissingLibraryError, ConfigError 5267 */ 5268QueryBuilder.define('sortable', function(options) { 5269 if (!('interact' in window)) { 5270 Utils.error('MissingLibrary', 'interact.js is required to use "sortable" plugin. Get it here: http://interactjs.io'); 5271 } 5272 5273 if (options.default_no_sortable !== undefined) { 5274 Utils.error(false, 'Config', 'Sortable plugin : "default_no_sortable" options is deprecated, use standard "default_rule_flags" and "default_group_flags" instead'); 5275 this.settings.default_rule_flags.no_sortable = this.settings.default_group_flags.no_sortable = options.default_no_sortable; 5276 } 5277 5278 // recompute drop-zones during drag (when a rule is hidden) 5279 interact.dynamicDrop(true); 5280 5281 // set move threshold to 10px 5282 interact.pointerMoveTolerance(10); 5283 5284 var placeholder; 5285 var ghost; 5286 var src; 5287 var moved; 5288 5289 // Init drag and drop 5290 this.on('afterAddRule afterAddGroup', function(e, node) { 5291 if (node == placeholder) { 5292 return; 5293 } 5294 5295 var self = e.builder; 5296 5297 // Inherit flags 5298 if (options.inherit_no_sortable && node.parent && node.parent.flags.no_sortable) { 5299 node.flags.no_sortable = true; 5300 } 5301 if (options.inherit_no_drop && node.parent && node.parent.flags.no_drop) { 5302 node.flags.no_drop = true; 5303 } 5304 5305 // Configure drag 5306 if (!node.flags.no_sortable) { 5307 interact(node.$el[0]) 5308 .draggable({ 5309 allowFrom: QueryBuilder.selectors.drag_handle, 5310 onstart: function(event) { 5311 moved = false; 5312 5313 // get model of dragged element 5314 src = self.getModel(event.target); 5315 5316 // create ghost 5317 ghost = src.$el.clone() 5318 .appendTo(src.$el.parent()) 5319 .width(src.$el.outerWidth()) 5320 .addClass('dragging'); 5321 5322 // create drop placeholder 5323 var ph = $($.parseHTML('<div class="rule-placeholder"> </div>')) 5324 .height(src.$el.outerHeight()); 5325 5326 placeholder = src.parent.addRule(ph, src.getPos()); 5327 5328 // hide dragged element 5329 src.$el.hide(); 5330 }, 5331 onmove: function(event) { 5332 // make the ghost follow the cursor 5333 ghost[0].style.top = event.clientY - 15 + 'px'; 5334 ghost[0].style.left = event.clientX - 15 + 'px'; 5335 }, 5336 onend: function(event) { 5337 // starting from Interact 1.3.3, onend is called before ondrop 5338 if (event.dropzone) { 5339 moveSortableToTarget(src, $(event.relatedTarget), self); 5340 moved = true; 5341 } 5342 5343 // remove ghost 5344 ghost.remove(); 5345 ghost = undefined; 5346 5347 // remove placeholder 5348 placeholder.drop(); 5349 placeholder = undefined; 5350 5351 // show element 5352 src.$el.css('display', ''); 5353 5354 /** 5355 * After a node has been moved with {@link module:plugins.Sortable} 5356 * @event afterMove 5357 * @memberof module:plugins.Sortable 5358 * @param {Node} node 5359 */ 5360 self.trigger('afterMove', src); 5361 5362 self.trigger('rulesChanged'); 5363 } 5364 }); 5365 } 5366 5367 if (!node.flags.no_drop) { 5368 // Configure drop on groups and rules 5369 interact(node.$el[0]) 5370 .dropzone({ 5371 accept: QueryBuilder.selectors.rule_and_group_containers, 5372 ondragenter: function(event) { 5373 moveSortableToTarget(placeholder, $(event.target), self); 5374 }, 5375 ondrop: function(event) { 5376 if (!moved) { 5377 moveSortableToTarget(src, $(event.target), self); 5378 } 5379 } 5380 }); 5381 5382 // Configure drop on group headers 5383 if (node instanceof Group) { 5384 interact(node.$el.find(QueryBuilder.selectors.group_header)[0]) 5385 .dropzone({ 5386 accept: QueryBuilder.selectors.rule_and_group_containers, 5387 ondragenter: function(event) { 5388 moveSortableToTarget(placeholder, $(event.target), self); 5389 }, 5390 ondrop: function(event) { 5391 if (!moved) { 5392 moveSortableToTarget(src, $(event.target), self); 5393 } 5394 } 5395 }); 5396 } 5397 } 5398 }); 5399 5400 // Detach interactables 5401 this.on('beforeDeleteRule beforeDeleteGroup', function(e, node) { 5402 if (!e.isDefaultPrevented()) { 5403 interact(node.$el[0]).unset(); 5404 5405 if (node instanceof Group) { 5406 interact(node.$el.find(QueryBuilder.selectors.group_header)[0]).unset(); 5407 } 5408 } 5409 }); 5410 5411 // Remove drag handle from non-sortable items 5412 this.on('afterApplyRuleFlags afterApplyGroupFlags', function(e, node) { 5413 if (node.flags.no_sortable) { 5414 node.$el.find('.drag-handle').remove(); 5415 } 5416 }); 5417 5418 // Modify templates 5419 if (!options.disable_template) { 5420 this.on('getGroupTemplate.filter', function(h, level) { 5421 if (level > 1) { 5422 var $h = $($.parseHTML(h.value)); 5423 $h.find(QueryBuilder.selectors.condition_container).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>'); 5424 h.value = $h.prop('outerHTML'); 5425 } 5426 }); 5427 5428 this.on('getRuleTemplate.filter', function(h) { 5429 var $h = $($.parseHTML(h.value)); 5430 $h.find(QueryBuilder.selectors.rule_header).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>'); 5431 h.value = $h.prop('outerHTML'); 5432 }); 5433 } 5434}, { 5435 inherit_no_sortable: true, 5436 inherit_no_drop: true, 5437 icon: 'glyphicon glyphicon-sort', 5438 disable_template: false 5439}); 5440 5441QueryBuilder.selectors.rule_and_group_containers = QueryBuilder.selectors.rule_container + ', ' + QueryBuilder.selectors.group_container; 5442QueryBuilder.selectors.drag_handle = '.drag-handle'; 5443 5444QueryBuilder.defaults({ 5445 default_rule_flags: { 5446 no_sortable: false, 5447 no_drop: false 5448 }, 5449 default_group_flags: { 5450 no_sortable: false, 5451 no_drop: false 5452 } 5453}); 5454 5455/** 5456 * Moves an element (placeholder or actual object) depending on active target 5457 * @memberof module:plugins.Sortable 5458 * @param {Node} node 5459 * @param {jQuery} target 5460 * @param {QueryBuilder} [builder] 5461 * @private 5462 */ 5463function moveSortableToTarget(node, target, builder) { 5464 var parent, method; 5465 var Selectors = QueryBuilder.selectors; 5466 5467 // on rule 5468 parent = target.closest(Selectors.rule_container); 5469 if (parent.length) { 5470 method = 'moveAfter'; 5471 } 5472 5473 // on group header 5474 if (!method) { 5475 parent = target.closest(Selectors.group_header); 5476 if (parent.length) { 5477 parent = target.closest(Selectors.group_container); 5478 method = 'moveAtBegin'; 5479 } 5480 } 5481 5482 // on group 5483 if (!method) { 5484 parent = target.closest(Selectors.group_container); 5485 if (parent.length) { 5486 method = 'moveAtEnd'; 5487 } 5488 } 5489 5490 if (method) { 5491 node[method](builder.getModel(parent)); 5492 5493 // refresh radio value 5494 if (builder && node instanceof Rule) { 5495 builder.setRuleInputValue(node, node.value); 5496 } 5497 } 5498} 5499 5500 5501/** 5502 * @class SqlSupport 5503 * @memberof module:plugins 5504 * @description Allows to export rules as a SQL WHERE statement as well as populating the builder from an SQL query. 5505 * @param {object} [options] 5506 * @param {boolean} [options.boolean_as_integer=true] - `true` to convert boolean values to integer in the SQL output 5507 */ 5508QueryBuilder.define('sql-support', function(options) { 5509 5510}, { 5511 boolean_as_integer: true 5512}); 5513 5514QueryBuilder.defaults({ 5515 // operators for internal -> SQL conversion 5516 sqlOperators: { 5517 equal: { op: '= ?' }, 5518 not_equal: { op: '!= ?' }, 5519 in: { op: 'IN(?)', sep: ', ' }, 5520 not_in: { op: 'NOT IN(?)', sep: ', ' }, 5521 less: { op: '< ?' }, 5522 less_or_equal: { op: '<= ?' }, 5523 greater: { op: '> ?' }, 5524 greater_or_equal: { op: '>= ?' }, 5525 between: { op: 'BETWEEN ?', sep: ' AND ' }, 5526 not_between: { op: 'NOT BETWEEN ?', sep: ' AND ' }, 5527 begins_with: { op: 'LIKE ?', mod: '{0}%', escape: '%_' }, 5528 not_begins_with: { op: 'NOT LIKE ?', mod: '{0}%', escape: '%_' }, 5529 contains: { op: 'LIKE ?', mod: '%{0}%', escape: '%_' }, 5530 not_contains: { op: 'NOT LIKE ?', mod: '%{0}%', escape: '%_' }, 5531 ends_with: { op: 'LIKE ?', mod: '%{0}', escape: '%_' }, 5532 not_ends_with: { op: 'NOT LIKE ?', mod: '%{0}', escape: '%_' }, 5533 is_empty: { op: '= \'\'' }, 5534 is_not_empty: { op: '!= \'\'' }, 5535 is_null: { op: 'IS NULL' }, 5536 is_not_null: { op: 'IS NOT NULL' } 5537 }, 5538 5539 // operators for SQL -> internal conversion 5540 sqlRuleOperator: { 5541 '=': function(v) { 5542 return { 5543 val: v, 5544 op: v === '' ? 'is_empty' : 'equal' 5545 }; 5546 }, 5547 '!=': function(v) { 5548 return { 5549 val: v, 5550 op: v === '' ? 'is_not_empty' : 'not_equal' 5551 }; 5552 }, 5553 'LIKE': function(v) { 5554 if (v.slice(0, 1) == '%' && v.slice(-1) == '%') { 5555 return { 5556 val: v.slice(1, -1), 5557 op: 'contains' 5558 }; 5559 } 5560 else if (v.slice(0, 1) == '%') { 5561 return { 5562 val: v.slice(1), 5563 op: 'ends_with' 5564 }; 5565 } 5566 else if (v.slice(-1) == '%') { 5567 return { 5568 val: v.slice(0, -1), 5569 op: 'begins_with' 5570 }; 5571 } 5572 else { 5573 Utils.error('SQLParse', 'Invalid value for LIKE operator "{0}"', v); 5574 } 5575 }, 5576 'NOT LIKE': function(v) { 5577 if (v.slice(0, 1) == '%' && v.slice(-1) == '%') { 5578 return { 5579 val: v.slice(1, -1), 5580 op: 'not_contains' 5581 }; 5582 } 5583 else if (v.slice(0, 1) == '%') { 5584 return { 5585 val: v.slice(1), 5586 op: 'not_ends_with' 5587 }; 5588 } 5589 else if (v.slice(-1) == '%') { 5590 return { 5591 val: v.slice(0, -1), 5592 op: 'not_begins_with' 5593 }; 5594 } 5595 else { 5596 Utils.error('SQLParse', 'Invalid value for NOT LIKE operator "{0}"', v); 5597 } 5598 }, 5599 'IN': function(v) { 5600 return { val: v, op: 'in' }; 5601 }, 5602 'NOT IN': function(v) { 5603 return { val: v, op: 'not_in' }; 5604 }, 5605 '<': function(v) { 5606 return { val: v, op: 'less' }; 5607 }, 5608 '<=': function(v) { 5609 return { val: v, op: 'less_or_equal' }; 5610 }, 5611 '>': function(v) { 5612 return { val: v, op: 'greater' }; 5613 }, 5614 '>=': function(v) { 5615 return { val: v, op: 'greater_or_equal' }; 5616 }, 5617 'BETWEEN': function(v) { 5618 return { val: v, op: 'between' }; 5619 }, 5620 'NOT BETWEEN': function(v) { 5621 return { val: v, op: 'not_between' }; 5622 }, 5623 'IS': function(v) { 5624 if (v !== null) { 5625 Utils.error('SQLParse', 'Invalid value for IS operator'); 5626 } 5627 return { val: null, op: 'is_null' }; 5628 }, 5629 'IS NOT': function(v) { 5630 if (v !== null) { 5631 Utils.error('SQLParse', 'Invalid value for IS operator'); 5632 } 5633 return { val: null, op: 'is_not_null' }; 5634 } 5635 }, 5636 5637 // statements for internal -> SQL conversion 5638 sqlStatements: { 5639 'question_mark': function() { 5640 var params = []; 5641 return { 5642 add: function(rule, value) { 5643 params.push(value); 5644 return '?'; 5645 }, 5646 run: function() { 5647 return params; 5648 } 5649 }; 5650 }, 5651 5652 'numbered': function(char) { 5653 if (!char || char.length > 1) char = '$'; 5654 var index = 0; 5655 var params = []; 5656 return { 5657 add: function(rule, value) { 5658 params.push(value); 5659 index++; 5660 return char + index; 5661 }, 5662 run: function() { 5663 return params; 5664 } 5665 }; 5666 }, 5667 5668 'named': function(char) { 5669 if (!char || char.length > 1) char = ':'; 5670 var indexes = {}; 5671 var params = {}; 5672 return { 5673 add: function(rule, value) { 5674 if (!indexes[rule.field]) indexes[rule.field] = 1; 5675 var key = rule.field + '_' + (indexes[rule.field]++); 5676 params[key] = value; 5677 return char + key; 5678 }, 5679 run: function() { 5680 return params; 5681 } 5682 }; 5683 } 5684 }, 5685 5686 // statements for SQL -> internal conversion 5687 sqlRuleStatement: { 5688 'question_mark': function(values) { 5689 var index = 0; 5690 return { 5691 parse: function(v) { 5692 return v == '?' ? values[index++] : v; 5693 }, 5694 esc: function(sql) { 5695 return sql.replace(/\?/g, '\'?\''); 5696 } 5697 }; 5698 }, 5699 5700 'numbered': function(values, char) { 5701 if (!char || char.length > 1) char = '$'; 5702 var regex1 = new RegExp('^\\' + char + '[0-9]+$'); 5703 var regex2 = new RegExp('\\' + char + '([0-9]+)', 'g'); 5704 return { 5705 parse: function(v) { 5706 return regex1.test(v) ? values[v.slice(1) - 1] : v; 5707 }, 5708 esc: function(sql) { 5709 return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\''); 5710 } 5711 }; 5712 }, 5713 5714 'named': function(values, char) { 5715 if (!char || char.length > 1) char = ':'; 5716 var regex1 = new RegExp('^\\' + char); 5717 var regex2 = new RegExp('\\' + char + '(' + Object.keys(values).join('|') + ')\\b', 'g'); 5718 return { 5719 parse: function(v) { 5720 return regex1.test(v) ? values[v.slice(1)] : v; 5721 }, 5722 esc: function(sql) { 5723 return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\''); 5724 } 5725 }; 5726 } 5727 } 5728}); 5729 5730/** 5731 * @typedef {object} SqlQuery 5732 * @memberof module:plugins.SqlSupport 5733 * @property {string} sql 5734 * @property {object} params 5735 */ 5736 5737QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ { 5738 /** 5739 * Returns rules as a SQL query 5740 * @param {boolean|string} [stmt] - use prepared statements: false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)' 5741 * @param {boolean} [nl=false] output with new lines 5742 * @param {object} [data] - current rules by default 5743 * @returns {module:plugins.SqlSupport.SqlQuery} 5744 * @fires module:plugins.SqlSupport.changer:getSQLField 5745 * @fires module:plugins.SqlSupport.changer:ruleToSQL 5746 * @fires module:plugins.SqlSupport.changer:groupToSQL 5747 * @throws UndefinedSQLConditionError, UndefinedSQLOperatorError 5748 */ 5749 getSQL: function(stmt, nl, data) { 5750 data = (data === undefined) ? this.getRules() : data; 5751 5752 if (!data) { 5753 return null; 5754 } 5755 5756 nl = !!nl ? '\n' : ' '; 5757 var boolean_as_integer = this.getPluginOptions('sql-support', 'boolean_as_integer'); 5758 5759 if (stmt === true) { 5760 stmt = 'question_mark'; 5761 } 5762 if (typeof stmt == 'string') { 5763 var config = getStmtConfig(stmt); 5764 stmt = this.settings.sqlStatements[config[1]](config[2]); 5765 } 5766 5767 var self = this; 5768 5769 var sql = (function parse(group) { 5770 if (!group.condition) { 5771 group.condition = self.settings.default_condition; 5772 } 5773 if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) { 5774 Utils.error('UndefinedSQLCondition', 'Unable to build SQL query with condition "{0}"', group.condition); 5775 } 5776 5777 if (!group.rules) { 5778 return ''; 5779 } 5780 5781 var parts = []; 5782 5783 group.rules.forEach(function(rule) { 5784 if (rule.rules && rule.rules.length > 0) { 5785 parts.push('(' + nl + parse(rule) + nl + ')' + nl); 5786 } 5787 else { 5788 var sql = self.settings.sqlOperators[rule.operator]; 5789 var ope = self.getOperatorByType(rule.operator); 5790 var value = ''; 5791 5792 if (sql === undefined) { 5793 Utils.error('UndefinedSQLOperator', 'Unknown SQL operation for operator "{0}"', rule.operator); 5794 } 5795 5796 if (ope.nb_inputs !== 0) { 5797 if (!(rule.value instanceof Array)) { 5798 rule.value = [rule.value]; 5799 } 5800 5801 rule.value.forEach(function(v, i) { 5802 if (i > 0) { 5803 value += sql.sep; 5804 } 5805 5806 if (rule.type == 'boolean' && boolean_as_integer) { 5807 v = v ? 1 : 0; 5808 } 5809 else if (!stmt && rule.type !== 'integer' && rule.type !== 'double' && rule.type !== 'boolean') { 5810 v = Utils.escapeString(v, sql.escape); 5811 } 5812 5813 if (sql.mod) { 5814 v = Utils.fmt(sql.mod, v); 5815 } 5816 5817 if (stmt) { 5818 value += stmt.add(rule, v); 5819 } 5820 else { 5821 if (typeof v == 'string') { 5822 v = '\'' + v + '\''; 5823 } 5824 5825 value += v; 5826 } 5827 }); 5828 } 5829 5830 var sqlFn = function(v) { 5831 return sql.op.replace('?', function() { 5832 return v; 5833 }); 5834 }; 5835 5836 /** 5837 * Modifies the SQL field used by a rule 5838 * @event changer:getSQLField 5839 * @memberof module:plugins.SqlSupport 5840 * @param {string} field 5841 * @param {Rule} rule 5842 * @returns {string} 5843 */ 5844 var field = self.change('getSQLField', rule.field, rule); 5845 5846 var ruleExpression = field + ' ' + sqlFn(value); 5847 5848 /** 5849 * Modifies the SQL generated for a rule 5850 * @event changer:ruleToSQL 5851 * @memberof module:plugins.SqlSupport 5852 * @param {string} expression 5853 * @param {Rule} rule 5854 * @param {*} value 5855 * @param {function} valueWrapper - function that takes the value and adds the operator 5856 * @returns {string} 5857 */ 5858 parts.push(self.change('ruleToSQL', ruleExpression, rule, value, sqlFn)); 5859 } 5860 }); 5861 5862 var groupExpression = parts.join(' ' + group.condition + nl); 5863 5864 /** 5865 * Modifies the SQL generated for a group 5866 * @event changer:groupToSQL 5867 * @memberof module:plugins.SqlSupport 5868 * @param {string} expression 5869 * @param {Group} group 5870 * @returns {string} 5871 */ 5872 return self.change('groupToSQL', groupExpression, group); 5873 }(data)); 5874 5875 if (stmt) { 5876 return { 5877 sql: sql, 5878 params: stmt.run() 5879 }; 5880 } 5881 else { 5882 return { 5883 sql: sql 5884 }; 5885 } 5886 }, 5887 5888 /** 5889 * Convert a SQL query to rules 5890 * @param {string|module:plugins.SqlSupport.SqlQuery} query 5891 * @param {boolean|string} stmt 5892 * @returns {object} 5893 * @fires module:plugins.SqlSupport.changer:parseSQLNode 5894 * @fires module:plugins.SqlSupport.changer:getSQLFieldID 5895 * @fires module:plugins.SqlSupport.changer:sqlToRule 5896 * @fires module:plugins.SqlSupport.changer:sqlToGroup 5897 * @throws MissingLibraryError, SQLParseError, UndefinedSQLOperatorError 5898 */ 5899 getRulesFromSQL: function(query, stmt) { 5900 if (!('SQLParser' in window)) { 5901 Utils.error('MissingLibrary', 'SQLParser is required to parse SQL queries. Get it here https://github.com/mistic100/sql-parser'); 5902 } 5903 5904 var self = this; 5905 5906 if (typeof query == 'string') { 5907 query = { sql: query }; 5908 } 5909 5910 if (stmt === true) stmt = 'question_mark'; 5911 if (typeof stmt == 'string') { 5912 var config = getStmtConfig(stmt); 5913 stmt = this.settings.sqlRuleStatement[config[1]](query.params, config[2]); 5914 } 5915 5916 if (stmt) { 5917 query.sql = stmt.esc(query.sql); 5918 } 5919 5920 if (query.sql.toUpperCase().indexOf('SELECT') !== 0) { 5921 query.sql = 'SELECT * FROM table WHERE ' + query.sql; 5922 } 5923 5924 var parsed = SQLParser.parse(query.sql); 5925 5926 if (!parsed.where) { 5927 Utils.error('SQLParse', 'No WHERE clause found'); 5928 } 5929 5930 /** 5931 * 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 5932 * @event changer:parseSQLNode 5933 * @memberof module:plugins.SqlSupport 5934 * @param {object} AST node 5935 * @returns {object} tree, rule or group 5936 */ 5937 var data = self.change('parseSQLNode', parsed.where.conditions); 5938 5939 // a plugin returned a group 5940 if ('rules' in data && 'condition' in data) { 5941 return data; 5942 } 5943 5944 // a plugin returned a rule 5945 if ('id' in data && 'operator' in data && 'value' in data) { 5946 return { 5947 condition: this.settings.default_condition, 5948 rules: [data] 5949 }; 5950 } 5951 5952 // create root group 5953 var out = self.change('sqlToGroup', { 5954 condition: this.settings.default_condition, 5955 rules: [] 5956 }, data); 5957 5958 // keep track of current group 5959 var curr = out; 5960 5961 (function flatten(data, i) { 5962 if (data === null) { 5963 return; 5964 } 5965 5966 // allow plugins to manually parse or handle special cases 5967 data = self.change('parseSQLNode', data); 5968 5969 // a plugin returned a group 5970 if ('rules' in data && 'condition' in data) { 5971 curr.rules.push(data); 5972 return; 5973 } 5974 5975 // a plugin returned a rule 5976 if ('id' in data && 'operator' in data && 'value' in data) { 5977 curr.rules.push(data); 5978 return; 5979 } 5980 5981 // data must be a SQL parser node 5982 if (!('left' in data) || !('right' in data) || !('operation' in data)) { 5983 Utils.error('SQLParse', 'Unable to parse WHERE clause'); 5984 } 5985 5986 // it's a node 5987 if (['AND', 'OR'].indexOf(data.operation.toUpperCase()) !== -1) { 5988 // create a sub-group if the condition is not the same and it's not the first level 5989 5990 /** 5991 * Given an existing group and an AST node, determines if a sub-group must be created 5992 * @event changer:sqlGroupsDistinct 5993 * @memberof module:plugins.SqlSupport 5994 * @param {boolean} create - true by default if the group condition is different 5995 * @param {object} group 5996 * @param {object} AST 5997 * @param {int} current group level 5998 * @returns {boolean} 5999 */ 6000 var createGroup = self.change('sqlGroupsDistinct', i > 0 && curr.condition != data.operation.toUpperCase(), curr, data, i); 6001 6002 if (createGroup) { 6003 /** 6004 * Modifies the group generated from the SQL expression (this is called before the group is filled with rules) 6005 * @event changer:sqlToGroup 6006 * @memberof module:plugins.SqlSupport 6007 * @param {object} group 6008 * @param {object} AST 6009 * @returns {object} 6010 */ 6011 var group = self.change('sqlToGroup', { 6012 condition: self.settings.default_condition, 6013 rules: [] 6014 }, data); 6015 6016 curr.rules.push(group); 6017 curr = group; 6018 } 6019 6020 curr.condition = data.operation.toUpperCase(); 6021 i++; 6022 6023 // some magic ! 6024 var next = curr; 6025 flatten(data.left, i); 6026 6027 curr = next; 6028 flatten(data.right, i); 6029 } 6030 // it's a leaf 6031 else { 6032 if ($.isPlainObject(data.right.value)) { 6033 Utils.error('SQLParse', 'Value format not supported for {0}.', data.left.value); 6034 } 6035 6036 // convert array 6037 var value; 6038 if ($.isArray(data.right.value)) { 6039 value = data.right.value.map(function(v) { 6040 return v.value; 6041 }); 6042 } 6043 else { 6044 value = data.right.value; 6045 } 6046 6047 // get actual values 6048 if (stmt) { 6049 if ($.isArray(value)) { 6050 value = value.map(stmt.parse); 6051 } 6052 else { 6053 value = stmt.parse(value); 6054 } 6055 } 6056 6057 // convert operator 6058 var operator = data.operation.toUpperCase(); 6059 if (operator == '<>') { 6060 operator = '!='; 6061 } 6062 6063 var sqlrl = self.settings.sqlRuleOperator[operator]; 6064 if (sqlrl === undefined) { 6065 Utils.error('UndefinedSQLOperator', 'Invalid SQL operation "{0}".', data.operation); 6066 } 6067 6068 var opVal = sqlrl.call(this, value, data.operation); 6069 6070 // find field name 6071 var field; 6072 if ('values' in data.left) { 6073 field = data.left.values.join('.'); 6074 } 6075 else if ('value' in data.left) { 6076 field = data.left.value; 6077 } 6078 else { 6079 Utils.error('SQLParse', 'Cannot find field name in {0}', JSON.stringify(data.left)); 6080 } 6081 6082 // unescape chars declared by the operator 6083 var finalValue = opVal.val; 6084 var sql = self.settings.sqlOperators[opVal.op]; 6085 if (!stmt && sql && sql.escape) { 6086 var searchChars = sql.escape.split('').map(function(c) { 6087 return '\\\\' + c; 6088 }).join('|'); 6089 finalValue = finalValue 6090 .replace(new RegExp('(' + searchChars + ')', 'g'), function(s) { 6091 return s[1]; 6092 }); 6093 } 6094 6095 var id = self.getSQLFieldID(field, value); 6096 6097 /** 6098 * Modifies the rule generated from the SQL expression 6099 * @event changer:sqlToRule 6100 * @memberof module:plugins.SqlSupport 6101 * @param {object} rule 6102 * @param {object} AST 6103 * @returns {object} 6104 */ 6105 var rule = self.change('sqlToRule', { 6106 id: id, 6107 field: field, 6108 operator: opVal.op, 6109 value: finalValue 6110 }, data); 6111 6112 curr.rules.push(rule); 6113 } 6114 }(data, 0)); 6115 6116 return out; 6117 }, 6118 6119 /** 6120 * Sets the builder's rules from a SQL query 6121 * @see module:plugins.SqlSupport.getRulesFromSQL 6122 */ 6123 setRulesFromSQL: function(query, stmt) { 6124 this.setRules(this.getRulesFromSQL(query, stmt)); 6125 }, 6126 6127 /** 6128 * Returns a filter identifier from the SQL field. 6129 * Automatically use the only one filter with a matching field, fires a changer otherwise. 6130 * @param {string} field 6131 * @param {*} value 6132 * @fires module:plugins.SqlSupport:changer:getSQLFieldID 6133 * @returns {string} 6134 * @private 6135 */ 6136 getSQLFieldID: function(field, value) { 6137 var matchingFilters = this.filters.filter(function(filter) { 6138 return filter.field.toLowerCase() === field.toLowerCase(); 6139 }); 6140 6141 var id; 6142 if (matchingFilters.length === 1) { 6143 id = matchingFilters[0].id; 6144 } 6145 else { 6146 /** 6147 * Returns a filter identifier from the SQL field 6148 * @event changer:getSQLFieldID 6149 * @memberof module:plugins.SqlSupport 6150 * @param {string} field 6151 * @param {*} value 6152 * @returns {string} 6153 */ 6154 id = this.change('getSQLFieldID', field, value); 6155 } 6156 6157 return id; 6158 } 6159}); 6160 6161/** 6162 * Parses the statement configuration 6163 * @memberof module:plugins.SqlSupport 6164 * @param {string} stmt 6165 * @returns {Array} null, mode, option 6166 * @private 6167 */ 6168function getStmtConfig(stmt) { 6169 var config = stmt.match(/(question_mark|numbered|named)(?:\((.)\))?/); 6170 if (!config) config = [null, 'question_mark', undefined]; 6171 return config; 6172} 6173 6174 6175/** 6176 * @class UniqueFilter 6177 * @memberof module:plugins 6178 * @description Allows to define some filters as "unique": ie which can be used for only one rule, globally or in the same group. 6179 */ 6180QueryBuilder.define('unique-filter', function() { 6181 this.status.used_filters = {}; 6182 6183 this.on('afterUpdateRuleFilter', this.updateDisabledFilters); 6184 this.on('afterDeleteRule', this.updateDisabledFilters); 6185 this.on('afterCreateRuleFilters', this.applyDisabledFilters); 6186 this.on('afterReset', this.clearDisabledFilters); 6187 this.on('afterClear', this.clearDisabledFilters); 6188 6189 // Ensure that the default filter is not already used if unique 6190 this.on('getDefaultFilter.filter', function(e, model) { 6191 var self = e.builder; 6192 6193 self.updateDisabledFilters(); 6194 6195 if (e.value.id in self.status.used_filters) { 6196 var found = self.filters.some(function(filter) { 6197 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) { 6198 e.value = filter; 6199 return true; 6200 } 6201 }); 6202 6203 if (!found) { 6204 Utils.error(false, 'UniqueFilter', 'No more non-unique filters available'); 6205 e.value = undefined; 6206 } 6207 } 6208 }); 6209}); 6210 6211QueryBuilder.extend(/** @lends module:plugins.UniqueFilter.prototype */ { 6212 /** 6213 * Updates the list of used filters 6214 * @param {$.Event} [e] 6215 * @private 6216 */ 6217 updateDisabledFilters: function(e) { 6218 var self = e ? e.builder : this; 6219 6220 self.status.used_filters = {}; 6221 6222 if (!self.model) { 6223 return; 6224 } 6225 6226 // get used filters 6227 (function walk(group) { 6228 group.each(function(rule) { 6229 if (rule.filter && rule.filter.unique) { 6230 if (!self.status.used_filters[rule.filter.id]) { 6231 self.status.used_filters[rule.filter.id] = []; 6232 } 6233 if (rule.filter.unique == 'group') { 6234 self.status.used_filters[rule.filter.id].push(rule.parent); 6235 } 6236 } 6237 }, function(group) { 6238 walk(group); 6239 }); 6240 }(self.model.root)); 6241 6242 self.applyDisabledFilters(e); 6243 }, 6244 6245 /** 6246 * Clear the list of used filters 6247 * @param {$.Event} [e] 6248 * @private 6249 */ 6250 clearDisabledFilters: function(e) { 6251 var self = e ? e.builder : this; 6252 6253 self.status.used_filters = {}; 6254 6255 self.applyDisabledFilters(e); 6256 }, 6257 6258 /** 6259 * Disabled filters depending on the list of used ones 6260 * @param {$.Event} [e] 6261 * @private 6262 */ 6263 applyDisabledFilters: function(e) { 6264 var self = e ? e.builder : this; 6265 6266 // re-enable everything 6267 self.$el.find(QueryBuilder.selectors.filter_container + ' option').prop('disabled', false); 6268 6269 // disable some 6270 $.each(self.status.used_filters, function(filterId, groups) { 6271 if (groups.length === 0) { 6272 self.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true); 6273 } 6274 else { 6275 groups.forEach(function(group) { 6276 group.each(function(rule) { 6277 rule.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true); 6278 }); 6279 }); 6280 } 6281 }); 6282 6283 // update Selectpicker 6284 if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) { 6285 self.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render'); 6286 } 6287 } 6288}); 6289 6290 6291/*! 6292 * jQuery QueryBuilder 2.7.0 6293 * Locale: English (en) 6294 * Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr 6295 * Licensed under MIT (https://opensource.org/licenses/MIT) 6296 */ 6297 6298QueryBuilder.regional['en'] = { 6299 "__locale": "English (en)", 6300 "__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr", 6301 "add_rule": "Add rule", 6302 "add_group": "Add group", 6303 "delete_rule": "Delete", 6304 "delete_group": "Delete", 6305 "conditions": { 6306 "AND": "AND", 6307 "OR": "OR" 6308 }, 6309 "operators": { 6310 "equal": "equal", 6311 "not_equal": "not equal", 6312 "in": "in", 6313 "not_in": "not in", 6314 "less": "less", 6315 "less_or_equal": "less or equal", 6316 "greater": "greater", 6317 "greater_or_equal": "greater or equal", 6318 "between": "between", 6319 "not_between": "not between", 6320 "begins_with": "begins with", 6321 "not_begins_with": "doesn't begin with", 6322 "contains": "contains", 6323 "not_contains": "doesn't contain", 6324 "ends_with": "ends with", 6325 "not_ends_with": "doesn't end with", 6326 "is_empty": "is empty", 6327 "is_not_empty": "is not empty", 6328 "is_null": "is null", 6329 "is_not_null": "is not null" 6330 }, 6331 "errors": { 6332 "no_filter": "No filter selected", 6333 "empty_group": "The group is empty", 6334 "radio_empty": "No value selected", 6335 "checkbox_empty": "No value selected", 6336 "select_empty": "No value selected", 6337 "string_empty": "Empty value", 6338 "string_exceed_min_length": "Must contain at least {0} characters", 6339 "string_exceed_max_length": "Must not contain more than {0} characters", 6340 "string_invalid_format": "Invalid format ({0})", 6341 "number_nan": "Not a number", 6342 "number_not_integer": "Not an integer", 6343 "number_not_double": "Not a real number", 6344 "number_exceed_min": "Must be greater than {0}", 6345 "number_exceed_max": "Must be lower than {0}", 6346 "number_wrong_step": "Must be a multiple of {0}", 6347 "number_between_invalid": "Invalid values, {0} is greater than {1}", 6348 "datetime_empty": "Empty value", 6349 "datetime_invalid": "Invalid date format ({0})", 6350 "datetime_exceed_min": "Must be after {0}", 6351 "datetime_exceed_max": "Must be before {0}", 6352 "datetime_between_invalid": "Invalid values, {0} is greater than {1}", 6353 "boolean_not_valid": "Not a boolean", 6354 "operator_not_multiple": "Operator \"{1}\" cannot accept multiple values" 6355 }, 6356 "invert": "Invert", 6357 "NOT": "NOT" 6358}; 6359 6360QueryBuilder.defaults({ lang_code: 'en' }); 6361return QueryBuilder; 6362 6363}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.