blob: e3087a81dd37dd4311402e5815f7d9ecf508e9f8 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../core/common/common.js';
import * as i18n from '../core/i18n/i18n.js';
import * as UI from '../ui/legacy/legacy.js';
import {SearchConfig, SearchResult, SearchScope} from './SearchConfig.js'; // eslint-disable-line no-unused-vars
import {SearchResultsPane} from './SearchResultsPane.js';
const UIStrings = {
/**
*@description Title of a search bar or tool
*/
search: 'Search',
/**
*@description Accessibility label for search query text box
*/
searchQuery: 'Search Query',
/**
*@description Text to search by matching case of the input
*/
matchCase: 'Match Case',
/**
*@description Text for searching with regular expressinn
*/
useRegularExpression: 'Use Regular Expression',
/**
*@description Text to refresh the page
*/
refresh: 'Refresh',
/**
*@description Text to clear content
*/
clear: 'Clear',
/**
*@description Search message element text content in Search View of the Search tab
*/
indexing: 'Indexing…',
/**
*@description Text to indicate the searching is in progress
*/
searching: 'Searching…',
/**
*@description Text in Search View of the Search tab
*/
indexingInterrupted: 'Indexing interrupted.',
/**
*@description Search results message element text content in Search View of the Search tab
*/
foundMatchingLineInFile: 'Found 1 matching line in 1 file.',
/**
*@description Search results message element text content in Search View of the Search tab
*@example {2} PH1
*/
foundDMatchingLinesInFile: 'Found {PH1} matching lines in 1 file.',
/**
*@description Search results message element text content in Search View of the Search tab
*@example {2} PH1
*@example {2} PH2
*/
foundDMatchingLinesInDFiles: 'Found {PH1} matching lines in {PH2} files.',
/**
*@description Search results message element text content in Search View of the Search tab
*/
noMatchesFound: 'No matches found.',
/**
*@description Text in Search View of the Search tab
*/
searchFinished: 'Search finished.',
/**
*@description Text in Search View of the Search tab
*/
searchInterrupted: 'Search interrupted.',
};
const str_ = i18n.i18n.registerUIStrings('search/SearchView.js', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class SearchView extends UI.Widget.VBox {
/**
* @param {string} settingKey
*/
constructor(settingKey) {
super(true);
this.setMinimumSize(0, 40);
this.registerRequiredCSS('search/searchView.css', {enableLegacyPatching: false});
this._focusOnShow = false;
this._isIndexing = false;
this._searchId = 1;
this._searchMatchesCount = 0;
this._searchResultsCount = 0;
this._nonEmptySearchResultsCount = 0;
/** @type {?UI.Widget.Widget} */
this._searchingView = null;
/** @type {?UI.Widget.Widget} */
this._notFoundView = null;
/** @type {?SearchConfig} */
this._searchConfig = null;
/** @type {?SearchConfig} */
this._pendingSearchConfig = null;
/** @type {?SearchResultsPane} */
this._searchResultsPane = null;
/** @type {?UI.ProgressIndicator.ProgressIndicator} */
this._progressIndicator = null;
/** @type {?UI.Widget.Widget} */
this._visiblePane = null;
this.contentElement.classList.add('search-view');
this._searchPanelElement = this.contentElement.createChild('div', 'search-drawer-header');
this._searchResultsElement = this.contentElement.createChild('div');
this._searchResultsElement.className = 'search-results';
const searchContainer = document.createElement('div');
searchContainer.style.flex = 'auto';
searchContainer.style.justifyContent = 'start';
searchContainer.style.maxWidth = '300px';
this._search = UI.HistoryInput.HistoryInput.create();
this._search.addEventListener('keydown', event => {
this._onKeyDown(/** @type {!KeyboardEvent} */ (event));
});
searchContainer.appendChild(this._search);
this._search.placeholder = i18nString(UIStrings.search);
this._search.setAttribute('type', 'text');
this._search.setAttribute('results', '0');
this._search.setAttribute('size', '42');
UI.ARIAUtils.setAccessibleName(this._search, i18nString(UIStrings.searchQuery));
const searchItem = new UI.Toolbar.ToolbarItem(searchContainer);
const toolbar = new UI.Toolbar.Toolbar('search-toolbar', this._searchPanelElement);
this._matchCaseButton = SearchView._appendToolbarToggle(toolbar, 'Aa', i18nString(UIStrings.matchCase));
this._regexButton = SearchView._appendToolbarToggle(toolbar, '.*', i18nString(UIStrings.useRegularExpression));
toolbar.appendToolbarItem(searchItem);
const refreshButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.refresh), 'largeicon-refresh');
const clearButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.clear), 'largeicon-clear');
toolbar.appendToolbarItem(refreshButton);
toolbar.appendToolbarItem(clearButton);
refreshButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => this._onAction());
clearButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => {
this._resetSearch();
this._onSearchInputClear();
});
const searchStatusBarElement = this.contentElement.createChild('div', 'search-toolbar-summary');
this._searchMessageElement = searchStatusBarElement.createChild('div', 'search-message');
this._searchProgressPlaceholderElement = searchStatusBarElement.createChild('div', 'flex-centered');
this._searchResultsMessageElement = searchStatusBarElement.createChild('div', 'search-message');
this._advancedSearchConfig = Common.Settings.Settings.instance().createLocalSetting(
settingKey + 'SearchConfig', new SearchConfig('', true, false).toPlainObject());
this._load();
/** @type {?SearchScope} */
this._searchScope = null;
}
/**
* @param {!UI.Toolbar.Toolbar} toolbar
* @param {string} text
* @param {string} tooltip
* @return {!UI.Toolbar.ToolbarToggle}
*/
static _appendToolbarToggle(toolbar, text, tooltip) {
const toggle = new UI.Toolbar.ToolbarToggle(tooltip);
toggle.setText(text);
toggle.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => toggle.setToggled(!toggle.toggled()));
toolbar.appendToolbarItem(toggle);
return toggle;
}
/**
* @return {!SearchConfig}
*/
_buildSearchConfig() {
return new SearchConfig(this._search.value, !this._matchCaseButton.toggled(), this._regexButton.toggled());
}
/**
* @param {string} queryCandidate
* @param {boolean=} searchImmediately
*/
async toggle(queryCandidate, searchImmediately) {
if (queryCandidate) {
this._search.value = queryCandidate;
}
if (this.isShowing()) {
this.focus();
} else {
this._focusOnShow = true;
}
this._initScope();
if (searchImmediately) {
this._onAction();
} else {
this._startIndexing();
}
}
/**
* @protected
* @return {!SearchScope}
*/
createScope() {
throw new Error('Not implemented');
}
_initScope() {
this._searchScope = this.createScope();
}
/**
* @override
*/
wasShown() {
if (this._focusOnShow) {
this.focus();
this._focusOnShow = false;
}
}
_onIndexingFinished() {
if (!this._progressIndicator) {
return;
}
const finished = !this._progressIndicator.isCanceled();
this._progressIndicator.done();
this._progressIndicator = null;
this._isIndexing = false;
this._indexingFinished(finished);
if (!finished) {
this._pendingSearchConfig = null;
}
if (!this._pendingSearchConfig) {
return;
}
const searchConfig = this._pendingSearchConfig;
this._pendingSearchConfig = null;
this._innerStartSearch(searchConfig);
}
_startIndexing() {
this._isIndexing = true;
if (this._progressIndicator) {
this._progressIndicator.done();
}
this._progressIndicator = new UI.ProgressIndicator.ProgressIndicator();
this._searchMessageElement.textContent = i18nString(UIStrings.indexing);
this._progressIndicator.show(this._searchProgressPlaceholderElement);
if (this._searchScope) {
this._searchScope.performIndexing(
new Common.Progress.ProgressProxy(this._progressIndicator, this._onIndexingFinished.bind(this)));
}
}
_onSearchInputClear() {
this._search.value = '';
this._save();
this.focus();
}
/**
* @param {number} searchId
* @param {!SearchResult} searchResult
*/
_onSearchResult(searchId, searchResult) {
if (searchId !== this._searchId || !this._progressIndicator) {
return;
}
if (this._progressIndicator && this._progressIndicator.isCanceled()) {
this._onIndexingFinished();
return;
}
this._addSearchResult(searchResult);
if (!searchResult.matchesCount()) {
return;
}
if (!this._searchResultsPane) {
this._searchResultsPane = new SearchResultsPane(/** @type {!SearchConfig} */ (this._searchConfig));
this._showPane(this._searchResultsPane);
}
this._searchResultsPane.addSearchResult(searchResult);
}
/**
* @param {number} searchId
* @param {boolean} finished
*/
_onSearchFinished(searchId, finished) {
if (searchId !== this._searchId || !this._progressIndicator) {
return;
}
if (!this._searchResultsPane) {
this._nothingFound();
}
this._searchFinished(finished);
this._searchConfig = null;
UI.ARIAUtils.alert(
this._searchMessageElement.textContent + ' ' + this._searchResultsMessageElement.textContent,
this._searchMessageElement);
}
/**
* @param {!SearchConfig} searchConfig
*/
async _startSearch(searchConfig) {
this._resetSearch();
++this._searchId;
this._initScope();
if (!this._isIndexing) {
this._startIndexing();
}
this._pendingSearchConfig = searchConfig;
}
/**
*
* @param {!SearchConfig} searchConfig
*/
_innerStartSearch(searchConfig) {
this._searchConfig = searchConfig;
if (this._progressIndicator) {
this._progressIndicator.done();
}
this._progressIndicator = new UI.ProgressIndicator.ProgressIndicator();
this._searchStarted(this._progressIndicator);
if (this._searchScope) {
this._searchScope.performSearch(
searchConfig, this._progressIndicator, this._onSearchResult.bind(this, this._searchId),
this._onSearchFinished.bind(this, this._searchId));
}
}
_resetSearch() {
this._stopSearch();
this._showPane(null);
this._searchResultsPane = null;
this._clearSearchMessage();
}
_clearSearchMessage() {
this._searchMessageElement.textContent = '';
this._searchResultsMessageElement.textContent = '';
}
_stopSearch() {
if (this._progressIndicator && !this._isIndexing) {
this._progressIndicator.cancel();
}
if (this._searchScope) {
this._searchScope.stopSearch();
}
this._searchConfig = null;
}
/**
* @param {!UI.ProgressIndicator.ProgressIndicator} progressIndicator
*/
_searchStarted(progressIndicator) {
this._resetCounters();
if (!this._searchingView) {
this._searchingView = new UI.EmptyWidget.EmptyWidget(i18nString(UIStrings.searching));
}
this._showPane(this._searchingView);
this._searchMessageElement.textContent = i18nString(UIStrings.searching);
progressIndicator.show(this._searchProgressPlaceholderElement);
this._updateSearchResultsMessage();
}
/**
* @param {boolean} finished
*/
_indexingFinished(finished) {
this._searchMessageElement.textContent = finished ? '' : i18nString(UIStrings.indexingInterrupted);
}
_updateSearchResultsMessage() {
if (this._searchMatchesCount && this._searchResultsCount) {
if (this._searchMatchesCount === 1 && this._nonEmptySearchResultsCount === 1) {
this._searchResultsMessageElement.textContent = i18nString(UIStrings.foundMatchingLineInFile);
} else if (this._searchMatchesCount > 1 && this._nonEmptySearchResultsCount === 1) {
this._searchResultsMessageElement.textContent =
i18nString(UIStrings.foundDMatchingLinesInFile, {PH1: this._searchMatchesCount});
} else {
this._searchResultsMessageElement.textContent = i18nString(
UIStrings.foundDMatchingLinesInDFiles,
{PH1: this._searchMatchesCount, PH2: this._nonEmptySearchResultsCount});
}
} else {
this._searchResultsMessageElement.textContent = '';
}
}
/**
* @param {?UI.Widget.Widget} panel
*/
_showPane(panel) {
if (this._visiblePane) {
this._visiblePane.detach();
}
if (panel) {
panel.show(this._searchResultsElement);
}
this._visiblePane = panel;
}
_resetCounters() {
this._searchMatchesCount = 0;
this._searchResultsCount = 0;
this._nonEmptySearchResultsCount = 0;
}
_nothingFound() {
if (!this._notFoundView) {
this._notFoundView = new UI.EmptyWidget.EmptyWidget(i18nString(UIStrings.noMatchesFound));
}
this._showPane(this._notFoundView);
this._searchResultsMessageElement.textContent = i18nString(UIStrings.noMatchesFound);
}
/**
* @param {!SearchResult} searchResult
*/
_addSearchResult(searchResult) {
const matchesCount = searchResult.matchesCount();
this._searchMatchesCount += matchesCount;
this._searchResultsCount++;
if (matchesCount) {
this._nonEmptySearchResultsCount++;
}
this._updateSearchResultsMessage();
}
/**
* @param {boolean} finished
*/
_searchFinished(finished) {
this._searchMessageElement.textContent =
finished ? i18nString(UIStrings.searchFinished) : i18nString(UIStrings.searchInterrupted);
}
/**
* @override
*/
focus() {
this._search.focus();
this._search.select();
}
/**
* @override
*/
willHide() {
this._stopSearch();
}
/**
* @param {!KeyboardEvent} event
*/
_onKeyDown(event) {
this._save();
switch (event.keyCode) {
case UI.KeyboardShortcut.Keys.Enter.code:
this._onAction();
break;
}
}
_save() {
this._advancedSearchConfig.set(this._buildSearchConfig().toPlainObject());
}
_load() {
const searchConfig = SearchConfig.fromPlainObject(this._advancedSearchConfig.get());
this._search.value = searchConfig.query();
this._matchCaseButton.setToggled(!searchConfig.ignoreCase());
this._regexButton.setToggled(searchConfig.isRegex());
}
_onAction() {
// Resetting alert variable to prime for next search query result.
UI.ARIAUtils.alert(' ', this._searchMessageElement);
const searchConfig = this._buildSearchConfig();
if (!searchConfig.query() || !searchConfig.query().length) {
return;
}
this._startSearch(searchConfig);
}
}