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