| // 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; |
| |
| /** @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.updateVisibility_(); |
| this.updateEliding_(); |
| } |
| |
| /** @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.updateVisibility_(); |
| this.updateFilterHighlights_(); |
| } |
| |
| /** |
| * @param {string} url |
| * @param {string} data |
| */ |
| updateAnswerImage(url, data) { |
| this.autocompleteMatches.forEach( |
| match => match.updateAnswerImage(url, data)); |
| } |
| |
| /** |
| * 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 */ |
| updateFilterHighlights_() { |
| this.autocompleteMatches.forEach(match => match.filter(this.filterText_)); |
| } |
| |
| /** @return {!Array<!OutputMatch>} */ |
| get autocompleteMatches() { |
| return this.resultsGroups_.flatMap( |
| resultsGroup => resultsGroup.autocompleteMatches); |
| } |
| |
| /** @return {string} */ |
| get visibleTableText() { |
| return this.resultsGroups_ |
| .flatMap(resultsGroup => resultsGroup.visibleText) |
| .join('\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.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)); |
| } |
| |
| /** |
| * @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); |
| } |
| |
| /** @return {!Array<string>} */ |
| get visibleText() { |
| return Array.from(this.shadowRoot.querySelectorAll(':host > :not(style)')) |
| .map(child => child.innerText); |
| } |
| } |
| |
| 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 { |
| /** |
| * @param {Column} column |
| * @param {!Array<*>} values |
| * @return {!OutputProperty} |
| */ |
| static create(column, values) { |
| const outputProperty = new column.outputClass(); |
| outputProperty.classList.add(column.cellClassName); |
| outputProperty.name = column.headerText.join('.'); |
| 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 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.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 OutputProperty { |
| constructor() { |
| super(); |
| |
| /** @private {!Element} */ |
| this.container_ = document.createElement('div'); |
| this.container_.classList.add('pair-container'); |
| this.appendChild(this.container_); |
| |
| /** @type {!Element} */ |
| this.image_ = document.createElement('img'); |
| this.image_.classList.add('pair-item', 'image'); |
| 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_); |
| } |
| |
| /** @param {string} imageData */ |
| setAnswerImageData(imageData) { |
| this.image_.src = imageData; |
| } |
| |
| /** @private @override */ |
| render_() { |
| this.contents_.textContent = this.values_[1]; |
| this.description_.textContent = this.values_[2]; |
| this.answer_.textContent = this.values_[3]; |
| } |
| |
| /** @override @return {string} */ |
| get text() { |
| return this.values_.join('.'); |
| } |
| } |
| |
| 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 => { |
| 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 {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 OutputUrlProperty extends OutputProperty { |
| constructor() { |
| super(); |
| |
| /** @private {!Element} */ |
| this.container_ = document.createElement('div'); |
| this.container_.classList.add('pair-container'); |
| this.appendChild(this.container_); |
| |
| /** @private {!Element} */ |
| this.icon_ = document.createElement('img'); |
| this.container_.appendChild(this.icon_); |
| |
| /** @private {!Element} */ |
| this.link_ = document.createElement('a'); |
| this.container_.appendChild(this.link_); |
| } |
| |
| /** @private @override */ |
| render_() { |
| if (this.values_[1]) { |
| this.icon_.removeAttribute('src'); |
| } else { |
| this.icon_.src = `chrome://favicon/${this.value}`; |
| } |
| 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 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', 'Answer'], '', 'contentsAndDescription', |
| true, |
| 'The text that is presented identifying the result. / The page title ' + |
| 'of the result.', |
| ['image', 'contents', 'description', 'answer'], 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'], '', 'destinationUrl', true, 'The URL for the result.', |
| ['destinationUrl', 'isSearchType'], OutputUrlProperty), |
| 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-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-key-value-tuple-property', OutputKeyValueTuplesProperty, |
| {extends: 'td'}); |
| customElements.define( |
| 'output-url-property', OutputUrlProperty, {extends: 'td'}); |
| customElements.define( |
| 'output-text-property', OutputTextProperty, {extends: 'td'}); |
| |
| return {OmniboxOutput: OmniboxOutput}; |
| }); |