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.
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
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.
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.
34 * @extends {WebInspector.VBox}
35 * @param {!WebInspector.Searchable} searchable
36 * @param {string=} settingName
38 WebInspector.SearchableView = function(searchable, settingName)
40 WebInspector.VBox.call(this);
42 this._searchProvider = searchable;
43 this._settingName = settingName;
45 this.element.addEventListener("keydown", this._onKeyDown.bind(this), false);
47 this._footerElementContainer = this.element.createChild("div", "search-bar status-bar hidden");
48 this._footerElementContainer.style.order = 100;
50 this._footerElement = this._footerElementContainer.createChild("table", "toolbar-search");
51 this._footerElement.cellSpacing = 0;
53 this._firstRowElement = this._footerElement.createChild("tr");
54 this._secondRowElement = this._footerElement.createChild("tr", "hidden");
56 if (this._searchProvider.supportsCaseSensitiveSearch() || this._searchProvider.supportsRegexSearch()) {
57 var searchSettingsPrefixColumn = this._firstRowElement.createChild("td");
58 searchSettingsPrefixColumn.createChild("div", "search-settings-prefix")
59 this._secondRowElement.createChild("td");
62 if (this._searchProvider.supportsCaseSensitiveSearch()) {
63 var caseSensitiveColumn = this._firstRowElement.createChild("td");
64 this._caseSensitiveButton = new WebInspector.StatusBarTextButton(WebInspector.UIString("Case sensitive"), "case-sensitive-search", "Aa", 2);
65 this._caseSensitiveButton.addEventListener("click", this._toggleCaseSensitiveSearch, this);
66 caseSensitiveColumn.appendChild(this._caseSensitiveButton.element);
67 this._secondRowElement.createChild("td");
70 if (this._searchProvider.supportsRegexSearch()) {
71 var regexColumn = this._firstRowElement.createChild("td");
72 this._regexButton = new WebInspector.StatusBarTextButton(WebInspector.UIString("Regex"), "regex-search", ".*", 2);
73 this._regexButton.addEventListener("click", this._toggleRegexSearch, this);
74 regexColumn.appendChild(this._regexButton.element);
75 this._secondRowElement.createChild("td");
79 var searchControlElementColumn = this._firstRowElement.createChild("td");
80 this._searchControlElement = searchControlElementColumn.createChild("span", "toolbar-search-control");
81 this._searchInputElement = this._searchControlElement.createChild("input", "search-replace");
82 this._searchInputElement.id = "search-input-field";
83 this._searchInputElement.placeholder = WebInspector.UIString("Find");
85 this._matchesElement = this._searchControlElement.createChild("label", "search-results-matches");
86 this._matchesElement.setAttribute("for", "search-input-field");
88 this._searchNavigationElement = this._searchControlElement.createChild("div", "toolbar-search-navigation-controls");
90 this._searchNavigationPrevElement = this._searchNavigationElement.createChild("div", "toolbar-search-navigation toolbar-search-navigation-prev");
91 this._searchNavigationPrevElement.addEventListener("click", this._onPrevButtonSearch.bind(this), false);
92 this._searchNavigationPrevElement.title = WebInspector.UIString("Search Previous");
94 this._searchNavigationNextElement = this._searchNavigationElement.createChild("div", "toolbar-search-navigation toolbar-search-navigation-next");
95 this._searchNavigationNextElement.addEventListener("click", this._onNextButtonSearch.bind(this), false);
96 this._searchNavigationNextElement.title = WebInspector.UIString("Search Next");
98 this._searchInputElement.addEventListener("mousedown", this._onSearchFieldManualFocus.bind(this), false); // when the search field is manually selected
99 this._searchInputElement.addEventListener("keydown", this._onSearchKeyDown.bind(this), true);
100 this._searchInputElement.addEventListener("input", this._onInput.bind(this), false);
102 this._replaceInputElement = this._secondRowElement.createChild("td").createChild("input", "search-replace toolbar-replace-control");
103 this._replaceInputElement.addEventListener("keydown", this._onReplaceKeyDown.bind(this), true);
104 this._replaceInputElement.placeholder = WebInspector.UIString("Replace");
107 this._findButtonElement = this._firstRowElement.createChild("td").createChild("button", "search-action-button hidden");
108 this._findButtonElement.textContent = WebInspector.UIString("Find");
109 this._findButtonElement.tabIndex = -1;
110 this._findButtonElement.addEventListener("click", this._onFindClick.bind(this), false);
112 this._replaceButtonElement = this._secondRowElement.createChild("td").createChild("button", "search-action-button");
113 this._replaceButtonElement.textContent = WebInspector.UIString("Replace");
114 this._replaceButtonElement.disabled = true;
115 this._replaceButtonElement.tabIndex = -1;
116 this._replaceButtonElement.addEventListener("click", this._replace.bind(this), false);
119 this._prevButtonElement = this._firstRowElement.createChild("td").createChild("button", "search-action-button hidden");
120 this._prevButtonElement.textContent = WebInspector.UIString("Previous");
121 this._prevButtonElement.tabIndex = -1;
122 this._prevButtonElement.addEventListener("click", this._onPreviousClick.bind(this), false);
124 this._replaceAllButtonElement = this._secondRowElement.createChild("td").createChild("button", "search-action-button");
125 this._replaceAllButtonElement.textContent = WebInspector.UIString("Replace All");
126 this._replaceAllButtonElement.addEventListener("click", this._replaceAll.bind(this), false);
129 this._replaceElement = this._firstRowElement.createChild("td").createChild("span");
131 this._replaceCheckboxElement = this._replaceElement.createChild("input");
132 this._replaceCheckboxElement.type = "checkbox";
133 this._uniqueId = ++WebInspector.SearchableView._lastUniqueId;
134 var replaceCheckboxId = "search-replace-trigger" + this._uniqueId;
135 this._replaceCheckboxElement.id = replaceCheckboxId;
136 this._replaceCheckboxElement.addEventListener("change", this._updateSecondRowVisibility.bind(this), false);
138 this._replaceLabelElement = this._replaceElement.createChild("label");
139 this._replaceLabelElement.textContent = WebInspector.UIString("Replace");
140 this._replaceLabelElement.setAttribute("for", replaceCheckboxId);
143 var cancelButtonElement = this._firstRowElement.createChild("td").createChild("button", "search-action-button");
144 cancelButtonElement.textContent = WebInspector.UIString("Cancel");
145 cancelButtonElement.tabIndex = -1;
146 cancelButtonElement.addEventListener("click", this.closeSearch.bind(this), false);
147 this._minimalSearchQuerySize = 3;
149 this._registerShortcuts();
153 WebInspector.SearchableView._lastUniqueId = 0;
156 * @return {!Array.<!WebInspector.KeyboardShortcut.Descriptor>}
158 WebInspector.SearchableView.findShortcuts = function()
160 if (WebInspector.SearchableView._findShortcuts)
161 return WebInspector.SearchableView._findShortcuts;
162 WebInspector.SearchableView._findShortcuts = [WebInspector.KeyboardShortcut.makeDescriptor("f", WebInspector.KeyboardShortcut.Modifiers.CtrlOrMeta)];
163 if (!WebInspector.isMac())
164 WebInspector.SearchableView._findShortcuts.push(WebInspector.KeyboardShortcut.makeDescriptor(WebInspector.KeyboardShortcut.Keys.F3));
165 return WebInspector.SearchableView._findShortcuts;
169 * @return {!Array.<!WebInspector.KeyboardShortcut.Descriptor>}
171 WebInspector.SearchableView.cancelSearchShortcuts = function()
173 if (WebInspector.SearchableView._cancelSearchShortcuts)
174 return WebInspector.SearchableView._cancelSearchShortcuts;
175 WebInspector.SearchableView._cancelSearchShortcuts = [WebInspector.KeyboardShortcut.makeDescriptor(WebInspector.KeyboardShortcut.Keys.Esc)];
176 return WebInspector.SearchableView._cancelSearchShortcuts;
180 * @return {!Array.<!WebInspector.KeyboardShortcut.Descriptor>}
182 WebInspector.SearchableView.findNextShortcut = function()
184 if (WebInspector.SearchableView._findNextShortcut)
185 return WebInspector.SearchableView._findNextShortcut;
186 WebInspector.SearchableView._findNextShortcut = [];
187 if (WebInspector.isMac())
188 WebInspector.SearchableView._findNextShortcut.push(WebInspector.KeyboardShortcut.makeDescriptor("g", WebInspector.KeyboardShortcut.Modifiers.Meta));
189 return WebInspector.SearchableView._findNextShortcut;
193 * @return {!Array.<!WebInspector.KeyboardShortcut.Descriptor>}
195 WebInspector.SearchableView.findPreviousShortcuts = function()
197 if (WebInspector.SearchableView._findPreviousShortcuts)
198 return WebInspector.SearchableView._findPreviousShortcuts;
199 WebInspector.SearchableView._findPreviousShortcuts = [];
200 if (WebInspector.isMac())
201 WebInspector.SearchableView._findPreviousShortcuts.push(WebInspector.KeyboardShortcut.makeDescriptor("g", WebInspector.KeyboardShortcut.Modifiers.Meta | WebInspector.KeyboardShortcut.Modifiers.Shift));
202 return WebInspector.SearchableView._findPreviousShortcuts;
205 WebInspector.SearchableView.prototype = {
206 _toggleCaseSensitiveSearch: function()
208 this._caseSensitiveButton.toggled = !this._caseSensitiveButton.toggled;
210 this._performSearch(false, true);
213 _toggleRegexSearch: function()
215 this._regexButton.toggled = !this._regexButton.toggled;
217 this._performSearch(false, true);
221 * @return {?WebInspector.Setting}
225 if (!this._settingName)
227 if (!WebInspector.settings[this._settingName])
228 WebInspector.settings[this._settingName] = WebInspector.settings.createSetting(this._settingName, {});
229 return WebInspector.settings[this._settingName];
232 _saveSetting: function()
234 var setting = this._setting();
237 var settingValue = setting.get() || {};
238 settingValue.caseSensitive = this._caseSensitiveButton.toggled;
239 settingValue.isRegex = this._regexButton.toggled;
240 setting.set(settingValue);
243 _loadSetting: function()
245 var settingValue = this._setting() ? (this._setting().get() || {}) : {};
246 if (this._searchProvider.supportsCaseSensitiveSearch())
247 this._caseSensitiveButton.toggled = !!settingValue.caseSensitive;
248 if (this._searchProvider.supportsRegexSearch())
249 this._regexButton.toggled = !!settingValue.isRegex;
255 defaultFocusedElement: function()
257 var children = this.children();
258 for (var i = 0; i < children.length; ++i) {
259 var element = children[i].defaultFocusedElement();
263 return WebInspector.View.prototype.defaultFocusedElement.call(this);
267 * @param {!Event} event
269 _onKeyDown: function(event)
271 var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(/**@type {!KeyboardEvent}*/(event));
272 var handler = this._shortcuts[shortcutKey];
273 if (handler && handler(event))
277 _registerShortcuts: function()
279 this._shortcuts = {};
282 * @param {!Array.<!WebInspector.KeyboardShortcut.Descriptor>} shortcuts
283 * @param {function()} handler
284 * @this {WebInspector.SearchableView}
286 function register(shortcuts, handler)
288 for (var i = 0; i < shortcuts.length; ++i)
289 this._shortcuts[shortcuts[i].key] = handler;
292 register.call(this, WebInspector.SearchableView.findShortcuts(), this.handleFindShortcut.bind(this));
293 register.call(this, WebInspector.SearchableView.cancelSearchShortcuts(), this.handleCancelSearchShortcut.bind(this));
294 register.call(this, WebInspector.SearchableView.findNextShortcut(), this.handleFindNextShortcut.bind(this));
295 register.call(this, WebInspector.SearchableView.findPreviousShortcuts(), this.handleFindPreviousShortcut.bind(this));
299 * @param {number} minimalSearchQuerySize
301 setMinimalSearchQuerySize: function(minimalSearchQuerySize)
303 this._minimalSearchQuerySize = minimalSearchQuerySize;
307 * @param {boolean} replaceable
309 setReplaceable: function(replaceable)
311 this._replaceable = replaceable;
315 * @param {number} matches
317 updateSearchMatchesCount: function(matches)
319 this._searchProvider.currentSearchMatches = matches;
320 this._updateSearchMatchesCountAndCurrentMatchIndex(this._searchProvider.currentQuery ? matches : 0, -1);
324 * @param {number} currentMatchIndex
326 updateCurrentMatchIndex: function(currentMatchIndex)
328 this._updateSearchMatchesCountAndCurrentMatchIndex(this._searchProvider.currentSearchMatches, currentMatchIndex);
334 isSearchVisible: function()
336 return this._searchIsVisible;
339 closeSearch: function()
342 if (WebInspector.currentFocusElement().isDescendant(this._footerElementContainer))
346 _toggleSearchBar: function(toggled)
348 this._footerElementContainer.classList.toggle("hidden", !toggled);
352 cancelSearch: function()
354 if (!this._searchIsVisible)
357 delete this._searchIsVisible;
358 this._toggleSearchBar(false);
361 resetSearch: function()
364 this._updateReplaceVisibility();
365 this._matchesElement.textContent = "";
368 refreshSearch: function()
370 if (!this._searchIsVisible)
373 this._performSearch(false, false);
379 handleFindNextShortcut: function()
381 if (!this._searchIsVisible)
383 this._searchProvider.jumpToNextSearchResult();
390 handleFindPreviousShortcut: function()
392 if (!this._searchIsVisible)
394 this._searchProvider.jumpToPreviousSearchResult();
401 handleFindShortcut: function()
403 this.showSearchField();
410 handleCancelSearchShortcut: function()
412 if (!this._searchIsVisible)
419 * @param {boolean} enabled
421 _updateSearchNavigationButtonState: function(enabled)
423 this._replaceButtonElement.disabled = !enabled;
425 this._searchNavigationPrevElement.classList.add("enabled");
426 this._searchNavigationNextElement.classList.add("enabled");
428 this._searchNavigationPrevElement.classList.remove("enabled");
429 this._searchNavigationNextElement.classList.remove("enabled");
434 * @param {number} matches
435 * @param {number} currentMatchIndex
437 _updateSearchMatchesCountAndCurrentMatchIndex: function(matches, currentMatchIndex)
439 if (!this._currentQuery)
440 this._matchesElement.textContent = "";
441 else if (matches === 0 || currentMatchIndex >= 0)
442 this._matchesElement.textContent = WebInspector.UIString("%d of %d", currentMatchIndex + 1, matches);
443 else if (matches === 1)
444 this._matchesElement.textContent = WebInspector.UIString("1 match");
446 this._matchesElement.textContent = WebInspector.UIString("%d matches", matches);
447 this._updateSearchNavigationButtonState(matches > 0);
450 showSearchField: function()
452 if (this._searchIsVisible)
456 if (WebInspector.currentFocusElement() !== this._searchInputElement) {
457 var selection = window.getSelection();
458 if (selection.rangeCount)
459 queryCandidate = selection.toString().replace(/\r?\n.*/, "");
462 this._toggleSearchBar(true);
463 this._updateReplaceVisibility();
465 this._searchInputElement.value = queryCandidate;
466 this._performSearch(false, false);
467 this._searchInputElement.focus();
468 this._searchInputElement.select();
469 this._searchIsVisible = true;
472 _updateReplaceVisibility: function()
474 this._replaceElement.classList.toggle("hidden", !this._replaceable);
475 if (!this._replaceable) {
476 this._replaceCheckboxElement.checked = false;
477 this._updateSecondRowVisibility();
482 * @param {!Event} event
484 _onSearchFieldManualFocus: function(event)
486 WebInspector.setCurrentFocusElement(event.target);
490 * @param {!Event} event
492 _onSearchKeyDown: function(event)
494 if (!isEnterKey(event))
497 if (!this._currentQuery)
498 this._performSearch(true, true, event.shiftKey);
500 this._jumpToNextSearchResult(event.shiftKey);
504 * @param {!Event} event
506 _onReplaceKeyDown: function(event)
508 if (isEnterKey(event))
513 * @param {boolean=} isBackwardSearch
515 _jumpToNextSearchResult: function(isBackwardSearch)
517 if (!this._currentQuery || !this._searchNavigationPrevElement.classList.contains("enabled"))
520 if (isBackwardSearch)
521 this._searchProvider.jumpToPreviousSearchResult();
523 this._searchProvider.jumpToNextSearchResult();
526 _onNextButtonSearch: function(event)
528 if (!this._searchNavigationNextElement.classList.contains("enabled"))
530 this._jumpToNextSearchResult();
531 this._searchInputElement.focus();
534 _onPrevButtonSearch: function(event)
536 if (!this._searchNavigationPrevElement.classList.contains("enabled"))
538 this._jumpToNextSearchResult(true);
539 this._searchInputElement.focus();
542 _onFindClick: function(event)
544 if (!this._currentQuery)
545 this._performSearch(true, true);
547 this._jumpToNextSearchResult();
548 this._searchInputElement.focus();
551 _onPreviousClick: function(event)
553 if (!this._currentQuery)
554 this._performSearch(true, true, true);
556 this._jumpToNextSearchResult(true);
557 this._searchInputElement.focus();
560 _clearSearch: function()
562 delete this._currentQuery;
563 if (!!this._searchProvider.currentQuery) {
564 delete this._searchProvider.currentQuery;
565 this._searchProvider.searchCanceled();
567 this._updateSearchMatchesCountAndCurrentMatchIndex(0, -1);
571 * @param {boolean} forceSearch
572 * @param {boolean} shouldJump
573 * @param {boolean=} jumpBackwards
575 _performSearch: function(forceSearch, shouldJump, jumpBackwards)
577 var query = this._searchInputElement.value;
578 if (!query || (!forceSearch && query.length < this._minimalSearchQuerySize && !this._currentQuery)) {
583 this._currentQuery = query;
584 this._searchProvider.currentQuery = query;
586 var searchConfig = this._currentSearchConfig();
587 this._searchProvider.performSearch(searchConfig, shouldJump, jumpBackwards);
591 * @return {!WebInspector.SearchableView.SearchConfig}
593 _currentSearchConfig: function()
595 var query = this._searchInputElement.value;
596 var caseSensitive = this._caseSensitiveButton ? this._caseSensitiveButton.toggled : false;
597 var isRegex = this._regexButton ? this._regexButton.toggled : false;
598 return new WebInspector.SearchableView.SearchConfig(query, caseSensitive, isRegex);
601 _updateSecondRowVisibility: function()
603 var secondRowVisible = this._replaceCheckboxElement.checked;
604 this._footerElementContainer.classList.toggle("replaceable", secondRowVisible);
605 this._footerElement.classList.toggle("toolbar-search-replace", secondRowVisible);
606 this._secondRowElement.classList.toggle("hidden", !secondRowVisible);
607 this._prevButtonElement.classList.toggle("hidden", !secondRowVisible);
608 this._findButtonElement.classList.toggle("hidden", !secondRowVisible);
609 this._replaceCheckboxElement.tabIndex = secondRowVisible ? -1 : 0;
611 if (secondRowVisible)
612 this._replaceInputElement.focus();
614 this._searchInputElement.focus();
620 var searchConfig = this._currentSearchConfig();
621 /** @type {!WebInspector.Replaceable} */ (this._searchProvider).replaceSelectionWith(searchConfig, this._replaceInputElement.value);
622 delete this._currentQuery;
623 this._performSearch(true, true);
626 _replaceAll: function()
628 var searchConfig = this._currentSearchConfig();
629 /** @type {!WebInspector.Replaceable} */ (this._searchProvider).replaceAllWith(searchConfig, this._replaceInputElement.value);
632 _onInput: function(event)
634 this._onValueChanged();
637 _onValueChanged: function()
639 this._performSearch(false, true);
642 __proto__: WebInspector.VBox.prototype
648 WebInspector.Searchable = function()
652 WebInspector.Searchable.prototype = {
653 searchCanceled: function() { },
656 * @param {!WebInspector.SearchableView.SearchConfig} searchConfig
657 * @param {boolean} shouldJump
658 * @param {boolean=} jumpBackwards
660 performSearch: function(searchConfig, shouldJump, jumpBackwards) { },
662 jumpToNextSearchResult: function() { },
664 jumpToPreviousSearchResult: function() { },
669 supportsCaseSensitiveSearch: function() { },
674 supportsRegexSearch: function() { }
680 WebInspector.Replaceable = function()
684 WebInspector.Replaceable.prototype = {
686 * @param {!WebInspector.SearchableView.SearchConfig} searchConfig
687 * @param {string} replacement
689 replaceSelectionWith: function(searchConfig, replacement) { },
692 * @param {!WebInspector.SearchableView.SearchConfig} searchConfig
693 * @param {string} replacement
695 replaceAllWith: function(searchConfig, replacement) { }
700 * @param {string} query
701 * @param {boolean} caseSensitive
702 * @param {boolean} isRegex
704 WebInspector.SearchableView.SearchConfig = function(query, caseSensitive, isRegex)
707 this.caseSensitive = caseSensitive;
708 this.isRegex = isRegex;