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