| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import {assert} from 'chrome://resources/js/assert.js'; |
| |
| import {OmniboxElement} from './omnibox_element.js'; |
| import type {DisplayInputs} from './omnibox_input.js'; |
| import {OmniboxInput} from './omnibox_input.js'; |
| import type {ACMatchClassification, AutocompleteControllerType, AutocompleteMatch, OmniboxResponse, Signals} from './omnibox_internals.mojom-webui.js'; |
| import outputColumnWidthSheet from './omnibox_output_column_widths.css' with {type : 'css'}; |
| import {clearChildren, createEl} from './omnibox_util.js'; |
| import outputResultsGroupSheet from './output_results_group.css' with {type : 'css'}; |
| |
| interface ResultsDetails { |
| cursorPosition: number; |
| time: number; |
| done: boolean; |
| type: string; |
| host: string; |
| isTypedHost: boolean; |
| } |
| |
| export class OmniboxOutput extends OmniboxElement { |
| private selectedResponseIndex: number = 0; |
| responsesHistory: OmniboxResponse[][] = []; |
| private resultsGroups: OutputResultsGroup[] = []; |
| private displayInputs: DisplayInputs = OmniboxInput.defaultDisplayInputs; |
| private filterText: string = ''; |
| |
| constructor() { |
| super('omnibox-output-template'); |
| } |
| |
| updateDisplayInputs(displayInputs: DisplayInputs) { |
| this.displayInputs = displayInputs; |
| this.updateDisplay(); |
| } |
| |
| updateFilterText(filterText: string) { |
| this.filterText = filterText; |
| this.updateFilterHighlights(); |
| } |
| |
| setResponsesHistory(responsesHistory: OmniboxResponse[][]) { |
| this.responsesHistory = responsesHistory; |
| this.dispatchEvent(new CustomEvent( |
| 'responses-count-changed', {detail: responsesHistory.length})); |
| this.updateSelectedResponseIndex(this.selectedResponseIndex); |
| } |
| |
| updateSelectedResponseIndex(selection: number) { |
| const response = this.responsesHistory[selection]; |
| if (!response) { |
| // `selection` may be out of bounds when e.g. the user clears the |
| // corresponding input field. |
| return; |
| } |
| this.selectedResponseIndex = selection; |
| this.clearResultsGroups(); |
| response.forEach(this.createResultsGroup.bind(this)); |
| } |
| |
| prepareNewQuery() { |
| this.responsesHistory.push([]); |
| this.dispatchEvent(new CustomEvent( |
| 'responses-count-changed', {detail: this.responsesHistory.length})); |
| } |
| |
| addAutocompleteResponse(response: OmniboxResponse) { |
| const lastIndex = this.responsesHistory.length - 1; |
| const responses = this.responsesHistory[lastIndex]; |
| assert(responses); |
| responses.push(response); |
| if (lastIndex === this.selectedResponseIndex) { |
| this.createResultsGroup(response); |
| } |
| } |
| |
| /** |
| * Clears result groups from the UI. |
| */ |
| private clearResultsGroups() { |
| this.resultsGroups = []; |
| clearChildren(this.$<HTMLElement>('#contents')!); |
| } |
| |
| /** |
| * Creates and adds a result group to the UI. |
| */ |
| private createResultsGroup(response: OmniboxResponse) { |
| const resultsGroup = OutputResultsGroup.create(response); |
| this.resultsGroups.push(resultsGroup); |
| const contents = this.$<HTMLElement>('#contents'); |
| assert(contents); |
| contents.appendChild(resultsGroup); |
| |
| this.updateDisplay(); |
| this.updateFilterHighlights(); |
| } |
| |
| updateAnswerIconImage( |
| _controllerType: AutocompleteControllerType, url: string, data: string) { |
| this.outputMatches.forEach(match => match.updateAnswerIconImage(url, data)); |
| } |
| |
| private updateDisplay() { |
| this.updateVisibility(); |
| this.updateEliding(); |
| this.updateRowHeights(); |
| } |
| |
| /** |
| * Show or hide various output elements depending on display inputs. |
| * 1) Show non-last result groups only if showIncompleteResults is true. |
| * 2) Show the details section above each table if showDetails or |
| * showIncompleteResults are true. |
| * 3) Show individual results when showAllProviders is true. |
| * 4) Show certain columns and headers only if they showDetails is true. |
| */ |
| private updateVisibility() { |
| // Show non-last result groups only if showIncompleteResults is true. |
| this.resultsGroups.forEach((resultsGroup, i) => { |
| resultsGroup.hidden = !this.displayInputs.showIncompleteResults && |
| i !== this.resultsGroups.length - 1; |
| }); |
| |
| this.resultsGroups.forEach(resultsGroup => { |
| resultsGroup.updateVisibility( |
| this.displayInputs.showDetails, this.displayInputs.showAllProviders); |
| }); |
| } |
| |
| private updateEliding() { |
| this.resultsGroups.forEach( |
| resultsGroup => |
| resultsGroup.updateEliding(this.displayInputs.elideCells)); |
| } |
| |
| private updateRowHeights() { |
| this.resultsGroups.forEach( |
| resultsGroup => |
| resultsGroup.updateRowHeights(this.displayInputs.thinRows)); |
| } |
| |
| private updateFilterHighlights() { |
| this.outputMatches.forEach(match => match.filter(this.filterText)); |
| } |
| |
| private get outputMatches(): OutputMatch[] { |
| return this.resultsGroups.flatMap( |
| resultsGroup => resultsGroup.outputMatches); |
| } |
| } |
| |
| /** |
| * Helps track and render a results group. C++ Autocomplete typically returns |
| * 3 result groups per query. It may return less if the next query is |
| * submitted before all 3 have been returned. Each result group contains |
| * top level information (e.g., how long the result took to generate), as well |
| * as a single list of combined results and multiple lists of individual |
| * results. Each of these lists is tracked and rendered by OutputResultsTable |
| * below. |
| */ |
| class OutputResultsGroup extends OmniboxElement { |
| private details: ResultsDetails; |
| private headers: OutputHeader[]; |
| private combinedResults: OutputResultsTable; |
| private individualResultsList: OutputResultsTable[]; |
| private innerHeaders: HTMLElement[]; |
| |
| static create(resultsGroup: OmniboxResponse): OutputResultsGroup { |
| const outputResultsGroup = new OutputResultsGroup(); |
| outputResultsGroup.setResultsGroup(resultsGroup); |
| return outputResultsGroup; |
| } |
| |
| constructor() { |
| super('output-results-group-template'); |
| this.shadowRoot!.adoptedStyleSheets = |
| [outputColumnWidthSheet, outputResultsGroupSheet]; |
| } |
| |
| setResultsGroup(resultsGroup: OmniboxResponse) { |
| this.details = { |
| cursorPosition: resultsGroup.cursorPosition, |
| time: resultsGroup.timeSinceOmniboxStartedMs, |
| done: resultsGroup.done, |
| type: resultsGroup.type, |
| host: resultsGroup.host, |
| isTypedHost: resultsGroup.isTypedHost, |
| }; |
| this.headers = COLUMNS.map(column => new OutputHeader(column)); |
| this.combinedResults = new OutputResultsTable(resultsGroup.combinedResults); |
| this.individualResultsList = |
| resultsGroup.resultsByProvider |
| .map(resultsWrapper => resultsWrapper.results) |
| .filter(results => results.length > 0) |
| .map(result => new OutputResultsTable(result)); |
| |
| clearChildren(this); |
| |
| this.innerHeaders = []; |
| |
| const outputResultsDetails = |
| this.$<OutputResultsDetails>('output-results-details'); |
| assert(outputResultsDetails); |
| customElements.whenDefined(outputResultsDetails.localName) |
| .then(() => outputResultsDetails.setDetails(this.details)); |
| |
| const table = this.$('#table'); |
| const head = createEl('thead', table, ['head']); |
| const row = createEl('tr', head); |
| this.headers.forEach(cell => row.appendChild(cell)); |
| assert(table); |
| |
| table.appendChild(this.combinedResults); |
| this.individualResultsList.forEach(results => { |
| const innerHeader = this.renderInnerHeader(results); |
| this.innerHeaders.push(innerHeader); |
| table.appendChild(innerHeader); |
| table.appendChild(results); |
| }); |
| } |
| |
| private renderInnerHeader(results: OutputResultsTable): HTMLElement { |
| const head = createEl('tbody', null, ['head']); |
| const row = createEl('tr', head); |
| createEl('th', row, [], results.innerHeaderText).colSpan = COLUMNS.length; |
| return head; |
| } |
| |
| updateVisibility(showDetails: boolean, showAllProviders: boolean) { |
| // TODO(manukh): Replace with `this.classList.toggle('show-details', |
| // showDetails);` (and likewise for `showAllProviders`) and use CSS to |
| // apply to the children. Show individual results when `showAllProviders` |
| // is true. |
| this.individualResultsList.forEach( |
| individualResults => individualResults.hidden = !showAllProviders); |
| this.innerHeaders.forEach( |
| innerHeader => innerHeader.hidden = !showAllProviders); |
| |
| // Show certain column headers only if they `showDetails` is true. |
| COLUMNS.forEach(({displayAlways}, i) => { |
| const header = this.headers[i]; |
| assert(header); |
| header.hidden = !showDetails && !displayAlways; |
| }); |
| |
| // Show certain columns only if `showDetails` is true. |
| this.outputMatches.forEach(match => match.updateVisibility(showDetails)); |
| } |
| |
| updateEliding(elideCells: boolean) { |
| // TODO(manukh): Replace with `this.classList.toggle('elide-cells', |
| // elideCells);` and use CSS to apply to the children. |
| this.outputMatches.forEach(match => match.updateEliding(elideCells)); |
| } |
| |
| updateRowHeights(thinRows: boolean) { |
| // TODO(manukh): Replace with `this.classList.toggle('thin', thinRows);` and |
| // use CSS to apply to the children. |
| this.outputMatches.forEach( |
| match => match.classList.toggle('thin', thinRows)); |
| } |
| |
| get outputMatches(): OutputMatch[] { |
| return [this.combinedResults] |
| .concat(this.individualResultsList) |
| .flatMap(results => results.outputMatches); |
| } |
| } |
| |
| class OutputResultsDetails extends OmniboxElement { |
| constructor() { |
| super('output-results-details-template'); |
| } |
| |
| setDetails(details: ResultsDetails) { |
| const cursorPosition = this.$('#cursor-position'); |
| assert(cursorPosition); |
| cursorPosition.textContent = String(details.cursorPosition); |
| const time = this.$('#time'); |
| assert(time); |
| time.textContent = String(details.time); |
| const done = this.$('#done'); |
| assert(done); |
| done.textContent = String(details.done); |
| const type = this.$('#type'); |
| assert(type); |
| type.textContent = details.type; |
| const host = this.$('#host'); |
| assert(host); |
| host.textContent = details.host; |
| const isTypedHost = this.$('#is-typed-host'); |
| assert(isTypedHost); |
| isTypedHost.textContent = String(details.isTypedHost); |
| } |
| } |
| |
| /** |
| * Helps track and render a list of results. Each result is tracked and |
| * rendered by OutputMatch below. |
| */ |
| class OutputResultsTable extends HTMLTableSectionElement { |
| private autocompleteMatches: AutocompleteMatch[]; |
| readonly outputMatches: OutputMatch[]; |
| |
| constructor(matches: AutocompleteMatch[]) { |
| super(); |
| this.autocompleteMatches = matches; |
| this.classList.add('body'); |
| this.outputMatches = matches.map(match => new OutputMatch(match)); |
| this.outputMatches.forEach(this.appendChild.bind(this)); |
| } |
| |
| get innerHeaderText(): string { |
| return this.autocompleteMatches[0]?.providerName || ''; |
| } |
| } |
| |
| /** Helps track and render a single match. */ |
| class OutputMatch extends HTMLTableRowElement { |
| private imageAndIcon: OutputImageAndIconProperty; |
| |
| constructor(match: AutocompleteMatch) { |
| super(); |
| this.addEventListener( |
| 'click', |
| () => !document.getSelection()?.toString() && |
| this.classList.toggle('expanded')); |
| |
| COLUMNS.forEach(column => { |
| const property = column.create(match); |
| property.classList.add(column.cellClassName); |
| this.appendChild(property); |
| if (property instanceof OutputImageAndIconProperty) { |
| this.imageAndIcon = property; |
| } |
| }); |
| } |
| |
| updateAnswerIconImage(url: string, data: string) { |
| if (this.imageAndIcon) { |
| if (this.imageAndIcon.imageUrl.href.endsWith(url)) { |
| this.imageAndIcon.setAnswerImageData(data); |
| } |
| if (this.imageAndIcon.iconUrl.href.endsWith(url)) { |
| this.imageAndIcon.setAnswerIconData(data); |
| } |
| } |
| } |
| |
| updateVisibility(showDetails: boolean) { |
| // Show certain columns only if they showDetails is true. |
| COLUMNS.forEach(({displayAlways}, i) => { |
| const outputProperty = this.outputProperties[i]; |
| assert(outputProperty); |
| return outputProperty.hidden = !showDetails && !displayAlways; |
| }); |
| } |
| |
| updateEliding(elideCells: boolean) { |
| this.outputProperties.forEach( |
| property => property.classList.toggle('elided', elideCells)); |
| } |
| |
| filter(filterText: string) { |
| this.classList.remove('filtered-highlighted'); |
| this.outputProperties.forEach( |
| property => property.classList.remove('filtered-highlighted-nested')); |
| |
| if (!filterText) { |
| return; |
| } |
| |
| const matchedProperties = this.outputProperties.filter( |
| property => FilterUtil.filterText(property.filterText, filterText)); |
| const isMatch = matchedProperties.length > 0; |
| this.classList.toggle('filtered-highlighted', isMatch); |
| matchedProperties.forEach( |
| property => property.classList.add('filtered-highlighted-nested')); |
| } |
| |
| private get outputProperties(): OutputProperty[] { |
| return [...this.children] as OutputProperty[]; |
| } |
| } |
| |
| class OutputHeader extends HTMLTableCellElement { |
| constructor(column: Column) { |
| super(); |
| this.classList.add(column.headerClassName); |
| |
| const container = |
| createEl(column.url ? 'a' : 'div', this, ['header-container']); |
| if (column.url) { |
| (container as HTMLAnchorElement).href = column.url; |
| } |
| column.headerText.forEach(text => createEl('span', container, [], text)); |
| |
| this.title = column.tooltip; |
| } |
| } |
| |
| abstract class OutputProperty extends HTMLTableCellElement { |
| readonly filterText: string; |
| |
| constructor(filterText: string) { |
| super(); |
| this.filterText = filterText; |
| } |
| } |
| |
| abstract class FlexWrappingOutputProperty extends OutputProperty { |
| protected readonly container: HTMLElement; |
| |
| protected constructor(filterText: string) { |
| super(filterText); |
| // margin-right is used on .pair-item's to separate them. To compensate, |
| // .pair-container has negative margin-right. This means .pair-container's |
| // overflow their parent. Overflowing a table cell is problematic, as 1) |
| // scroll bars overlay adjacent cell, and 2) the page receives a |
| // horizontal scroll bar when the right most column overflows. To avoid |
| // this, the parent of any element with negative margins (e.g. |
| // .pair-container) must not be a table cell; hence, the use of |
| // scrollContainer. |
| // Flex gutters may provide a cleaner alternative once implemented. |
| // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Mastering_Wrapping_of_Flex_Items#Creating_gutters_between_items |
| const scrollContainer = createEl('div', this); |
| this.container = createEl('div', scrollContainer, ['pair-container']); |
| } |
| } |
| |
| class OutputPairProperty extends FlexWrappingOutputProperty { |
| constructor(value1: string, value2: string) { |
| super(`${value1}.${value2}`); |
| createEl('div', this.container, ['pair-item'], value1); |
| createEl('div', this.container, ['pair-item'], value2); |
| } |
| } |
| |
| class OutputOverlappingPairProperty extends OutputPairProperty { |
| constructor(value1: string, value2: string) { |
| const overlap = value1.endsWith(value2); |
| super(value2 && overlap ? value1.slice(0, -value2.length) : value1, value2); |
| createEl( |
| 'div', this.container, ['overlap-warning'], |
| overlap ? '' : |
| `btw, these texts do not overlap; '${ |
| value2}' was expected to be a suffix of '${value1}'`); |
| } |
| } |
| |
| class OutputImageAndIconProperty extends FlexWrappingOutputProperty { |
| readonly iconUrl: HTMLAnchorElement; |
| private readonly iconElement: HTMLImageElement; |
| readonly imageUrl: HTMLAnchorElement; |
| private readonly imageElement: HTMLImageElement; |
| |
| constructor(iconUrl: string, imageUrl: string) { |
| super([iconUrl, imageUrl].join('.')); |
| |
| this.iconUrl = createEl('a', this.container, ['pair-item', 'icon-url']); |
| this.iconUrl.href = iconUrl; |
| this.iconUrl.title = 'Icon URL: ' + iconUrl; |
| this.iconUrl.target = '_blank'; |
| this.iconElement = createEl('img', this.iconUrl, ['icon-image']); |
| |
| this.imageUrl = createEl('a', this.container, ['pair-item', 'image-url']); |
| this.imageUrl.href = imageUrl; |
| this.imageUrl.title = 'Image URL: ' + imageUrl; |
| this.imageUrl.target = '_blank'; |
| this.imageElement = createEl('img', this.imageUrl, ['image-image']); |
| } |
| |
| setAnswerIconData(iconData: string) { |
| this.iconElement.src = iconData; |
| } |
| |
| setAnswerImageData(imageData: string) { |
| this.imageElement.src = imageData; |
| } |
| } |
| |
| class OutputAnswerProperty extends FlexWrappingOutputProperty { |
| constructor( |
| contents: string, description: string, answer: string, |
| contentsClassification: ACMatchClassification[], |
| descriptionClassification: ACMatchClassification[]) { |
| super([contents, description, answer].join('.')); |
| |
| const contentsDiv = |
| createEl('div', this.container, ['pair-item', 'contents']); |
| OutputAnswerProperty.renderClassifiedText( |
| contentsDiv, contents, contentsClassification); |
| |
| const descriptionDiv = |
| createEl('div', this.container, ['pair-item', 'description']); |
| OutputAnswerProperty.renderClassifiedText( |
| descriptionDiv, description, descriptionClassification); |
| |
| createEl('div', this.container, ['pair-item', 'answer'], answer); |
| } |
| |
| private static renderClassifiedText( |
| container: HTMLElement, string: string, |
| classes: ACMatchClassification[]) { |
| clearChildren(container); |
| OutputAnswerProperty.classify(string, classes) |
| .forEach( |
| ({string, style}) => createEl( |
| 'span', container, OutputAnswerProperty.styleToClasses(style), |
| string)); |
| } |
| |
| private static classify(string: string, classes: ACMatchClassification[]): |
| Array<{string: string, style: number}> { |
| return classes.map(({offset, style}, i) => { |
| const next = classes[i + 1]; |
| const end = next ? next.offset : string.length; |
| return {string: string.substring(offset, end), style}; |
| }); |
| } |
| |
| private static styleToClasses(style: number): string[] { |
| // Maps the bitmask enum AutocompleteMatch::ACMatchClassification::Style |
| // to strings. See autocomplete_match.h for more details. |
| // E.g., maps the style 5 to classes ['style-url', 'style-dim']. |
| return ['style-url', 'style-match', 'style-dim'].filter( |
| (_, i) => (style >> i) % 2); |
| } |
| } |
| |
| class OutputBooleanProperty extends OutputProperty { |
| constructor(value: boolean, filterName: string) { |
| super((value ? 'is: ' : 'not: ') + filterName); |
| createEl('div', this, ['icon', value ? 'check-icon' : 'x-icon']); |
| } |
| } |
| |
| class OutputDictionaryProperty extends OutputProperty { |
| protected readonly container: HTMLElement; |
| |
| constructor(value: {[key: string]: string}) { |
| super(Object.entries(value) |
| .map(([key, value]) => `${key}: ${value}`) |
| .join('\n')); |
| this.container = createEl('div', this); |
| const pre = createEl('pre', this.container, ['json']); |
| Object.entries(value).forEach(([key, value]) => { |
| createEl('span', pre, ['key'], key + ': '); |
| createEl('span', pre, ['value'], value + '\n'); |
| }); |
| } |
| } |
| |
| class OutputScoringSignalsProperty extends OutputDictionaryProperty { |
| constructor(value: Signals) { |
| super(Object.fromEntries( |
| Object.entries(value).filter(([, value]) => value !== null))); |
| const link = createEl('a', null, ['icon', 'edit-icon']); |
| link.href = `chrome://omnibox/ml?signals=${Object.values(value).join()}`; |
| this.container.insertBefore(link, this.container.firstChild); |
| } |
| } |
| |
| class OutputAdditionalInfoProperty extends OutputDictionaryProperty { |
| constructor(value: {[key: string]: string}) { |
| super(value); |
| const link = createEl('a', null, ['icon', 'download-icon']); |
| link.download = 'AdditionalInfo.json'; |
| link.href = OutputAdditionalInfoProperty.createDownloadLink(value); |
| this.container.insertBefore(link, this.container.firstChild); |
| } |
| |
| private static createDownloadLink(value: {[key: string]: string}): string { |
| const text = JSON.stringify(value, null, 2); |
| const obj64 = btoa(unescape(encodeURIComponent(text))); |
| return `data:application/json;base64,${obj64}`; |
| } |
| } |
| |
| |
| class OutputUrlProperty extends FlexWrappingOutputProperty { |
| constructor( |
| destinationUrl: string, isSearchType: boolean, |
| strippedDestinationUrl: string) { |
| super(destinationUrl); |
| const iconAndUrlContainer = createEl('div', this.container, ['pair-item']); |
| if (!isSearchType) { |
| createEl('img', iconAndUrlContainer).src = |
| `chrome://favicon/${destinationUrl}`; |
| } |
| createEl('a', iconAndUrlContainer, [], destinationUrl).href = |
| destinationUrl; |
| createEl('a', this.container, ['pair-item'], strippedDestinationUrl).href = |
| strippedDestinationUrl; |
| } |
| } |
| |
| class OutputTextProperty extends OutputProperty { |
| constructor(text: string) { |
| super(text); |
| createEl('div', this, [], text); |
| } |
| } |
| |
| /** Responsible for highlighting and hiding rows using filter text. */ |
| class FilterUtil { |
| /** |
| * Checks if a string fuzzy-matches a filter string. Each character |
| * of filterText must be present in the search text, either adjacent to the |
| * previous matched character, or at the start of a new word (see |
| * textToWords). |
| * E.g. `abc` matches `abc`, `a big cat`, `a-bigCat`, `a very big cat`, and |
| * `an amBer cat`; but does not match `abigcat` or `an amber cat`. |
| * `green rainbow` is matched by `gre rain`, but not by `gre bow`. |
| * One exception is the first character, which may be matched mid-word. |
| * E.g. `een rain` can also match `green rainbow`. |
| */ |
| static filterText(searchText: string, filterText: string): boolean { |
| const regexFilter = |
| Array.from(filterText) |
| .map(word => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) |
| .join('(.*\\.)?'); |
| const words = FilterUtil.textToWords(searchText).join('.'); |
| return words.match(new RegExp(regexFilter, 'i')) !== null; |
| } |
| |
| /** |
| * Splits a string into words, delimited by either capital letters, groups |
| * of digits, or non alpha characters. |
| * E.g., `https://google.com/the-dog-ate-134pies` will be split to: |
| * https, :, /, /, google, ., com, /, the, -, dog, -, ate, -, 134, pies |
| * This differs from `Array.split` in that this groups digits, e.g. 134. |
| */ |
| private static textToWords(text: string): string[] { |
| const MAX_TEXT_LENGTH = 200; |
| if (text.length > MAX_TEXT_LENGTH) { |
| text = text.slice(0, MAX_TEXT_LENGTH); |
| console.warn(`text to be filtered too long, truncated; max length: ${ |
| MAX_TEXT_LENGTH}, truncated text: ${text}`); |
| } |
| return text.match(/[a-z]+|[A-Z][a-z]*|\d+|./g) || []; |
| } |
| } |
| |
| class Column { |
| headerText: string[]; |
| url: string; |
| hyphenatedName: string; |
| displayAlways: boolean; |
| tooltip: string; |
| create: (match: AutocompleteMatch) => OutputProperty; |
| |
| constructor( |
| headerText: string[], url: string, hyphenatedName: string, |
| displayAlways: boolean, tooltip: string, |
| create: (match: AutocompleteMatch) => OutputProperty) { |
| this.headerText = headerText; |
| this.url = url; |
| this.hyphenatedName = hyphenatedName; |
| this.displayAlways = displayAlways; |
| this.tooltip = tooltip; |
| this.create = create; |
| } |
| |
| get cellClassName(): string { |
| return 'cell-' + this.hyphenatedName; |
| } |
| |
| get headerClassName(): string { |
| return 'header-' + this.hyphenatedName; |
| } |
| } |
| |
| /** |
| * A constant that's used to decide what autocomplete result |
| * properties to output in what order. |
| */ |
| const COLUMNS: Column[] = [ |
| new Column( |
| ['Provider', 'Type'], '', 'provider-and-type', true, |
| 'Provider & Type\nThe AutocompleteProvider suggesting this result. / ' + |
| 'The type of the result.', |
| match => new OutputPairProperty(match.providerName, match.type)), |
| new Column( |
| ['Relevance'], '', 'relevance', true, |
| 'Relevance\nThe result score. Higher is more relevant.', |
| match => new OutputTextProperty(String(match.relevance))), |
| new Column( |
| ['Icon', 'Image'], '', 'image-and-icon', true, |
| 'Icon & Image\nClickable icon and image associated with the result.', |
| match => new OutputImageAndIconProperty(match.icon.url, match.image.url)), |
| new Column( |
| ['Contents', 'Description', 'Answer'], '', 'contents-and-description', |
| true, |
| 'Contents & Description & Answer\nURL classifications are styled ' + |
| 'blue.\nMATCH classifications are styled bold.\nDIM ' + |
| 'classifications are styled with a gray background.', |
| match => new OutputAnswerProperty( |
| match.contents, match.description, match.answer, match.contentsClass, |
| match.descriptionClass)), |
| new Column( |
| ['sw'], '', 'swap-contents-and-description', false, |
| 'Swap Contents and Description', |
| match => new OutputBooleanProperty( |
| match.swapContentsAndDescription, 'Swap Contents and Description')), |
| new Column( |
| ['df'], '', 'allowed-to-be-default-match', true, |
| 'Can be Default\nA green checkmark indicates that the result can be ' + |
| 'the default match (i.e., can be the match that pressing enter ' + |
| 'in the omnibox navigates to).', |
| match => new OutputBooleanProperty( |
| match.allowedToBeDefaultMatch, 'Can be Default')), |
| new Column( |
| ['bk'], '', 'starred', false, |
| 'Bookmarked\nA green checkmark indicates that the result has been ' + |
| 'bookmarked.', |
| match => new OutputBooleanProperty(match.starred, 'Bookmarked')), |
| new Column( |
| ['tb'], '', 'has-tab-match', false, |
| 'Has Tab Match\nA green checkmark indicates that the result URL ' + |
| 'matches an open tab.', |
| match => new OutputBooleanProperty(match.hasTabMatch, 'Has Tab Match')), |
| new Column( |
| ['URL', 'Stripped URL'], '', 'destination-url', true, |
| 'URL & Stripped URL\nThe URL for the result. / The stripped URL for ' + |
| 'the result.', |
| match => new OutputUrlProperty( |
| match.destinationUrl.url, match.isSearchType, |
| match.strippedDestinationUrl.url)), |
| new Column( |
| ['AQS Type & Subtypes'], '', 'aqs-type-subtypes', false, |
| 'The type and subtypes reported in the Assisted Query Stats (AQS) url ' + |
| 'query param.', |
| match => new OutputTextProperty(match.aqsTypeSubtypes)), |
| new Column( |
| ['Fill', 'Inline'], '', 'fill-and-inline', false, |
| 'Fill & Inline\nThe text shown in the omnibox when the result is ' + |
| 'selected. / The text shown in the omnibox as a blue highlight ' + |
| 'selection following the cursor, if this match is shown inline.', |
| match => new OutputOverlappingPairProperty( |
| match.fillIntoEdit, match.inlineAutocompletion)), |
| new Column( |
| ['dl'], '', 'deletable', false, |
| 'Deletable\nA green checkmark indicates that the result can be ' + |
| 'deleted from the visit history.', |
| match => new OutputBooleanProperty(match.deletable, 'Deletable')), |
| new Column( |
| ['pr'], '', 'from-previous', false, |
| 'From Previous\nTrue if this match is from a previous result.', |
| match => new OutputBooleanProperty(match.fromPrevious, 'From Previous')), |
| new Column( |
| ['Tran'], |
| 'https://cs.chromium.org/chromium/src/ui/base/page_transition_types.h' + |
| '?q=page_transition_types.h&sq=package:chromium&dr=CSs&l=14', |
| 'transition', false, 'Transition\nHow the user got to the result.', |
| match => new OutputTextProperty(match.transition)), |
| new Column( |
| ['dn'], '', 'provider-done', false, |
| 'Done\nA green checkmark indicates that the provider is done looking ' + |
| 'for more results.', |
| match => new OutputBooleanProperty(match.providerDone, 'Done')), |
| new Column( |
| ['Associated Keyword'], '', 'associated-keyword', false, |
| 'Associated Keyword\nIf non-empty, a "press tab to search" hint will ' + |
| 'be shown and will engage this keyword.', |
| match => new OutputTextProperty(match.associatedKeyword)), |
| new Column( |
| ['Keyword'], '', 'keyword', false, |
| 'Keyword\nThe keyword of the search engine to be used.', |
| match => new OutputTextProperty(match.keyword)), |
| new Column( |
| ['dp'], '', 'duplicates', false, |
| 'Duplicates\nThe number of matches that have been marked as ' + |
| 'duplicates of this match.', |
| match => new OutputTextProperty(String(match.duplicates))), |
| new Column( |
| ['pi'], |
| 'https://source.chromium.org/chromium/chromium/src/+/main:components/omnibox/browser/omnibox_pedal_concepts.h;l=19;drc=c741e070dbfcc33b2369e7a5131be87c7b21bb99', |
| 'pedal-id', false, 'Pedal ID\nThe ID of attached Pedal, or zero if none.', |
| match => new OutputTextProperty(String(match.pedalId))), |
| new Column( |
| ['Scoring Signals'], '', 'scoring-signals', false, |
| 'Scoring Signals\nSignals used by the ML Model to score suggestions.', |
| match => new OutputScoringSignalsProperty(match.scoringSignals)), |
| new Column( |
| ['Additional Info'], '', 'additional-info', true, |
| 'Additional Info\nProvider-specific information about the result.', |
| match => new OutputAdditionalInfoProperty(match.additionalInfo)), |
| ]; |
| |
| customElements.define('omnibox-output', OmniboxOutput); |
| customElements.define('output-results-group', OutputResultsGroup); |
| customElements.define('output-results-details', OutputResultsDetails); |
| customElements.define( |
| 'output-results-table', OutputResultsTable, {extends: 'tbody'}); |
| customElements.define('output-match', OutputMatch, {extends: 'tr'}); |
| customElements.define('output-header', OutputHeader, {extends: 'th'}); |
| customElements.define( |
| 'output-pair-property', OutputPairProperty, {extends: 'td'}); |
| customElements.define( |
| 'output-overlapping-pair-property', OutputOverlappingPairProperty, |
| {extends: 'td'}); |
| customElements.define( |
| 'output-image-and-icon-property', OutputImageAndIconProperty, |
| {extends: 'td'}); |
| customElements.define( |
| 'output-answer-property', OutputAnswerProperty, {extends: 'td'}); |
| customElements.define( |
| 'output-boolean-property', OutputBooleanProperty, {extends: 'td'}); |
| customElements.define( |
| 'output-scoring-signals-property', OutputScoringSignalsProperty, |
| {extends: 'td'}); |
| customElements.define( |
| 'output-additional-info-property', OutputAdditionalInfoProperty, |
| {extends: 'td'}); |
| customElements.define( |
| 'output-url-property', OutputUrlProperty, {extends: 'td'}); |
| customElements.define( |
| 'output-text-property', OutputTextProperty, {extends: 'td'}); |
| |
| // TODO(manukh): Partition into smaller files. |