// 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 type {ACMatchClassification, AutocompleteControllerType, AutocompleteMatch, DictionaryEntry, OmniboxResponse, Signals} from './omnibox.mojom-webui.js';
import {OmniboxElement} from './omnibox_element.js';
import type {DisplayInputs} from './omnibox_input.js';
import {OmniboxInput} from './omnibox_input.js';
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
// @ts-ignore:next-line
import outputColumnWidthSheet from './omnibox_output_column_widths.css' with {type : 'css'};
import {clearChildren, createEl} from './omnibox_util.js';
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
// @ts-ignore:next-line
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();
}
updateAnswerImage(
_controllerType: AutocompleteControllerType, url: string, data: string) {
this.outputMatches.forEach(match => match.updateAnswerImage(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 contentsAndDescription: OutputAnswerProperty;
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 OutputAnswerProperty) {
this.contentsAndDescription = property;
}
});
}
updateAnswerImage(url: string, data: string) {
if (this.contentsAndDescription.image === url) {
this.contentsAndDescription.setAnswerImageData(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 OutputAnswerProperty extends FlexWrappingOutputProperty {
readonly image: string;
private readonly imageElement: HTMLImageElement;
constructor(
image: string, contents: string, description: string, answer: string,
contentsClassification: ACMatchClassification[],
descriptionClassification: ACMatchClassification[]) {
super([image, contents, description, answer].join('.'));
this.image = image;
this.imageElement = createEl('img', this.container, ['pair-item']);
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);
createEl('a', this.container, ['pair-item', 'image-url'], image).href =
image;
}
setAnswerImageData(imageData: string) {
this.imageElement.src = imageData;
}
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: DictionaryEntry[]) {
super(value.map(({key, value}) => `${key}: ${value}`).join('\n'));
this.container = createEl('div', this);
const pre = createEl('pre', this.container, ['json']);
value.forEach(({key, value}) => {
createEl('span', pre, ['key'], key + ': ');
createEl('span', pre, ['value'], value + '\n');
});
}
}
class OutputScoringSignalsProperty extends OutputDictionaryProperty {
constructor(value: Signals) {
super(Object.entries(value)
.filter(([, value]) => value !== null)
.map(([key, value]) => ({
key,
value,
} as DictionaryEntry)));
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: DictionaryEntry[]) {
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: DictionaryEntry[]): string {
const obj = value.reduce((obj: Record<string, string>, {key, value}) => {
obj[key] = value;
return obj;
}, {});
const text = JSON.stringify(obj, 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(
['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.image, 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, match.isSearchType,
match.strippedDestinationUrl)),
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-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.