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") {
externs_list = [
"$root_gen_dir/chrome/browser/ui/webui/omnibox/omnibox.mojom-lite.externs.js",
"$externs_path/mojo.js",
"$externs_path/pending.js",
]
}
......
......@@ -72,6 +72,8 @@
Show results per provider, not just merged results
</label>
</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 id="omnibox-output-template">
......
......@@ -26,53 +26,6 @@
*/
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 {
constructor() {
/** @private {!mojom.OmniboxPageCallbackRouter} */
......@@ -83,7 +36,7 @@
// match. Response refers to the data returned from the C++
// AutocompleteController.
this.callbackRouter_.handleNewAutocompleteResult.addListener(
result => outputController.addAutocompleteResponse(result));
result => omniboxOutput.addAutocompleteResponse(result));
/** @private {!mojom.OmniboxPageHandlerProxy} */
this.handler_ = mojom.OmniboxPageHandler.getProxy();
......@@ -118,16 +71,15 @@
let omniboxInputs;
/** @type {omnibox_output.OmniboxOutput} */
let omniboxOutput;
/** @type {OutputController} */
const outputController = new OutputController();
document.addEventListener('DOMContentLoaded', () => {
omniboxInputs = /** @type {!OmniboxInputs} */ ($('omnibox-inputs'));
omniboxOutput =
/** @type {!omnibox_output.OmniboxOutput} */ ($('omnibox-output'));
omniboxInputs.addEventListener('query-inputs-changed', event => {
outputController.clearAutocompleteResponses();
outputController.updateQueryInputs(event.detail);
omniboxOutput.clearAutocompleteResponses();
omniboxOutput.updateQueryInputs(event.detail);
browserProxy.makeRequest(
event.detail.inputText,
event.detail.cursorPosition,
......@@ -137,6 +89,11 @@
});
omniboxInputs.addEventListener(
'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 {
this.$$('show-details'),
this.$$('show-all-providers'),
].forEach(elem => elem.addEventListener('change', onDisplayInputsChanged));
this.$$('copy-text')
.addEventListener('click', () => this.onCopyOutput_('text'));
this.$$('copy-json')
.addEventListener('click', () => this.onCopyOutput_('json'));
}
/** @private */
......@@ -92,6 +96,14 @@ class OmniboxInputs extends OmniboxElement {
this.$$('input-text').value.length :
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);
......@@ -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 {
/** @return {string} */
static get is() {
......@@ -178,6 +194,55 @@ cr.define('omnibox_output', function() {
constructor() {
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() {
* @param {DisplayInputs} displayInputs
*/
refresh(queryInputs, responses, displayInputs) {
/** @private {QueryInputs} */
this.queryInputs_ = queryInputs;
/** @private {!Array<mojom.OmniboxResult>} */
this.responses_ = responses;
/** @private {DisplayInputs} */
this.displayInputs_ = displayInputs;
this.clearOutput_();
if (responses.length) {
if (this.displayInputs_.showIncompleteResults)
responses.forEach(this.addOutputResultsGroup_.bind(this));
else
this.addOutputResultsGroup_(responses[responses.length - 1]);
if (displayInputs.showIncompleteResults) {
responses.forEach(
response => this.addOutputResultsGroup_(
response, queryInputs, displayInputs));
} else {
this.addOutputResultsGroup_(
responses[responses.length - 1], queryInputs, displayInputs);
}
}
}
/**
* @private
* @param {!mojom.OmniboxResult} response
* @param {QueryInputs} queryInputs
* @param {DisplayInputs} displayInputs
*/
addOutputResultsGroup_(response) {
this.$$('contents')
.appendChild(
new OutputResultsGroup(response, this.queryInputs_.cursorPosition)
.render(
this.displayInputs_.showDetails,
this.displayInputs_.showIncompleteResults,
this.displayInputs_.showAllProviders));
addOutputResultsGroup_(response, queryInputs, displayInputs) {
this.containerElement.appendChild(
new OutputResultsGroup(response, queryInputs.cursorPosition)
.render(
displayInputs.showDetails,
displayInputs.showIncompleteResults,
displayInputs.showAllProviders));
}
/** @private */
clearOutput_() {
let contents = this.$$('contents');
let contents = this.containerElement;
// Clears all children.
while (contents.firstChild)
contents.removeChild(contents.firstChild);
}
/** @return {string} */
get visibletableText() {
return this.containerElement.innerText;
}
}
/**
......@@ -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);
// TODO(manukh) remove PROPERTY_OUTPUT_ORDER from exports after the remaining
// 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,
};
return {OmniboxOutput: OmniboxOutput};
});
......@@ -92,3 +92,23 @@ Polymer.RenderStatus.beforeNextRender = function(element, fn, args) {};
*/
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