Upgrade to ExtJS 4.0.7 - Released 10/19/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         return value;
173     },
174     
175     /**
176      * Finds all strings that matches the searched value in each grid cells.
177      * @private
178      */
179      onTextFieldChange: function() {
180          var me = this,
181              count = 0;
182
183          me.view.refresh();
184          // reset the statusbar
185          me.statusBar.setStatus({
186              text: me.defaultStatusText,
187              iconCls: ''
188          });
189
190          me.searchValue = me.getSearchValue();
191          me.indexes = [];
192          me.currentIndex = null;
193
194          if (me.searchValue !== null) {
195              me.searchRegExp = new RegExp(me.searchValue, 'g' + (me.caseSensitive ? '' : 'i'));
196              
197              
198              me.store.each(function(record, idx) {
199                  var td = Ext.fly(me.view.getNode(idx)).down('td'),
200                      cell, matches, cellHTML;
201                  while(td) {
202                      cell = td.down('.x-grid-cell-inner');
203                      matches = cell.dom.innerHTML.match(me.tagsRe);
204                      cellHTML = cell.dom.innerHTML.replace(me.tagsRe, me.tagsProtect);
205                      
206                      // populate indexes array, set currentIndex, and replace wrap matched string in a span
207                      cellHTML = cellHTML.replace(me.searchRegExp, function(m) {
208                         count += 1;
209                         if (Ext.Array.indexOf(me.indexes, idx) === -1) {
210                             me.indexes.push(idx);
211                         }
212                         if (me.currentIndex === null) {
213                             me.currentIndex = idx;
214                         }
215                         return '<span class="' + me.matchCls + '">' + m + '</span>';
216                      });
217                      // restore protected tags
218                      Ext.each(matches, function(match) {
219                         cellHTML = cellHTML.replace(me.tagsProtect, match); 
220                      });
221                      // update cell html
222                      cell.dom.innerHTML = cellHTML;
223                      td = td.next();
224                  }
225              }, me);
226
227              // results found
228              if (me.currentIndex !== null) {
229                  me.getSelectionModel().select(me.currentIndex);
230                  me.statusBar.setStatus({
231                      text: count + ' matche(s) found.',
232                      iconCls: 'x-status-valid'
233                  });
234              }
235          }
236
237          // no results found
238          if (me.currentIndex === null) {
239              me.getSelectionModel().deselectAll();
240          }
241
242          // force textfield focus
243          me.textField.focus();
244      },
245     
246     /**
247      * Selects the previous row containing a match.
248      * @private
249      */   
250     onPreviousClick: function() {
251         var me = this,
252             idx;
253             
254         if ((idx = Ext.Array.indexOf(me.indexes, me.currentIndex)) !== -1) {
255             me.currentIndex = me.indexes[idx - 1] || me.indexes[me.indexes.length - 1];
256             me.getSelectionModel().select(me.currentIndex);
257          }
258     },
259     
260     /**
261      * Selects the next row containing a match.
262      * @private
263      */    
264     onNextClick: function() {
265          var me = this,
266              idx;
267              
268          if ((idx = Ext.Array.indexOf(me.indexes, me.currentIndex)) !== -1) {
269             me.currentIndex = me.indexes[idx + 1] || me.indexes[0];
270             me.getSelectionModel().select(me.currentIndex);
271          }
272     },
273     
274     /**
275      * Switch to case sensitive mode.
276      * @private
277      */    
278     caseSensitiveToggle: function(checkbox, checked) {
279         this.caseSensitive = checked;
280         this.onTextFieldChange();
281     },
282     
283     /**
284      * Switch to regular expression mode
285      * @private
286      */
287     regExpToggle: function(checkbox, checked) {
288         this.regExpMode = checked;
289         this.onTextFieldChange();
290     }
291 });