UNPKG

21.2 kBJavaScriptView Raw
1/**
2 * Performs value validation
3 * @param {Rule} rule
4 * @param {string|string[]} value
5 * @returns {array|boolean} true or error array
6 * @fires QueryBuilder.changer:validateValue
7 */
8QueryBuilder.prototype.validateValue = function(rule, value) {
9 var validation = rule.filter.validation || {};
10 var result = true;
11
12 if (validation.callback) {
13 result = validation.callback.call(this, value, rule);
14 }
15 else {
16 result = this._validateValue(rule, value);
17 }
18
19 /**
20 * Modifies the result of the rule validation method
21 * @event changer:validateValue
22 * @memberof QueryBuilder
23 * @param {array|boolean} result - true or an error array
24 * @param {*} value
25 * @param {Rule} rule
26 * @returns {array|boolean}
27 */
28 return this.change('validateValue', result, value, rule);
29};
30
31/**
32 * Default validation function
33 * @param {Rule} rule
34 * @param {string|string[]} value
35 * @returns {array|boolean} true or error array
36 * @throws ConfigError
37 * @private
38 */
39QueryBuilder.prototype._validateValue = function(rule, value) {
40 var filter = rule.filter;
41 var operator = rule.operator;
42 var validation = filter.validation || {};
43 var result = true;
44 var tmp, tempValue;
45
46 if (rule.operator.nb_inputs === 1) {
47 value = [value];
48 }
49
50 for (var i = 0; i < operator.nb_inputs; i++) {
51 if (!operator.multiple && $.isArray(value[i]) && value[i].length > 1) {
52 result = ['operator_not_multiple', operator.type, this.translate('operators', operator.type)];
53 break;
54 }
55
56 switch (filter.input) {
57 case 'radio':
58 if (value[i] === undefined || value[i].length === 0) {
59 if (!validation.allow_empty_value) {
60 result = ['radio_empty'];
61 }
62 break;
63 }
64 break;
65
66 case 'checkbox':
67 if (value[i] === undefined || value[i].length === 0) {
68 if (!validation.allow_empty_value) {
69 result = ['checkbox_empty'];
70 }
71 break;
72 }
73 break;
74
75 case 'select':
76 if (value[i] === undefined || value[i].length === 0 || (filter.placeholder && value[i] == filter.placeholder_value)) {
77 if (!validation.allow_empty_value) {
78 result = ['select_empty'];
79 }
80 break;
81 }
82 break;
83
84 default:
85 tempValue = $.isArray(value[i]) ? value[i] : [value[i]];
86
87 for (var j = 0; j < tempValue.length; j++) {
88 switch (QueryBuilder.types[filter.type]) {
89 case 'string':
90 if (tempValue[j] === undefined || tempValue[j].length === 0) {
91 if (!validation.allow_empty_value) {
92 result = ['string_empty'];
93 }
94 break;
95 }
96 if (validation.min !== undefined) {
97 if (tempValue[j].length < parseInt(validation.min)) {
98 result = [this.getValidationMessage(validation, 'min', 'string_exceed_min_length'), validation.min];
99 break;
100 }
101 }
102 if (validation.max !== undefined) {
103 if (tempValue[j].length > parseInt(validation.max)) {
104 result = [this.getValidationMessage(validation, 'max', 'string_exceed_max_length'), validation.max];
105 break;
106 }
107 }
108 if (validation.format) {
109 if (typeof validation.format == 'string') {
110 validation.format = new RegExp(validation.format);
111 }
112 if (!validation.format.test(tempValue[j])) {
113 result = [this.getValidationMessage(validation, 'format', 'string_invalid_format'), validation.format];
114 break;
115 }
116 }
117 break;
118
119 case 'number':
120 if (tempValue[j] === undefined || tempValue[j].length === 0) {
121 if (!validation.allow_empty_value) {
122 result = ['number_nan'];
123 }
124 break;
125 }
126 if (isNaN(tempValue[j])) {
127 result = ['number_nan'];
128 break;
129 }
130 if (filter.type == 'integer') {
131 if (parseInt(tempValue[j]) != tempValue[j]) {
132 result = ['number_not_integer'];
133 break;
134 }
135 }
136 else {
137 if (parseFloat(tempValue[j]) != tempValue[j]) {
138 result = ['number_not_double'];
139 break;
140 }
141 }
142 if (validation.min !== undefined) {
143 if (tempValue[j] < parseFloat(validation.min)) {
144 result = [this.getValidationMessage(validation, 'min', 'number_exceed_min'), validation.min];
145 break;
146 }
147 }
148 if (validation.max !== undefined) {
149 if (tempValue[j] > parseFloat(validation.max)) {
150 result = [this.getValidationMessage(validation, 'max', 'number_exceed_max'), validation.max];
151 break;
152 }
153 }
154 if (validation.step !== undefined && validation.step !== 'any') {
155 var v = (tempValue[j] / validation.step).toPrecision(14);
156 if (parseInt(v) != v) {
157 result = [this.getValidationMessage(validation, 'step', 'number_wrong_step'), validation.step];
158 break;
159 }
160 }
161 break;
162
163 case 'datetime':
164 if (tempValue[j] === undefined || tempValue[j].length === 0) {
165 if (!validation.allow_empty_value) {
166 result = ['datetime_empty'];
167 }
168 break;
169 }
170
171 // we need MomentJS
172 if (validation.format) {
173 if (!('moment' in window)) {
174 Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
175 }
176
177 var datetime = moment(tempValue[j], validation.format);
178 if (!datetime.isValid()) {
179 result = [this.getValidationMessage(validation, 'format', 'datetime_invalid'), validation.format];
180 break;
181 }
182 else {
183 if (validation.min) {
184 if (datetime < moment(validation.min, validation.format)) {
185 result = [this.getValidationMessage(validation, 'min', 'datetime_exceed_min'), validation.min];
186 break;
187 }
188 }
189 if (validation.max) {
190 if (datetime > moment(validation.max, validation.format)) {
191 result = [this.getValidationMessage(validation, 'max', 'datetime_exceed_max'), validation.max];
192 break;
193 }
194 }
195 }
196 }
197 break;
198
199 case 'boolean':
200 if (tempValue[j] === undefined || tempValue[j].length === 0) {
201 if (!validation.allow_empty_value) {
202 result = ['boolean_not_valid'];
203 }
204 break;
205 }
206 tmp = ('' + tempValue[j]).trim().toLowerCase();
207 if (tmp !== 'true' && tmp !== 'false' && tmp !== '1' && tmp !== '0' && tempValue[j] !== 1 && tempValue[j] !== 0) {
208 result = ['boolean_not_valid'];
209 break;
210 }
211 }
212
213 if (result !== true) {
214 break;
215 }
216 }
217 }
218
219 if (result !== true) {
220 break;
221 }
222 }
223
224 return result;
225};
226
227/**
228 * Returns an incremented group ID
229 * @returns {string}
230 * @private
231 */
232QueryBuilder.prototype.nextGroupId = function() {
233 return this.status.id + '_group_' + (this.status.group_id++);
234};
235
236/**
237 * Returns an incremented rule ID
238 * @returns {string}
239 * @private
240 */
241QueryBuilder.prototype.nextRuleId = function() {
242 return this.status.id + '_rule_' + (this.status.rule_id++);
243};
244
245/**
246 * Returns the operators for a filter
247 * @param {string|object} filter - filter id or filter object
248 * @returns {object[]}
249 * @fires QueryBuilder.changer:getOperators
250 * @private
251 */
252QueryBuilder.prototype.getOperators = function(filter) {
253 if (typeof filter == 'string') {
254 filter = this.getFilterById(filter);
255 }
256
257 var result = [];
258
259 for (var i = 0, l = this.operators.length; i < l; i++) {
260 // filter operators check
261 if (filter.operators) {
262 if (filter.operators.indexOf(this.operators[i].type) == -1) {
263 continue;
264 }
265 }
266 // type check
267 else if (this.operators[i].apply_to.indexOf(QueryBuilder.types[filter.type]) == -1) {
268 continue;
269 }
270
271 result.push(this.operators[i]);
272 }
273
274 // keep sort order defined for the filter
275 if (filter.operators) {
276 result.sort(function(a, b) {
277 return filter.operators.indexOf(a.type) - filter.operators.indexOf(b.type);
278 });
279 }
280
281 /**
282 * Modifies the operators available for a filter
283 * @event changer:getOperators
284 * @memberof QueryBuilder
285 * @param {QueryBuilder.Operator[]} operators
286 * @param {QueryBuilder.Filter} filter
287 * @returns {QueryBuilder.Operator[]}
288 */
289 return this.change('getOperators', result, filter);
290};
291
292/**
293 * Returns a particular filter by its id
294 * @param {string} id
295 * @param {boolean} [doThrow=true]
296 * @returns {object|null}
297 * @throws UndefinedFilterError
298 * @private
299 */
300QueryBuilder.prototype.getFilterById = function(id, doThrow) {
301 if (id == '-1') {
302 return null;
303 }
304
305 for (var i = 0, l = this.filters.length; i < l; i++) {
306 if (this.filters[i].id == id) {
307 return this.filters[i];
308 }
309 }
310
311 Utils.error(doThrow !== false, 'UndefinedFilter', 'Undefined filter "{0}"', id);
312
313 return null;
314};
315
316/**
317 * Returns a particular operator by its type
318 * @param {string} type
319 * @param {boolean} [doThrow=true]
320 * @returns {object|null}
321 * @throws UndefinedOperatorError
322 * @private
323 */
324QueryBuilder.prototype.getOperatorByType = function(type, doThrow) {
325 if (type == '-1') {
326 return null;
327 }
328
329 for (var i = 0, l = this.operators.length; i < l; i++) {
330 if (this.operators[i].type == type) {
331 return this.operators[i];
332 }
333 }
334
335 Utils.error(doThrow !== false, 'UndefinedOperator', 'Undefined operator "{0}"', type);
336
337 return null;
338};
339
340/**
341 * Returns rule's current input value
342 * @param {Rule} rule
343 * @returns {*}
344 * @fires QueryBuilder.changer:getRuleValue
345 * @private
346 */
347QueryBuilder.prototype.getRuleInputValue = function(rule) {
348 var filter = rule.filter;
349 var operator = rule.operator;
350 var value = [];
351
352 if (filter.valueGetter) {
353 value = filter.valueGetter.call(this, rule);
354 }
355 else {
356 var $value = rule.$el.find(QueryBuilder.selectors.value_container);
357
358 for (var i = 0; i < operator.nb_inputs; i++) {
359 var name = Utils.escapeElementId(rule.id + '_value_' + i);
360 var tmp;
361
362 switch (filter.input) {
363 case 'radio':
364 value.push($value.find('[name=' + name + ']:checked').val());
365 break;
366
367 case 'checkbox':
368 tmp = [];
369 // jshint loopfunc:true
370 $value.find('[name=' + name + ']:checked').each(function() {
371 tmp.push($(this).val());
372 });
373 // jshint loopfunc:false
374 value.push(tmp);
375 break;
376
377 case 'select':
378 if (filter.multiple) {
379 tmp = [];
380 // jshint loopfunc:true
381 $value.find('[name=' + name + '] option:selected').each(function() {
382 tmp.push($(this).val());
383 });
384 // jshint loopfunc:false
385 value.push(tmp);
386 }
387 else {
388 value.push($value.find('[name=' + name + '] option:selected').val());
389 }
390 break;
391
392 default:
393 value.push($value.find('[name=' + name + ']').val());
394 }
395 }
396
397 if (operator.multiple && filter.value_separator) {
398 value = value.map(function(val) {
399 return val.split(filter.value_separator);
400 });
401 }
402
403 if (operator.nb_inputs === 1) {
404 value = value[0];
405 }
406
407 // @deprecated
408 if (filter.valueParser) {
409 value = filter.valueParser.call(this, rule, value);
410 }
411 }
412
413 /**
414 * Modifies the rule's value grabbed from the DOM
415 * @event changer:getRuleValue
416 * @memberof QueryBuilder
417 * @param {*} value
418 * @param {Rule} rule
419 * @returns {*}
420 */
421 return this.change('getRuleValue', value, rule);
422};
423
424/**
425 * Sets the value of a rule's input
426 * @param {Rule} rule
427 * @param {*} value
428 * @private
429 */
430QueryBuilder.prototype.setRuleInputValue = function(rule, value) {
431 var filter = rule.filter;
432 var operator = rule.operator;
433
434 if (!filter || !operator) {
435 return;
436 }
437
438 this._updating_input = true;
439
440 if (filter.valueSetter) {
441 filter.valueSetter.call(this, rule, value);
442 }
443 else {
444 var $value = rule.$el.find(QueryBuilder.selectors.value_container);
445
446 if (operator.nb_inputs == 1) {
447 value = [value];
448 }
449
450 for (var i = 0; i < operator.nb_inputs; i++) {
451 var name = Utils.escapeElementId(rule.id + '_value_' + i);
452
453 switch (filter.input) {
454 case 'radio':
455 $value.find('[name=' + name + '][value="' + value[i] + '"]').prop('checked', true).trigger('change');
456 break;
457
458 case 'checkbox':
459 if (!$.isArray(value[i])) {
460 value[i] = [value[i]];
461 }
462 // jshint loopfunc:true
463 value[i].forEach(function(value) {
464 $value.find('[name=' + name + '][value="' + value + '"]').prop('checked', true).trigger('change');
465 });
466 // jshint loopfunc:false
467 break;
468
469 default:
470 if (operator.multiple && filter.value_separator && $.isArray(value[i])) {
471 value[i] = value[i].join(filter.value_separator);
472 }
473 $value.find('[name=' + name + ']').val(value[i]).trigger('change');
474 break;
475 }
476 }
477 }
478
479 this._updating_input = false;
480};
481
482/**
483 * Parses rule flags
484 * @param {object} rule
485 * @returns {object}
486 * @fires QueryBuilder.changer:parseRuleFlags
487 * @private
488 */
489QueryBuilder.prototype.parseRuleFlags = function(rule) {
490 var flags = $.extend({}, this.settings.default_rule_flags);
491
492 if (rule.readonly) {
493 $.extend(flags, {
494 filter_readonly: true,
495 operator_readonly: true,
496 value_readonly: true,
497 no_delete: true
498 });
499 }
500
501 if (rule.flags) {
502 $.extend(flags, rule.flags);
503 }
504
505 /**
506 * Modifies the consolidated rule's flags
507 * @event changer:parseRuleFlags
508 * @memberof QueryBuilder
509 * @param {object} flags
510 * @param {object} rule - <b>not</b> a Rule object
511 * @returns {object}
512 */
513 return this.change('parseRuleFlags', flags, rule);
514};
515
516/**
517 * Gets a copy of flags of a rule
518 * @param {object} flags
519 * @param {boolean} [all=false] - return all flags or only changes from default flags
520 * @returns {object}
521 * @private
522 */
523QueryBuilder.prototype.getRuleFlags = function(flags, all) {
524 if (all) {
525 return $.extend({}, flags);
526 }
527 else {
528 var ret = {};
529 $.each(this.settings.default_rule_flags, function(key, value) {
530 if (flags[key] !== value) {
531 ret[key] = flags[key];
532 }
533 });
534 return ret;
535 }
536};
537
538/**
539 * Parses group flags
540 * @param {object} group
541 * @returns {object}
542 * @fires QueryBuilder.changer:parseGroupFlags
543 * @private
544 */
545QueryBuilder.prototype.parseGroupFlags = function(group) {
546 var flags = $.extend({}, this.settings.default_group_flags);
547
548 if (group.readonly) {
549 $.extend(flags, {
550 condition_readonly: true,
551 no_add_rule: true,
552 no_add_group: true,
553 no_delete: true
554 });
555 }
556
557 if (group.flags) {
558 $.extend(flags, group.flags);
559 }
560
561 /**
562 * Modifies the consolidated group's flags
563 * @event changer:parseGroupFlags
564 * @memberof QueryBuilder
565 * @param {object} flags
566 * @param {object} group - <b>not</b> a Group object
567 * @returns {object}
568 */
569 return this.change('parseGroupFlags', flags, group);
570};
571
572/**
573 * Gets a copy of flags of a group
574 * @param {object} flags
575 * @param {boolean} [all=false] - return all flags or only changes from default flags
576 * @returns {object}
577 * @private
578 */
579QueryBuilder.prototype.getGroupFlags = function(flags, all) {
580 if (all) {
581 return $.extend({}, flags);
582 }
583 else {
584 var ret = {};
585 $.each(this.settings.default_group_flags, function(key, value) {
586 if (flags[key] !== value) {
587 ret[key] = flags[key];
588 }
589 });
590 return ret;
591 }
592};
593
594/**
595 * Translate a label either by looking in the `lang` object or in itself if it's an object where keys are language codes
596 * @param {string} [category]
597 * @param {string|object} key
598 * @returns {string}
599 * @fires QueryBuilder.changer:translate
600 */
601QueryBuilder.prototype.translate = function(category, key) {
602 if (!key) {
603 key = category;
604 category = undefined;
605 }
606
607 var translation;
608 if (typeof key === 'object') {
609 translation = key[this.settings.lang_code] || key['en'];
610 }
611 else {
612 translation = (category ? this.lang[category] : this.lang)[key] || key;
613 }
614
615 /**
616 * Modifies the translated label
617 * @event changer:translate
618 * @memberof QueryBuilder
619 * @param {string} translation
620 * @param {string|object} key
621 * @param {string} [category]
622 * @returns {string}
623 */
624 return this.change('translate', translation, key, category);
625};
626
627/**
628 * Returns a validation message
629 * @param {object} validation
630 * @param {string} type
631 * @param {string} def
632 * @returns {string}
633 * @private
634 */
635QueryBuilder.prototype.getValidationMessage = function(validation, type, def) {
636 return validation.messages && validation.messages[type] || def;
637};