| // 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() { |
| /** |
| * Details how to display an autocomplete result data field. |
| * @typedef {{ |
| * header: string, |
| * url: string, |
| * propertyName: string, |
| * displayAlways: boolean, |
| * tooltip: string, |
| * }} |
| */ |
| let PresentationInfoRecord; |
| |
| /** |
| * A constant that's used to decide what autocomplete result |
| * properties to output in what order. |
| * @type {!Array<!PresentationInfoRecord>} |
| */ |
| const PROPERTY_OUTPUT_ORDER = [ |
| { |
| header: 'Provider', |
| url: '', |
| propertyName: 'providerName', |
| displayAlways: true, |
| tooltip: 'The AutocompleteProvider suggesting this result.' |
| }, |
| { |
| header: 'Type', |
| url: '', |
| propertyName: 'type', |
| displayAlways: true, |
| tooltip: 'The type of the result.' |
| }, |
| { |
| header: 'Relevance', |
| url: '', |
| propertyName: 'relevance', |
| displayAlways: true, |
| tooltip: 'The result score. Higher is more relevant.' |
| }, |
| { |
| header: 'Contents', |
| url: '', |
| propertyName: 'contents', |
| displayAlways: true, |
| tooltip: 'The text that is presented identifying the result.' |
| }, |
| { |
| header: 'Description', |
| url: '', |
| propertyName: 'description', |
| displayAlways: false, |
| tooltip: 'The page title of the result.' |
| }, |
| { |
| header: 'CanBeDefault', |
| url: '', |
| propertyName: 'allowedToBeDefaultMatch', |
| displayAlways: false, |
| tooltip: |
| 'A 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).' |
| }, |
| { |
| header: 'Starred', |
| url: '', |
| propertyName: 'starred', |
| displayAlways: false, |
| tooltip: |
| 'A green checkmark indicates that the result has been bookmarked.' |
| }, |
| { |
| header: 'Hastabmatch', |
| url: '', |
| propertyName: 'hasTabMatch', |
| displayAlways: false, |
| tooltip: |
| 'A green checkmark indicates that the result URL matches an open' + |
| 'tab.' |
| }, |
| { |
| header: 'URL', |
| url: '', |
| propertyName: 'destinationUrl', |
| displayAlways: true, |
| tooltip: 'The URL for the result.' |
| }, |
| { |
| header: 'FillIntoEdit', |
| url: '', |
| propertyName: 'fillIntoEdit', |
| displayAlways: false, |
| tooltip: 'The text shown in the omnibox when the result is selected.' |
| }, |
| { |
| header: 'InlineAutocompletion', |
| url: '', |
| propertyName: 'inlineAutocompletion', |
| displayAlways: false, |
| tooltip: 'The text shown in the omnibox as a blue highlight selection ' + |
| 'following the cursor, if this match is shown inline.' |
| }, |
| { |
| header: 'Del', |
| url: '', |
| propertyName: 'deletable', |
| displayAlways: false, |
| tooltip: |
| 'A green checkmark indicates that the result can be deleted from ' + |
| 'the visit history.' |
| }, |
| { |
| header: 'Prev', |
| url: '', |
| propertyName: 'fromPrevious', |
| displayAlways: false, |
| tooltip: '' |
| }, |
| { |
| header: 'Tran', |
| url: |
| 'https://cs.chromium.org/chromium/src/ui/base/page_transition_types.h?q=page_transition_types.h&sq=package:chromium&dr=CSs&l=14', |
| propertyName: 'transition', |
| displayAlways: false, |
| tooltip: 'How the user got to the result.' |
| }, |
| { |
| header: 'Done', |
| url: '', |
| propertyName: 'providerDone', |
| displayAlways: false, |
| tooltip: |
| 'A green checkmark indicates that the provider is done looking ' + |
| 'for more results.' |
| }, |
| { |
| header: 'AssociatedKeyword', |
| url: '', |
| propertyName: 'associatedKeyword', |
| displayAlways: false, |
| tooltip: 'If non-empty, a "press tab to search" hint will be shown and ' + |
| 'will engage this keyword.' |
| }, |
| { |
| header: 'Keyword', |
| url: '', |
| propertyName: 'keyword', |
| displayAlways: false, |
| tooltip: 'The keyword of the search engine to be used.' |
| }, |
| { |
| header: 'Duplicates', |
| url: '', |
| propertyName: 'duplicates', |
| displayAlways: false, |
| tooltip: 'The number of matches that have been marked as duplicates of ' + |
| 'this match..' |
| }, |
| { |
| header: 'AdditionalInfo', |
| url: '', |
| propertyName: 'additionalInfo', |
| displayAlways: false, |
| tooltip: 'Provider-specific information about the result.' |
| } |
| ]; |
| |
| /** |
| * In addition to representing the rendered HTML element, OmniboxOutput also |
| * provides a single public interface to interact with the output: |
| * 1. Render tables from responses (RenderDelegate) |
| * 2. Control visibility based on display options (TODO) |
| * 3. Control visibility and coloring based on search text (FilterDelegate) |
| * 4. Export and copy output (CopyDelegate) |
| * 5. Preserve inputs and reset inputs to default (TODO) |
| * 6. Export and import inputs (TODO) |
| * With regards to interacting with RenderDelegate, OmniboxOutput tracks and |
| * aggregates responses from the C++ autocomplete controller. Typically, the |
| * C++ controller returns 3 sets of results per query, unless a new query is |
| * submitted before all 3 responses. OmniboxController also triggers |
| * appending to and clearing of OmniboxOutput when appropriate (e.g., upon |
| * receiving a new response or a change in display inputs). |
| */ |
| class OmniboxOutput extends OmniboxElement { |
| /** @return {string} */ |
| static get is() { |
| return 'omnibox-output'; |
| } |
| |
| constructor() { |
| super('omnibox-output-template'); |
| |
| /** @type {!RenderDelegate} */ |
| this.renderDelegate = new RenderDelegate(this.$$('contents')); |
| /** @type {!CopyDelegate} */ |
| this.copyDelegate = new CopyDelegate(this); |
| /** @type {!FilterDelegate} */ |
| this.filterDelegate = new FilterDelegate(this); |
| |
| /** @type {!Array<!mojom.OmniboxResult>} */ |
| this.responses = []; |
| /** @private {!QueryInputs} */ |
| this.queryInputs_ = /** @type {!QueryInputs} */ ({}); |
| /** @private {!DisplayInputs} */ |
| this.displayInputs_ = /** @type {!DisplayInputs} */ ({}); |
| } |
| |
| /** @param {!QueryInputs} queryInputs */ |
| updateQueryInputs(queryInputs) { |
| this.queryInputs_ = queryInputs; |
| this.refresh_(); |
| } |
| |
| /** @param {!DisplayInputs} displayInputs */ |
| updateDisplayInputs(displayInputs) { |
| this.displayInputs_ = displayInputs; |
| this.refresh_(); |
| } |
| |
| clearAutocompleteResponses() { |
| this.responses = []; |
| this.refresh_(); |
| } |
| |
| /** @param {!mojom.OmniboxResult} response */ |
| addAutocompleteResponse(response) { |
| this.responses.push(response); |
| this.refresh_(); |
| } |
| |
| /** @private */ |
| refresh_() { |
| this.renderDelegate.refresh( |
| this.queryInputs_, this.responses, this.displayInputs_); |
| } |
| |
| /** @return {!Array<!OutputMatch>} */ |
| get matches() { |
| return this.renderDelegate.matches; |
| } |
| } |
| |
| // Responsible for rendering the output HTML. |
| class RenderDelegate { |
| /** @param {!Element} containerElement */ |
| constructor(containerElement) { |
| /** @private {!Element} */ |
| this.containerElement_ = containerElement; |
| } |
| |
| /** |
| * @param {!QueryInputs} queryInputs |
| * @param {!Array<!mojom.OmniboxResult>} responses |
| * @param {!DisplayInputs} displayInputs |
| */ |
| refresh(queryInputs, responses, displayInputs) { |
| if (!responses.length) |
| return; |
| |
| /** @private {!Array<OutputResultsGroup>} */ |
| this.resultsGroup_; |
| |
| if (displayInputs.showIncompleteResults) { |
| this.resultsGroup_ = responses.map( |
| response => |
| new OutputResultsGroup(response, queryInputs.cursorPosition)); |
| } else { |
| const lastResponse = responses[responses.length - 1]; |
| this.resultsGroup_ = |
| [new OutputResultsGroup(lastResponse, queryInputs.cursorPosition)]; |
| } |
| |
| this.clearOutput_(); |
| this.resultsGroup_.forEach( |
| resultsGroup => |
| this.containerElement_.appendChild(resultsGroup.render( |
| displayInputs.showDetails, |
| displayInputs.showIncompleteResults, |
| displayInputs.showAllProviders))); |
| } |
| |
| /** @private */ |
| clearOutput_() { |
| const contents = this.containerElement_; |
| // Clears all children. |
| while (contents.firstChild) |
| contents.removeChild(contents.firstChild); |
| } |
| |
| /** @return {string} */ |
| get visibletableText() { |
| return this.containerElement_.innerText; |
| } |
| |
| /** @return {!Array<!OutputMatch>} */ |
| get matches() { |
| return this.resultsGroup_.flatMap(resultsGroup => resultsGroup.matches); |
| } |
| } |
| |
| /** |
| * 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 { |
| /** |
| * @param {!mojom.OmniboxResult} resultsGroup |
| * @param {number} cursorPosition |
| */ |
| constructor(resultsGroup, cursorPosition) { |
| /** @struct */ |
| this.details = { |
| cursorPosition, |
| time: resultsGroup.timeSinceOmniboxStartedMs, |
| done: resultsGroup.done, |
| host: resultsGroup.host, |
| isTypedHost: resultsGroup.isTypedHost |
| }; |
| /** @type {!OutputResultsTable} */ |
| this.combinedResults = |
| new OutputResultsTable(resultsGroup.combinedResults); |
| /** @type {!Array<!OutputResultsTable>} */ |
| this.individualResultsList = |
| resultsGroup.resultsByProvider |
| .map(resultsWrapper => resultsWrapper.results) |
| .filter(results => results.length > 0) |
| .map(results => new OutputResultsTable(results)); |
| } |
| |
| /** |
| * Creates a HTML Node representing this data. |
| * @param {boolean} showDetails |
| * @param {boolean} showIncompleteResults |
| * @param {boolean} showAllProviders |
| * @return {!Element} |
| */ |
| render(showDetails, showIncompleteResults, showAllProviders) { |
| const detailsAndTable = |
| OmniboxElement.getTemplate('details-and-table-template'); |
| if (showDetails || showIncompleteResults) { |
| detailsAndTable.querySelector('.details') |
| .appendChild(this.renderDetails_()); |
| } |
| |
| const showAdditionalPropertiesColumn = |
| this.showAdditionalPropertiesColumn_(showDetails); |
| |
| detailsAndTable.querySelector('.table').appendChild( |
| OutputResultsTable.renderHeader( |
| showDetails, showAdditionalPropertiesColumn)); |
| detailsAndTable.querySelector('.table').appendChild( |
| this.combinedResults.render(showDetails)); |
| if (showAllProviders) { |
| this.individualResultsList.forEach(individualResults => { |
| detailsAndTable.querySelector('.table').appendChild( |
| individualResults.renderInnerHeader( |
| showDetails, showAdditionalPropertiesColumn)); |
| detailsAndTable.querySelector('.table').appendChild( |
| individualResults.render(showDetails)); |
| }); |
| } |
| return detailsAndTable; |
| } |
| |
| /** |
| * @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 |
| * @param {boolean} showDetails |
| * @return {boolean} |
| */ |
| showAdditionalPropertiesColumn_(showDetails) { |
| return showDetails && |
| (this.combinedResults.hasAdditionalProperties || |
| this.individualResultsList.some( |
| results => results.hasAdditionalProperties)); |
| } |
| |
| /** @return {!Array<!OutputMatch>} */ |
| get matches() { |
| return [this.combinedResults] |
| .concat(this.individualResultsList) |
| .flatMap(results => results.matches); |
| } |
| } |
| |
| /** |
| * Helps track and render a list of results. Each result is tracked and |
| * rendered by OutputMatch below. |
| */ |
| class OutputResultsTable { |
| /** @param {!Array<!mojom.AutocompleteMatch>} results */ |
| constructor(results) { |
| /** @type {!Array<!OutputMatch>} */ |
| this.matches = results.map(match => new OutputMatch(match)); |
| } |
| |
| /** |
| * @param {boolean} showDetails |
| * @param {boolean} showAdditionalPropertiesColumn |
| * @return {Element} |
| */ |
| static renderHeader(showDetails, showAdditionalPropertiesColumn) { |
| const head = document.createElement('thead'); |
| const row = document.createElement('tr'); |
| const cells = |
| OutputMatch.displayedProperties(showDetails) |
| .map( |
| ({header, url, tooltip}) => |
| OutputMatch.renderHeaderCell(header, url, tooltip)); |
| if (showAdditionalPropertiesColumn) |
| cells.push(OutputMatch.renderHeaderCell('Additional Properties')); |
| cells.forEach(cell => row.appendChild(cell)); |
| head.appendChild(row); |
| return head; |
| } |
| |
| /** |
| * Creates a HTML Node representing this data. |
| * @param {boolean} showDetails |
| * @return {!Element} |
| */ |
| render(showDetails) { |
| const body = document.createElement('tbody'); |
| this.matches.forEach( |
| match => body.appendChild(match.render(showDetails))); |
| return body; |
| } |
| |
| /** |
| * @param {boolean} showDetails |
| * @param {boolean} showAdditionalPropertiesColumn |
| * @return {!Element} |
| */ |
| renderInnerHeader(showDetails, showAdditionalPropertiesColumn) { |
| const head = document.createElement('thead'); |
| const row = document.createElement('tr'); |
| const cell = document.createElement('th'); |
| // Reserve 1 more column if showing the additional properties column. |
| cell.colSpan = OutputMatch.displayedProperties(showDetails).length + |
| showAdditionalPropertiesColumn; |
| cell.textContent = this.matches[0].properties.providerName.value; |
| row.appendChild(cell); |
| head.appendChild(row); |
| return head; |
| } |
| |
| /** @return {boolean} */ |
| get hasAdditionalProperties() { |
| return this.matches.some(match => match.hasAdditionalProperties); |
| } |
| } |
| |
| /** Helps track and render a single match. */ |
| class OutputMatch { |
| /** @param {!mojom.AutocompleteMatch} match */ |
| constructor(match) { |
| /** @dict */ |
| this.properties = {}; |
| let unconsumedProperties = {}; |
| Object.entries(match).forEach(propertyNameValueTuple => { |
| // TODO(manukh) replace with destructuring when the styleguide is |
| // updated |
| // https://chromium-review.googlesource.com/c/chromium/src/+/1271915 |
| const propertyName = propertyNameValueTuple[0]; |
| const propertyValue = propertyNameValueTuple[1]; |
| |
| if (PROPERTY_OUTPUT_ORDER.some( |
| property => property.propertyName === propertyName)) { |
| this.properties[propertyName] = |
| OutputProperty.constructProperty(propertyName, propertyValue); |
| } else { |
| unconsumedProperties[propertyName] = propertyValue; |
| } |
| }); |
| /** @type {!OutputProperty} */ |
| this.additionalProperties = OutputProperty.constructProperty( |
| 'additionalProperties', unconsumedProperties); |
| |
| /** @type {!Element} */ |
| this.associatedElement; |
| } |
| |
| /** |
| * Creates a HTML Node representing this data. |
| * @param {boolean} showDetails |
| * @return {!Element} |
| */ |
| render(showDetails) { |
| const row = document.createElement('tr'); |
| OutputMatch.displayedProperties(showDetails) |
| .map(property => this.properties[property.propertyName].render()) |
| .forEach(cell => row.appendChild(cell)); |
| |
| if (showDetails && this.hasAdditionalProperties) |
| row.appendChild(this.additionalProperties.render()); |
| |
| this.associatedElement = row; |
| return this.associatedElement; |
| } |
| |
| /** |
| * @param {string} name |
| * @param {string=} url |
| * @param {string=} tooltip |
| * @return {!Element} |
| */ |
| static renderHeaderCell(name, url, tooltip) { |
| const cell = document.createElement('th'); |
| if (url) { |
| const link = document.createElement('a'); |
| link.textContent = name; |
| link.href = url; |
| cell.appendChild(link); |
| } else { |
| cell.textContent = name; |
| } |
| cell.className = |
| 'column-' + name.replace(/[A-Z]/g, c => '-' + c.toLowerCase()); |
| cell.title = tooltip || ''; |
| return cell; |
| } |
| |
| /** |
| * @return {!Array<!PresentationInfoRecord>} Array representing which |
| * columns need to be displayed. |
| */ |
| static displayedProperties(showDetails) { |
| return showDetails ? |
| PROPERTY_OUTPUT_ORDER : |
| PROPERTY_OUTPUT_ORDER.filter(property => property.displayAlways); |
| } |
| |
| /** |
| * @return {boolean} Used to determine if the additional properties column |
| * needs to be displayed for this match. |
| */ |
| get hasAdditionalProperties() { |
| return Object.keys(this.additionalProperties).length > 0; |
| } |
| |
| /** @return !Array<!OutputProperty> */ |
| get allProperties() { |
| return Object.values(this.properties).concat(this.additionalProperties); |
| } |
| } |
| |
| /** @abstract */ |
| class OutputProperty { |
| /** |
| * @param {string} name |
| * @param {*} value |
| */ |
| constructor(name, value) { |
| /** @type {string} */ |
| this.name = name; |
| /** @type {*} */ |
| this.value = value; |
| } |
| |
| /** |
| * @param {string} name |
| * @param {*} value |
| * @return {!OutputProperty} |
| */ |
| static constructProperty(name, value) { |
| if (typeof value === 'boolean') |
| return new OutputBooleanProperty(name, value); |
| if (typeof value === 'object') |
| // We check if the first element has key and value properties. |
| if (value && value[0] && value[0].key && value[0].value) |
| return new OutputKeyValueTuplesProperty(name, value); |
| else |
| return new OutputJsonProperty(name, value); |
| const LINK_REGEX = /^(http|https|ftp|chrome|file):\/\//; |
| if (LINK_REGEX.test(value)) |
| return new OutputLinkProperty(name, value); |
| return new OutputTextProperty(name, value); |
| } |
| |
| /** |
| * @abstract |
| * @return {!Element} |
| */ |
| render() {} |
| |
| /** @return {string} */ |
| get text() { |
| return this.value + ''; |
| } |
| } |
| |
| class OutputBooleanProperty extends OutputProperty { |
| /** |
| * @override |
| * @return {!Element} |
| */ |
| render() { |
| const cell = document.createElement('td'); |
| const icon = document.createElement('div'); |
| icon.className = this.value ? 'check-mark' : 'x-mark'; |
| icon.textContent = this.value; |
| cell.appendChild(icon); |
| return cell; |
| } |
| } |
| |
| class OutputKeyValueTuplesProperty extends OutputProperty { |
| /** |
| * @override |
| * @return {!Element} |
| */ |
| render() { |
| const cell = document.createElement('td'); |
| const pre = document.createElement('pre'); |
| pre.textContent = this.text; |
| cell.appendChild(pre); |
| return cell; |
| } |
| |
| /** |
| * @override |
| * @return {string} |
| */ |
| get text() { |
| return this.value.reduce( |
| (prev, {key, value}) => `${prev}${key}: ${value}\n`, ''); |
| } |
| } |
| |
| class OutputJsonProperty extends OutputProperty { |
| /** |
| * @override |
| * @return {!Element} |
| */ |
| render() { |
| const cell = document.createElement('td'); |
| const pre = document.createElement('pre'); |
| pre.textContent = this.text; |
| cell.appendChild(pre); |
| return cell; |
| } |
| |
| /** |
| * @override |
| * @return {string} |
| */ |
| get text() { |
| return JSON.stringify(this.value, null, 2); |
| } |
| } |
| |
| class OutputLinkProperty extends OutputProperty { |
| /** |
| * @override |
| * @return {!Element} |
| */ |
| render() { |
| const cell = document.createElement('td'); |
| const link = document.createElement('a'); |
| link.textContent = this.value; |
| link.href = this.value; |
| cell.appendChild(link); |
| return cell; |
| } |
| } |
| |
| class OutputTextProperty extends OutputProperty { |
| /** |
| * @override |
| * @return {!Element} |
| */ |
| render() { |
| const cell = document.createElement('td'); |
| cell.textContent = this.value; |
| return cell; |
| } |
| } |
| |
| /** 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_.renderDelegate.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 FilterDelegate { |
| /** @param {!omnibox_output.OmniboxOutput} omniboxOutput */ |
| constructor(omniboxOutput) { |
| /** @private {!omnibox_output.OmniboxOutput} */ |
| this.omniboxOutput_ = omniboxOutput; |
| } |
| |
| /** |
| * @param {string} filterText |
| * @param {boolean} filterHide |
| */ |
| filter(filterText, filterHide) { |
| this.omniboxOutput_.matches.filter(match => match.associatedElement) |
| .forEach(match => { |
| const row = match.associatedElement; |
| row.classList.remove('filtered-hidden'); |
| row.classList.remove('filtered-highlighted'); |
| |
| if (!filterText) |
| return; |
| |
| const isMatch = FilterDelegate.filterMatch_(match, filterText); |
| row.classList.toggle('filtered-hidden', filterHide && !isMatch); |
| row.classList.toggle( |
| 'filtered-highlighted', !filterHide && isMatch); |
| }); |
| } |
| |
| /** |
| * Checks if a omnibox match fuzzy-matches a filter string. Each character |
| * of filterText must be present in the match 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`. |
| * @private |
| * @param {!OutputMatch} match |
| * @param {string} filterText |
| * @return {boolean} |
| */ |
| static filterMatch_(match, filterText) { |
| const regexFilter = Array.from(filterText).join('(.*\\.)?'); |
| return match.allProperties |
| .map(property => property.text) |
| .map(text => FilterDelegate.textToWords_(text).join('.')) |
| .some(text => text.match(regexFilter)); |
| } |
| |
| /** |
| * 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) { |
| return text.match(/[a-z]+|[A-Z][a-z]*|\d+|./g) || []; |
| } |
| } |
| |
| window.customElements.define(OmniboxOutput.is, OmniboxOutput); |
| |
| return {OmniboxOutput: OmniboxOutput}; |
| }); |