LiveSearchGridPanel.js 8.2 KB

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