LiveSearchGridPanel.js 8.8 KB

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