UNPKG

chrome-devtools-frontend

Version:
325 lines (281 loc) • 13 kB
// Copyright 2024 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-lit-render-outside-of-view */ /* eslint-disable rulesdir/no-imperative-dom-api */ import '../../../ui/components/menus/menus.js'; 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 Bindings from '../../../models/bindings/bindings.js'; import * as Buttons from '../../../ui/components/buttons/buttons.js'; import * as Dialogs from '../../../ui/components/dialogs/dialogs.js'; import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js'; import * as UI from '../../../ui/legacy/legacy.js'; import * as Lit from '../../../ui/lit/lit.js'; import ignoreListSettingStyles from './ignoreListSetting.css.js'; const {html} = Lit; const UIStrings = { /** * @description Text title for the button to open the ignore list setting. */ showIgnoreListSettingDialog: 'Show ignore list setting dialog', /** * @description Text title for ignore list setting. */ ignoreList: 'Ignore list', /** * @description Text description for ignore list setting. */ ignoreListDescription: 'Add regular expression rules to remove matching scripts from the flame chart.', /** *@description Pattern title in Framework Ignore List Settings Tab of the Settings *@example {ad.*?} regex */ ignoreScriptsWhoseNamesMatchS: 'Ignore scripts whose names match \'\'{regex}\'\'', /** *@description Label for the button to remove an regex *@example {ad.*?} regex */ removeRegex: 'Remove the regex: \'\'{regex}\'\'', /** *@description Aria accessible name in Ignore List Settings Dialog in Performance panel. It labels the input * field used to add new or edit existing regular expressions that match file names to ignore in the debugger. */ addNewRegex: 'Add a regular expression rule for the script\'s URL', /** * @description Aria accessible name in Ignore List Settings Dialog in Performance panel. It labels the checkbox of * the input field used to enable the new regular expressions that match file names to ignore in the debugger. */ ignoreScriptsWhoseNamesMatchNewRegex: 'Ignore scripts whose names match the new regex', } as const; const str_ = i18n.i18n.registerUIStrings('panels/timeline/components/IgnoreListSetting.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export class IgnoreListSetting extends HTMLElement { readonly #shadow = this.attachShadow({mode: 'open'}); readonly #ignoreListEnabled: Common.Settings.Setting<boolean> = Common.Settings.Settings.instance().moduleSetting('enable-ignore-listing'); readonly #regexPatterns = this.#getSkipStackFramesPatternSetting().getAsArray(); #newRegexCheckbox = UI.UIUtils.CheckboxLabel.create( /* title*/ undefined, /* checked*/ false, /* subtitle*/ undefined, /* jslogContext*/ 'timeline.ignore-list-new-regex.checkbox'); #newRegexInput = UI.UIUtils.createInput( /* className*/ 'new-regex-text-input', /* type*/ 'text', /* jslogContext*/ 'timeline.ignore-list-new-regex.text'); #editingRegexSetting: Common.Settings.RegExpSettingItem|null = null; constructor() { super(); this.#initAddNewItem(); Common.Settings.Settings.instance() .moduleSetting('skip-stack-frames-pattern') .addChangeListener(this.#scheduleRender.bind(this)); Common.Settings.Settings.instance() .moduleSetting('enable-ignore-listing') .addChangeListener(this.#scheduleRender.bind(this)); } connectedCallback(): void { this.#scheduleRender(); // Prevent the event making its way to the TimelinePanel element which will // cause the "Load Profile" context menu to appear. this.addEventListener('contextmenu', e => { e.stopPropagation(); }); } #scheduleRender(): void { void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render); } #getSkipStackFramesPatternSetting(): Common.Settings.RegExpSetting { return Common.Settings.Settings.instance().moduleSetting('skip-stack-frames-pattern') as Common.Settings.RegExpSetting; } #startEditing(): void { // Do not need to trim here because this is a temporary one, we will trim the input when finish editing, this.#editingRegexSetting = {pattern: this.#newRegexInput.value, disabled: false, disabledForUrl: undefined}; // We need to push the temp regex here to update the flame chart. // We are using the "skip-stack-frames-pattern" setting to determine which is rendered on flame chart. And the push // here will update the setting's value. this.#regexPatterns.push(this.#editingRegexSetting); } #finishEditing(): void { if (!this.#editingRegexSetting) { return; } const lastRegex = this.#regexPatterns.pop(); // Add a sanity check to make sure the last one is the editing one. // In case the check fails, add back the last element. if (lastRegex && lastRegex !== this.#editingRegexSetting) { console.warn('The last regex is not the editing one.'); this.#regexPatterns.push(lastRegex); } this.#editingRegexSetting = null; this.#getSkipStackFramesPatternSetting().setAsArray(this.#regexPatterns); } #resetInput(): void { this.#newRegexCheckbox.checked = false; this.#newRegexInput.value = ''; } #addNewRegexToIgnoreList(): void { const newRegex = this.#newRegexInput.value.trim(); this.#finishEditing(); if (!regexInputIsValid(newRegex)) { // It the new regex is invalid, let's skip it. return; } Bindings.IgnoreListManager.IgnoreListManager.instance().addRegexToIgnoreList(newRegex); this.#resetInput(); } #handleKeyDown(event: KeyboardEvent): void { // When user press the 'Enter', the current regex will be added and user can keep adding more regexes. if (event.key === Platform.KeyboardUtilities.ENTER_KEY) { this.#addNewRegexToIgnoreList(); this.#startEditing(); return; } // When user press the 'Escape', it means cancel the editing, so the current regex won't be added and the input will // lose focus. if (event.key === Platform.KeyboardUtilities.ESCAPE_KEY) { // Escape key will close the dialog, and toggle the `Console` drawer. So we need to ignore other listeners. event.stopImmediatePropagation(); this.#finishEditing(); this.#resetInput(); this.#newRegexInput.blur(); } } /** * When it is in the 'preview' mode, the last regex in the array is the editing one. * So we want to remove it for some usage, like rendering the existed rules or validating the rules. */ #getExistingRegexes(): Common.Settings.RegExpSettingItem[] { if (this.#editingRegexSetting) { const lastRegex = this.#regexPatterns[this.#regexPatterns.length - 1]; // Add a sanity check to make sure the last one is the editing one. if (lastRegex && lastRegex === this.#editingRegexSetting) { // We don't want to modify the array itself, so just return a shadow copy of it. return this.#regexPatterns.slice(0, -1); } } return this.#regexPatterns; } #handleInputChange(): void { const newRegex = this.#newRegexInput.value.trim(); if (this.#editingRegexSetting && regexInputIsValid(newRegex)) { this.#editingRegexSetting.pattern = newRegex; this.#editingRegexSetting.disabled = !Boolean(newRegex); this.#getSkipStackFramesPatternSetting().setAsArray(this.#regexPatterns); } } #initAddNewItem(): void { this.#newRegexInput.placeholder = '/framework\\.js$'; const checkboxHelpText = i18nString(UIStrings.ignoreScriptsWhoseNamesMatchNewRegex); const inputHelpText = i18nString(UIStrings.addNewRegex); UI.Tooltip.Tooltip.install(this.#newRegexCheckbox, checkboxHelpText); UI.Tooltip.Tooltip.install(this.#newRegexInput, inputHelpText); this.#newRegexInput.addEventListener('blur', this.#addNewRegexToIgnoreList.bind(this), false); this.#newRegexInput.addEventListener('keydown', this.#handleKeyDown.bind(this), false); this.#newRegexInput.addEventListener('input', this.#handleInputChange.bind(this), false); this.#newRegexInput.addEventListener('focus', this.#startEditing.bind(this), false); } #renderNewRegexRow(): Lit.TemplateResult { // clang-format off return html` <div class='new-regex-row'>${this.#newRegexCheckbox}${this.#newRegexInput}</div> `; // clang-format on } /** * Deal with an existing regex being toggled. Note that this handler only * deals with enabling/disabling regexes already in the ignore list, it does * not deal with enabling/disabling the new regex. */ #onExistingRegexEnableToggle(regex: Common.Settings.RegExpSettingItem, checkbox: UI.UIUtils.CheckboxLabel): void { regex.disabled = !checkbox.checked; // Technically we don't need to call the set function, because the regex is a reference, so it changed the setting // value directly. // But we need to call the set function to trigger the setting change event. which is needed by view update of flame // chart. this.#getSkipStackFramesPatternSetting().setAsArray(this.#regexPatterns); // There is no need to update this component, since the only UI change is this checkbox, which is already done by // the user. } #removeRegexByIndex(index: number): void { this.#regexPatterns.splice(index, 1); // Call the set function to trigger the setting change event. we listen to this event and will update this component // and the flame chart. this.#getSkipStackFramesPatternSetting().setAsArray(this.#regexPatterns); } #renderItem(regex: Common.Settings.RegExpSettingItem, index: number): Lit.TemplateResult { const checkboxWithLabel = UI.UIUtils.CheckboxLabel.createWithStringLiteral( regex.pattern, !regex.disabled, /* jslogContext*/ 'timeline.ignore-list-pattern'); const helpText = i18nString(UIStrings.ignoreScriptsWhoseNamesMatchS, {regex: regex.pattern}); UI.Tooltip.Tooltip.install(checkboxWithLabel, helpText); checkboxWithLabel.ariaLabel = helpText; checkboxWithLabel.addEventListener( 'change', this.#onExistingRegexEnableToggle.bind(this, regex, checkboxWithLabel), false); // clang-format off return html` <div class='regex-row'> ${checkboxWithLabel} <devtools-button @click=${this.#removeRegexByIndex.bind(this, index)} .data=${{ variant: Buttons.Button.Variant.ICON, iconName: 'bin', title: i18nString(UIStrings.removeRegex, {regex: regex.pattern}), jslogContext: 'timeline.ignore-list-pattern.remove', } as Buttons.Button.ButtonData}></devtools-button> </div> `; // clang-format on } #render(): void { if (!ComponentHelpers.ScheduledRender.isScheduledRender(this)) { throw new Error('Ignore List setting dialog render was not scheduled'); } // clang-format off const output = html` <style>${ignoreListSettingStyles}</style> <devtools-button-dialog .data=${{ openOnRender: false, jslogContext: 'timeline.ignore-list', variant: Buttons.Button.Variant.TOOLBAR, iconName: 'compress', disabled: !this.#ignoreListEnabled.get(), iconTitle: i18nString(UIStrings.showIgnoreListSettingDialog), horizontalAlignment: Dialogs.Dialog.DialogHorizontalAlignment.AUTO, closeButton: true, dialogTitle: i18nString(UIStrings.ignoreList), } as Dialogs.ButtonDialog.ButtonDialogData}> <div class='ignore-list-setting-content'> <div class='ignore-list-setting-description'>${i18nString(UIStrings.ignoreListDescription)}</div> ${this.#getExistingRegexes().map(this.#renderItem.bind(this))} ${this.#renderNewRegexRow()} </div> </devtools-button-dialog> `; // clang-format on Lit.render(output, this.#shadow, {host: this}); } } customElements.define('devtools-perf-ignore-list-setting', IgnoreListSetting); declare global { interface HTMLElementTagNameMap { 'devtools-perf-ignore-list-setting': IgnoreListSetting; } } /** * Returns if a new regex string is valid to be added to the ignore list. * Note that things like duplicates are handled by the IgnoreList for us. * * @param inputValue the text input from the user we need to validate. */ export function regexInputIsValid(inputValue: string): boolean { const pattern = inputValue.trim(); if (!pattern.length) { return false; } let regex; try { regex = new RegExp(pattern); } catch { } return Boolean(regex); }