Commit d557ace8 authored by manuk's avatar manuk Committed by Commit Bot

[chrome:omnibox] Add buttons to copy response data.

Two copy buttons are added. The first copies the visible table as text; display options, which affect the visibility of tables, columns, rows, also affect what text is copied. The second copies the responses as JSON.

Also, as this CL introduces a second outputs interface, I took this opportunity to consolidate this new interface and the previous OutputController (which served as an interface for OmniboxOutput) into a single interface, OmniboxController, which will be responsible for interfacing to individual Output classes responsible for:
1 render tables from responses
2 control visibility based on display options
3 control visibility and coloring based on search text
4 export/copy output
5 preserve inputs and reset default inputs
6 export/import inputs

Bug: 891303
Change-Id: I1b6e10e6aa2213912235863f61163c9d8e9f66f7
Reviewed-on: https://chromium-review.googlesource.com/c/1329842
Commit-Queue: manuk hovanesian <manukh@chromium.org>
Reviewed-by: default avatarDemetrios Papadopoulos <dpapad@chromium.org>
Reviewed-by: default avatarTommy Li <tommycli@chromium.org>
Cr-Commit-Position: refs/heads/master@{#608370}
parent 5104e13a
...@@ -21,6 +21,7 @@ js_library("omnibox") { ...@@ -21,6 +21,7 @@ js_library("omnibox") {
externs_list = [ externs_list = [
"$root_gen_dir/chrome/browser/ui/webui/omnibox/omnibox.mojom-lite.externs.js", "$root_gen_dir/chrome/browser/ui/webui/omnibox/omnibox.mojom-lite.externs.js",
"$externs_path/mojo.js", "$externs_path/mojo.js",
"$externs_path/pending.js",
] ]
} }
......
...@@ -72,6 +72,8 @@ ...@@ -72,6 +72,8 @@
Show results per provider, not just merged results Show results per provider, not just merged results
</label> </label>
</p> </p>
<button id="copy-text" title="Copy visible table in text format. This is affected by the visibility of ouput; i.e. toggling `Show all details` affects what will be copied.">Copy as text</button>
<button id="copy-json" title="Copy responses in JSON format. This is not affected by the visibility of output and will copy responses in their entirety.">Copy as JSON</button>
</template> </template>
<template id="omnibox-output-template"> <template id="omnibox-output-template">
......
...@@ -26,53 +26,6 @@ ...@@ -26,53 +26,6 @@
*/ */
let cursorPosition = -1; let cursorPosition = -1;
/**
* 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. OutputController also
* triggers appending to and clearing of OmniboxOutput when appropriate (e.g.,
* upon receiving a new response or a change in display inputs).
*/
class OutputController {
constructor() {
/** @private {!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_() {
omniboxOutput.refresh(
this.queryInputs_, this.responses_, this.displayInputs_);
}
}
class BrowserProxy { class BrowserProxy {
constructor() { constructor() {
/** @private {!mojom.OmniboxPageCallbackRouter} */ /** @private {!mojom.OmniboxPageCallbackRouter} */
...@@ -83,7 +36,7 @@ ...@@ -83,7 +36,7 @@
// match. Response refers to the data returned from the C++ // match. Response refers to the data returned from the C++
// AutocompleteController. // AutocompleteController.
this.callbackRouter_.handleNewAutocompleteResult.addListener( this.callbackRouter_.handleNewAutocompleteResult.addListener(
result => outputController.addAutocompleteResponse(result)); result => omniboxOutput.addAutocompleteResponse(result));
/** @private {!mojom.OmniboxPageHandlerProxy} */ /** @private {!mojom.OmniboxPageHandlerProxy} */
this.handler_ = mojom.OmniboxPageHandler.getProxy(); this.handler_ = mojom.OmniboxPageHandler.getProxy();
...@@ -118,16 +71,15 @@ ...@@ -118,16 +71,15 @@
let omniboxInputs; let omniboxInputs;
/** @type {omnibox_output.OmniboxOutput} */ /** @type {omnibox_output.OmniboxOutput} */
let omniboxOutput; let omniboxOutput;
/** @type {OutputController} */
const outputController = new OutputController();
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
omniboxInputs = /** @type {!OmniboxInputs} */ ($('omnibox-inputs')); omniboxInputs = /** @type {!OmniboxInputs} */ ($('omnibox-inputs'));
omniboxOutput = omniboxOutput =
/** @type {!omnibox_output.OmniboxOutput} */ ($('omnibox-output')); /** @type {!omnibox_output.OmniboxOutput} */ ($('omnibox-output'));
omniboxInputs.addEventListener('query-inputs-changed', event => { omniboxInputs.addEventListener('query-inputs-changed', event => {
outputController.clearAutocompleteResponses(); omniboxOutput.clearAutocompleteResponses();
outputController.updateQueryInputs(event.detail); omniboxOutput.updateQueryInputs(event.detail);
browserProxy.makeRequest( browserProxy.makeRequest(
event.detail.inputText, event.detail.inputText,
event.detail.cursorPosition, event.detail.cursorPosition,
...@@ -137,6 +89,11 @@ ...@@ -137,6 +89,11 @@
}); });
omniboxInputs.addEventListener( omniboxInputs.addEventListener(
'display-inputs-changed', 'display-inputs-changed',
event => outputController.updateDisplayInputs(event.detail)); event => omniboxOutput.updateDisplayInputs(event.detail));
omniboxInputs.addEventListener(
'copy-request',
event => event.detail === 'text' ?
omniboxOutput.copyDelegate.copyTextOutput() :
omniboxOutput.copyDelegate.copyJsonOutput());
}); });
})(); })();
...@@ -55,6 +55,10 @@ class OmniboxInputs extends OmniboxElement { ...@@ -55,6 +55,10 @@ class OmniboxInputs extends OmniboxElement {
this.$$('show-details'), this.$$('show-details'),
this.$$('show-all-providers'), this.$$('show-all-providers'),
].forEach(elem => elem.addEventListener('change', onDisplayInputsChanged)); ].forEach(elem => elem.addEventListener('change', onDisplayInputsChanged));
this.$$('copy-text')
.addEventListener('click', () => this.onCopyOutput_('text'));
this.$$('copy-json')
.addEventListener('click', () => this.onCopyOutput_('json'));
} }
/** @private */ /** @private */
...@@ -92,6 +96,14 @@ class OmniboxInputs extends OmniboxElement { ...@@ -92,6 +96,14 @@ class OmniboxInputs extends OmniboxElement {
this.$$('input-text').value.length : this.$$('input-text').value.length :
this.$$('input-text').selectionEnd; this.$$('input-text').selectionEnd;
} }
/**
* @param {string} format Either 'text' or 'json'.
* @private
*/
onCopyOutput_(format) {
this.dispatchEvent(new CustomEvent('copy-request', {detail: format}));
}
} }
window.customElements.define(OmniboxInputs.is, OmniboxInputs); window.customElements.define(OmniboxInputs.is, OmniboxInputs);
...@@ -170,6 +170,22 @@ cr.define('omnibox_output', function() { ...@@ -170,6 +170,22 @@ cr.define('omnibox_output', function() {
} }
]; ];
/**
* 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 (TODO)
* 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 { class OmniboxOutput extends OmniboxElement {
/** @return {string} */ /** @return {string} */
static get is() { static get is() {
...@@ -178,6 +194,55 @@ cr.define('omnibox_output', function() { ...@@ -178,6 +194,55 @@ cr.define('omnibox_output', function() {
constructor() { constructor() {
super('omnibox-output-template'); super('omnibox-output-template');
/** @type {RenderDelegate} */
this.renderDelegate = new RenderDelegate(this.$$('contents'));
/** @type {CopyDelegate} */
this.copyDelegate = new CopyDelegate(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_);
}
}
// Responsible for rendering the output HTML.
class RenderDelegate {
/** @param {Element} containerElement */
constructor(containerElement) {
this.containerElement = containerElement;
} }
/** /**
...@@ -186,43 +251,46 @@ cr.define('omnibox_output', function() { ...@@ -186,43 +251,46 @@ cr.define('omnibox_output', function() {
* @param {DisplayInputs} displayInputs * @param {DisplayInputs} displayInputs
*/ */
refresh(queryInputs, responses, displayInputs) { refresh(queryInputs, responses, displayInputs) {
/** @private {QueryInputs} */
this.queryInputs_ = queryInputs;
/** @private {!Array<mojom.OmniboxResult>} */
this.responses_ = responses;
/** @private {DisplayInputs} */
this.displayInputs_ = displayInputs;
this.clearOutput_(); this.clearOutput_();
if (responses.length) { if (responses.length) {
if (this.displayInputs_.showIncompleteResults) if (displayInputs.showIncompleteResults) {
responses.forEach(this.addOutputResultsGroup_.bind(this)); responses.forEach(
else response => this.addOutputResultsGroup_(
this.addOutputResultsGroup_(responses[responses.length - 1]); response, queryInputs, displayInputs));
} else {
this.addOutputResultsGroup_(
responses[responses.length - 1], queryInputs, displayInputs);
}
} }
} }
/** /**
* @private * @private
* @param {!mojom.OmniboxResult} response * @param {!mojom.OmniboxResult} response
* @param {QueryInputs} queryInputs
* @param {DisplayInputs} displayInputs
*/ */
addOutputResultsGroup_(response) { addOutputResultsGroup_(response, queryInputs, displayInputs) {
this.$$('contents') this.containerElement.appendChild(
.appendChild( new OutputResultsGroup(response, queryInputs.cursorPosition)
new OutputResultsGroup(response, this.queryInputs_.cursorPosition) .render(
.render( displayInputs.showDetails,
this.displayInputs_.showDetails, displayInputs.showIncompleteResults,
this.displayInputs_.showIncompleteResults, displayInputs.showAllProviders));
this.displayInputs_.showAllProviders));
} }
/** @private */ /** @private */
clearOutput_() { clearOutput_() {
let contents = this.$$('contents'); let contents = this.containerElement;
// Clears all children. // Clears all children.
while (contents.firstChild) while (contents.firstChild)
contents.removeChild(contents.firstChild); contents.removeChild(contents.firstChild);
} }
/** @return {string} */
get visibletableText() {
return this.containerElement.innerText;
}
} }
/** /**
...@@ -516,13 +584,33 @@ cr.define('omnibox_output', function() { ...@@ -516,13 +584,33 @@ cr.define('omnibox_output', function() {
} }
} }
/** Responsible for setting clipboard contents. */
class CopyDelegate {
/** @param {omnibox_output.OmniboxOutput} omniboxOutput */
constructor(omniboxOutput) {
/** @type {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));
}
}
window.customElements.define(OmniboxOutput.is, OmniboxOutput); window.customElements.define(OmniboxOutput.is, OmniboxOutput);
// TODO(manukh) remove PROPERTY_OUTPUT_ORDER from exports after the remaining return {OmniboxOutput: OmniboxOutput};
// OmniboxOutput classes are extracted
// TODO(manukh) use shorthand object creation if/when approved
// https://chromium.googlesource.com/chromium/src/+/master/styleguide/web/es6.md#object-literal-extensions
return {
OmniboxOutput: OmniboxOutput,
};
}); });
...@@ -92,3 +92,23 @@ Polymer.RenderStatus.beforeNextRender = function(element, fn, args) {}; ...@@ -92,3 +92,23 @@ Polymer.RenderStatus.beforeNextRender = function(element, fn, args) {};
*/ */
let BigInt = function(value) {}; let BigInt = function(value) {};
/**
* TODO(manukh): Remove this once it is added to Closure Compiler itself.
* @see https://w3c.github.io/clipboard-apis/#async-clipboard-api
* @interface
*/
function Clipboard() {}
/**
* @return {!Promise<string>}
*/
Clipboard.prototype.readText = function() {};
/**
* @param {string} text
* @return {!Promise<void>}
*/
Clipboard.prototype.writeText = function(text) {};
/** @const {!Clipboard} */
Navigator.prototype.clipboard;
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment