chrome-devtools-frontend
Version:
Chrome DevTools UI
748 lines (662 loc) • 28.7 kB
text/typescript
// Copyright 2021 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.
/* eslint-disable rulesdir/no-imperative-dom-api */
/*
* Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
* Copyright (C) 2007 Matt Lilek (pewtermoose@gmail.com).
* Copyright (C) 2009 Joseph Pecoraro
* Copyright (C) 2011 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import './Toolbar.js'; // eslint-disable-line import/no-duplicates
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import * as Buttons from '../components/buttons/buttons.js';
import * as IconButton from '../components/icon_button/icon_button.js';
import * as ARIAUtils from './ARIAUtils.js';
import {InspectorView} from './InspectorView.js';
import searchableViewStyles from './searchableView.css.js';
import {ToolbarButton, ToolbarText, ToolbarToggle} from './Toolbar.js'; // eslint-disable-line import/no-duplicates
import {createHistoryInput, createTextButton} from './UIUtils.js';
import {VBox} from './Widget.js';
const UIStrings = {
/**
*@description Text on a button to replace one instance with input text for the ctrl+F search bar
*/
replace: 'Replace',
/**
*@description Tooltip text on a toggle to enable replacing one instance with input text for the ctrl+F search bar
*/
enableFindAndReplace: 'Find and replace',
/**
*@description Tooltip text on a toggle to disable replacing one instance with input text for the ctrl+F search bar
*/
disableFindAndReplace: 'Disable find and replace',
/**
*@description Text to find an item
*/
findString: 'Find',
/**
*@description Tooltip text on a button to search previous instance for the ctrl+F search bar
*/
searchPrevious: 'Show previous result',
/**
*@description Tooltip text on a button to search next instance for the ctrl+F search bar
*/
searchNext: 'Show next result',
/**
*@description Tooltip text on a toggle to enable search by matching case of the input
*/
enableCaseSensitive: 'Enable case sensitive search',
/**
*@description Tooltip text on a toggle to disable search by matching case of the input
*/
disableCaseSensitive: 'Disable case sensitive search',
/**
*@description Tooltip text on a toggle to enable searching with regular expression
*/
enableRegularExpression: 'Enable regular expressions',
/**
*@description Tooltip text on a toggle to disable searching with regular expression
*/
disableRegularExpression: 'Disable regular expressions',
/**
*@description Tooltip text on a button to close the search bar
*/
closeSearchBar: 'Close search bar',
/**
*@description Text on a button to replace all instances with input text for the ctrl+F search bar
*/
replaceAll: 'Replace all',
/**
*@description Text to indicate the current match index and the total number of matches for the ctrl+F search bar
*@example {2} PH1
*@example {3} PH2
*/
dOfD: '{PH1} of {PH2}',
/**
*@description Tooltip text to indicate the current match index and the total number of matches for the ctrl+F search bar
*@example {2} PH1
*@example {3} PH2
*/
accessibledOfD: 'Shows result {PH1} of {PH2}',
/**
*@description Text to indicate search result for the ctrl+F search bar
*/
matchString: '1 match',
/**
*@description Text to indicate search result for the ctrl+F search bar
*@example {2} PH1
*/
dMatches: '{PH1} matches',
/**
*@description Text on a button to search previous instance for the ctrl+F search bar
*/
clearInput: 'Clear',
} as const;
const str_ = i18n.i18n.registerUIStrings('ui/legacy/SearchableView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
function createClearButton(jslogContext: string): Buttons.Button.Button {
const button = new Buttons.Button.Button();
button.data = {
variant: Buttons.Button.Variant.ICON,
size: Buttons.Button.Size.SMALL,
jslogContext,
title: i18nString(UIStrings.clearInput),
iconName: 'cross-circle-filled',
};
button.ariaLabel = i18nString(UIStrings.clearInput);
button.classList.add('clear-button');
button.tabIndex = -1;
return button;
}
export class SearchableView extends VBox {
private searchProvider: Searchable;
private replaceProvider: Replaceable|null;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private setting: Common.Settings.Setting<any>|null;
private replaceable: boolean;
private readonly footerElementContainer: HTMLElement;
private readonly footerElement: HTMLElement;
private replaceToggleButton: ToolbarToggle;
private searchInputElement: HTMLInputElement;
private matchesElement: HTMLElement;
private searchNavigationPrevElement: ToolbarButton;
private searchNavigationNextElement: ToolbarButton;
private readonly replaceInputElement: HTMLInputElement;
private caseSensitiveButton: Buttons.Button.Button|undefined;
private regexButton: Buttons.Button.Button|undefined;
private replaceButtonElement: Buttons.Button.Button;
private replaceAllButtonElement: Buttons.Button.Button;
private minimalSearchQuerySize: number;
private searchIsVisible?: boolean;
private currentQuery?: string;
private valueChangedTimeoutId?: number;
constructor(searchable: Searchable, replaceable: Replaceable|null, settingName?: string) {
super(true);
this.registerRequiredCSS(searchableViewStyles);
searchableViewsByElement.set(this.element, this);
this.searchProvider = searchable;
this.replaceProvider = replaceable;
this.setting = settingName ? Common.Settings.Settings.instance().createSetting(settingName, {}) : null;
this.replaceable = false;
this.contentElement.createChild('slot');
this.footerElementContainer = this.contentElement.createChild('div', 'search-bar hidden');
this.footerElementContainer.style.order = '100';
this.footerElement = this.footerElementContainer.createChild('div', 'toolbar-search');
this.footerElement.setAttribute('jslog', `${VisualLogging.toolbar('search').track({resize: true})}`);
const replaceToggleToolbar = this.footerElement.createChild('devtools-toolbar', 'replace-toggle-toolbar');
this.replaceToggleButton =
new ToolbarToggle(i18nString(UIStrings.enableFindAndReplace), 'replace', undefined, 'replace');
ARIAUtils.setLabel(this.replaceToggleButton.element, i18nString(UIStrings.enableFindAndReplace));
this.replaceToggleButton.addEventListener(ToolbarButton.Events.CLICK, this.toggleReplace, this);
replaceToggleToolbar.appendToolbarItem(this.replaceToggleButton);
// Elements within `searchInputElements` are added according to their expected tab order.
const searchInputElements = this.footerElement.createChild('div', 'search-inputs');
const iconAndInput = searchInputElements.createChild('div', 'icon-and-input');
const searchIcon = IconButton.Icon.create('search');
iconAndInput.appendChild(searchIcon);
this.searchInputElement = createHistoryInput('search', 'search-replace search');
this.searchInputElement.id = 'search-input-field';
this.searchInputElement.autocomplete = 'off';
this.searchInputElement.placeholder = i18nString(UIStrings.findString);
this.searchInputElement.setAttribute(
'jslog',
`${VisualLogging.textField('search').track({change: true, keydown: 'ArrowUp|ArrowDown|Enter|Escape'})}`);
this.searchInputElement.addEventListener('keydown', this.onSearchKeyDown.bind(this), true);
this.searchInputElement.addEventListener('input', this.onInput.bind(this), false);
iconAndInput.appendChild(this.searchInputElement);
const replaceInputElements = searchInputElements.createChild('div', 'replace-element input-line');
this.replaceInputElement = replaceInputElements.createChild('input', 'search-replace');
this.replaceInputElement.addEventListener('keydown', this.onReplaceKeyDown.bind(this), true);
this.replaceInputElement.placeholder = i18nString(UIStrings.replace);
this.replaceInputElement.setAttribute(
'jslog', `${VisualLogging.textField('replace').track({change: true, keydown: 'Enter'})}`);
const replaceInputClearButton = createClearButton('clear-replace-input');
replaceInputClearButton.addEventListener('click', () => {
this.replaceInputElement.value = '';
this.replaceInputElement.focus();
});
replaceInputElements.appendChild(replaceInputClearButton);
const searchConfigButtons = searchInputElements.createChild('div', 'search-config-buttons');
const clearButton = createClearButton('clear-search-input');
clearButton.addEventListener('click', () => {
this.searchInputElement.value = '';
this.clearSearch();
this.searchInputElement.focus();
});
searchConfigButtons.appendChild(clearButton);
if (this.searchProvider.supportsRegexSearch()) {
const iconName = 'regular-expression';
this.regexButton = new Buttons.Button.Button();
this.regexButton.data = {
variant: Buttons.Button.Variant.ICON_TOGGLE,
size: Buttons.Button.Size.SMALL,
iconName,
toggledIconName: iconName,
toggleType: Buttons.Button.ToggleType.PRIMARY,
toggled: false,
jslogContext: iconName,
title: i18nString(UIStrings.enableCaseSensitive),
};
this.regexButton.addEventListener('click', () => this.toggleRegexSearch());
searchConfigButtons.appendChild(this.regexButton);
}
if (this.searchProvider.supportsCaseSensitiveSearch()) {
const iconName = 'match-case';
this.caseSensitiveButton = new Buttons.Button.Button();
this.caseSensitiveButton.data = {
variant: Buttons.Button.Variant.ICON_TOGGLE,
size: Buttons.Button.Size.SMALL,
iconName,
toggledIconName: iconName,
toggled: false,
toggleType: Buttons.Button.ToggleType.PRIMARY,
title: i18nString(UIStrings.enableCaseSensitive),
jslogContext: iconName,
};
this.caseSensitiveButton.addEventListener('click', () => this.toggleCaseSensitiveSearch());
searchConfigButtons.appendChild(this.caseSensitiveButton);
}
// Introduce a separate element for the background of the `Find` input line (instead of
// grouping together the `Find` input together with all search config option buttons
// and styling the parent's background).
// This allows for a tabbing order that can jump from the `Find` input, to
// the `Replace` input, and back to all search config option buttons.
searchInputElements.createChild('div', 'input-line search-input-background');
const buttonsContainer = this.footerElement.createChild('div', 'toolbar-search-buttons');
const firstRowButtons = buttonsContainer.createChild('div', 'first-row-buttons');
const toolbar = firstRowButtons.createChild('devtools-toolbar', 'toolbar-search-options');
this.searchNavigationPrevElement =
new ToolbarButton(i18nString(UIStrings.searchPrevious), 'chevron-up', undefined, 'select-previous');
this.searchNavigationPrevElement.addEventListener(ToolbarButton.Events.CLICK, () => this.onPrevButtonSearch());
toolbar.appendToolbarItem(this.searchNavigationPrevElement);
ARIAUtils.setLabel(this.searchNavigationPrevElement.element, i18nString(UIStrings.searchPrevious));
this.searchNavigationNextElement =
new ToolbarButton(i18nString(UIStrings.searchNext), 'chevron-down', undefined, 'select-next');
this.searchNavigationNextElement.addEventListener(ToolbarButton.Events.CLICK, () => this.onNextButtonSearch());
ARIAUtils.setLabel(this.searchNavigationNextElement.element, i18nString(UIStrings.searchNext));
toolbar.appendToolbarItem(this.searchNavigationNextElement);
const matchesText = new ToolbarText();
this.matchesElement = matchesText.element;
this.matchesElement.style.fontVariantNumeric = 'tabular-nums';
this.matchesElement.style.color = 'var(--sys-color-on-surface-subtle)';
this.matchesElement.style.padding = '0 var(--sys-size-3)';
this.matchesElement.classList.add('search-results-matches');
toolbar.appendToolbarItem(matchesText);
const cancelButtonElement = new Buttons.Button.Button();
cancelButtonElement.data = {
variant: Buttons.Button.Variant.TOOLBAR,
size: Buttons.Button.Size.REGULAR,
iconName: 'cross',
title: i18nString(UIStrings.closeSearchBar),
jslogContext: 'close-search',
};
cancelButtonElement.classList.add('close-search-button');
cancelButtonElement.addEventListener('click', () => this.closeSearch());
firstRowButtons.appendChild(cancelButtonElement);
const secondRowButtons = buttonsContainer.createChild('div', 'second-row-buttons replace-element');
this.replaceButtonElement = createTextButton(i18nString(UIStrings.replace), this.replace.bind(this), {
className: 'search-action-button',
jslogContext: 'replace',
});
this.replaceButtonElement.disabled = true;
secondRowButtons.appendChild(this.replaceButtonElement);
this.replaceAllButtonElement = createTextButton(i18nString(UIStrings.replaceAll), this.replaceAll.bind(this), {
className: 'search-action-button',
jslogContext: 'replace-all',
});
secondRowButtons.appendChild(this.replaceAllButtonElement);
this.replaceAllButtonElement.disabled = true;
this.minimalSearchQuerySize = 3;
this.loadSetting();
}
static fromElement(element: Element|null): SearchableView|null {
let view: (SearchableView|null)|null = null;
while (element && !view) {
view = searchableViewsByElement.get(element) || null;
element = element.parentElementOrShadowHost();
}
return view;
}
private toggleCaseSensitiveSearch(): void {
if (this.caseSensitiveButton) {
this.caseSensitiveButton.title = this.caseSensitiveButton.toggled ? i18nString(UIStrings.disableCaseSensitive) :
i18nString(UIStrings.enableCaseSensitive);
}
this.saveSetting();
this.performSearch(false, true);
}
private toggleRegexSearch(): void {
if (this.regexButton) {
this.regexButton.title = this.regexButton.toggled ? i18nString(UIStrings.disableRegularExpression) :
i18nString(UIStrings.enableRegularExpression);
}
this.saveSetting();
this.performSearch(false, true);
}
private toggleReplace(): void {
const replaceEnabled = this.replaceToggleButton.isToggled();
const label =
replaceEnabled ? i18nString(UIStrings.disableFindAndReplace) : i18nString(UIStrings.enableFindAndReplace);
ARIAUtils.setLabel(this.replaceToggleButton.element, label);
this.replaceToggleButton.element.title = label;
this.updateSecondRowVisibility();
}
private saveSetting(): void {
if (!this.setting) {
return;
}
const settingValue = this.setting.get() || {};
if (this.caseSensitiveButton) {
settingValue.caseSensitive = this.caseSensitiveButton.toggled;
}
if (this.regexButton) {
settingValue.isRegex = this.regexButton.toggled;
}
this.setting.set(settingValue);
}
private loadSetting(): void {
const settingValue = this.setting ? (this.setting.get() || {}) : {};
if (this.searchProvider.supportsCaseSensitiveSearch() && this.caseSensitiveButton) {
this.caseSensitiveButton.toggled = Boolean(settingValue.caseSensitive);
const label = settingValue.caseSensitive ? i18nString(UIStrings.disableCaseSensitive) :
i18nString(UIStrings.enableCaseSensitive);
this.caseSensitiveButton.title = label;
ARIAUtils.setLabel(this.caseSensitiveButton, label);
}
if (this.searchProvider.supportsRegexSearch() && this.regexButton) {
this.regexButton.toggled = Boolean(settingValue.isRegex);
const label = settingValue.regular ? i18nString(UIStrings.disableRegularExpression) :
i18nString(UIStrings.enableRegularExpression);
this.regexButton.title = label;
ARIAUtils.setLabel(this.regexButton, label);
}
}
setMinimalSearchQuerySize(minimalSearchQuerySize: number): void {
this.minimalSearchQuerySize = minimalSearchQuerySize;
}
setPlaceholder(placeholder: string, ariaLabel?: string): void {
this.searchInputElement.placeholder = placeholder;
if (ariaLabel) {
ARIAUtils.setLabel(this.searchInputElement, ariaLabel);
}
}
setReplaceable(replaceable: boolean): void {
this.replaceable = replaceable;
}
updateSearchMatchesCount(matches: number): void {
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const untypedSearchProvider = (this.searchProvider as any);
if (untypedSearchProvider.currentSearchMatches === matches) {
return;
}
untypedSearchProvider.currentSearchMatches = matches;
this.updateSearchMatchesCountAndCurrentMatchIndex(untypedSearchProvider.currentQuery ? matches : 0, -1);
}
updateCurrentMatchIndex(currentMatchIndex: number): void {
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const untypedSearchProvider = (this.searchProvider as any);
this.updateSearchMatchesCountAndCurrentMatchIndex(untypedSearchProvider.currentSearchMatches, currentMatchIndex);
}
closeSearch(): void {
this.cancelSearch();
if (this.footerElementContainer.hasFocus()) {
this.focus();
}
this.searchProvider.onSearchClosed?.();
}
private toggleSearchBar(toggled: boolean): void {
this.footerElementContainer.classList.toggle('hidden', !toggled);
this.doResize();
}
cancelSearch(): void {
if (!this.searchIsVisible) {
return;
}
this.resetSearch();
delete this.searchIsVisible;
this.toggleSearchBar(false);
}
resetSearch(): void {
this.clearSearch();
this.updateReplaceVisibility();
this.matchesElement.textContent = '';
}
refreshSearch(): void {
if (!this.searchIsVisible) {
return;
}
this.resetSearch();
this.performSearch(false, false);
}
handleFindNextShortcut(): boolean {
if (!this.searchIsVisible) {
return false;
}
this.searchProvider.jumpToNextSearchResult();
return true;
}
handleFindPreviousShortcut(): boolean {
if (!this.searchIsVisible) {
return false;
}
this.searchProvider.jumpToPreviousSearchResult();
return true;
}
handleFindShortcut(): boolean {
this.showSearchField();
return true;
}
handleCancelSearchShortcut(): boolean {
if (!this.searchIsVisible) {
return false;
}
this.closeSearch();
return true;
}
private updateSearchNavigationButtonState(enabled: boolean): void {
this.replaceButtonElement.disabled = !enabled;
this.replaceAllButtonElement.disabled = !enabled;
this.searchNavigationPrevElement.setEnabled(enabled);
this.searchNavigationNextElement.setEnabled(enabled);
}
private updateSearchMatchesCountAndCurrentMatchIndex(matches: number, currentMatchIndex: number): void {
if (!this.currentQuery) {
this.matchesElement.textContent = '';
} else if (matches === 0 || currentMatchIndex >= 0) {
this.matchesElement.textContent = i18nString(UIStrings.dOfD, {PH1: currentMatchIndex + 1, PH2: matches});
ARIAUtils.setLabel(
this.matchesElement, i18nString(UIStrings.accessibledOfD, {PH1: currentMatchIndex + 1, PH2: matches}));
} else if (matches === 1) {
this.matchesElement.textContent = i18nString(UIStrings.matchString);
} else {
this.matchesElement.textContent = i18nString(UIStrings.dMatches, {PH1: matches});
}
this.updateSearchNavigationButtonState(matches > 0);
}
showSearchField(): void {
if (this.searchIsVisible) {
this.cancelSearch();
}
let queryCandidate;
if (!this.searchInputElement.hasFocus()) {
const selection = InspectorView.instance().element.window().getSelection();
if (selection?.rangeCount) {
queryCandidate = selection.toString().replace(/\r?\n.*/, '');
}
}
this.toggleSearchBar(true);
this.updateReplaceVisibility();
if (queryCandidate) {
this.searchInputElement.value = queryCandidate;
}
this.performSearch(false, false);
this.searchInputElement.focus();
this.searchInputElement.select();
this.searchIsVisible = true;
}
private updateReplaceVisibility(): void {
this.replaceToggleButton.setVisible(this.replaceable);
if (!this.replaceable) {
this.replaceToggleButton.setToggled(false);
this.updateSecondRowVisibility();
}
}
private onSearchKeyDown(event: KeyboardEvent): void {
if (Platform.KeyboardUtilities.isEscKey(event)) {
this.closeSearch();
event.consume(true);
return;
}
if (!(event.key === 'Enter')) {
return;
}
if (!this.currentQuery) {
this.performSearch(true, true, event.shiftKey);
} else {
this.jumpToNextSearchResult(event.shiftKey);
}
}
private onReplaceKeyDown(event: KeyboardEvent): void {
if (event.key === 'Enter') {
this.replace();
}
}
private jumpToNextSearchResult(isBackwardSearch?: boolean): void {
if (!this.currentQuery) {
return;
}
if (isBackwardSearch) {
this.searchProvider.jumpToPreviousSearchResult();
} else {
this.searchProvider.jumpToNextSearchResult();
}
}
private onNextButtonSearch(): void {
this.jumpToNextSearchResult();
}
private onPrevButtonSearch(): void {
this.jumpToNextSearchResult(true);
}
private clearSearch(): void {
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const untypedSearchProvider = (this.searchProvider as any);
delete this.currentQuery;
if (Boolean(untypedSearchProvider.currentQuery)) {
delete untypedSearchProvider.currentQuery;
this.searchProvider.onSearchCanceled();
}
this.updateSearchMatchesCountAndCurrentMatchIndex(0, -1);
}
private performSearch(forceSearch: boolean, shouldJump: boolean, jumpBackwards?: boolean): void {
const query = this.searchInputElement.value;
if (!query || (!forceSearch && query.length < this.minimalSearchQuerySize && !this.currentQuery)) {
this.clearSearch();
return;
}
this.currentQuery = query;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.searchProvider as any).currentQuery = query;
const searchConfig = this.currentSearchConfig();
this.searchProvider.performSearch(searchConfig, shouldJump, jumpBackwards);
}
private currentSearchConfig(): SearchConfig {
const query = this.searchInputElement.value;
const caseSensitive = this.caseSensitiveButton ? this.caseSensitiveButton.toggled : false;
const isRegex = this.regexButton ? this.regexButton.toggled : false;
return new SearchConfig(query, caseSensitive, isRegex);
}
private updateSecondRowVisibility(): void {
const secondRowVisible = this.replaceToggleButton.isToggled();
this.footerElementContainer.classList.toggle('replaceable', secondRowVisible);
if (secondRowVisible) {
this.replaceInputElement.focus();
} else {
this.searchInputElement.focus();
}
this.doResize();
}
private replace(): void {
if (!this.replaceProvider) {
throw new Error('No \'replacable\' provided to SearchableView!');
}
const searchConfig = this.currentSearchConfig();
this.replaceProvider.replaceSelectionWith(searchConfig, this.replaceInputElement.value);
delete this.currentQuery;
this.performSearch(true, true);
}
private replaceAll(): void {
if (!this.replaceProvider) {
throw new Error('No \'replacable\' provided to SearchableView!');
}
const searchConfig = this.currentSearchConfig();
this.replaceProvider.replaceAllWith(searchConfig, this.replaceInputElement.value);
}
private onInput(): void {
if (!Common.Settings.Settings.instance().moduleSetting('search-as-you-type').get()) {
this.clearSearch();
return;
}
if (this.valueChangedTimeoutId) {
clearTimeout(this.valueChangedTimeoutId);
}
const timeout = this.searchInputElement.value.length < 3 ? 200 : 0;
this.valueChangedTimeoutId = window.setTimeout(this.onValueChanged.bind(this), timeout);
}
private onValueChanged(): void {
if (!this.searchIsVisible) {
return;
}
delete this.valueChangedTimeoutId;
this.performSearch(false, true);
}
}
const searchableViewsByElement = new WeakMap<Element, SearchableView>();
export interface Searchable {
onSearchCanceled(): void;
// Called when the search toolbar is closed
onSearchClosed?: () => void;
performSearch(searchConfig: SearchConfig, shouldJump: boolean, jumpBackwards?: boolean): void;
jumpToNextSearchResult(): void;
jumpToPreviousSearchResult(): void;
supportsCaseSensitiveSearch(): boolean;
supportsRegexSearch(): boolean;
}
export interface Replaceable {
replaceSelectionWith(searchConfig: SearchConfig, replacement: string): void;
replaceAllWith(searchConfig: SearchConfig, replacement: string): void;
}
export interface SearchRegexResult {
regex: RegExp;
fromQuery: boolean;
}
export class SearchConfig {
query: string;
caseSensitive: boolean;
isRegex: boolean;
constructor(query: string, caseSensitive: boolean, isRegex: boolean) {
this.query = query;
this.caseSensitive = caseSensitive;
this.isRegex = isRegex;
}
toSearchRegex(global?: boolean): SearchRegexResult {
let modifiers = this.caseSensitive ? '' : 'i';
if (global) {
modifiers += 'g';
}
// Check if query is surrounded by forward slashes
const isRegexFormatted = this.query.startsWith('/') && this.query.endsWith('/');
const query = this.isRegex && !isRegexFormatted ? '/' + this.query + '/' : this.query;
let regex: RegExp|undefined;
let fromQuery = false;
// First try creating regex if user knows the / / hint.
try {
if (/^\/.+\/$/.test(query) && this.isRegex) {
regex = new RegExp(query.substring(1, query.length - 1), modifiers);
fromQuery = true;
}
} catch {
// Silent catch.
}
// Otherwise just do a plain text search.
if (!regex) {
regex = Platform.StringUtilities.createPlainTextSearchRegex(query, modifiers);
}
return {
regex,
fromQuery,
};
}
}