Upgrade to ExtJS 4.0.0 - Released 04/26/2011
[extjs.git] / examples / ux / LiveSearchGridPanel.js
diff --git a/examples/ux/LiveSearchGridPanel.js b/examples/ux/LiveSearchGridPanel.js
new file mode 100644 (file)
index 0000000..794534c
--- /dev/null
@@ -0,0 +1,289 @@
+/**
+ * @class Ext.ux.LiveSearchGridPanel
+ * @extends Ext.grid.Panel
+ * <p>A GridPanel class with live search support.</p>
+ * @author Nicolas Ferrero
+ */
+Ext.define('Ext.ux.LiveSearchGridPanel', {
+    extend: 'Ext.grid.Panel',
+    requires: [
+        'Ext.toolbar.TextItem',
+        'Ext.form.field.Checkbox',
+        'Ext.form.field.Text',
+        'Ext.ux.statusbar.StatusBar'
+    ],
+    
+    /**
+     * @private
+     * search value initialization
+     */
+    searchValue: null,
+    
+    /**
+     * @private
+     * The row indexes where matching strings are found. (used by previous and next buttons)
+     */
+    indexes: [],
+    
+    /**
+     * @private
+     * The row index of the first search, it could change if next or previous buttons are used.
+     */
+    currentIndex: null,
+    
+    /**
+     * @private
+     * The generated regular expression used for searching.
+     */
+    searchRegExp: null,
+    
+    /**
+     * @private
+     * Case sensitive mode.
+     */
+    caseSensitive: false,
+    
+    /**
+     * @private
+     * Regular expression mode.
+     */
+    regExpMode: false,
+    
+    /**
+     * @cfg {String} matchCls
+     * The matched string css classe.
+     */
+    matchCls: 'x-livesearch-match',
+    
+    defaultStatusText: 'Nothing Found',
+    
+    // Component initialization override: adds the top and bottom toolbars and setup headers renderer.
+    initComponent: function() {
+        var me = this;
+        me.tbar = ['Search',{
+                 xtype: 'textfield',
+                 name: 'searchField',
+                 hideLabel: true,
+                 width: 200,
+                 listeners: {
+                     change: {
+                         fn: me.onTextFieldChange,
+                         scope: this,
+                         buffer: 100
+                     }
+                 }
+            }, {
+                xtype: 'button',
+                text: '<',
+                tooltip: 'Find Previous Row',
+                handler: me.onPreviousClick,
+                scope: me
+            },{
+                xtype: 'button',
+                text: '>',
+                tooltip: 'Find Next Row',
+                handler: me.onNextClick,
+                scope: me
+            }, '-', {
+                xtype: 'checkbox',
+                hideLabel: true,
+                margin: '0 0 0 4px',
+                handler: me.regExpToggle,
+                scope: me                
+            }, 'Regular expression', {
+                xtype: 'checkbox',
+                hideLabel: true,
+                margin: '0 0 0 4px',
+                handler: me.caseSensitiveToggle,
+                scope: me
+            }, 'Case sensitive'];
+
+        me.bbar = Ext.create('Ext.ux.StatusBar', {
+            defaultText: me.defaultStatusText,
+            name: 'searchStatusBar'
+        });
+        
+        me.callParent(arguments);
+    },
+    
+    // afterRender override: it adds textfield and statusbar reference and start monitoring keydown events in textfield input 
+    afterRender: function() {
+        var me = this;
+        me.callParent(arguments);
+        me.textField = me.down('textfield[name=searchField]');
+        me.statusBar = me.down('statusbar[name=searchStatusBar]');
+    },
+    // detects html tag
+    tagsRe: /<[^>]*>/gm,
+    
+    // DEL ASCII code
+    tagsProtect: '\x0f',
+    
+    // detects regexp reserved word
+    regExpProtect: /\\|\/|\+|\\|\.|\[|\]|\{|\}|\?|\$|\*|\^|\|/gm,
+    
+    /**
+     * In normal mode it returns the value with protected regexp characters.
+     * In regular expression mode it returns the raw value except if the regexp is invalid.
+     * @return {String} The value to process or null if the textfield value is blank or invalid.
+     * @private
+     */
+    getSearchValue: function() {
+        var me = this,
+            value = me.textField.getValue();
+            
+        if (value === '') {
+            return null;
+        }
+        if (!me.regExpMode) {
+            value = value.replace(me.regExpProtect, function(m) {
+                return '\\' + m;
+            });
+        } else {
+            try {
+                new RegExp(value);
+            } catch (error) {
+                me.statusBar.setStatus({
+                    text: error.message,
+                    iconCls: 'x-status-error'
+                });
+                return null;
+            }
+            // this is stupid
+            if (value === '^' || value === '$') {
+                return null;
+            }
+        }
+        
+        var length = value.length,
+            resultArray = [me.tagsProtect + '*'],
+            i = 0,
+            c;
+            
+        for(; i < length; i++) {
+            c = value.charAt(i);
+            resultArray.push(c);
+            if (c !== '\\') {
+                resultArray.push(me.tagsProtect + '*');
+            } 
+        }
+        return resultArray.join('');
+    },
+    
+    /**
+     * Finds all strings that matches the searched value in each grid cells.
+     * @private
+     */
+     onTextFieldChange: function() {
+         var me = this,
+             count = 0;
+
+         me.view.refresh();
+         // reset the statusbar
+         me.statusBar.setStatus({
+             text: me.defaultStatusText,
+             iconCls: ''
+         });
+
+         me.searchValue = me.getSearchValue();
+         me.indexes = [];
+         me.currentIndex = null;
+
+         if (me.searchValue !== null) {
+             me.searchRegExp = new RegExp(me.searchValue, 'g' + (me.caseSensitive ? '' : 'i'));
+             
+             
+             me.store.each(function(record, idx) {
+                 var td = Ext.fly(me.view.getNode(idx)).down('td'),
+                     cell, matches, cellHTML;
+                 while(td) {
+                     cell = td.down('.x-grid-cell-inner');
+                     matches = cell.dom.innerHTML.match(me.tagsRe);
+                     cellHTML = cell.dom.innerHTML.replace(me.tagsRe, me.tagsProtect);
+                     
+                     // populate indexes array, set currentIndex, and replace wrap matched string in a span
+                     cellHTML = cellHTML.replace(me.searchRegExp, function(m) {
+                        count += 1;
+                        if (Ext.Array.indexOf(me.indexes, idx) === -1) {
+                            me.indexes.push(idx);
+                        }
+                        if (me.currentIndex === null) {
+                            me.currentIndex = idx;
+                        }
+                        return '<span class="' + me.matchCls + '">' + m + '</span>';
+                     });
+                     // restore protected tags
+                     Ext.each(matches, function(match) {
+                        cellHTML = cellHTML.replace(me.tagsProtect, match); 
+                     });
+                     // update cell html
+                     cell.dom.innerHTML = cellHTML;
+                     td = td.next();
+                 }
+             }, me);
+
+             // results found
+             if (me.currentIndex !== null) {
+                 me.getSelectionModel().select(me.currentIndex);
+                 me.statusBar.setStatus({
+                     text: count + ' matche(s) found.',
+                     iconCls: 'x-status-valid'
+                 });
+             }
+         }
+
+         // no results found
+         if (me.currentIndex === null) {
+             me.getSelectionModel().deselectAll();
+         }
+
+         // force textfield focus
+         me.textField.focus();
+     },
+    
+    /**
+     * Selects the previous row containing a match.
+     * @private
+     */   
+    onPreviousClick: function() {
+        var me = this,
+            idx;
+            
+        if ((idx = Ext.Array.indexOf(me.indexes, me.currentIndex)) !== -1) {
+            me.currentIndex = me.indexes[idx - 1] || me.indexes[me.indexes.length - 1];
+            me.getSelectionModel().select(me.currentIndex);
+         }
+    },
+    
+    /**
+     * Selects the next row containing a match.
+     * @private
+     */    
+    onNextClick: function() {
+         var me = this,
+             idx;
+             
+         if ((idx = Ext.Array.indexOf(me.indexes, me.currentIndex)) !== -1) {
+            me.currentIndex = me.indexes[idx + 1] || me.indexes[0];
+            me.getSelectionModel().select(me.currentIndex);
+         }
+    },
+    
+    /**
+     * Switch to case sensitive mode.
+     * @private
+     */    
+    caseSensitiveToggle: function(checkbox, checked) {
+        this.caseSensitive = checked;
+        this.onTextFieldChange();
+    },
+    
+    /**
+     * Switch to regular expression mode
+     * @private
+     */
+    regExpToggle: function(checkbox, checked) {
+        this.regExpMode = checked;
+        this.onTextFieldChange();
+    }
+});
\ No newline at end of file