Upgrade to ExtJS 4.0.0 - Released 04/26/2011
[extjs.git] / examples / ux / LiveSearchGridPanel.js
1 /**
2  * @class Ext.ux.LiveSearchGridPanel
3  * @extends Ext.grid.Panel
4  * <p>A GridPanel class with live search support.</p>
5  * @author Nicolas Ferrero
6  */
7 Ext.define('Ext.ux.LiveSearchGridPanel', {
8     extend: 'Ext.grid.Panel',
9     requires: [
10         'Ext.toolbar.TextItem',
11         'Ext.form.field.Checkbox',
12         'Ext.form.field.Text',
13         'Ext.ux.statusbar.StatusBar'
14     ],
15     
16     /**
17      * @private
18      * search value initialization
19      */
20     searchValue: null,
21     
22     /**
23      * @private
24      * The row indexes where matching strings are found. (used by previous and next buttons)
25      */
26     indexes: [],
27     
28     /**
29      * @private
30      * The row index of the first search, it could change if next or previous buttons are used.
31      */
32     currentIndex: null,
33     
34     /**
35      * @private
36      * The generated regular expression used for searching.
37      */
38     searchRegExp: null,
39     
40     /**
41      * @private
42      * Case sensitive mode.
43      */
44     caseSensitive: false,
45     
46     /**
47      * @private
48      * Regular expression mode.
49      */
50     regExpMode: false,
51     
52     /**
53      * @cfg {String} matchCls
54      * The matched string css classe.
55      */
56     matchCls: 'x-livesearch-match',
57     
58     defaultStatusText: 'Nothing Found',
59     
60     // Component initialization override: adds the top and bottom toolbars and setup headers renderer.
61     initComponent: function() {
62         var me = this;
63         me.tbar = ['Search',{
64                  xtype: 'textfield',
65                  name: 'searchField',
66                  hideLabel: true,
67                  width: 200,
68                  listeners: {
69                      change: {
70                          fn: me.onTextFieldChange,
71                          scope: this,
72                          buffer: 100
73                      }
74                  }
75             }, {
76                 xtype: 'button',
77                 text: '<',
78                 tooltip: 'Find Previous Row',
79                 handler: me.onPreviousClick,
80                 scope: me
81             },{
82                 xtype: 'button',
83                 text: '>',
84                 tooltip: 'Find Next Row',
85                 handler: me.onNextClick,
86                 scope: me
87             }, '-', {
88                 xtype: 'checkbox',
89                 hideLabel: true,
90                 margin: '0 0 0 4px',
91                 handler: me.regExpToggle,
92                 scope: me                
93             }, 'Regular expression', {
94                 xtype: 'checkbox',
95                 hideLabel: true,
96                 margin: '0 0 0 4px',
97                 handler: me.caseSensitiveToggle,
98                 scope: me
99             }, 'Case sensitive'];
100
101         me.bbar = Ext.create('Ext.ux.StatusBar', {
102             defaultText: me.defaultStatusText,
103             name: 'searchStatusBar'
104         });
105         
106         me.callParent(arguments);
107     },
108     
109     // afterRender override: it adds textfield and statusbar reference and start monitoring keydown events in textfield input 
110     afterRender: function() {
111         var me = this;
112         me.callParent(arguments);
113         me.textField = me.down('textfield[name=searchField]');
114         me.statusBar = me.down('statusbar[name=searchStatusBar]');
115     },
116     // detects html tag
117     tagsRe: /<[^>]*>/gm,
118     
119     // DEL ASCII code
120     tagsProtect: '\x0f',
121     
122     // detects regexp reserved word
123     regExpProtect: /\\|\/|\+|\\|\.|\[|\]|\{|\}|\?|\$|\*|\^|\|/gm,
124     
125     /**
126      * In normal mode it returns the value with protected regexp characters.
127      * In regular expression mode it returns the raw value except if the regexp is invalid.
128      * @return {String} The value to process or null if the textfield value is blank or invalid.
129      * @private
130      */
131     getSearchValue: function() {
132         var me = this,
133             value = me.textField.getValue();
134             
135         if (value === '') {
136             return null;
137         }
138         if (!me.regExpMode) {
139             value = value.replace(me.regExpProtect, function(m) {
140                 return '\\' + m;
141             });
142         } else {
143             try {
144                 new RegExp(value);
145             } catch (error) {
146                 me.statusBar.setStatus({
147                     text: error.message,
148                     iconCls: 'x-status-error'
149                 });
150                 return null;
151             }
152             // this is stupid
153             if (value === '^' || value === '$') {
154                 return null;
155             }
156         }
157         
158         var length = value.length,
159             resultArray = [me.tagsProtect + '*'],
160             i = 0,
161             c;
162             
163         for(; i < length; i++) {
164             c = value.charAt(i);
165             resultArray.push(c);
166             if (c !== '\\') {
167                 resultArray.push(me.tagsProtect + '*');
168             } 
169         }
170         return resultArray.join('');
171     },
172     
173     /**
174      * Finds all strings that matches the searched value in each grid cells.
175      * @private
176      */
177      onTextFieldChange: function() {
178          var me = this,
179              count = 0;
180
181          me.view.refresh();
182          // reset the statusbar
183          me.statusBar.setStatus({
184              text: me.defaultStatusText,
185              iconCls: ''
186          });
187
188          me.searchValue = me.getSearchValue();
189          me.indexes = [];
190          me.currentIndex = null;
191
192          if (me.searchValue !== null) {
193              me.searchRegExp = new RegExp(me.searchValue, 'g' + (me.caseSensitive ? '' : 'i'));
194              
195              
196              me.store.each(function(record, idx) {
197                  var td = Ext.fly(me.view.getNode(idx)).down('td'),
198                      cell, matches, cellHTML;
199                  while(td) {
200                      cell = td.down('.x-grid-cell-inner');
201                      matches = cell.dom.innerHTML.match(me.tagsRe);
202                      cellHTML = cell.dom.innerHTML.replace(me.tagsRe, me.tagsProtect);
203                      
204                      // populate indexes array, set currentIndex, and replace wrap matched string in a span
205                      cellHTML = cellHTML.replace(me.searchRegExp, function(m) {
206                         count += 1;
207                         if (Ext.Array.indexOf(me.indexes, idx) === -1) {
208                             me.indexes.push(idx);
209                         }
210                         if (me.currentIndex === null) {
211                             me.currentIndex = idx;
212                         }
213                         return '<span class="' + me.matchCls + '">' + m + '</span>';
214                      });
215                      // restore protected tags
216                      Ext.each(matches, function(match) {
217                         cellHTML = cellHTML.replace(me.tagsProtect, match); 
218                      });
219                      // update cell html
220                      cell.dom.innerHTML = cellHTML;
221                      td = td.next();
222                  }
223              }, me);
224
225              // results found
226              if (me.currentIndex !== null) {
227                  me.getSelectionModel().select(me.currentIndex);
228                  me.statusBar.setStatus({
229                      text: count + ' matche(s) found.',
230                      iconCls: 'x-status-valid'
231                  });
232              }
233          }
234
235          // no results found
236          if (me.currentIndex === null) {
237              me.getSelectionModel().deselectAll();
238          }
239
240          // force textfield focus
241          me.textField.focus();
242      },
243     
244     /**
245      * Selects the previous row containing a match.
246      * @private
247      */   
248     onPreviousClick: function() {
249         var me = this,
250             idx;
251             
252         if ((idx = Ext.Array.indexOf(me.indexes, me.currentIndex)) !== -1) {
253             me.currentIndex = me.indexes[idx - 1] || me.indexes[me.indexes.length - 1];
254             me.getSelectionModel().select(me.currentIndex);
255          }
256     },
257     
258     /**
259      * Selects the next row containing a match.
260      * @private
261      */    
262     onNextClick: function() {
263          var me = this,
264              idx;
265              
266          if ((idx = Ext.Array.indexOf(me.indexes, me.currentIndex)) !== -1) {
267             me.currentIndex = me.indexes[idx + 1] || me.indexes[0];
268             me.getSelectionModel().select(me.currentIndex);
269          }
270     },
271     
272     /**
273      * Switch to case sensitive mode.
274      * @private
275      */    
276     caseSensitiveToggle: function(checkbox, checked) {
277         this.caseSensitive = checked;
278         this.onTextFieldChange();
279     },
280     
281     /**
282      * Switch to regular expression mode
283      * @private
284      */
285     regExpToggle: function(checkbox, checked) {
286         this.regExpMode = checked;
287         this.onTextFieldChange();
288     }
289 });