SearchController.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. /*
  2. * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
  3. * Copyright (C) 2007 Matt Lilek (pewtermoose@gmail.com).
  4. * Copyright (C) 2009 Joseph Pecoraro
  5. * Copyright (C) 2011 Google Inc. All rights reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions
  9. * are met:
  10. *
  11. * 1. Redistributions of source code must retain the above copyright
  12. * notice, this list of conditions and the following disclaimer.
  13. * 2. Redistributions in binary form must reproduce the above copyright
  14. * notice, this list of conditions and the following disclaimer in the
  15. * documentation and/or other materials provided with the distribution.
  16. * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
  17. * its contributors may be used to endorse or promote products derived
  18. * from this software without specific prior written permission.
  19. *
  20. * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
  21. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  22. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  23. * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
  24. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  25. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  26. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  27. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  28. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  29. * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  30. */
  31. /**
  32. * @constructor
  33. */
  34. WebInspector.SearchController = function()
  35. {
  36. this._element = document.createElement("table");
  37. this._element.className = "toolbar-search";
  38. this._element.cellSpacing = 0;
  39. this._firstRowElement = this._element.createChild("tr");
  40. this._secondRowElement = this._element.createChild("tr", "hidden");
  41. // Column 1
  42. var searchControlElementColumn = this._firstRowElement.createChild("td");
  43. this._searchControlElement = searchControlElementColumn.createChild("span", "toolbar-search-control");
  44. this._searchInputElement = this._searchControlElement.createChild("input", "search-replace");
  45. this._searchInputElement.id = "search-input-field";
  46. this._searchInputElement.placeholder = WebInspector.UIString("Find");
  47. this._filterControlElement = searchControlElementColumn.createChild("span", "toolbar-search-control");
  48. this._filterControlElement.addStyleClass("hidden");
  49. this._filterInputElement = this._filterControlElement.createChild("input", "filter");
  50. this._filterInputElement.id = "filter-input-field";
  51. this._filterInputElement.placeholder = WebInspector.UIString("Filter");
  52. this._matchesElement = this._searchControlElement.createChild("label", "search-results-matches");
  53. this._matchesElement.setAttribute("for", "search-input-field");
  54. var searchNavigationElement = this._searchControlElement.createChild("div", "toolbar-search-navigation-controls");
  55. this._searchNavigationPrevElement = searchNavigationElement.createChild("div", "toolbar-search-navigation toolbar-search-navigation-prev");
  56. this._searchNavigationPrevElement.addEventListener("click", this._onPrevButtonSearch.bind(this), false);
  57. this._searchNavigationPrevElement.title = WebInspector.UIString("Search Previous");
  58. this._searchNavigationNextElement = searchNavigationElement.createChild("div", "toolbar-search-navigation toolbar-search-navigation-next");
  59. this._searchNavigationNextElement.addEventListener("click", this._onNextButtonSearch.bind(this), false);
  60. this._searchNavigationNextElement.title = WebInspector.UIString("Search Next");
  61. this._searchInputElement.addEventListener("mousedown", this._onSearchFieldManualFocus.bind(this), false); // when the search field is manually selected
  62. this._searchInputElement.addEventListener("keydown", this._onKeyDown.bind(this), true);
  63. this._filterInputElement.addEventListener("keydown", this._onKeyDown.bind(this), true);
  64. this._filterInputElement.addEventListener("input", this._onFilterInput.bind(this), false);
  65. this._searchInputElement.addEventListener("input", this._onSearchInput.bind(this), false);
  66. this._replaceInputElement = this._secondRowElement.createChild("td").createChild("input", "search-replace toolbar-replace-control");
  67. this._replaceInputElement.addEventListener("keydown", this._onKeyDown.bind(this), true);
  68. this._replaceInputElement.placeholder = WebInspector.UIString("Replace");
  69. // Column 2
  70. this._findButtonElement = this._firstRowElement.createChild("td").createChild("button", "hidden");
  71. this._findButtonElement.textContent = WebInspector.UIString("Find");
  72. this._findButtonElement.tabIndex = -1;
  73. this._findButtonElement.addEventListener("click", this._onNextButtonSearch.bind(this), false);
  74. this._replaceButtonElement = this._secondRowElement.createChild("td").createChild("button");
  75. this._replaceButtonElement.textContent = WebInspector.UIString("Replace");
  76. this._replaceButtonElement.disabled = true;
  77. this._replaceButtonElement.tabIndex = -1;
  78. this._replaceButtonElement.addEventListener("click", this._replace.bind(this), false);
  79. // Column 3
  80. this._prevButtonElement = this._firstRowElement.createChild("td").createChild("button", "hidden");
  81. this._prevButtonElement.textContent = WebInspector.UIString("Previous");
  82. this._prevButtonElement.disabled = true;
  83. this._prevButtonElement.tabIndex = -1;
  84. this._prevButtonElement.addEventListener("click", this._onPrevButtonSearch.bind(this), false);
  85. this._replaceAllButtonElement = this._secondRowElement.createChild("td").createChild("button");
  86. this._replaceAllButtonElement.textContent = WebInspector.UIString("Replace All");
  87. this._replaceAllButtonElement.addEventListener("click", this._replaceAll.bind(this), false);
  88. // Column 4
  89. this._replaceElement = this._firstRowElement.createChild("td").createChild("span");
  90. this._replaceCheckboxElement = this._replaceElement.createChild("input");
  91. this._replaceCheckboxElement.type = "checkbox";
  92. this._replaceCheckboxElement.id = "search-replace-trigger";
  93. this._replaceCheckboxElement.addEventListener("click", this._updateSecondRowVisibility.bind(this), false);
  94. this._replaceLabelElement = this._replaceElement.createChild("label");
  95. this._replaceLabelElement.textContent = WebInspector.UIString("Replace");
  96. this._replaceLabelElement.setAttribute("for", "search-replace-trigger");
  97. // Column 5
  98. this._filterCheckboxContainer = this._firstRowElement.createChild("td").createChild("span");
  99. this._filterCheckboxElement = this._filterCheckboxContainer.createChild("input");
  100. this._filterCheckboxElement.type = "checkbox";
  101. this._filterCheckboxElement.id = "filter-trigger";
  102. this._filterCheckboxElement.addEventListener("click", this._filterCheckboxClick.bind(this), false);
  103. this._filterLabelElement = this._filterCheckboxContainer.createChild("label");
  104. this._filterLabelElement.textContent = WebInspector.UIString("Filter");
  105. this._filterLabelElement.setAttribute("for", "filter-trigger");
  106. // Column 6
  107. var cancelButtonElement = this._firstRowElement.createChild("td").createChild("button");
  108. cancelButtonElement.textContent = WebInspector.UIString("Cancel");
  109. cancelButtonElement.tabIndex = -1;
  110. cancelButtonElement.addEventListener("click", this.cancelSearch.bind(this), false);
  111. }
  112. WebInspector.SearchController.prototype = {
  113. updateSearchMatchesCount: function(matches, panel)
  114. {
  115. if (!panel)
  116. panel = WebInspector.inspectorView.currentPanel();
  117. panel.currentSearchMatches = matches;
  118. if (panel === WebInspector.inspectorView.currentPanel())
  119. this._updateSearchMatchesCountAndCurrentMatchIndex(WebInspector.inspectorView.currentPanel().currentQuery ? matches : 0, -1);
  120. },
  121. updateCurrentMatchIndex: function(currentMatchIndex, panel)
  122. {
  123. if (panel === WebInspector.inspectorView.currentPanel())
  124. this._updateSearchMatchesCountAndCurrentMatchIndex(panel.currentSearchMatches, currentMatchIndex);
  125. },
  126. cancelSearch: function()
  127. {
  128. if (!this._searchIsVisible)
  129. return;
  130. if (this._filterCheckboxElement.checked) {
  131. this._filterCheckboxElement.checked = false;
  132. this._switchFilterToSearch();
  133. }
  134. delete this._searchIsVisible;
  135. WebInspector.inspectorView.setFooterElement(null);
  136. this.resetSearch();
  137. },
  138. resetSearch: function()
  139. {
  140. this._performSearch("", false, false);
  141. this._updateReplaceVisibility();
  142. this._matchesElement.textContent = "";
  143. },
  144. disableSearchUntilExplicitAction: function()
  145. {
  146. this._performSearch("", false, false);
  147. },
  148. /**
  149. * @param {Event} event
  150. * @return {boolean}
  151. */
  152. handleShortcut: function(event)
  153. {
  154. var isMac = WebInspector.isMac();
  155. switch (event.keyIdentifier) {
  156. case "U+0046": // F key
  157. if (isMac)
  158. var isFindKey = event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey;
  159. else
  160. var isFindKey = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey;
  161. if (isFindKey) {
  162. this.showSearchField();
  163. event.consume(true);
  164. return true;
  165. }
  166. break;
  167. case "F3":
  168. if (!isMac) {
  169. this.showSearchField();
  170. event.consume();
  171. }
  172. break;
  173. case "U+0047": // G key
  174. var currentPanel = WebInspector.inspectorView.currentPanel();
  175. if (isMac && event.metaKey && !event.ctrlKey && !event.altKey) {
  176. if (event.shiftKey)
  177. currentPanel.jumpToPreviousSearchResult();
  178. else
  179. currentPanel.jumpToNextSearchResult();
  180. event.consume(true);
  181. return true;
  182. }
  183. break;
  184. }
  185. return false;
  186. },
  187. _updateSearchNavigationButtonState: function(enabled)
  188. {
  189. this._replaceButtonElement.disabled = !enabled;
  190. this._prevButtonElement.disabled = !enabled;
  191. var panel = WebInspector.inspectorView.currentPanel();
  192. if (enabled) {
  193. this._searchNavigationPrevElement.addStyleClass("enabled");
  194. this._searchNavigationNextElement.addStyleClass("enabled");
  195. } else {
  196. this._searchNavigationPrevElement.removeStyleClass("enabled");
  197. this._searchNavigationNextElement.removeStyleClass("enabled");
  198. }
  199. },
  200. /**
  201. * @param {number} matches
  202. * @param {number} currentMatchIndex
  203. */
  204. _updateSearchMatchesCountAndCurrentMatchIndex: function(matches, currentMatchIndex)
  205. {
  206. if (matches === 0 || currentMatchIndex >= 0)
  207. this._matchesElement.textContent = WebInspector.UIString("%d of %d", currentMatchIndex + 1, matches);
  208. this._updateSearchNavigationButtonState(matches > 0);
  209. },
  210. showSearchField: function()
  211. {
  212. WebInspector.inspectorView.setFooterElement(this._element);
  213. this._updateReplaceVisibility();
  214. this._updateFilterVisibility();
  215. if (WebInspector.currentFocusElement() !== this._searchInputElement) {
  216. var selection = window.getSelection();
  217. if (selection.rangeCount)
  218. this._searchInputElement.value = selection.toString().replace(/\r?\n.*/, "");
  219. }
  220. this._performSearch(this._searchInputElement.value, true, false);
  221. this._searchInputElement.focus();
  222. this._searchInputElement.select();
  223. this._searchIsVisible = true;
  224. },
  225. _switchFilterToSearch: function()
  226. {
  227. this._filterControlElement.addStyleClass("hidden");
  228. this._searchControlElement.removeStyleClass("hidden");
  229. this._searchInputElement.focus();
  230. this._searchInputElement.select();
  231. this._searchInputElement.value = this._filterInputElement.value;
  232. this.resetFilter();
  233. },
  234. _switchSearchToFilter: function()
  235. {
  236. this._filterControlElement.removeStyleClass("hidden");
  237. this._searchControlElement.addStyleClass("hidden");
  238. this._filterInputElement.focus();
  239. this._filterInputElement.select();
  240. this._filterInputElement.value = this._searchInputElement.value;
  241. this.resetSearch();
  242. },
  243. _updateFilterVisibility: function()
  244. {
  245. if (WebInspector.inspectorView.currentPanel().canFilter())
  246. this._filterCheckboxContainer.removeStyleClass("hidden");
  247. else
  248. this._filterCheckboxContainer.addStyleClass("hidden");
  249. },
  250. _updateReplaceVisibility: function()
  251. {
  252. var panel = WebInspector.inspectorView.currentPanel();
  253. if (panel && panel.canSearchAndReplace())
  254. this._replaceElement.removeStyleClass("hidden");
  255. else {
  256. this._replaceElement.addStyleClass("hidden");
  257. this._replaceCheckboxElement.checked = false;
  258. this._updateSecondRowVisibility();
  259. }
  260. },
  261. _onSearchFieldManualFocus: function(event)
  262. {
  263. WebInspector.setCurrentFocusElement(event.target);
  264. },
  265. _onKeyDown: function(event)
  266. {
  267. // Escape Key will clear the field and clear the search results
  268. if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code) {
  269. event.consume(true);
  270. this.cancelSearch();
  271. WebInspector.setCurrentFocusElement(WebInspector.previousFocusElement());
  272. if (WebInspector.currentFocusElement() === event.target)
  273. WebInspector.currentFocusElement().select();
  274. return false;
  275. }
  276. if (isEnterKey(event)) {
  277. if (event.target === this._searchInputElement)
  278. this._performSearch(event.target.value, true, event.shiftKey);
  279. else if (event.target === this._replaceInputElement)
  280. this._replace();
  281. }
  282. },
  283. _onNextButtonSearch: function(event)
  284. {
  285. // Simulate next search on search-navigation-button click.
  286. this._performSearch(this._searchInputElement.value, true, false);
  287. this._searchInputElement.focus();
  288. },
  289. _onPrevButtonSearch: function(event)
  290. {
  291. if (!this._searchNavigationPrevElement.hasStyleClass("enabled"))
  292. return;
  293. // Simulate previous search on search-navigation-button click.
  294. this._performSearch(this._searchInputElement.value, true, true);
  295. this._searchInputElement.focus();
  296. },
  297. /**
  298. * @param {string} query
  299. * @param {boolean} forceSearch
  300. * @param {boolean} isBackwardSearch
  301. */
  302. _performSearch: function(query, forceSearch, isBackwardSearch)
  303. {
  304. if (!query || !query.length) {
  305. delete this._currentQuery;
  306. for (var panelName in WebInspector.panels) {
  307. var panel = WebInspector.panels[panelName];
  308. var hadCurrentQuery = !!panel.currentQuery;
  309. delete panel.currentQuery;
  310. if (hadCurrentQuery)
  311. panel.searchCanceled();
  312. }
  313. this._updateSearchMatchesCountAndCurrentMatchIndex(0, -1);
  314. return;
  315. }
  316. var currentPanel = WebInspector.inspectorView.currentPanel();
  317. if (query === currentPanel.currentQuery && currentPanel.currentQuery === this._currentQuery) {
  318. // When this is the same query and a forced search, jump to the next
  319. // search result for a good user experience.
  320. if (forceSearch) {
  321. if (!isBackwardSearch)
  322. currentPanel.jumpToNextSearchResult();
  323. else if (isBackwardSearch)
  324. currentPanel.jumpToPreviousSearchResult();
  325. }
  326. return;
  327. }
  328. if (!forceSearch && query.length < 3 && !this._currentQuery)
  329. return;
  330. this._currentQuery = query;
  331. currentPanel.currentQuery = query;
  332. currentPanel.performSearch(query);
  333. },
  334. _updateSecondRowVisibility: function()
  335. {
  336. if (!this._searchIsVisible)
  337. return;
  338. if (this._replaceCheckboxElement.checked) {
  339. this._element.addStyleClass("toolbar-search-replace");
  340. this._secondRowElement.removeStyleClass("hidden");
  341. this._prevButtonElement.removeStyleClass("hidden");
  342. this._findButtonElement.removeStyleClass("hidden");
  343. this._replaceCheckboxElement.tabIndex = -1;
  344. this._replaceInputElement.focus();
  345. } else {
  346. this._element.removeStyleClass("toolbar-search-replace");
  347. this._secondRowElement.addStyleClass("hidden");
  348. this._prevButtonElement.addStyleClass("hidden");
  349. this._findButtonElement.addStyleClass("hidden");
  350. this._replaceCheckboxElement.tabIndex = 0;
  351. this._searchInputElement.focus();
  352. }
  353. WebInspector.inspectorView.setFooterElement(this._element);
  354. },
  355. _replace: function()
  356. {
  357. var currentPanel = WebInspector.inspectorView.currentPanel();
  358. currentPanel.replaceSelectionWith(this._replaceInputElement.value);
  359. var query = this._currentQuery;
  360. delete this._currentQuery;
  361. this._performSearch(query, true, false);
  362. },
  363. _replaceAll: function()
  364. {
  365. var currentPanel = WebInspector.inspectorView.currentPanel();
  366. currentPanel.replaceAllWith(this._searchInputElement.value, this._replaceInputElement.value);
  367. },
  368. _filterCheckboxClick: function()
  369. {
  370. if (this._filterCheckboxElement.checked) {
  371. this._switchSearchToFilter();
  372. this._performFilter(this._filterInputElement.value);
  373. } else {
  374. this._switchFilterToSearch();
  375. this._performSearch(this._searchInputElement.value, false, false);
  376. }
  377. },
  378. /**
  379. * @param {string} query
  380. */
  381. _performFilter: function(query)
  382. {
  383. WebInspector.inspectorView.currentPanel().performFilter(query);
  384. },
  385. _onFilterInput: function(event)
  386. {
  387. this._performFilter(event.target.value);
  388. },
  389. _onSearchInput: function(event)
  390. {
  391. this._performSearch(event.target.value, false, false);
  392. },
  393. resetFilter: function()
  394. {
  395. this._performFilter("");
  396. }
  397. }
  398. /**
  399. * @type {?WebInspector.SearchController}
  400. */
  401. WebInspector.searchController = null;