blob: 71a17be14613abdfea7b4d348201a47a20ccf620 [file] [log] [blame]
// Copyright 2018 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.
cr.define('omnibox_output', function() {
/** @param {!Element} element*/
function clearChildren(element) {
while (element.firstChild)
element.firstChild.remove();
}
class OmniboxOutput extends OmniboxElement {
constructor() {
super('omnibox-output-template');
/** @type {!CopyDelegate} */
this.copyDelegate = new CopyDelegate(this);
/** @type {!Array<!mojom.OmniboxResult>} */
this.responses = [];
/** @private {!Array<!OutputResultsGroup>} */
this.resultsGroups_ = [];
/** @private {!QueryInputs} */
this.queryInputs_ = /** @type {!QueryInputs} */ ({});
/** @private {!DisplayInputs} */
this.displayInputs_ = OmniboxInputs.defaultDisplayInputs;
/** @private {string} */
this.filterText_ = '';
}
/** @param {!QueryInputs} queryInputs */
updateQueryInputs(queryInputs) {
this.queryInputs_ = queryInputs;
}
/** @param {!DisplayInputs} displayInputs */
updateDisplayInputs(displayInputs) {
this.displayInputs_ = displayInputs;
this.updateVisibility_();
}
/** @param {string} filterText */
updateFilterText(filterText) {
this.filterText_ = filterText;
this.updateFilterHighlights_();
}
clearAutocompleteResponses() {
this.responses = [];
this.resultsGroups_ = [];
clearChildren(this.$$('contents'));
}
/** @param {!mojom.OmniboxResult} response */
addAutocompleteResponse(response) {
this.responses.push(response);
const resultsGroup =
OutputResultsGroup.create(response, this.queryInputs_.cursorPosition);
this.resultsGroups_.push(resultsGroup);
this.$$('contents').appendChild(resultsGroup);
this.updateVisibility_();
this.updateFilterHighlights_();
}
/**
* 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, index) => resultsGroup.hidden =
!this.displayInputs_.showIncompleteResults &&
index !== this.resultsGroups_.length - 1);
this.resultsGroups_.forEach(resultsGroup => {
resultsGroup.updateVisibility(
this.displayInputs_.showIncompleteResults,
this.displayInputs_.showDetails,
this.displayInputs_.showAllProviders);
});
}
/** @private */
updateFilterHighlights_() {
this.matches.forEach(match => match.filter(this.filterText_));
}
/** @return {!Array<!OutputMatch>} */
get matches() {
return this.resultsGroups_.flatMap(resultsGroup => resultsGroup.matches);
}
/** @return {string} */
get visibleTableText() {
return this.resultsGroups_
.flatMap(resultsGroup => resultsGroup.visibleText)
.reduce((prev, cur) => `${prev}${cur}\n`, '');
}
}
/**
* 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 {
/**
* @param {!mojom.OmniboxResult} resultsGroup
* @param {number} cursorPosition
* @return {!OutputResultsGroup}
*/
static create(resultsGroup, cursorPosition) {
const outputResultsGroup = new OutputResultsGroup();
outputResultsGroup.setResultsGroup(resultsGroup, cursorPosition);
return outputResultsGroup;
}
constructor() {
super('details-and-table-template');
}
/**
* @param {!mojom.OmniboxResult} resultsGroup
* @param {number} cursorPosition
*/
setResultsGroup(resultsGroup, cursorPosition) {
/**
* @type {{cursorPosition: number, time: number, done: boolean, host:
* string, isTypedHost: boolean}}
*/
this.details = {
cursorPosition: cursorPosition,
time: resultsGroup.timeSinceOmniboxStartedMs,
done: resultsGroup.done,
host: resultsGroup.host,
isTypedHost: resultsGroup.isTypedHost
};
/** @type {!Array<!OutputHeader>} */
this.headers = COLUMNS.map(OutputHeader.create);
/** @type {!OutputResultsTable} */
this.combinedResults =
OutputResultsTable.create(resultsGroup.combinedResults);
/** @type {!Array<!OutputResultsTable>} */
this.individualResultsList =
resultsGroup.resultsByProvider
.map(resultsWrapper => resultsWrapper.results)
.filter(results => results.length > 0)
.map(OutputResultsTable.create);
if (this.hasAdditionalProperties)
this.headers.push(OutputHeader.create(ADDITIONAL_PROPERTIES_COLUMN));
this.render_();
}
/**
* Creates a HTML Node representing this data.
* @private
*/
render_() {
clearChildren(this);
/** @private {!Array<!Element>} */
this.innerHeaders_ = [];
this.$$('details').appendChild(this.renderDetails_());
this.$$('table').appendChild(this.renderHeader_());
this.$$('table').appendChild(this.combinedResults);
this.individualResultsList.forEach(results => {
const innerHeader = this.renderInnerHeader_(results);
this.innerHeaders_.push(innerHeader);
this.$$('table').appendChild(innerHeader);
this.$$('table').appendChild(results);
});
}
/** @private @return {!Element} */
renderDetails_() {
const details = OmniboxElement.getTemplate('details-template');
details.querySelector('.cursor-position').textContent =
this.details.cursorPosition;
details.querySelector('.time').textContent = this.details.time;
details.querySelector('.done').textContent = this.details.done;
details.querySelector('.host').textContent = this.details.host;
details.querySelector('.is-typed-host').textContent =
this.details.isTypedHost;
return details;
}
/** @private @return {!Element} */
renderHeader_() {
const head = document.createElement('thead');
const row = document.createElement('tr');
this.headers.forEach(cell => row.appendChild(cell));
head.appendChild(row);
return head;
}
/**
* @private
* @param {!OutputResultsTable} results
* @return {!Element}
*/
renderInnerHeader_(results) {
const head = document.createElement('thead');
const row = document.createElement('tr');
const cell = document.createElement('th');
// Reserve 1 more column for showing the additional properties column.
cell.colSpan = COLUMNS.length + 1;
cell.textContent = results.innerHeaderText;
row.appendChild(cell);
head.appendChild(row);
return head;
}
/**
* @param {boolean} showIncompleteResults
* @param {boolean} showDetails
* @param {boolean} showAllProviders
*/
updateVisibility(showIncompleteResults, showDetails, showAllProviders) {
// Show the details section above each table if showDetails or
// showIncompleteResults are true.
this.$$('details').hidden = !showDetails && !showIncompleteResults;
// 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(
(column, index) => this.headers[index].hidden =
!showDetails && !column.displayAlways);
// Show certain columns only if they showDetails is true.
this.matches.forEach(match => match.updateVisibility(showDetails));
}
/**
* @private
* @return {boolean}
*/
get hasAdditionalProperties() {
return this.combinedResults.hasAdditionalProperties ||
this.individualResultsList.some(
results => results.hasAdditionalProperties);
}
/** @return {!Array<!OutputMatch>} */
get matches() {
return [this.combinedResults]
.concat(this.individualResultsList)
.flatMap(results => results.matches);
}
/** @return {!Array<string>} */
get visibleText() {
return Array.from(this.shadowRoot.querySelectorAll(':host > :not(style)'))
.map(child => child.innerText);
}
}
/**
* Helps track and render a list of results. Each result is tracked and
* rendered by OutputMatch below.
*/
class OutputResultsTable extends HTMLTableSectionElement {
/**
* @param {!Array<!mojom.AutocompleteMatch>} results
* @return {!OutputResultsTable}
*/
static create(results) {
const resultsTable = new OutputResultsTable();
resultsTable.results = results;
return resultsTable;
}
constructor() {
super();
/** @type {!Array<!OutputMatch>} */
this.matches = [];
}
/** @param {!Array<!mojom.AutocompleteMatch>} results */
set results(results) {
this.matches.forEach(match => match.remove());
this.matches = results.map(OutputMatch.create);
this.matches.forEach(this.appendChild.bind(this));
}
/** @return {?string} */
get innerHeaderText() {
return this.matches[0].providerName;
}
/** @return {boolean} */
get hasAdditionalProperties() {
return this.matches.some(match => match.hasAdditionalProperties);
}
}
/** Helps track and render a single match. */
class OutputMatch extends HTMLTableRowElement {
/**
* @param {!mojom.AutocompleteMatch} match
* @return {!OutputMatch}
*/
static create(match) {
/** @suppress {checkTypes} */
const outputMatch = new OutputMatch();
outputMatch.match = match;
return outputMatch;
}
/** @param {!mojom.AutocompleteMatch} match */
set match(match) {
/** @type {!Object} */
this.properties = {};
/** @type {?string} */
this.providerName = match.providerName || null;
COLUMNS.forEach(column => {
const values = column.sourceProperties.map(
propertyName =>
/** @type {Object} */ (match)[propertyName]);
this.properties[column.matchKey] =
OutputProperty.create(column, values);
});
const unconsumedProperties = {};
Object.entries(match)
.filter(propertyNameValueTuple => {
const propertyName = propertyNameValueTuple[0];
return !CONSUMED_SOURCE_PROPERTIES.includes(propertyName);
})
.forEach(propertyNameValueTuple => {
const propertyName = propertyNameValueTuple[0];
const propertyValue = propertyNameValueTuple[1];
unconsumedProperties[propertyName] = propertyValue;
});
/** @type {!OutputProperty} */
this.additionalProperties = OutputProperty.create(
ADDITIONAL_PROPERTIES_COLUMN, [unconsumedProperties]);
this.render_();
}
/** @private */
render_() {
clearChildren(this);
COLUMNS.map(column => this.properties[column.matchKey])
.forEach(cell => this.appendChild(cell));
if (this.hasAdditionalProperties)
this.appendChild(this.additionalProperties);
}
/** @param {boolean} showDetails */
updateVisibility(showDetails) {
// Show certain columns only if they showDetails is true.
COLUMNS.forEach(column => {
this.properties[column.matchKey].hidden =
!showDetails && !column.displayAlways;
});
}
/** @param {string} filterText */
filter(filterText) {
this.classList.remove('filtered-highlighted');
this.allProperties_.forEach(
property => property.classList.remove('filtered-highlighted-nested'));
if (!filterText)
return;
const matchedProperties = this.allProperties_.filter(
property => FilterUtil.filterText(property.text, filterText));
const isMatch = matchedProperties.length > 0;
this.classList.toggle('filtered-highlighted', isMatch);
matchedProperties.forEach(
property => property.classList.add('filtered-highlighted-nested'));
}
/**
* @return {boolean} Used to determine if the additional properties column
* needs to be displayed for this match.
*/
get hasAdditionalProperties() {
return Object.keys(this.additionalProperties.value).length > 0;
}
/** @private @return {!Array<!OutputProperty>} */
get allProperties_() {
return Object.values(this.properties).concat(this.additionalProperties);
}
}
class OutputHeader extends HTMLTableCellElement {
/**
* @param {Column} column
* @return {!OutputHeader}
*/
static create(column) {
const header = new OutputHeader();
header.classList.add(column.headerClassName);
header.setContents(column.headerText, column.url);
header.title = column.tooltip;
return header;
}
/**
* @param {!Array<string>} texts
* @param {string=} url
*/
setContents(texts, url) {
clearChildren(this);
let container;
if (url) {
container = document.createElement('a');
container.href = url;
} else {
container = document.createElement('div');
}
container.classList.add('header-container');
texts.forEach(text => {
const part = document.createElement('span');
part.textContent = text;
container.appendChild(part);
});
this.appendChild(container);
}
}
class OutputProperty extends HTMLTableCellElement {
/**
* @param {Column} column
* @param {!Array<!Object>} values
* @return {!OutputProperty}
*/
static create(column, values) {
const outputProperty = new column.outputClass();
outputProperty.className = column.cellClassName;
outputProperty.name = column.headerText.join('.');
outputProperty.values = values;
return outputProperty;
}
/** @return {!Object} */
get value() {
return this.value_;
}
/** @param {!Array<!Object>} values */
set values(values) {
/** @private {!Object} */
this.value_ = values[0];
/** @private {!Array<!Object>} */
this.values_ = values;
/** @override */
this.render_();
}
/** @private */
render_() {}
/** @return {string} */
get text() {
return this.value_ + '';
}
}
class OutputPairProperty extends OutputProperty {
constructor() {
super();
this.container_ = document.createElement('div');
this.container_.classList.add('pair-container');
this.appendChild(this.container_);
/** @type {!Element} */
this.first_ = document.createElement('div');
this.first_.classList.add('pair-item');
this.container_.appendChild(this.first_);
/** @type {!Element} */
this.second_ = document.createElement('div');
this.second_.classList.add('pair-item');
this.container_.appendChild(this.second_);
}
/** @private @override */
render_() {
this.first_.textContent = this.values_[0];
this.second_.textContent = this.values_[1];
}
/** @override @return {string} */
get text() {
return `${this.values_[0]}.${this.values_[1]}`;
}
}
class OutputOverlappingPairProperty extends OutputPairProperty {
constructor() {
super();
this.notOverlapWarning_ = document.createElement('div');
this.notOverlapWarning_.classList.add('overlap-warning');
this.container_.appendChild(this.notOverlapWarning_);
}
/** @private @override */
render_() {
const overlap = this.values_[0].endsWith(this.values_[1]);
const firstText = this.values_[1] && overlap ?
this.values_[0].slice(0, -this.values_[1].length) :
this.values_[0];
this.first_.textContent = firstText;
this.second_.textContent = this.values_[1];
this.notOverlapWarning_.textContent = overlap ?
'' :
`btw, these texts do not overlap; '${
this.values_[1]}' was expected to be a suffix of '${
this.values_[0]}'`;
}
}
class OutputBooleanProperty extends OutputProperty {
constructor() {
super();
/** @private {!Element} */
this.icon_ = document.createElement('div');
this.appendChild(this.icon_);
}
/** @private @override */
render_() {
this.icon_.classList.toggle('check-mark', !!this.value);
this.icon_.classList.toggle('x-mark', !this.value);
this.icon_.textContent = this.value;
}
get text() {
return (this.value_ ? 'is: ' : 'not: ') + this.name;
}
}
class OutputJsonProperty extends OutputProperty {
constructor() {
super();
/** @private {!Element} */
this.pre_ = document.createElement('pre');
this.pre_.classList.add('json');
this.appendChild(this.pre_);
}
/** @private @override */
render_() {
clearChildren(this.pre_);
this.text.split(/("(?:[^"\\]|\\.)*":?|\w+)/)
.map(
word => OutputJsonProperty.renderJsonWord(
word, OutputJsonProperty.classifyJsonWord(word)))
.forEach(jsonSpan => this.pre_.appendChild(jsonSpan));
}
/** @override @return {string} */
get text() {
return JSON.stringify(this.value_, null, 2);
}
/**
* @param {string} word
* @param {string|undefined} cls
* @return {!Element}
*/
static renderJsonWord(word, cls) {
const span = document.createElement('span');
if (cls)
span.classList.add(cls);
span.textContent = word;
return span;
}
/**
* @param {string} word
* @return {string|undefined}
*/
static classifyJsonWord(word) {
if (/^\d+$/.test(word))
return 'number';
if (/^"[^]*":$/.test(word))
return 'key';
if (/^"[^]*"$/.test(word))
return 'string';
if (/true|false/.test(word))
return 'boolean';
if (/null/.test(word))
return 'null';
}
}
class OutputKeyValueTuplesProperty extends OutputJsonProperty {
/** @private @override */
render_() {
clearChildren(this.pre_);
this.value.forEach(({key, value}) => {
this.pre_.appendChild(
OutputJsonProperty.renderJsonWord(key + ': ', 'key'));
this.pre_.appendChild(
OutputJsonProperty.renderJsonWord(value + '\n', 'number'));
});
}
/** @override @return {string} */
get text() {
return this.value_.reduce(
(prev, {key, value}) => `${prev}${key}: ${value}\n`, '');
}
}
class OutputLinkProperty extends OutputProperty {
constructor() {
super();
/** @private {!Element} */
this.link_ = document.createElement('a');
this.appendChild(this.link_);
}
/** @private @override */
render_() {
this.link_.textContent = this.value_;
this.link_.href = this.value_;
}
}
class OutputTextProperty extends OutputProperty {
constructor() {
super();
/** @private {!Element} */
this.div_ = document.createElement('div');
this.appendChild(this.div_);
}
/** @private @override */
render_() {
this.div_.textContent = this.value_;
}
}
/** Responsible for setting clipboard contents. */
class CopyDelegate {
/** @param {!omnibox_output.OmniboxOutput} omniboxOutput */
constructor(omniboxOutput) {
/** @private {!omnibox_output.OmniboxOutput} */
this.omniboxOutput_ = omniboxOutput;
}
copyTextOutput() {
this.copy_(this.omniboxOutput_.visibleTableText);
}
copyJsonOutput() {
this.copy_(JSON.stringify(this.omniboxOutput_.responses, null, 2));
}
/**
* @private
* @param {string} value
*/
copy_(value) {
navigator.clipboard.writeText(value).catch(
error => console.error('unable to copy to clipboard:', error));
}
}
/** 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`.
* @param {string} searchText
* @param {string} filterText
* @return {boolean}
*/
static filterText(searchText, filterText) {
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
* We don't use `Array.split`, because we want to group digits, e.g. 134.
* @private
* @param {string} text
* @return {!Array<string>}
*/
static textToWords_(text) {
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, truncatd; max length: ${
MAX_TEXT_LENGTH}, truncated text: ${text}`);
}
return text.match(/[a-z]+|[A-Z][a-z]*|\d+|./g) || [];
}
}
class Column {
/**
* @param {!Array<string>} headerText
* @param {string} url
* @param {string} matchKey
* @param {boolean} displayAlways
* @param {string} tooltip
* @param {function(new:OutputProperty)} outputClass
* @param {!Array<string>} sourceProperties
*/
constructor(
headerText, url, matchKey, displayAlways, tooltip, sourceProperties,
outputClass) {
/** @type {!Array<string>} split per span container to support styling. */
this.headerText = headerText;
/** @type {string} header link href or blank if non-hyperlink header. */
this.url = url;
/** @type {string} the field name used in the Match.properties object. */
this.matchKey = matchKey;
/** @type {boolean} if shown when showDetails option is false. */
this.displayAlways = displayAlways;
/** @type {string} header tooltip. */
this.tooltip = tooltip;
/** @type {!Array<string>} related mojo AutocompleteMatch properties. */
this.sourceProperties = sourceProperties;
/** @type {function(new:OutputProperty)} */
this.outputClass = outputClass;
const hyphenatedName =
matchKey.replace(/[A-Z]/g, c => '-' + c.toLowerCase());
/** @type {string} */
this.cellClassName = 'cell-' + hyphenatedName;
/** @type {string} */
this.headerClassName = 'header-' + hyphenatedName;
}
}
/**
* A constant that's used to decide what autocomplete result
* properties to output in what order.
* @type {!Array<!Column>}
*/
const COLUMNS = [
new Column(
['Provider', 'Type'], '', 'providerAndType', true,
'The AutocompleteProvider suggesting this result. / The type of the ' +
'result.',
['providerName', 'type'], OutputPairProperty),
new Column(
['Relevance'], '', 'relevance', true,
'The result score. Higher is more relevant.', ['relevance'],
OutputTextProperty),
new Column(
['Contents', 'Description'], '', 'contentsAndDescription', true,
'The text that is presented identifying the result. / The page title ' +
'of the result.',
['contents', 'description'], OutputPairProperty),
new Column(
['D'], '', 'allowedToBeDefaultMatch', 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).',
['allowedToBeDefaultMatch'], OutputBooleanProperty),
new Column(
['S'], '', 'starred', false,
'Starred\nA green checkmark indicates that the result has been ' +
'bookmarked.',
['starred'], OutputBooleanProperty),
new Column(
['T'], '', 'hasTabMatch', false,
'Has Tab Match\nA green checkmark indicates that the result URL ' +
'matches an open tab.',
['hasTabMatch'], OutputBooleanProperty),
new Column(
['URL'], '', 'destinationUrl', true, 'The URL for the result.',
['destinationUrl'], OutputLinkProperty),
new Column(
['Fill', 'Inline'], '', 'fillAndInline', false,
'The 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.',
['fillIntoEdit', 'inlineAutocompletion'],
OutputOverlappingPairProperty),
new Column(
['D'], '', 'deletable', false,
'Deletable\nA green checkmark indicates that the result can be ' +
'deleted from the visit history.',
['deletable'], OutputBooleanProperty),
new Column(
['P'], '', 'fromPrevious', false,
'From Previous\nTrue if this match is from a previous result.',
['fromPrevious'], OutputBooleanProperty),
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, 'How the user got to the result.', ['transition'],
OutputTextProperty),
new Column(
['D'], '', 'providerDone', false,
'Done\nA green checkmark indicates that the provider is done looking ' +
'for more results.',
['providerDone'], OutputBooleanProperty),
new Column(
['Associated Keyword'], '', 'associatedKeyword', false,
'If non-empty, a "press tab to search" hint will be shown and will ' +
'engage this keyword.',
['associatedKeyword'], OutputTextProperty),
new Column(
['Keyword'], '', 'keyword', false,
'The keyword of the search engine to be used.', ['keyword'],
OutputTextProperty),
new Column(
['D'], '', 'duplicates', false,
'Duplicates\nThe number of matches that have been marked as ' +
'duplicates of this match.',
['duplicates'], OutputTextProperty),
new Column(
['Additional Info'], '', 'additionalInfo', false,
'Provider-specific information about the result.', ['additionalInfo'],
OutputKeyValueTuplesProperty)
];
/** @type {!Column} */
const ADDITIONAL_PROPERTIES_COLUMN = new Column(
['Additional Properties'], '', 'additionalProperties', false,
'Properties not accounted for.', [], OutputJsonProperty);
const CONSUMED_SOURCE_PROPERTIES =
COLUMNS.flatMap(column => column.sourceProperties);
customElements.define('omnibox-output', OmniboxOutput);
customElements.define('output-results-group', OutputResultsGroup);
customElements.define(
'output-results-table', OutputResultsTable, {extends: 'tbody'});
customElements.define('output-match', OutputMatch, {extends: 'tr'});
customElements.define('output-haeder', OutputHeader, {extends: 'th'});
customElements.define(
'output-pair-property', OutputPairProperty, {extends: 'td'});
customElements.define(
'output-overlapping-pair-property', OutputOverlappingPairProperty,
{extends: 'td'});
customElements.define(
'output-boolean-property', OutputBooleanProperty, {extends: 'td'});
customElements.define(
'output-json-property', OutputJsonProperty, {extends: 'td'});
customElements.define(
'output-key-value-tuple-property', OutputKeyValueTuplesProperty,
{extends: 'td'});
customElements.define(
'output-link-property', OutputLinkProperty, {extends: 'td'});
customElements.define(
'output-text-property', OutputTextProperty, {extends: 'td'});
return {OmniboxOutput: OmniboxOutput};
});