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