blob: 4fa0ac2abd4157669ceea022a67660a6d018a346 [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() {
/**
* @typedef {{
* cursorPosition: number,
* time: number,
* done: boolean,
* host: string,
* isTypedHost: boolean,
* }}
*/
let ResultsDetails;
/** @typedef {Array<{key: string, value: string}>} */
let KeyValuePair;
/** @param {!Element} element*/
function clearChildren(element) {
while (element.firstChild) {
element.firstChild.remove();
}
}
class OmniboxOutput extends OmniboxElement {
constructor() {
super('omnibox-output-template');
/** @private {number} */
this.selectedResponseIndex_ = 0;
/** @type {!Array<!Array<!mojom.OmniboxResponse>>} */
this.responsesHistory = [];
/** @private {!Array<!OutputResultsGroup>} */
this.resultsGroups_ = [];
/** @private {!QueryInputs} */
this.queryInputs_ = /** @type {!QueryInputs} */ ({});
/** @private {!DisplayInputs} */
this.displayInputs_ = OmniboxInput.defaultDisplayInputs;
/** @private {string} */
this.filterText_ = '';
}
/** @param {!QueryInputs} queryInputs */
updateQueryInputs(queryInputs) {
this.queryInputs_ = queryInputs;
}
/** @param {!DisplayInputs} displayInputs */
updateDisplayInputs(displayInputs) {
this.displayInputs_ = displayInputs;
this.updateDisplay_();
}
/** @param {string} filterText */
updateFilterText(filterText) {
this.filterText_ = filterText;
this.updateFilterHighlights_();
}
/** @param {!Array<!Array<!mojom.OmniboxResponse>>} responsesHistory */
setResponsesHistory(responsesHistory) {
this.responsesHistory = responsesHistory;
this.dispatchEvent(new CustomEvent(
'responses-count-changed', {detail: responsesHistory.length}));
this.updateSelectedResponseIndex(this.selectedResponseIndex_);
}
/** @param {number} selection */
updateSelectedResponseIndex(selection) {
if (selection >= 0 && selection < this.responsesHistory.length) {
this.selectedResponseIndex_ = selection;
this.clearResultsGroups_();
this.responsesHistory[selection].forEach(
this.createResultsGroup_.bind(this));
}
}
prepareNewQuery() {
this.responsesHistory.push([]);
this.dispatchEvent(new CustomEvent(
'responses-count-changed', {detail: this.responsesHistory.length}));
}
/** @param {!mojom.OmniboxResponse} response */
addAutocompleteResponse(response) {
const lastIndex = this.responsesHistory.length - 1;
this.responsesHistory[lastIndex].push(response);
if (lastIndex === this.selectedResponseIndex_) {
this.createResultsGroup_(response);
}
}
/**
* Clears result groups from the UI.
* @private
*/
clearResultsGroups_() {
this.resultsGroups_ = [];
clearChildren(this.$$('#contents'));
}
/**
* Creates and adds a result group to the UI.
* @private @param {!mojom.OmniboxResponse} response
*/
createResultsGroup_(response) {
const resultsGroup =
OutputResultsGroup.create(response, this.queryInputs_.cursorPosition);
this.resultsGroups_.push(resultsGroup);
this.$$('#contents').appendChild(resultsGroup);
this.updateDisplay_();
this.updateFilterHighlights_();
}
/**
* @param {string} url
* @param {string} data
*/
updateAnswerImage(url, data) {
this.autocompleteMatches.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, 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 */
updateEliding_() {
this.resultsGroups_.forEach(
resultsGroup =>
resultsGroup.updateEliding(this.displayInputs_.elideCells));
}
/** @private */
updateRowHeights_() {
this.resultsGroups_.forEach(
resultsGroup =>
resultsGroup.updateRowHeights(this.displayInputs_.thinRows));
}
/** @private */
updateFilterHighlights_() {
this.autocompleteMatches.forEach(match => match.filter(this.filterText_));
}
/** @return {!Array<!OutputMatch>} */
get autocompleteMatches() {
return this.resultsGroups_.flatMap(
resultsGroup => resultsGroup.autocompleteMatches);
}
}
/**
* 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.OmniboxResponse} resultsGroup
* @param {number} cursorPosition
* @return {!OutputResultsGroup}
*/
static create(resultsGroup, cursorPosition) {
const outputResultsGroup = new OutputResultsGroup();
outputResultsGroup.setResultsGroup(resultsGroup, cursorPosition);
return outputResultsGroup;
}
constructor() {
super('output-results-group-template');
}
/**
* @param {!mojom.OmniboxResponse} resultsGroup
* @param {number} cursorPosition
*/
setResultsGroup(resultsGroup, cursorPosition) {
/** @private {ResultsDetails} */
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_ = [];
customElements.whenDefined(this.$$('output-results-details').localName)
.then(
() =>
this.$$('output-results-details').setDetails(this.details_));
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} */
renderHeader_() {
const head = document.createElement('thead');
head.classList.add('head');
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('tbody');
head.classList.add('head');
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.$$('output-results-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(({displayAlways}, index) => {
this.headers[index].hidden = !showDetails && !displayAlways;
});
// Show certain columns only if they showDetails is true.
this.autocompleteMatches.forEach(
match => match.updateVisibility(showDetails));
}
/** @param {boolean} elideCells */
updateEliding(elideCells) {
this.autocompleteMatches.forEach(
match => match.updateEliding(elideCells));
}
/** @param {boolean} thinRows */
updateRowHeights(thinRows) {
this.autocompleteMatches.forEach(
match => match.classList.toggle('thin', thinRows));
}
/**
* @private
* @return {boolean}
*/
get hasAdditionalProperties() {
return this.combinedResults.hasAdditionalProperties ||
this.individualResultsList.some(
results => results.hasAdditionalProperties);
}
/** @return {!Array<!OutputMatch>} */
get autocompleteMatches() {
return [this.combinedResults]
.concat(this.individualResultsList)
.flatMap(results => results.autocompleteMatches);
}
}
class OutputResultsDetails extends OmniboxElement {
constructor() {
super('output-results-details-template');
}
/** @param {ResultsDetails} details */
setDetails(details) {
this.$$('#cursor-position').textContent = details.cursorPosition;
this.$$('#time').textContent = details.time;
this.$$('#done').textContent = details.done;
this.$$('#host').textContent = details.host;
this.$$('#is-typed-host').textContent = details.isTypedHost;
}
}
/**
* 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();
this.classList.add('body');
/** @type {!Array<!OutputMatch>} */
this.autocompleteMatches = [];
}
/** @param {!Array<!mojom.AutocompleteMatch>} results */
set results(results) {
this.autocompleteMatches.forEach(match => match.remove());
this.autocompleteMatches = results.map(OutputMatch.create);
this.autocompleteMatches.forEach(this.appendChild.bind(this));
}
/** @return {?string} */
get innerHeaderText() {
return this.autocompleteMatches[0].providerName;
}
/** @return {boolean} */
get hasAdditionalProperties() {
return this.autocompleteMatches.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<string, !OutputProperty>} */
this.properties = {};
/** @type {!OutputProperty} */
this.properties.contentsAndDescription;
/** @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(([name]) => !CONSUMED_SOURCE_PROPERTIES.includes(name))
.forEach(([name, value]) => unconsumedProperties[name] = value);
/** @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 {string} url
* @param {string} data
*/
updateAnswerImage(url, data) {
if (this.properties.contentsAndDescription.value === url) {
this.properties.contentsAndDescription.setAnswerImageData(data);
}
}
/** @param {boolean} showDetails */
updateVisibility(showDetails) {
// Show certain columns only if they showDetails is true.
COLUMNS.forEach(({matchKey, displayAlways}) => {
this.properties[matchKey].hidden = !showDetails && !displayAlways;
});
}
/** @param {boolean} elideCells */
updateEliding(elideCells) {
Object.values(this.properties)
.forEach(property => property.classList.toggle('elided', elideCells));
}
/** @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(/** @type {!Object} */ (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 {
constructor() {
super();
/** @type {string} */
this.filterName;
}
/**
* @param {Column} column
* @param {!Array<*>} values
* @return {!OutputProperty}
*/
static create(column, values) {
const outputProperty = new column.outputClass();
outputProperty.classList.add(column.cellClassName);
outputProperty.filterName = column.tooltip.split('\n', 1)[0];
outputProperty.values = values;
return outputProperty;
}
/** @param {!Array<*>} values */
set values(values) {
/** @type {*} */
this.value = values[0];
/** @private {!Array<*>} */
this.values_ = values;
/** @override */
this.render_();
}
/** @private */
render_() {}
/** @return {string} */
get text() {
return this.value + '';
}
}
class FlexWrappingOutputProperty extends OutputProperty {
constructor() {
super();
// 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
/** @private {!Element} */
this.scrollContainer_ = document.createElement('div');
this.appendChild(this.scrollContainer_);
/** @private {!Element} */
this.container_ = document.createElement('div');
this.container_.classList.add('pair-container');
this.scrollContainer_.appendChild(this.container_);
}
}
class OutputPairProperty extends FlexWrappingOutputProperty {
constructor() {
super();
/** @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.second_.textContent] = this.values_;
}
/** @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 OutputAnswerProperty extends FlexWrappingOutputProperty {
constructor() {
super();
/** @type {!Element} */
this.image_ = document.createElement('img');
this.image_.classList.add('pair-item');
this.container_.appendChild(this.image_);
/** @type {!Element} */
this.contents_ = document.createElement('div');
this.contents_.classList.add('pair-item', 'contents');
this.container_.appendChild(this.contents_);
/** @type {!Element} */
this.description_ = document.createElement('div');
this.description_.classList.add('pair-item', 'description');
this.container_.appendChild(this.description_);
/** @type {!Element} */
this.answer_ = document.createElement('div');
this.answer_.classList.add('pair-item', 'answer');
this.container_.appendChild(this.answer_);
/** @type {!Element} */
this.imageUrl_ = document.createElement('a');
this.imageUrl_.classList.add('pair-item', 'image-url');
this.container_.appendChild(this.imageUrl_);
}
/** @param {string} imageData */
setAnswerImageData(imageData) {
this.image_.src = imageData;
}
/** @private @override */
render_() {
// TODO (manukh) Wrap this line when Clang is updated,
// https://b.corp.google.com/126708256 .
const [image, contents, description, answer, contentsClassification, descriptionClassification] =
this.values_;
OutputAnswerProperty.renderClassifiedText_(
this.contents_, /** @type {string} */ (contents),
/** @type {!Array<!mojom.ACMatchClassification>} */
(contentsClassification));
OutputAnswerProperty.renderClassifiedText_(
this.description_, /** @type {string} */ (description),
/** @type {!Array<!mojom.ACMatchClassification>} */
(descriptionClassification));
this.answer_.textContent = answer;
this.imageUrl_.textContent = image;
this.imageUrl_.href = image;
}
/** @override @return {string} */
get text() {
return this.values_.join('.');
}
/**
* @private
* @param {!Element} container
* @param {string} string
* @param {!Array<!mojom.ACMatchClassification>} classes
*/
static renderClassifiedText_(container, string, classes) {
clearChildren(container);
OutputAnswerProperty.classify(string + '\n', classes)
.map(
({string, style}) => OutputJsonProperty.renderJsonWord(
string, OutputAnswerProperty.styleToClasses_(style)))
.forEach(span => container.appendChild(span));
}
/**
* @param {string} string
* @param {!Array<!mojom.ACMatchClassification>} classes
* @return {!Array<{string: string, style: number}>}
*/
static classify(string, classes) {
return classes.map(({offset, style}, i) => {
const end = classes[i + 1] ? classes[i + 1].offset : string.length;
return {string: string.substring(offset, end), style};
});
}
/**
* @private
* @param {number} style
* @return {!Array<string>}
*/
static styleToClasses_(style) {
// 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() {
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);
}
get text() {
return (this.value ? 'is: ' : 'not: ') + this.filterName;
}
}
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 => {
return 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 {!Array<string>} classes
* @return {!Element}
*/
static renderJsonWord(word, classes) {
const span = document.createElement('span');
span.classList.add(...classes);
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 OutputAdditionalInfoProperty extends OutputJsonProperty {
/** @private @override */
render_() {
clearChildren(this.pre_);
this.tuples_.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.tuples_.reduce(
(prev, {key, value}) => `${prev}${key}: ${value}\n`, '');
}
/** @private @return {!KeyValuePair} */
get tuples_() {
return [
.../** @type {!KeyValuePair} */ (this.value),
{key: 'document_type', value: this.values_[1]}
];
}
}
class OutputUrlProperty extends FlexWrappingOutputProperty {
constructor() {
super();
/** @private {!Element} */
this.iconAndUrlContainer_ = document.createElement('div');
this.iconAndUrlContainer_.classList.add('pair-item');
this.container_.appendChild(this.iconAndUrlContainer_);
/** @private {!Element} */
this.icon_ = document.createElement('img');
this.iconAndUrlContainer_.appendChild(this.icon_);
/** @private {!Element} */
this.urlLink_ = document.createElement('a');
this.iconAndUrlContainer_.appendChild(this.urlLink_);
/** @private {!Element} */
this.strippedUrlLink_ = document.createElement('a');
this.strippedUrlLink_.classList.add('pair-item');
this.container_.appendChild(this.strippedUrlLink_);
}
/** @private @override */
render_() {
const [destinationUrl, isSearchType, strippedDestinationUrl] =
this.values_;
if (isSearchType) {
this.icon_.removeAttribute('src');
} else {
this.icon_.src = `chrome://favicon/${destinationUrl}`;
}
this.urlLink_.textContent = destinationUrl;
this.urlLink_.href = destinationUrl;
this.strippedUrlLink_.textContent = strippedDestinationUrl;
this.strippedUrlLink_.href = strippedDestinationUrl;
}
}
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 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
* This differs from `Array.split` in that this groups 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,
'Provider & Type\nThe AutocompleteProvider suggesting this result. / ' +
'The type of the result.',
['providerName', 'type'], OutputPairProperty),
new Column(
['Relevance'], '', 'relevance', true,
'Relevance\nThe result score. Higher is more relevant.', ['relevance'],
OutputTextProperty),
new Column(
['Contents', 'Description', 'Answer'], '', 'contentsAndDescription',
true,
'Contents & Description & Answer\nURL classifications are styled ' +
'blue.\nMATCH classifications are styled bold.\nDIM ' +
'classifications are styled with a gray background.',
[
'image', 'contents', 'description', 'answer', 'contentsClass',
'descriptionClass'
],
OutputAnswerProperty),
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', 'Stripped URL'], '', 'destinationUrl', true,
'URL & Stripped URL\nThe URL for the result. / The stripped URL for ' +
'the result.',
['destinationUrl', 'isSearchType', 'strippedDestinationUrl'],
OutputUrlProperty),
new Column(
['Fill', 'Inline'], '', 'fillAndInline', 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.',
['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, 'Transition\nHow 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,
'Associated Keyword\nIf non-empty, a "press tab to search" hint will ' +
'be shown and will engage this keyword.',
['associatedKeyword'], OutputTextProperty),
new Column(
['Keyword'], '', 'keyword', false,
'Keyword\nThe 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,
'Additional Info\nProvider-specific information about the result.',
['additionalInfo', 'documentType'], OutputAdditionalInfoProperty)
];
/** @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-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-json-property', OutputJsonProperty, {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'});
return {OmniboxOutput: OmniboxOutput};
});