SearchController.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  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._matchesElement = this._searchControlElement.createChild("label", "search-results-matches");
  47. this._matchesElement.setAttribute("for", "search-input-field");
  48. this._searchNavigationElement = this._searchControlElement.createChild("div", "toolbar-search-navigation-controls");
  49. this._toggleFilterUI(false);
  50. this._searchNavigationPrevElement = this._searchNavigationElement.createChild("div", "toolbar-search-navigation toolbar-search-navigation-prev");
  51. this._searchNavigationPrevElement.addEventListener("click", this._onPrevButtonSearch.bind(this), false);
  52. this._searchNavigationPrevElement.title = WebInspector.UIString("Search Previous");
  53. this._searchNavigationNextElement = this._searchNavigationElement.createChild("div", "toolbar-search-navigation toolbar-search-navigation-next");
  54. this._searchNavigationNextElement.addEventListener("click", this._onNextButtonSearch.bind(this), false);
  55. this._searchNavigationNextElement.title = WebInspector.UIString("Search Next");
  56. this._searchInputElement.addEventListener("mousedown", this._onSearchFieldManualFocus.bind(this), false); // when the search field is manually selected
  57. this._searchInputElement.addEventListener("keydown", this._onKeyDown.bind(this), true);
  58. this._searchInputElement.addEventListener("input", this._onInput.bind(this), false);
  59. this._replaceInputElement = this._secondRowElement.createChild("td").createChild("input", "search-replace toolbar-replace-control");
  60. this._replaceInputElement.addEventListener("keydown", this._onKeyDown.bind(this), true);
  61. this._replaceInputElement.placeholder = WebInspector.UIString("Replace");
  62. // Column 2
  63. this._findButtonElement = this._firstRowElement.createChild("td").createChild("button", "hidden");
  64. this._findButtonElement.textContent = WebInspector.UIString("Find");
  65. this._findButtonElement.tabIndex = -1;
  66. this._findButtonElement.addEventListener("click", this._onNextButtonSearch.bind(this), false);
  67. this._replaceButtonElement = this._secondRowElement.createChild("td").createChild("button");
  68. this._replaceButtonElement.textContent = WebInspector.UIString("Replace");
  69. this._replaceButtonElement.disabled = true;
  70. this._replaceButtonElement.tabIndex = -1;
  71. this._replaceButtonElement.addEventListener("click", this._replace.bind(this), false);
  72. // Column 3
  73. this._prevButtonElement = this._firstRowElement.createChild("td").createChild("button", "hidden");
  74. this._prevButtonElement.textContent = WebInspector.UIString("Previous");
  75. this._prevButtonElement.disabled = true;
  76. this._prevButtonElement.tabIndex = -1;
  77. this._prevButtonElement.addEventListener("click", this._onPrevButtonSearch.bind(this), false);
  78. this._replaceAllButtonElement = this._secondRowElement.createChild("td").createChild("button");
  79. this._replaceAllButtonElement.textContent = WebInspector.UIString("Replace All");
  80. this._replaceAllButtonElement.addEventListener("click", this._replaceAll.bind(this), false);
  81. // Column 4
  82. this._replaceElement = this._firstRowElement.createChild("td").createChild("span");
  83. this._replaceCheckboxElement = this._replaceElement.createChild("input");
  84. this._replaceCheckboxElement.type = "checkbox";
  85. this._replaceCheckboxElement.id = "search-replace-trigger";
  86. this._replaceCheckboxElement.addEventListener("click", this._updateSecondRowVisibility.bind(this), false);
  87. this._replaceLabelElement = this._replaceElement.createChild("label");
  88. this._replaceLabelElement.textContent = WebInspector.UIString("Replace");
  89. this._replaceLabelElement.setAttribute("for", "search-replace-trigger");
  90. // Column 5
  91. this._filterCheckboxContainer = this._firstRowElement.createChild("td").createChild("label");
  92. this._filterCheckboxContainer.setAttribute("for", "filter-trigger");
  93. this._filterCheckboxElement = this._filterCheckboxContainer.createChild("input");
  94. this._filterCheckboxElement.type = "checkbox";
  95. this._filterCheckboxElement.id = "filter-trigger";
  96. this._filterCheckboxElement.addEventListener("click", this._filterCheckboxClick.bind(this), false);
  97. this._filterCheckboxContainer.createTextChild(WebInspector.UIString("Filter"));
  98. // Column 6
  99. var cancelButtonElement = this._firstRowElement.createChild("td").createChild("button");
  100. cancelButtonElement.textContent = WebInspector.UIString("Cancel");
  101. cancelButtonElement.tabIndex = -1;
  102. cancelButtonElement.addEventListener("click", this.closeSearch.bind(this), false);
  103. }
  104. WebInspector.SearchController.prototype = {
  105. /**
  106. * @param {number} matches
  107. * @param {WebInspector.Searchable} provider
  108. */
  109. updateSearchMatchesCount: function(matches, provider)
  110. {
  111. provider.currentSearchMatches = matches;
  112. if (provider === this._searchProvider)
  113. this._updateSearchMatchesCountAndCurrentMatchIndex(provider.currentQuery ? matches : 0, -1);
  114. },
  115. /**
  116. * @param {number} currentMatchIndex
  117. * @param {WebInspector.Searchable} provider
  118. */
  119. updateCurrentMatchIndex: function(currentMatchIndex, provider)
  120. {
  121. if (provider === this._searchProvider)
  122. this._updateSearchMatchesCountAndCurrentMatchIndex(provider.currentSearchMatches, currentMatchIndex);
  123. },
  124. isSearchVisible: function()
  125. {
  126. return this._searchIsVisible;
  127. },
  128. closeSearch: function()
  129. {
  130. this.cancelSearch();
  131. WebInspector.setCurrentFocusElement(WebInspector.previousFocusElement());
  132. },
  133. cancelSearch: function()
  134. {
  135. if (!this._searchIsVisible)
  136. return;
  137. if (this._filterCheckboxElement.checked) {
  138. this._filterCheckboxElement.checked = false;
  139. this._toggleFilterUI(false);
  140. this.resetFilter();
  141. } else
  142. this.resetSearch();
  143. delete this._searchIsVisible;
  144. this._searchHost.setFooterElement(null);
  145. this.resetSearch();
  146. delete this._searchHost;
  147. delete this._searchProvider;
  148. },
  149. resetSearch: function()
  150. {
  151. this._clearSearch();
  152. this._updateReplaceVisibility();
  153. this._matchesElement.textContent = "";
  154. },
  155. /**
  156. * @param {Event} event
  157. * @return {boolean}
  158. */
  159. handleShortcut: function(event)
  160. {
  161. var isMac = WebInspector.isMac();
  162. switch (event.keyIdentifier) {
  163. case "U+0046": // F key
  164. if (isMac)
  165. var isFindKey = event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey;
  166. else
  167. var isFindKey = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey;
  168. if (isFindKey) {
  169. this.showSearchField();
  170. event.consume(true);
  171. return true;
  172. }
  173. break;
  174. case "F3":
  175. if (!isMac) {
  176. this.showSearchField();
  177. event.consume(true);
  178. return true;
  179. }
  180. break;
  181. case "U+0047": // G key
  182. if (isMac && event.metaKey && !event.ctrlKey && !event.altKey && this._searchHost) {
  183. if (event.shiftKey)
  184. this._searchProvider.jumpToPreviousSearchResult();
  185. else
  186. this._searchProvider.jumpToNextSearchResult();
  187. event.consume(true);
  188. return true;
  189. }
  190. break;
  191. }
  192. return false;
  193. },
  194. /**
  195. * @param {boolean} enabled
  196. */
  197. _updateSearchNavigationButtonState: function(enabled)
  198. {
  199. this._replaceButtonElement.disabled = !enabled;
  200. this._prevButtonElement.disabled = !enabled;
  201. if (enabled) {
  202. this._searchNavigationPrevElement.addStyleClass("enabled");
  203. this._searchNavigationNextElement.addStyleClass("enabled");
  204. } else {
  205. this._searchNavigationPrevElement.removeStyleClass("enabled");
  206. this._searchNavigationNextElement.removeStyleClass("enabled");
  207. }
  208. },
  209. /**
  210. * @param {number} matches
  211. * @param {number} currentMatchIndex
  212. */
  213. _updateSearchMatchesCountAndCurrentMatchIndex: function(matches, currentMatchIndex)
  214. {
  215. if (!this._currentQuery)
  216. this._matchesElement.textContent = "";
  217. else if (matches === 0 || currentMatchIndex >= 0)
  218. this._matchesElement.textContent = WebInspector.UIString("%d of %d", currentMatchIndex + 1, matches);
  219. else if (matches === 1)
  220. this._matchesElement.textContent = WebInspector.UIString("1 match");
  221. else
  222. this._matchesElement.textContent = WebInspector.UIString("%d matches", matches);
  223. this._updateSearchNavigationButtonState(matches > 0);
  224. },
  225. showSearchField: function()
  226. {
  227. if (this._searchIsVisible)
  228. this.cancelSearch();
  229. if (WebInspector.drawer.element.isAncestor(document.activeElement) && WebInspector.drawer.getSearchProvider())
  230. this._searchHost = WebInspector.drawer;
  231. else
  232. this._searchHost = WebInspector.inspectorView;
  233. this._searchProvider = this._searchHost.getSearchProvider();
  234. this._searchHost.setFooterElement(this._element);
  235. this._updateReplaceVisibility();
  236. this._updateFilterVisibility();
  237. if (WebInspector.currentFocusElement() !== this._searchInputElement) {
  238. var selection = window.getSelection();
  239. if (selection.rangeCount) {
  240. var queryCandidate = selection.toString().replace(/\r?\n.*/, "");
  241. if (queryCandidate)
  242. this._searchInputElement.value = queryCandidate;
  243. }
  244. }
  245. this._performSearch(false, false);
  246. this._searchInputElement.focus();
  247. this._searchInputElement.select();
  248. this._searchIsVisible = true;
  249. },
  250. /**
  251. * @param {boolean} filter
  252. */
  253. _toggleFilterUI: function(filter)
  254. {
  255. this._matchesElement.enableStyleClass("hidden", filter);
  256. this._searchNavigationElement.enableStyleClass("hidden", filter);
  257. this._searchInputElement.placeholder = filter ? WebInspector.UIString("Filter") : WebInspector.UIString("Find");
  258. },
  259. _updateFilterVisibility: function()
  260. {
  261. if (this._searchProvider.canFilter())
  262. this._filterCheckboxContainer.removeStyleClass("hidden");
  263. else
  264. this._filterCheckboxContainer.addStyleClass("hidden");
  265. },
  266. _updateReplaceVisibility: function()
  267. {
  268. if (!this._searchProvider)
  269. return;
  270. if (this._searchProvider.canSearchAndReplace())
  271. this._replaceElement.removeStyleClass("hidden");
  272. else {
  273. this._replaceElement.addStyleClass("hidden");
  274. this._replaceCheckboxElement.checked = false;
  275. this._updateSecondRowVisibility();
  276. }
  277. },
  278. /**
  279. * @param {Event} event
  280. */
  281. _onSearchFieldManualFocus: function(event)
  282. {
  283. WebInspector.setCurrentFocusElement(event.target);
  284. },
  285. /**
  286. * @param {KeyboardEvent} event
  287. */
  288. _onKeyDown: function(event)
  289. {
  290. if (isEnterKey(event)) {
  291. if (event.target === this._searchInputElement) {
  292. // FIXME: This won't start backwards search with Shift+Enter correctly.
  293. if (!this._currentQuery)
  294. this._performSearch(true, true);
  295. else
  296. this._jumpToNextSearchResult(event.shiftKey);
  297. } else if (event.target === this._replaceInputElement)
  298. this._replace();
  299. }
  300. },
  301. /**
  302. * @param {boolean=} isBackwardSearch
  303. */
  304. _jumpToNextSearchResult: function(isBackwardSearch)
  305. {
  306. if (!this._currentQuery || !this._searchNavigationPrevElement.hasStyleClass("enabled"))
  307. return;
  308. if (isBackwardSearch)
  309. this._searchProvider.jumpToPreviousSearchResult();
  310. else
  311. this._searchProvider.jumpToNextSearchResult();
  312. },
  313. _onNextButtonSearch: function(event)
  314. {
  315. if (!this._searchNavigationNextElement.hasStyleClass("enabled"))
  316. return;
  317. // Simulate next search on search-navigation-button click.
  318. this._jumpToNextSearchResult();
  319. this._searchInputElement.focus();
  320. },
  321. _onPrevButtonSearch: function(event)
  322. {
  323. if (!this._searchNavigationPrevElement.hasStyleClass("enabled"))
  324. return;
  325. // Simulate previous search on search-navigation-button click.
  326. this._jumpToNextSearchResult(true);
  327. this._searchInputElement.focus();
  328. },
  329. _clearSearch: function()
  330. {
  331. delete this._currentQuery;
  332. if (this._searchHost){
  333. var searchProvider = this._searchHost.getSearchProvider();
  334. if (searchProvider && !!searchProvider.currentQuery) {
  335. delete searchProvider.currentQuery;
  336. searchProvider.searchCanceled();
  337. }
  338. }
  339. this._updateSearchMatchesCountAndCurrentMatchIndex(0, -1);
  340. },
  341. /**
  342. * @param {boolean} forceSearch
  343. * @param {boolean} shouldJump
  344. */
  345. _performSearch: function(forceSearch, shouldJump)
  346. {
  347. var query = this._searchInputElement.value;
  348. var minimalSearchQuerySize = this._searchProvider.minimalSearchQuerySize();
  349. if (!query || !this._searchProvider || (!forceSearch && query.length < minimalSearchQuerySize && !this._currentQuery)) {
  350. this._clearSearch();
  351. return;
  352. }
  353. this._currentQuery = query;
  354. this._searchProvider.currentQuery = query;
  355. this._searchProvider.performSearch(query, shouldJump);
  356. },
  357. _updateSecondRowVisibility: function()
  358. {
  359. if (!this._searchIsVisible || !this._searchHost)
  360. return;
  361. if (this._replaceCheckboxElement.checked) {
  362. this._element.addStyleClass("toolbar-search-replace");
  363. this._secondRowElement.removeStyleClass("hidden");
  364. this._prevButtonElement.removeStyleClass("hidden");
  365. this._findButtonElement.removeStyleClass("hidden");
  366. this._replaceCheckboxElement.tabIndex = -1;
  367. this._replaceInputElement.focus();
  368. } else {
  369. this._element.removeStyleClass("toolbar-search-replace");
  370. this._secondRowElement.addStyleClass("hidden");
  371. this._prevButtonElement.addStyleClass("hidden");
  372. this._findButtonElement.addStyleClass("hidden");
  373. this._replaceCheckboxElement.tabIndex = 0;
  374. this._searchInputElement.focus();
  375. }
  376. this._searchHost.setFooterElement(this._element);
  377. },
  378. _replace: function()
  379. {
  380. this._searchProvider.replaceSelectionWith(this._replaceInputElement.value);
  381. delete this._currentQuery;
  382. this._performSearch(true, true);
  383. },
  384. _replaceAll: function()
  385. {
  386. this._searchProvider.replaceAllWith(this._searchInputElement.value, this._replaceInputElement.value);
  387. },
  388. _filterCheckboxClick: function()
  389. {
  390. this._searchInputElement.focus();
  391. this._searchInputElement.select();
  392. if (this._filterCheckboxElement.checked) {
  393. this._toggleFilterUI(true);
  394. this.resetSearch();
  395. this._performFilter(this._searchInputElement.value);
  396. } else {
  397. this._toggleFilterUI(false);
  398. this.resetFilter();
  399. this._performSearch(false, false);
  400. }
  401. },
  402. /**
  403. * @param {string} query
  404. */
  405. _performFilter: function(query)
  406. {
  407. this._searchProvider.performFilter(query);
  408. },
  409. _onInput: function(event)
  410. {
  411. if (this._filterCheckboxElement.checked)
  412. this._performFilter(event.target.value);
  413. else
  414. this._performSearch(false, true);
  415. },
  416. resetFilter: function()
  417. {
  418. this._performFilter("");
  419. }
  420. }
  421. /**
  422. * @type {?WebInspector.SearchController}
  423. */
  424. WebInspector.searchController = null;
  425. /**
  426. * @interface
  427. */
  428. WebInspector.Searchable = function()
  429. {
  430. }
  431. WebInspector.Searchable.prototype = {
  432. /**
  433. * @return {boolean}
  434. */
  435. canSearchAndReplace: function() { },
  436. /**
  437. * @return {boolean}
  438. */
  439. canFilter: function() { },
  440. searchCanceled: function() { },
  441. /**
  442. * @param {string} query
  443. * @param {boolean} shouldJump
  444. * @param {WebInspector.Searchable=} self
  445. */
  446. performSearch: function(query, shouldJump, self) { },
  447. /**
  448. * @return {number}
  449. */
  450. minimalSearchQuerySize: function() { },
  451. /**
  452. * @param {WebInspector.Searchable=} self
  453. */
  454. jumpToNextSearchResult: function(self) { },
  455. /**
  456. * @param {WebInspector.Searchable=} self
  457. */
  458. jumpToPreviousSearchResult: function(self) { },
  459. }