86444a651217a18499a60acf05d6a5cb0704161e
[tine20] / tine20 / Tinebase / js / widgets / grid / FilterModel.js
1 /*
2  * Tine 2.0
3  * 
4  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
5  * @author      Cornelius Weiss <c.weiss@metaways.de>
6  * @copyright   Copyright (c) 2007-2011 Metaways Infosystems GmbH (http://www.metaways.de)
7  *
8  * TODO         add year to 'inweek' filter?
9  */
10 Ext.ns('Tine.widgets.grid');
11
12 /**
13  * Model of filter
14  * 
15  * @namespace   Tine.widgets.grid
16  * @class       Tine.widgets.grid.FilterModel
17  * @extends     Ext.util.Observable
18  * @constructor
19  */
20 Tine.widgets.grid.FilterModel = function(config) {
21     Ext.apply(this, config);
22     Tine.widgets.grid.FilterModel.superclass.constructor.call(this);
23     
24     this.addEvents(
25       /**
26        * @event filtertrigger
27        * is fired when user request to update list by filter
28        * @param {Tine.widgets.grid.FilterToolbar}
29        */
30       'filtertrigger'
31     );
32     
33     this.initComponent();
34 };
35
36 Ext.extend(Tine.widgets.grid.FilterModel, Ext.util.Observable, {
37     /**
38      * @cfg {String} label 
39      * label for the filter
40      */
41     label: '',
42     
43     /**
44      * @cfg {String} field
45      * name of th field to filter
46      */
47     field: '',
48     
49     /**
50      * @cfg {String} valueType
51      * type of value
52      */
53     valueType: 'string',
54     
55     /**
56      * @cfg {String} defaultValue
57      * default value
58      */
59     defaultValue: null,
60     
61     /**
62      * @cfg {Array} operators
63      * valid operators
64      */
65     operators: null,
66     
67     /**
68      * @cfg {String} defaultOperator
69      * name of the default operator
70      */
71     defaultOperator: null,
72     
73     /**
74      * @cfg {Array} customOperators
75      * define custom operators
76      */
77     customOperators: null,
78     
79     /**
80      * @cfg {Ext.data.Store|Array} 
81      * used by combo valueType
82      */
83     store: null,
84     
85     /**
86      * @cfg {String} displayField
87      * used by combo valueType
88      */
89     displayField: null,
90     
91     /**
92      * @cfg {String} valueField
93      * used by combo valueType
94      */
95     valueField: null,
96     filterValueWidth: 200,
97     
98     /**
99      * holds the future operators of date filters. Auto set by getDateFutureOps
100      * 
101      * @type {Array}
102      */
103     dateFutureOps: null,
104     
105     /**
106      * holds the future operators of date filters. Auto set by getDatePastOps
107      * 
108      * @type {Array}
109      */
110     datePastOps: null,
111     
112     /**
113      * @private
114      */
115     initComponent: function() {
116         this.isFilterModel = true;
117         
118         if (! this.operators) {
119             this.operators = [];
120         }
121         
122         
123         if (this.defaultOperator === null) {
124             switch (this.valueType) {
125                 
126                 case 'date':
127                     this.defaultOperator = 'within';
128                     break;
129                 case 'account':
130                 case 'group':
131                 case 'user':
132                 case 'bool':
133                 case 'number':
134                 case 'percentage':
135                 case 'combo':
136                 case 'country':
137                     this.defaultOperator = 'equals';
138                     break;
139                 case 'string':
140                 default:
141                     this.defaultOperator = 'contains';
142                     break;
143             }
144         }
145         
146         if (this.defaultValue === null) {
147             switch (this.valueType) {
148                 case 'customfield':
149                 case 'string':
150                     this.defaultValue = '';
151                     break;
152                 case 'bool':
153                     this.defaultValue = '1';
154                     break;
155                 case 'percentage':
156                     this.defaultValue = '0';
157                     break;
158                 case 'date':
159                 case 'account':
160                 case 'group':
161                 case 'user':
162                 case 'number':
163                 case 'country':
164                 default:
165                     break;
166             }
167         }
168         
169         this.datePastOps = this.getDatePastOps();
170         this.dateFutureOps = this.getDateFutureOps();
171     },
172     
173     /**
174      * returns past operators for date fields, may be overridden
175      * 
176      * @return {Array}
177      */
178     getDatePastOps: function() {
179         return [
180             ['dayThis',         i18n._('today')],
181             ['dayLast',         i18n._('yesterday')],
182             ['weekThis',        i18n._('this week')],
183             ['weekLast',        i18n._('last week')],
184             ['weekBeforeLast',  i18n._('the week before last')],
185             ['monthThis',       i18n._('this month')],
186             ['monthLast',       i18n._('last month')],
187             ['monthThreeLast',  i18n._('last three months')],
188             ['monthSixLast',    i18n._('last six months')],
189             ['anytime',         i18n._('anytime')],
190             ['quarterThis',     i18n._('this quarter')],
191             ['quarterLast',     i18n._('last quarter')],
192             ['yearThis',        i18n._('this year')],
193             ['yearLast',        i18n._('last year')]
194         ];
195     },
196     
197     /**
198      * returns future operators for date fields, may be overridden
199      * 
200      * @return {Array}
201      */
202     getDateFutureOps: function() {
203         return [
204             ['dayNext',         i18n._('tomorrow')],
205             ['weekNext',        i18n._('next week')],
206             ['monthNext',       i18n._('next month')],
207             ['quarterNext',     i18n._('next quarter')],
208             ['yearNext',        i18n._('next year')]
209         ];
210     },
211     
212     onDestroy: Ext.emptyFn,
213     
214     /**
215      * operator renderer
216      * 
217      * @param {Ext.data.Record} filter line
218      * @param {Ext.Element} element to render to 
219      */
220     operatorRenderer: function (filter, el) {
221         var operatorStore = new Ext.data.JsonStore({
222             fields: ['operator', 'label'],
223             data: [
224                 {operator: 'contains',      label: i18n._('contains')},
225                 {operator: 'notcontains',   label: i18n._('contains not')},
226                 {operator: 'regex',         label: i18n._('reg. exp.')},
227                 {operator: 'equals',        label: i18n._('is equal to')},
228                 {operator: 'equalsspecial', label: i18n._('is equal to without (-, )')},
229                 {operator: 'greater',       label: i18n._('is greater than')},
230                 {operator: 'less',          label: i18n._('is less than')},
231                 {operator: 'not',           label: i18n._('is not')},
232                 {operator: 'in',            label: i18n._('one of')},
233                 {operator: 'notin',         label: i18n._('none of')},
234                 {operator: 'before',        label: i18n._('is before')},
235                 {operator: 'after',         label: i18n._('is after')},
236                 {operator: 'within',        label: i18n._('is within')},
237                 {operator: 'inweek',        label: i18n._('is in week no.')},
238                 {operator: 'startswith',    label: i18n._('starts with')},
239                 {operator: 'endswith',      label: i18n._('ends with')},
240                 {operator: 'definedBy',     label: i18n._('defined by')}
241             ].concat(this.getCustomOperators() || []),
242             remoteSort: false,
243             sortInfo: {
244                 field: 'label',
245                 direction: 'ASC'
246             }
247         });
248
249         // filter operators
250         if (this.operators.length == 0) {
251             switch (this.valueType) {
252                 case 'string':
253                     this.operators.push('contains', 'notcontains', 'equals', 'startswith', 'endswith', 'not', 'in', 'notin');
254                     break;
255                 case 'customfield':
256                     this.operators.push('contains', 'equals', 'startswith', 'endswith', 'not');
257                     break;
258                 case 'date':
259                     this.operators.push('equals', 'before', 'after', 'within', 'inweek');
260                     break;
261                 case 'number':
262                 case 'percentage':
263                     this.operators.push('equals', 'greater', 'less');
264                     break;
265                 default:
266                     this.operators.push(this.defaultOperator);
267                     break;
268             }
269         }
270         
271         if (this.operators.length > 0) {
272             operatorStore.each(function(operator) {
273                 if (this.operators.indexOf(operator.get('operator')) < 0 ) {
274                     operatorStore.remove(operator);
275                 }
276             }, this);
277         }
278         
279         if (operatorStore.getCount() > 1) {
280             var operator = new Ext.form.ComboBox({
281                 filter: filter,
282                 width: 80,
283                 id: 'tw-ftb-frow-operatorcombo-' + filter.id,
284                 mode: 'local',
285                 lazyInit: false,
286                 emptyText: i18n._('select a operator'),
287                 forceSelection: true,
288                 typeAhead: true,
289                 triggerAction: 'all',
290                 store: operatorStore,
291                 displayField: 'label',
292                 valueField: 'operator',
293                 value: filter.get('operator') ? filter.get('operator') : this.defaultOperator,
294                 tpl: '<tpl for="."><div class="x-combo-list-item tw-ftb-operator-{operator}">{label}</div></tpl>',
295                 renderTo: el
296             });
297             operator.on('select', function(combo, newRecord, newKey) {
298                 if (combo.value != combo.filter.get('operator')) {
299                     this.onOperatorChange(combo.filter, combo.value);
300                 }
301             }, this);
302             
303             operator.on('blur', function(combo) {
304                 if (combo.value != combo.filter.get('operator')) {
305                     this.onOperatorChange(combo.filter, combo.value);
306                 }
307             }, this);
308             
309         } else if (this.operators[0] == 'freeform') {
310             var operator = new Ext.form.TextField({
311                 filter: filter,
312                 width: 100,
313                 emptyText: this.emptyTextOperator || '',
314                 value: filter.get('operator') ? filter.get('operator') : '',
315                 renderTo: el
316             });
317         } else {
318             var operator = new Ext.form.Label({
319                 filter: filter,
320                 width: 100,
321                 style: {margin: '0px 10px'},
322                 getValue: function() { return operatorStore.getAt(0).get('operator'); },
323                 text : operatorStore.getAt(0).get('label'),
324                 renderTo: el,
325                 setValue: Ext.emptyFn
326             });
327         }
328         
329         return operator;
330     },
331     
332     /**
333      * get custom operators
334      * 
335      * @return {Array}
336      */
337     getCustomOperators: function() {
338         return this.customOperators || [];
339     },
340     
341     /**
342      * called on operator change of a filter row
343      * @private
344      */
345     onOperatorChange: function(filter, newOperator) {
346         filter.set('operator', newOperator);
347         filter.set('value', '');
348         
349         // for date filters we need to rerender the value section
350         if (this.valueType == 'date') {
351             switch (newOperator) {
352                 case 'within':
353                     filter.numberfield.hide();
354                     filter.datePicker.hide();
355                     filter.withinCombo.show();
356                     filter.formFields.value = filter.withinCombo;
357                     break;
358                 case 'inweek':
359                     filter.withinCombo.hide();
360                     filter.datePicker.hide();
361                     filter.numberfield.show();
362                     filter.formFields.value = filter.numberfield;
363                     break;
364                 default:
365                     filter.withinCombo.hide();
366                     filter.numberfield.hide();
367                     filter.datePicker.show();
368                     filter.formFields.value = filter.datePicker;
369             }
370         }
371     },
372     
373     /**
374      * value renderer
375      * 
376      * @param {Ext.data.Record} filter line
377      * @param {Ext.Element} element to render to 
378      */
379     valueRenderer: function(filter, el) {
380         var value,
381             fieldWidth = this.filterValueWidth,
382             commonOptions = {
383                 filter: filter,
384                 width: fieldWidth,
385                 id: 'tw-ftb-frow-valuefield-' + filter.id,
386                 renderTo: el,
387                 value: filter.data.value ? filter.data.value : this.defaultValue
388             };
389         
390         switch (this.valueType) {
391             case 'date':
392                 value = this.dateValueRenderer(filter, el);
393                 break;
394             case 'percentage':
395                 value = new Ext.ux.PercentCombo(Ext.apply(commonOptions, {
396                     listeners: {
397                         'specialkey': function(field, e) {
398                              if(e.getKey() == e.ENTER){
399                                  this.onFiltertrigger();
400                              }
401                         },
402                         'select': this.onFiltertrigger,
403                         scope: this
404                     }
405                 }));
406                 break;
407             case 'user':
408                 value = new Tine.Addressbook.SearchCombo(Ext.apply(commonOptions, {
409                     listWidth: 350,
410                     emptyText: i18n._('Search Account ...'),
411                     userOnly: true,
412                     name: 'organizer',
413                     nameField: 'n_fileas',
414                     useAccountRecord: true,
415                     listeners: {
416                         'specialkey': function(field, e) {
417                              if(e.getKey() == e.ENTER){
418                                  this.onFiltertrigger();
419                              }
420                         },
421                         'select': this.onFiltertrigger,
422                         scope: this
423                     }
424                 }));
425                 break;
426             case 'bool':
427                 value = new Ext.form.ComboBox(Ext.apply(commonOptions, {
428                     mode: 'local',
429                     forceSelection: true,
430                     triggerAction: 'all',
431                     store: [
432                         [0, Locale.getTranslationData('Question', 'no').replace(/:.*/, '')], 
433                         [1, Locale.getTranslationData('Question', 'yes').replace(/:.*/, '')]
434                     ],
435                     listeners: {
436                         'specialkey': function(field, e) {
437                              if(e.getKey() == e.ENTER){
438                                  this.onFiltertrigger();
439                              }
440                         },
441                         'select': this.onFiltertrigger,
442                         scope: this
443                     }
444                 }));
445                 break;
446             case 'combo':
447                 var comboConfig = Ext.apply(commonOptions, {
448                     mode: 'local',
449                     forceSelection: true,
450                     triggerAction: 'all',
451                     store: this.store,
452                     listeners: {
453                         'specialkey': function(field, e) {
454                              if(e.getKey() == e.ENTER){
455                                  this.onFiltertrigger();
456                              }
457                         },
458                         'select': this.onFiltertrigger,
459                         scope: this
460                     }
461                 });
462                 if (this.displayField !== null && this.valueField !== null) {
463                     comboConfig.displayField = this.displayField;
464                     comboConfig.valueField = this.valueField;
465                 }
466                 value = new Ext.form.ComboBox(comboConfig);
467                 break;
468             case 'country':
469                 value = new Tine.widgets.CountryCombo(Ext.apply(commonOptions, {
470                 }));
471                 break;
472             case 'customfield':
473             case 'string':
474             case 'number':
475             default:
476                 value = new Ext.ux.form.ClearableTextField(Ext.apply(commonOptions, {
477                     emptyText: this.emptyText,
478                     listeners: {
479                         scope: this,
480                         specialkey: function(field, e){
481                             if(e.getKey() == e.ENTER){
482                                 this.onFiltertrigger();
483                             }
484                         }
485                     }
486                 }));
487                 break;
488         }
489         
490         return value;
491     },
492     
493     /**
494      * called on value change of a filter row
495      * @private
496      */
497     onValueChange: function(filter, newValue) {
498         filter.set('value', newValue);
499     },
500     
501     /**
502      * render a date value
503      * 
504      * we place a picker and a combo in the dom element and hide the one we don't need yet
505      */
506     dateValueRenderer: function(filter, el) {
507         var operator = filter.get('operator') ? filter.get('operator') : this.defaultOperator;
508         
509         var valueType = 'datePicker';
510         switch (operator) {
511             case 'within':
512                 valueType = 'withinCombo';
513                 break;
514             case 'inweek':
515                 valueType = 'numberfield';
516                 break;
517         }
518         
519         var comboOps = this.pastOnly ? this.datePastOps : this.dateFutureOps.concat(this.datePastOps);
520         var comboValue = 'weekThis';
521         if (filter.data.value && filter.data.value.toString().match(/^[a-zA-Z]+$/)) {
522             comboValue = filter.data.value.toString();
523         } else if (this.defaultValue && this.defaultValue.toString().match(/^[a-zA-Z]+$/)) {
524             comboValue = this.defaultValue.toString();
525         }
526         
527         filter.withinCombo = new Ext.form.ComboBox({
528             hidden: valueType != 'withinCombo',
529             filter: filter,
530             width: this.filterValueWidth,
531             value: comboValue,
532             renderTo: el,
533             mode: 'local',
534             lazyInit: false,
535             forceSelection: true,
536             typeAhead: true,
537             triggerAction: 'all',
538             store: comboOps,
539             editable: false,
540             listeners: {
541                 'specialkey': function(field, e) {
542                      if(e.getKey() == e.ENTER){
543                          this.onFiltertrigger();
544                      }
545                 },
546                 'select': this.onFiltertrigger,
547                 scope: this
548             }
549         });
550
551         var pickerValue = '';
552         if (Ext.isDate(filter.data.value)) {
553             pickerValue = filter.data.value;
554         } else if (Ext.isDate(Date.parseDate(filter.data.value, Date.patterns.ISO8601Long))) {
555             pickerValue = Date.parseDate(filter.data.value, Date.patterns.ISO8601Long);
556         } else if (Ext.isDate(this.defaultValue)) {
557             pickerValue = this.defaultValue;
558         }
559         
560         filter.datePicker = new Ext.form.DateField({
561             hidden: valueType != 'datePicker',
562             filter: filter,
563             width: this.filterValueWidth,
564             value: pickerValue,
565             renderTo: el,
566             listeners: {
567                 'specialkey': function(field, e) {
568                      if(e.getKey() == e.ENTER){
569                          this.onFiltertrigger();
570                      }
571                 },
572                 'select': this.onFiltertrigger,
573                 scope: this
574             }
575         });
576         
577         filter.numberfield = new Ext.form.NumberField({
578             hidden: valueType != 'numberfield',
579             filter: filter,
580             width: this.filterValueWidth,
581             value: pickerValue,
582             renderTo: el,
583             minValue: 1,
584             maxValue: 52,
585             maxLength: 2,   
586             allowDecimals: false,
587             allowNegative: false,
588             listeners: {
589                 scope: this,
590                 specialkey: function(field, e){
591                     if(e.getKey() == e.ENTER){
592                         this.onFiltertrigger();
593                     }
594                 }
595             }
596         });
597         
598         // upps, how to get a var i only know the name of???
599         return filter[valueType];
600     },
601     
602     /**
603      * @private
604      */
605     onFiltertrigger: function() {
606         // auto search on filter change only if set in user preferences
607         if (parseInt(Tine.Tinebase.registry.get('preferences').get('filterChangeAutoSearch'), 10) === 1) {
608             this.fireEvent('filtertrigger', this);
609         }
610     }
611 });
612
613 /**
614  * @namespace   Tine.widgets.grid
615  * @class       Tine.widgets.grid.FilterRegistry
616  * @singleton
617  */
618 Tine.widgets.grid.FilterRegistry = function() {
619     var filters = {};
620     
621     return {
622         register: function(appName, modelName, filter) {
623             var key = appName + '.' + modelName;
624             if (! filters[key]) {
625                 filters[key] = [];
626             }
627             
628             filters[key].push(filter);
629         },
630         
631         get: function(appName, modelName) {
632             if (Ext.isFunction(appName.getMeta)) {
633                 modelName = appName.getMeta('modelName');
634                 appName = appName.getMeta('appName');
635             }
636             
637             var key = appName + '.' + modelName;
638             
639             return filters[key] || [];
640         }
641     };
642 }();