LiveSearchGridPanel.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  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: '<',
  69. tooltip: 'Find Previous Row',
  70. handler: me.onPreviousClick,
  71. scope: me
  72. },{
  73. xtype: 'button',
  74. text: '>',
  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. var length = value.length,
  142. resultArray = [me.tagsProtect + '*'],
  143. i = 0,
  144. c;
  145. for(; i < length; i++) {
  146. c = value.charAt(i);
  147. resultArray.push(c);
  148. if (c !== '\\') {
  149. resultArray.push(me.tagsProtect + '*');
  150. }
  151. }
  152. return resultArray.join('');
  153. },
  154. /**
  155. * Finds all strings that matches the searched value in each grid cells.
  156. * @private
  157. */
  158. onTextFieldChange: function() {
  159. var me = this,
  160. count = 0;
  161. me.view.refresh();
  162. // reset the statusbar
  163. me.statusBar.setStatus({
  164. text: me.defaultStatusText,
  165. iconCls: ''
  166. });
  167. me.searchValue = me.getSearchValue();
  168. me.indexes = [];
  169. me.currentIndex = null;
  170. if (me.searchValue !== null) {
  171. me.searchRegExp = new RegExp(me.searchValue, 'g' + (me.caseSensitive ? '' : 'i'));
  172. me.store.each(function(record, idx) {
  173. var td = Ext.fly(me.view.getNode(idx)).down('td'),
  174. cell, matches, cellHTML;
  175. while(td) {
  176. cell = td.down('.x-grid-cell-inner');
  177. matches = cell.dom.innerHTML.match(me.tagsRe);
  178. cellHTML = cell.dom.innerHTML.replace(me.tagsRe, me.tagsProtect);
  179. // populate indexes array, set currentIndex, and replace wrap matched string in a span
  180. cellHTML = cellHTML.replace(me.searchRegExp, function(m) {
  181. count += 1;
  182. if (Ext.Array.indexOf(me.indexes, idx) === -1) {
  183. me.indexes.push(idx);
  184. }
  185. if (me.currentIndex === null) {
  186. me.currentIndex = idx;
  187. }
  188. return '<span class="' + me.matchCls + '">' + m + '</span>';
  189. });
  190. // restore protected tags
  191. Ext.each(matches, function(match) {
  192. cellHTML = cellHTML.replace(me.tagsProtect, match);
  193. });
  194. // update cell html
  195. cell.dom.innerHTML = cellHTML;
  196. td = td.next();
  197. }
  198. }, me);
  199. // results found
  200. if (me.currentIndex !== null) {
  201. me.getSelectionModel().select(me.currentIndex);
  202. me.statusBar.setStatus({
  203. text: count + ' matche(s) found.',
  204. iconCls: 'x-status-valid'
  205. });
  206. }
  207. }
  208. // no results found
  209. if (me.currentIndex === null) {
  210. me.getSelectionModel().deselectAll();
  211. }
  212. // force textfield focus
  213. me.textField.focus();
  214. },
  215. /**
  216. * Selects the previous row containing a match.
  217. * @private
  218. */
  219. onPreviousClick: function() {
  220. var me = this,
  221. idx;
  222. if ((idx = Ext.Array.indexOf(me.indexes, me.currentIndex)) !== -1) {
  223. me.currentIndex = me.indexes[idx - 1] || me.indexes[me.indexes.length - 1];
  224. me.getSelectionModel().select(me.currentIndex);
  225. }
  226. },
  227. /**
  228. * Selects the next row containing a match.
  229. * @private
  230. */
  231. onNextClick: function() {
  232. var me = this,
  233. idx;
  234. if ((idx = Ext.Array.indexOf(me.indexes, me.currentIndex)) !== -1) {
  235. me.currentIndex = me.indexes[idx + 1] || me.indexes[0];
  236. me.getSelectionModel().select(me.currentIndex);
  237. }
  238. },
  239. /**
  240. * Switch to case sensitive mode.
  241. * @private
  242. */
  243. caseSensitiveToggle: function(checkbox, checked) {
  244. this.caseSensitive = checked;
  245. this.onTextFieldChange();
  246. },
  247. /**
  248. * Switch to regular expression mode
  249. * @private
  250. */
  251. regExpToggle: function(checkbox, checked) {
  252. this.regExpMode = checked;
  253. this.onTextFieldChange();
  254. }
  255. });