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