UNPKG

chrome-devtools-frontend

Version:
401 lines (367 loc) • 17.6 kB
// Copyright 2023 The Chromium Authors // 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 */ import '../../ui/components/adorners/adorners.js'; import '../../ui/legacy/components/data_grid/data_grid.js'; import * as Common from '../../core/common/common.js'; import * as Host from '../../core/host/host.js'; import * as i18n from '../../core/i18n/i18n.js'; import type * as Platform from '../../core/platform/platform.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Protocol from '../../generated/protocol.js'; import * as AutofillManager from '../../models/autofill_manager/autofill_manager.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as Lit from '../../ui/lit/lit.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import autofillViewStyles from './autofillView.css.js'; const {html, render, Directives: {styleMap}} = Lit; const {FillingStrategy} = Protocol.Autofill; const {bindToSetting} = UI.SettingsUI; const UIStrings = { /** * @description Text shown when there is no data on autofill available. */ noAutofill: 'No autofill detected', /** * @description Explanation for how to populate the autofill panel with data. Shown when there is * no data available. */ toStartDebugging: 'To start debugging autofill, use Chrome\'s autofill menu to fill an address form.', /** * @description Column header for column containing form field values */ value: 'Value', /** * @description Column header for column containing the predicted autofill categories */ predictedAutofillValue: 'Predicted autofill value', /** * @description Column header for column containing the name/label/id of form fields */ formField: 'Form field', /** * @description Tooltip for an adorner for form fields which have an autocomplete attribute * (http://go/mdn/HTML/Attributes/autocomplete) */ autocompleteAttribute: 'Autocomplete attribute', /** * @description Abbreviation of 'attribute'. Text content of an adorner for form fields which * have an autocomplete attribute (http://go/mdn/HTML/Attributes/autocomplete) */ attr: 'attr', /** * @description Tooltip for an adorner for form fields which don't have an autocomplete attribute * (http://go/mdn/HTML/Attributes/autocomplete) and for which Chrome used heuristics to deduce * the form field's autocomplete category. */ inferredByHeuristics: 'Inferred by heuristics', /** * @description Abbreviation of 'heuristics'. Text content of an adorner for form fields which * don't have an autocomplete attribute (http://go/mdn/HTML/Attributes/autocomplete) and for * which Chrome used heuristics to deduce the form field's autocomplete category. */ heur: 'heur', /** * @description Label for checkbox in the Autofill panel. If checked, this panel will open * automatically whenever a form is being autofilled. */ autoShow: 'Automatically open this panel', /** * @description Label for checkbox in the Autofill panel. If checked, test addresses will be added to the Autofill popup. */ showTestAddressesInAutofillMenu: 'Show test addresses in autofill menu', /** * @description Tooltip text for a checkbox label in the Autofill panel. If checked, this panel * will open automatically whenever a form is being autofilled. */ autoShowTooltip: 'Open the autofill panel automatically when an autofill activity is detected.', /** * @description Aria text for the section of the autofill view containing a preview of the autofilled address. */ addressPreview: 'Address preview', /** * @description Aria text for the section of the autofill view containing the info about the autofilled form fields. */ formInspector: 'Form inspector', /** * @description Link text for a hyperlink to more documentation */ learnMore: 'Learn more', /** * @description Link text for a hyperlink to webpage for leaving user feedback */ sendFeedback: 'Send feedback', } as const; const AUTOFILL_INFO_URL = 'https://goo.gle/devtools-autofill-panel' as Platform.DevToolsPath.UrlString; const AUTOFILL_FEEDBACK_URL = 'https://crbug.com/329106326' as Platform.DevToolsPath.UrlString; const str_ = i18n.i18n.registerUIStrings('panels/autofill/AutofillView.ts', UIStrings); export const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); interface ViewInput { autoOpenViewSetting: Common.Settings.Setting<boolean>; showTestAddressesInAutofillMenuSetting: Common.Settings.Setting<boolean>; address: string; filledFields: Protocol.Autofill.FilledField[]; matches: AutofillManager.AutofillManager.Match[]; highlightedMatches: AutofillManager.AutofillManager.Match[]; onHighlightMatchesInAddress: (startIndex: number) => void; onHighlightMatchesInFilledFiels: (rowIndex: number) => void; onClearHighlightedMatches: () => void; } type ViewOutput = unknown; type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void; const DEFAULT_VIEW: View = (input: ViewInput, _output: ViewOutput, target: HTMLElement): void => { const renderAddress = (): Lit.LitTemplate => { const createSpan = (startIndex: number, endIndex: number): Lit.TemplateResult => { const textContentLines = input.address.substring(startIndex, endIndex).split('\n'); const templateLines = textContentLines.map((line, i) => i === textContentLines.length - 1 ? line : html`${line}<br>`); const hasMatches = input.matches.some(match => match.startIndex <= startIndex && match.endIndex > startIndex); if (!hasMatches) { return html`<span>${templateLines}</span>`; } const spanClasses = Lit.Directives.classMap({ 'matches-filled-field': hasMatches, highlighted: input.highlightedMatches.some(match => match.startIndex <= startIndex && match.endIndex > startIndex), }); // Disabled until https://crbug.com/1079231 is fixed. // clang-format off return html` <span class=${spanClasses} jslog=${VisualLogging.item('matched-address-item').track({hover: true})} @mouseenter=${() => input.onHighlightMatchesInAddress(startIndex)} @mouseleave=${input.onClearHighlightedMatches}> ${templateLines} </span>`; // clang-format on }; // Split the address string into multiple spans. Each span is connected to // 0 or more matches. This allows highlighting the corresponding grid rows // when hovering over a span. And vice versa finding the corresponding // spans to highlight when hovering over a grid line. const spans: Lit.TemplateResult[] = []; const matchIndices = new Set<number>([0, input.address.length]); for (const match of input.matches) { matchIndices.add(match.startIndex); matchIndices.add(match.endIndex); } const sortedMatchIndices = Array.from(matchIndices).sort((a, b) => a - b); for (let i = 0; i < sortedMatchIndices.length - 1; i++) { spans.push(createSpan(sortedMatchIndices[i], sortedMatchIndices[i + 1])); } return html` <div class="address"> ${spans} </div> `; }; const renderFilledFields = (): Lit.LitTemplate => { const highlightedGridRows = new Set(input.highlightedMatches.map(match => match.filledFieldIndex)); // Disabled until https://crbug.com/1079231 is fixed. // clang-format off return html` <div class="grid-wrapper" role="region" aria-label=${i18nString(UIStrings.formInspector)}> <devtools-data-grid striped class="filled-fields-grid"> <table> <tr> <th id="name" weight="50" sortable>${i18nString(UIStrings.formField)}</th> <th id="autofill-type" weight="50" sortable>${i18nString(UIStrings.predictedAutofillValue)}</th> <th id="value" weight="50" sortable>${i18nString(UIStrings.value)}</th> </tr> ${input.filledFields.map((field, index) => html` <tr style=${styleMap({ 'font-family': 'var(--monospace-font-family)', 'font-size': 'var(--monospace-font-size)', 'background-color': highlightedGridRows.has(index) ? 'var(--sys-color-state-hover-on-subtle)' : null})} @mouseenter=${() => input.onHighlightMatchesInFilledFiels(index)} @mouseleave=${input.onClearHighlightedMatches}> <td>${field.name || `#${field.id}`} (${field.htmlType})</td> <td> ${field.autofillType} ${field.fillingStrategy === FillingStrategy.AutocompleteAttribute ? html`<devtools-adorner title=${i18nString(UIStrings.autocompleteAttribute)} .data=${{name: field.fillingStrategy}}> <span>${i18nString(UIStrings.attr)}</span> </devtools-adorner>` : field.fillingStrategy === FillingStrategy.AutofillInferred ? html`<devtools-adorner title=${i18nString(UIStrings.inferredByHeuristics)} .data=${{name: field.fillingStrategy}}> <span>${i18nString(UIStrings.heur)}</span> </devtools-adorner>` : Lit.nothing} </td> <td>"${field.value}"</td> </tr>` )} </table> </devtools-data-grid> </div> `; // clang-format on }; if (!input.address && !input.filledFields.length) { // Disabled until https://crbug.com/1079231 is fixed. // clang-format off render(html` <style>${autofillViewStyles}</style> <style>${UI.inspectorCommonStyles}</style> <main> <div class="top-left-corner"> <devtools-checkbox ${bindToSetting(input.showTestAddressesInAutofillMenuSetting)} title=${i18nString(UIStrings.showTestAddressesInAutofillMenu)} jslog=${VisualLogging.toggle(input.showTestAddressesInAutofillMenuSetting.name).track({ change: true })}> ${i18nString(UIStrings.showTestAddressesInAutofillMenu)} </devtools-checkbox> <devtools-checkbox ${bindToSetting(input.autoOpenViewSetting)} title=${i18nString(UIStrings.autoShowTooltip)} jslog=${VisualLogging.toggle(input.autoOpenViewSetting.name).track({ change: true })}> ${i18nString(UIStrings.autoShow)} </devtools-checkbox> <x-link href=${AUTOFILL_FEEDBACK_URL} class="feedback link" jslog=${VisualLogging.link('feedback').track({click: true})}>${i18nString(UIStrings.sendFeedback)}</x-link> </div> <div class="placeholder-container" jslog=${VisualLogging.pane('autofill-empty')}> <div class="empty-state"> <span class="empty-state-header">${i18nString(UIStrings.noAutofill)}</span> <div class="empty-state-description"> <span>${i18nString(UIStrings.toStartDebugging)}</span> <x-link href=${AUTOFILL_INFO_URL} class="link" jslog=${VisualLogging.link('learn-more').track({click: true})}>${i18nString(UIStrings.learnMore)}</x-link> </div> </div> </div> </main> `, target, {host: this}); // clang-format on return; } // Disabled until https://crbug.com/1079231 is fixed. // clang-format off render(html` <style>${autofillViewStyles}</style> <style>${UI.inspectorCommonStyles}</style> <main> <div class="content-container" jslog=${VisualLogging.pane('autofill')}> <div class="right-to-left" role="region" aria-label=${i18nString(UIStrings.addressPreview)}> <div class="header"> <div class="label-container"> <devtools-checkbox ${bindToSetting(input.showTestAddressesInAutofillMenuSetting)} title=${i18nString(UIStrings.showTestAddressesInAutofillMenu)} jslog=${VisualLogging.toggle(input.showTestAddressesInAutofillMenuSetting.name).track({ change: true })}> ${i18nString(UIStrings.showTestAddressesInAutofillMenu)} </devtools-checkbox> </div> <div class="label-container"> <devtools-checkbox ${bindToSetting(input.autoOpenViewSetting)} title=${i18nString(UIStrings.autoShowTooltip)} jslog=${VisualLogging.toggle(input.autoOpenViewSetting.name).track({ change: true })}> ${i18nString(UIStrings.autoShow)} </devtools-checkbox> </div> <x-link href=${AUTOFILL_FEEDBACK_URL} class="feedback link" jslog=${VisualLogging.link('feedback').track({click: true})}>${i18nString(UIStrings.sendFeedback)}</x-link> </div> ${renderAddress()} </div> ${renderFilledFields()} </div> </main> `, target, {host: this}); // clang-format on }; export class AutofillView extends UI.Widget.VBox { readonly #view: View; readonly #autofillManager: AutofillManager.AutofillManager.AutofillManager; #autoOpenViewSetting: Common.Settings.Setting<boolean> = Common.Settings.Settings.instance().createSetting('auto-open-autofill-view-on-event', true); #showTestAddressesInAutofillMenuSetting: Common.Settings.Setting<boolean>; #address = ''; #filledFields: Protocol.Autofill.FilledField[] = []; #matches: AutofillManager.AutofillManager.Match[] = []; #highlightedMatches: AutofillManager.AutofillManager.Match[] = []; constructor(autofillManager = AutofillManager.AutofillManager.AutofillManager.instance(), view = DEFAULT_VIEW) { super({useShadowDom: true}); this.#autofillManager = autofillManager; this.#view = view; this.#showTestAddressesInAutofillMenuSetting = Common.Settings.Settings.instance().createSetting('show-test-addresses-in-autofill-menu-on-event', false); } override wasShown(): void { super.wasShown(); const formFilledEvent = this.#autofillManager.getLastFilledAddressForm(); if (formFilledEvent) { ({ address: this.#address, filledFields: this.#filledFields, matches: this.#matches, } = formFilledEvent); } this.#autofillManager.addEventListener( AutofillManager.AutofillManager.Events.ADDRESS_FORM_FILLED, this.#onAddressFormFilled, this); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.PrimaryPageChanged, this.#onPrimaryPageChanged, this); this.requestUpdate(); } override willHide(): void { SDK.TargetManager.TargetManager.instance().removeModelListener( SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.PrimaryPageChanged, this.#onPrimaryPageChanged, this); this.#autofillManager.removeEventListener( AutofillManager.AutofillManager.Events.ADDRESS_FORM_FILLED, this.#onAddressFormFilled, this); super.willHide(); } #onPrimaryPageChanged(): void { this.#address = ''; this.#filledFields = []; this.#matches = []; this.#highlightedMatches = []; this.requestUpdate(); } async #onAddressFormFilled( {data}: Common.EventTarget.EventTargetEvent<AutofillManager.AutofillManager.AddressFormFilledEvent>): Promise<void> { if (this.#autoOpenViewSetting.get()) { await UI.ViewManager.ViewManager.instance().showView('autofill-view'); Host.userMetrics.actionTaken(Host.UserMetrics.Action.AutofillReceivedAndTabAutoOpened); } else { Host.userMetrics.actionTaken(Host.UserMetrics.Action.AutofillReceived); } this.#address = data.address; this.#filledFields = data.filledFields; this.#matches = data.matches; this.#highlightedMatches = []; this.requestUpdate(); } override performUpdate(): Promise<void>|void { const onHighlightMatchesInAddress = (startIndex: number): void => { this.#highlightedMatches = this.#matches.filter(match => match.startIndex <= startIndex && match.endIndex > startIndex); this.requestUpdate(); }; const onHighlightMatchesInFilledFiels = (rowIndex: number): void => { this.#autofillManager.highlightFilledField(this.#filledFields[rowIndex]); this.#highlightedMatches = this.#matches.filter(match => match.filledFieldIndex === rowIndex); this.requestUpdate(); }; const onClearHighlightedMatches = (): void => { this.#autofillManager.clearHighlightedFilledFields(); this.#highlightedMatches = []; this.requestUpdate(); }; const input: ViewInput = { autoOpenViewSetting: this.#autoOpenViewSetting, showTestAddressesInAutofillMenuSetting: this.#showTestAddressesInAutofillMenuSetting, address: this.#address, filledFields: this.#filledFields, matches: this.#matches, highlightedMatches: this.#highlightedMatches, onHighlightMatchesInAddress, onHighlightMatchesInFilledFiels, onClearHighlightedMatches, }; const output: ViewOutput = undefined; this.#view(input, output, this.contentElement); } }