blob: a0fa78f0b803843014d7964f45e633aa57b369ea [file] [log] [blame]
Blink Reformat4c46d092018-04-07 15:32:371/*
2 * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
3 * Copyright (C) 2007 Matt Lilek ([email protected]).
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 * @unrestricted
34 */
35UI.SearchableView = class extends UI.VBox {
36 /**
37 * @param {!UI.Searchable} searchable
38 * @param {string=} settingName
39 */
40 constructor(searchable, settingName) {
41 super(true);
42 this.registerRequiredCSS('ui/searchableView.css');
43 this.element[UI.SearchableView._symbol] = this;
44
45 this._searchProvider = searchable;
46 this._setting = settingName ? Common.settings.createSetting(settingName, {}) : null;
47 this._replaceable = false;
48
49 this.contentElement.createChild('content');
50 this._footerElementContainer = this.contentElement.createChild('div', 'search-bar hidden');
51 this._footerElementContainer.style.order = 100;
52 this._footerElement = this._footerElementContainer.createChild('div', 'toolbar-search');
53
54 const replaceToggleToolbar = new UI.Toolbar('replace-toggle-toolbar', this._footerElement);
55 this._replaceToggleButton = new UI.ToolbarToggle(Common.UIString('Replace'), 'mediumicon-replace');
56 this._replaceToggleButton.addEventListener(UI.ToolbarButton.Events.Click, this._toggleReplace, this);
57 replaceToggleToolbar.appendToolbarItem(this._replaceToggleButton);
58
59 const searchInputElements = this._footerElement.createChild('div', 'toolbar-search-inputs');
60 const searchControlElement = searchInputElements.createChild('div', 'toolbar-search-control');
61
62 this._searchInputElement = UI.HistoryInput.create();
63 this._searchInputElement.classList.add('search-replace');
64 this._searchInputElement.id = 'search-input-field';
65 this._searchInputElement.placeholder = Common.UIString('Find');
66 searchControlElement.appendChild(this._searchInputElement);
67
68 this._matchesElement = searchControlElement.createChild('label', 'search-results-matches');
69 this._matchesElement.setAttribute('for', 'search-input-field');
70
71 const searchNavigationElement = searchControlElement.createChild('div', 'toolbar-search-navigation-controls');
72
73 this._searchNavigationPrevElement =
74 searchNavigationElement.createChild('div', 'toolbar-search-navigation toolbar-search-navigation-prev');
75 this._searchNavigationPrevElement.addEventListener('click', this._onPrevButtonSearch.bind(this), false);
76 this._searchNavigationPrevElement.title = Common.UIString('Search previous');
77
78 this._searchNavigationNextElement =
79 searchNavigationElement.createChild('div', 'toolbar-search-navigation toolbar-search-navigation-next');
80 this._searchNavigationNextElement.addEventListener('click', this._onNextButtonSearch.bind(this), false);
81 this._searchNavigationNextElement.title = Common.UIString('Search next');
82
83 this._searchInputElement.addEventListener('keydown', this._onSearchKeyDown.bind(this), true);
84 this._searchInputElement.addEventListener('input', this._onInput.bind(this), false);
85
86 this._replaceInputElement =
87 searchInputElements.createChild('input', 'search-replace toolbar-replace-control hidden');
88 this._replaceInputElement.addEventListener('keydown', this._onReplaceKeyDown.bind(this), true);
89 this._replaceInputElement.placeholder = Common.UIString('Replace');
90
91 this._buttonsContainer = this._footerElement.createChild('div', 'toolbar-search-buttons');
92 const firstRowButtons = this._buttonsContainer.createChild('div', 'first-row-buttons');
93
94 const toolbar = new UI.Toolbar('toolbar-search-options', firstRowButtons);
95
96 if (this._searchProvider.supportsCaseSensitiveSearch()) {
97 this._caseSensitiveButton = new UI.ToolbarToggle(Common.UIString('Match Case'));
98 this._caseSensitiveButton.setText('Aa');
99 this._caseSensitiveButton.addEventListener(UI.ToolbarButton.Events.Click, this._toggleCaseSensitiveSearch, this);
100 toolbar.appendToolbarItem(this._caseSensitiveButton);
101 }
102
103 if (this._searchProvider.supportsRegexSearch()) {
104 this._regexButton = new UI.ToolbarToggle(Common.UIString('Use Regular Expression'));
105 this._regexButton.setText('.*');
106 this._regexButton.addEventListener(UI.ToolbarButton.Events.Click, this._toggleRegexSearch, this);
107 toolbar.appendToolbarItem(this._regexButton);
108 }
109
110 const cancelButtonElement =
111 UI.createTextButton(Common.UIString('Cancel'), this.closeSearch.bind(this), 'search-action-button');
112 firstRowButtons.appendChild(cancelButtonElement);
113
114 this._secondRowButtons = this._buttonsContainer.createChild('div', 'second-row-buttons hidden');
115
116 this._replaceButtonElement =
117 UI.createTextButton(Common.UIString('Replace'), this._replace.bind(this), 'search-action-button');
118 this._replaceButtonElement.disabled = true;
119 this._secondRowButtons.appendChild(this._replaceButtonElement);
120
121 this._replaceAllButtonElement =
122 UI.createTextButton(Common.UIString('Replace all'), this._replaceAll.bind(this), 'search-action-button');
123 this._secondRowButtons.appendChild(this._replaceAllButtonElement);
124 this._replaceAllButtonElement.disabled = true;
125
126 this._minimalSearchQuerySize = 3;
127 this._loadSetting();
128 }
129
130 /**
131 * @param {?Element} element
132 * @return {?UI.SearchableView}
133 */
134 static fromElement(element) {
135 let view = null;
136 while (element && !view) {
137 view = element[UI.SearchableView._symbol];
138 element = element.parentElementOrShadowHost();
139 }
140 return view;
141 }
142
143 _toggleCaseSensitiveSearch() {
144 this._caseSensitiveButton.setToggled(!this._caseSensitiveButton.toggled());
145 this._saveSetting();
146 this._performSearch(false, true);
147 }
148
149 _toggleRegexSearch() {
150 this._regexButton.setToggled(!this._regexButton.toggled());
151 this._saveSetting();
152 this._performSearch(false, true);
153 }
154
155 _toggleReplace() {
156 this._replaceToggleButton.setToggled(!this._replaceToggleButton.toggled());
157 this._updateSecondRowVisibility();
158 }
159
160 _saveSetting() {
161 if (!this._setting)
162 return;
163 const settingValue = this._setting.get() || {};
164 settingValue.caseSensitive = this._caseSensitiveButton.toggled();
165 settingValue.isRegex = this._regexButton.toggled();
166 this._setting.set(settingValue);
167 }
168
169 _loadSetting() {
170 const settingValue = this._setting ? (this._setting.get() || {}) : {};
171 if (this._searchProvider.supportsCaseSensitiveSearch())
172 this._caseSensitiveButton.setToggled(!!settingValue.caseSensitive);
173 if (this._searchProvider.supportsRegexSearch())
174 this._regexButton.setToggled(!!settingValue.isRegex);
175 }
176
177 /**
178 * @param {number} minimalSearchQuerySize
179 */
180 setMinimalSearchQuerySize(minimalSearchQuerySize) {
181 this._minimalSearchQuerySize = minimalSearchQuerySize;
182 }
183
184 /**
185 * @param {string} placeholder
186 */
187 setPlaceholder(placeholder) {
188 this._searchInputElement.placeholder = placeholder;
189 }
190
191 /**
192 * @param {boolean} replaceable
193 */
194 setReplaceable(replaceable) {
195 this._replaceable = replaceable;
196 }
197
198 /**
199 * @param {number} matches
200 */
201 updateSearchMatchesCount(matches) {
202 if (this._searchProvider.currentSearchMatches === matches)
203 return;
204 this._searchProvider.currentSearchMatches = matches;
205 this._updateSearchMatchesCountAndCurrentMatchIndex(this._searchProvider.currentQuery ? matches : 0, -1);
206 }
207
208 /**
209 * @param {number} currentMatchIndex
210 */
211 updateCurrentMatchIndex(currentMatchIndex) {
212 this._updateSearchMatchesCountAndCurrentMatchIndex(this._searchProvider.currentSearchMatches, currentMatchIndex);
213 }
214
215 /**
216 * @return {boolean}
217 */
218 isSearchVisible() {
219 return this._searchIsVisible;
220 }
221
222 closeSearch() {
223 this.cancelSearch();
224 if (this._footerElementContainer.hasFocus())
225 this.focus();
226 }
227
228 _toggleSearchBar(toggled) {
229 this._footerElementContainer.classList.toggle('hidden', !toggled);
230 this.doResize();
231 }
232
233 cancelSearch() {
234 if (!this._searchIsVisible)
235 return;
236 this.resetSearch();
237 delete this._searchIsVisible;
238 this._toggleSearchBar(false);
239 }
240
241 resetSearch() {
242 this._clearSearch();
243 this._updateReplaceVisibility();
244 this._matchesElement.textContent = '';
245 }
246
247 refreshSearch() {
248 if (!this._searchIsVisible)
249 return;
250 this.resetSearch();
251 this._performSearch(false, false);
252 }
253
254 /**
255 * @return {boolean}
256 */
257 handleFindNextShortcut() {
258 if (!this._searchIsVisible)
259 return false;
260 this._searchProvider.jumpToNextSearchResult();
261 return true;
262 }
263
264 /**
265 * @return {boolean}
266 */
267 handleFindPreviousShortcut() {
268 if (!this._searchIsVisible)
269 return false;
270 this._searchProvider.jumpToPreviousSearchResult();
271 return true;
272 }
273
274 /**
275 * @return {boolean}
276 */
277 handleFindShortcut() {
278 this.showSearchField();
279 return true;
280 }
281
282 /**
283 * @return {boolean}
284 */
285 handleCancelSearchShortcut() {
286 if (!this._searchIsVisible)
287 return false;
288 this.closeSearch();
289 return true;
290 }
291
292 /**
293 * @param {boolean} enabled
294 */
295 _updateSearchNavigationButtonState(enabled) {
296 this._replaceButtonElement.disabled = !enabled;
297 this._replaceAllButtonElement.disabled = !enabled;
298 this._searchNavigationPrevElement.classList.toggle('enabled', enabled);
299 this._searchNavigationNextElement.classList.toggle('enabled', enabled);
300 }
301
302 /**
303 * @param {number} matches
304 * @param {number} currentMatchIndex
305 */
306 _updateSearchMatchesCountAndCurrentMatchIndex(matches, currentMatchIndex) {
307 if (!this._currentQuery)
308 this._matchesElement.textContent = '';
309 else if (matches === 0 || currentMatchIndex >= 0)
310 this._matchesElement.textContent = Common.UIString('%d of %d', currentMatchIndex + 1, matches);
311 else if (matches === 1)
312 this._matchesElement.textContent = Common.UIString('1 match');
313 else
314 this._matchesElement.textContent = Common.UIString('%d matches', matches);
315 this._updateSearchNavigationButtonState(matches > 0);
316 }
317
318 showSearchField() {
319 if (this._searchIsVisible)
320 this.cancelSearch();
321
322 let queryCandidate;
323 if (!this._searchInputElement.hasFocus()) {
324 const selection = UI.inspectorView.element.window().getSelection();
325 if (selection.rangeCount)
326 queryCandidate = selection.toString().replace(/\r?\n.*/, '');
327 }
328
329 this._toggleSearchBar(true);
330 this._updateReplaceVisibility();
331 if (queryCandidate)
332 this._searchInputElement.value = queryCandidate;
333 this._performSearch(false, false);
334 this._searchInputElement.focus();
335 this._searchInputElement.select();
336 this._searchIsVisible = true;
337 }
338
339 _updateReplaceVisibility() {
340 this._replaceToggleButton.setVisible(this._replaceable);
341 if (!this._replaceable) {
342 this._replaceToggleButton.setToggled(false);
343 this._updateSecondRowVisibility();
344 }
345 }
346
347 /**
348 * @param {!Event} event
349 */
350 _onSearchKeyDown(event) {
351 if (isEscKey(event)) {
352 this.closeSearch();
353 event.consume(true);
354 return;
355 }
356 if (!isEnterKey(event))
357 return;
358
359 if (!this._currentQuery)
360 this._performSearch(true, true, event.shiftKey);
361 else
362 this._jumpToNextSearchResult(event.shiftKey);
363 }
364
365 /**
366 * @param {!Event} event
367 */
368 _onReplaceKeyDown(event) {
369 if (isEnterKey(event))
370 this._replace();
371 }
372
373 /**
374 * @param {boolean=} isBackwardSearch
375 */
376 _jumpToNextSearchResult(isBackwardSearch) {
377 if (!this._currentQuery || !this._searchNavigationPrevElement.classList.contains('enabled'))
378 return;
379
380 if (isBackwardSearch)
381 this._searchProvider.jumpToPreviousSearchResult();
382 else
383 this._searchProvider.jumpToNextSearchResult();
384 }
385
386 _onNextButtonSearch(event) {
387 if (!this._searchNavigationNextElement.classList.contains('enabled'))
388 return;
389 this._jumpToNextSearchResult();
390 this._searchInputElement.focus();
391 }
392
393 _onPrevButtonSearch(event) {
394 if (!this._searchNavigationPrevElement.classList.contains('enabled'))
395 return;
396 this._jumpToNextSearchResult(true);
397 this._searchInputElement.focus();
398 }
399
400 _onFindClick(event) {
401 if (!this._currentQuery)
402 this._performSearch(true, true);
403 else
404 this._jumpToNextSearchResult();
405 this._searchInputElement.focus();
406 }
407
408 _onPreviousClick(event) {
409 if (!this._currentQuery)
410 this._performSearch(true, true, true);
411 else
412 this._jumpToNextSearchResult(true);
413 this._searchInputElement.focus();
414 }
415
416 _clearSearch() {
417 delete this._currentQuery;
418 if (!!this._searchProvider.currentQuery) {
419 delete this._searchProvider.currentQuery;
420 this._searchProvider.searchCanceled();
421 }
422 this._updateSearchMatchesCountAndCurrentMatchIndex(0, -1);
423 }
424
425 /**
426 * @param {boolean} forceSearch
427 * @param {boolean} shouldJump
428 * @param {boolean=} jumpBackwards
429 */
430 _performSearch(forceSearch, shouldJump, jumpBackwards) {
431 const query = this._searchInputElement.value;
432 if (!query || (!forceSearch && query.length < this._minimalSearchQuerySize && !this._currentQuery)) {
433 this._clearSearch();
434 return;
435 }
436
437 this._currentQuery = query;
438 this._searchProvider.currentQuery = query;
439
440 const searchConfig = this._currentSearchConfig();
441 this._searchProvider.performSearch(searchConfig, shouldJump, jumpBackwards);
442 }
443
444 /**
445 * @return {!UI.SearchableView.SearchConfig}
446 */
447 _currentSearchConfig() {
448 const query = this._searchInputElement.value;
449 const caseSensitive = this._caseSensitiveButton ? this._caseSensitiveButton.toggled() : false;
450 const isRegex = this._regexButton ? this._regexButton.toggled() : false;
451 return new UI.SearchableView.SearchConfig(query, caseSensitive, isRegex);
452 }
453
454 _updateSecondRowVisibility() {
455 const secondRowVisible = this._replaceToggleButton.toggled();
456 this._footerElementContainer.classList.toggle('replaceable', secondRowVisible);
457 this._secondRowButtons.classList.toggle('hidden', !secondRowVisible);
458 this._replaceInputElement.classList.toggle('hidden', !secondRowVisible);
459
460 if (secondRowVisible)
461 this._replaceInputElement.focus();
462 else
463 this._searchInputElement.focus();
464 this.doResize();
465 }
466
467 _replace() {
468 const searchConfig = this._currentSearchConfig();
469 /** @type {!UI.Replaceable} */ (this._searchProvider)
470 .replaceSelectionWith(searchConfig, this._replaceInputElement.value);
471 delete this._currentQuery;
472 this._performSearch(true, true);
473 }
474
475 _replaceAll() {
476 const searchConfig = this._currentSearchConfig();
477 /** @type {!UI.Replaceable} */ (this._searchProvider).replaceAllWith(searchConfig, this._replaceInputElement.value);
478 }
479
480 /**
481 * @param {!Event} event
482 */
483 _onInput(event) {
484 if (this._valueChangedTimeoutId)
485 clearTimeout(this._valueChangedTimeoutId);
486 const timeout = this._searchInputElement.value.length < 3 ? 200 : 0;
487 this._valueChangedTimeoutId = setTimeout(this._onValueChanged.bind(this), timeout);
488 }
489
490 _onValueChanged() {
491 if (!this._searchIsVisible)
492 return;
493 delete this._valueChangedTimeoutId;
494 this._performSearch(false, true);
495 }
496};
497
498
499UI.SearchableView._symbol = Symbol('searchableView');
500
501
502/**
503 * @interface
504 */
505UI.Searchable = function() {};
506
507UI.Searchable.prototype = {
508 searchCanceled() {},
509
510 /**
511 * @param {!UI.SearchableView.SearchConfig} searchConfig
512 * @param {boolean} shouldJump
513 * @param {boolean=} jumpBackwards
514 */
515 performSearch(searchConfig, shouldJump, jumpBackwards) {},
516
517 jumpToNextSearchResult() {},
518
519 jumpToPreviousSearchResult() {},
520
521 /**
522 * @return {boolean}
523 */
524 supportsCaseSensitiveSearch() {},
525
526 /**
527 * @return {boolean}
528 */
529 supportsRegexSearch() {}
530};
531
532/**
533 * @interface
534 */
535UI.Replaceable = function() {};
536
537UI.Replaceable.prototype = {
538 /**
539 * @param {!UI.SearchableView.SearchConfig} searchConfig
540 * @param {string} replacement
541 */
542 replaceSelectionWith(searchConfig, replacement) {},
543
544 /**
545 * @param {!UI.SearchableView.SearchConfig} searchConfig
546 * @param {string} replacement
547 */
548 replaceAllWith(searchConfig, replacement) {}
549};
550
551/**
552 * @unrestricted
553 */
554UI.SearchableView.SearchConfig = class {
555 /**
556 * @param {string} query
557 * @param {boolean} caseSensitive
558 * @param {boolean} isRegex
559 */
560 constructor(query, caseSensitive, isRegex) {
561 this.query = query;
562 this.caseSensitive = caseSensitive;
563 this.isRegex = isRegex;
564 }
565
566 /**
567 * @param {boolean=} global
568 * @return {!RegExp}
569 */
570 toSearchRegex(global) {
571 let modifiers = this.caseSensitive ? '' : 'i';
572 if (global)
573 modifiers += 'g';
574 const query = this.isRegex ? '/' + this.query + '/' : this.query;
575
576 let regex;
577
578 // First try creating regex if user knows the / / hint.
579 try {
580 if (/^\/.+\/$/.test(query)) {
581 regex = new RegExp(query.substring(1, query.length - 1), modifiers);
582 regex.__fromRegExpQuery = true;
583 }
584 } catch (e) {
585 // Silent catch.
586 }
587
588 // Otherwise just do a plain text search.
589 if (!regex)
590 regex = createPlainTextSearchRegex(query, modifiers);
591
592 return regex;
593 }
594};