Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / omnibox / omnibox.js
blob8631c277cff8c12fb1128a0475b0e9eedf72718a
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6  * Javascript for omnibox.html, served from chrome://omnibox/
7  * This is used to debug omnibox ranking.  The user enters some text
8  * into a box, submits it, and then sees lots of debug information
9  * from the autocompleter that shows what omnibox would do with that
10  * input.
11  *
12  * The simple object defined in this javascript file listens for
13  * certain events on omnibox.html, sends (when appropriate) the
14  * input text to C++ code to start the omnibox autcomplete controller
15  * working, and listens from callbacks from the C++ code saying that
16  * results are available.  When results (possibly intermediate ones)
17  * are available, the Javascript formats them and displays them.
18  */
19 define('main', [
20     'mojo/public/js/bindings',
21     'mojo/public/js/core',
22     'mojo/public/js/connection',
23     'chrome/browser/ui/webui/omnibox/omnibox.mojom',
24     'content/public/renderer/service_provider',
25 ], function(bindings, core, connection, browser, serviceProvider) {
26   'use strict';
28   var page;
30   /**
31    * Register our event handlers.
32    */
33   function initialize() {
34     $('omnibox-input-form').addEventListener(
35         'submit', startOmniboxQuery, false);
36     $('prevent-inline-autocomplete').addEventListener(
37         'change', startOmniboxQuery);
38     $('prefer-keyword').addEventListener('change', startOmniboxQuery);
39     $('page-classification').addEventListener('change', startOmniboxQuery);
40     $('show-details').addEventListener('change', refresh);
41     $('show-incomplete-results').addEventListener('change', refresh);
42     $('show-all-providers').addEventListener('change', refresh);
43   }
45   /**
46    * @type {OmniboxResultMojo} an array of all autocomplete results we've seen
47    *     for this query.  We append to this list once for every call to
48    *     handleNewAutocompleteResult.  See omnibox.mojom for details..
49    */
50   var progressiveAutocompleteResults = [];
52   /**
53    * @type {number} the value for cursor position we sent with the most
54    *     recent request.  We need to remember this in order to display it
55    *     in the output; otherwise it's hard or impossible to determine
56    *     from screen captures or print-to-PDFs.
57    */
58   var cursorPositionUsed = -1;
60   /**
61    * Extracts the input text from the text field and sends it to the
62    * C++ portion of chrome to handle.  The C++ code will iteratively
63    * call handleNewAutocompleteResult as results come in.
64    */
65   function startOmniboxQuery(event) {
66     // First, clear the results of past calls (if any).
67     progressiveAutocompleteResults = [];
68     // Then, call chrome with a five-element list:
69     // - first element: the value in the text box
70     // - second element: the location of the cursor in the text box
71     // - third element: the value of prevent-inline-autocomplete
72     // - forth element: the value of prefer-keyword
73     // - fifth element: the value of page-classification
74     cursorPositionUsed = $('input-text').selectionEnd;
75     var pipe = core.createMessagePipe();
76     var stub = connection.bindHandleToStub(pipe.handle0, browser.OmniboxPage);
77     bindings.StubBindings(stub).delegate = page;
78     page.stub_ = stub;
79     page.browser_.startOmniboxQuery(
80         $('input-text').value,
81         cursorPositionUsed,
82         $('prevent-inline-autocomplete').checked,
83         $('prefer-keyword').checked,
84         parseInt($('page-classification').value),
85         pipe.handle1);
86     // Cancel the submit action.  i.e., don't submit the form.  (We handle
87     // display the results solely with Javascript.)
88     event.preventDefault();
89   }
91   /**
92    * Returns a simple object with information about how to display an
93    * autocomplete result data field.
94    * @param {string} header the label for the top of the column/table.
95    * @param {string} urlLabelForHeader the URL that the header should point
96    *     to (if non-empty).
97    * @param {string} propertyName the name of the property in the autocomplete
98    *     result record that we lookup.
99    * @param {boolean} displayAlways whether the property should be displayed
100    *     regardless of whether we're in detailed more.
101    * @param {string} tooltip a description of the property that will be
102    *     presented as a tooltip when the mouse is hovered over the column title.
103    * @constructor
104    */
105   function PresentationInfoRecord(header, url, propertyName, displayAlways,
106                                   tooltip) {
107     this.header = header;
108     this.urlLabelForHeader = url;
109     this.propertyName = propertyName;
110     this.displayAlways = displayAlways;
111     this.tooltip = tooltip;
112   }
114   /**
115    * A constant that's used to decide what autocomplete result
116    * properties to output in what order.  This is an array of
117    * PresentationInfoRecord() objects; for details see that
118    * function.
119    * @type {Array<Object>}
120    * @const
121    */
122   var PROPERTY_OUTPUT_ORDER = [
123     new PresentationInfoRecord('Provider', '', 'provider_name', true,
124         'The AutocompleteProvider suggesting this result.'),
125     new PresentationInfoRecord('Type', '', 'type', true,
126         'The type of the result.'),
127     new PresentationInfoRecord('Relevance', '', 'relevance', true,
128         'The result score. Higher is more relevant.'),
129     new PresentationInfoRecord('Contents', '', 'contents', true,
130         'The text that is presented identifying the result.'),
131     new PresentationInfoRecord(
132         'Can Be Default', '', 'allowed_to_be_default_match', false,
133         'A green checkmark indicates that the result can be the default ' +
134         'match (i.e., can be the match that pressing enter in the omnibox ' +
135         'navigates to).'),
136     new PresentationInfoRecord('Starred', '', 'starred', false,
137         'A green checkmark indicates that the result has been bookmarked.'),
138     new PresentationInfoRecord('Description', '', 'description', false,
139         'The page title of the result.'),
140     new PresentationInfoRecord('URL', '', 'destination_url', true,
141         'The URL for the result.'),
142     new PresentationInfoRecord('Fill Into Edit', '', 'fill_into_edit', false,
143         'The text shown in the omnibox when the result is selected.'),
144     new PresentationInfoRecord(
145         'Inline Autocompletion', '', 'inline_autocompletion', false,
146         'The text shown in the omnibox as a blue highlight selection ' +
147         'following the cursor, if this match is shown inline.'),
148     new PresentationInfoRecord('Del', '', 'deletable', false,
149         'A green checkmark indicates that the result can be deleted from ' +
150         'the visit history.'),
151     new PresentationInfoRecord('Prev', '', 'from_previous', false, ''),
152     new PresentationInfoRecord(
153         'Tran',
154         'http://code.google.com/codesearch#OAMlx_jo-ck/src/content/public/' +
155         'common/page_transition_types.h&exact_package=chromium&l=24',
156         'transition', false,
157         'How the user got to the result.'),
158     new PresentationInfoRecord(
159         'Done', '', 'provider_done', false,
160         'A green checkmark indicates that the provider is done looking for ' +
161         'more results.'),
162     new PresentationInfoRecord(
163         'Associated Keyword', '', 'associated_keyword', false,
164         'If non-empty, a "press tab to search" hint will be shown and will ' +
165         'engage this keyword.'),
166     new PresentationInfoRecord(
167         'Keyword', '', 'keyword', false,
168         'The keyword of the search engine to be used.'),
169     new PresentationInfoRecord(
170         'Duplicates', '', 'duplicates', false,
171         'The number of matches that have been marked as duplicates of this ' +
172         'match.'),
173     new PresentationInfoRecord(
174         'Additional Info', '', 'additional_info', false,
175         'Provider-specific information about the result.')
176   ];
178   /**
179    * Returns an HTML Element of type table row that contains the
180    * headers we'll use for labeling the columns.  If we're in
181    * detailed_mode, we use all the headers.  If not, we only use ones
182    * marked displayAlways.
183    */
184   function createAutocompleteResultTableHeader() {
185     var row = document.createElement('tr');
186     var inDetailedMode = $('show-details').checked;
187     for (var i = 0; i < PROPERTY_OUTPUT_ORDER.length; i++) {
188       if (inDetailedMode || PROPERTY_OUTPUT_ORDER[i].displayAlways) {
189         var headerCell = document.createElement('th');
190         if (PROPERTY_OUTPUT_ORDER[i].urlLabelForHeader != '') {
191           // Wrap header text in URL.
192           var linkNode = document.createElement('a');
193           linkNode.href = PROPERTY_OUTPUT_ORDER[i].urlLabelForHeader;
194           linkNode.textContent = PROPERTY_OUTPUT_ORDER[i].header;
195           headerCell.appendChild(linkNode);
196         } else {
197           // Output header text without a URL.
198           headerCell.textContent = PROPERTY_OUTPUT_ORDER[i].header;
199           headerCell.className = 'table-header';
200           headerCell.title = PROPERTY_OUTPUT_ORDER[i].tooltip;
201         }
202         row.appendChild(headerCell);
203       }
204     }
205     return row;
206   }
208   /**
209    * @param {AutocompleteMatchMojo} autocompleteSuggestion the particular
210    *     autocomplete suggestion we're in the process of displaying.
211    * @param {string} propertyName the particular property of the autocomplete
212    *     suggestion that should go in this cell.
213    * @return {HTMLTableCellElement} that contains the value within this
214    *     autocompleteSuggestion associated with propertyName.
215    */
216   function createCellForPropertyAndRemoveProperty(autocompleteSuggestion,
217                                                   propertyName) {
218     var cell = document.createElement('td');
219     if (propertyName in autocompleteSuggestion) {
220       if (propertyName == 'additional_info') {
221         // |additional_info| embeds a two-column table of provider-specific data
222         // within this cell. |additional_info| is an array of
223         // AutocompleteAdditionalInfo.
224         var additionalInfoTable = document.createElement('table');
225         for (var i = 0; i < autocompleteSuggestion[propertyName].length; i++) {
226           var additionalInfo = autocompleteSuggestion[propertyName][i];
227           var additionalInfoRow = document.createElement('tr');
229           // Set the title (name of property) cell text.
230           var propertyCell = document.createElement('td');
231           propertyCell.textContent = additionalInfo.key + ':';
232           propertyCell.className = 'additional-info-property';
233           additionalInfoRow.appendChild(propertyCell);
235           // Set the value of the property cell text.
236           var valueCell = document.createElement('td');
237           valueCell.textContent = additionalInfo.value;
238           valueCell.className = 'additional-info-value';
239           additionalInfoRow.appendChild(valueCell);
241           additionalInfoTable.appendChild(additionalInfoRow);
242         }
243         cell.appendChild(additionalInfoTable);
244       } else if (typeof autocompleteSuggestion[propertyName] == 'boolean') {
245         // If this is a boolean, display a checkmark or an X instead of
246         // the strings true or false.
247         if (autocompleteSuggestion[propertyName]) {
248           cell.className = 'check-mark';
249           cell.textContent = '✔';
250         } else {
251           cell.className = 'x-mark';
252           cell.textContent = '✗';
253         }
254       } else {
255         var text = String(autocompleteSuggestion[propertyName]);
256         // If it's a URL wrap it in an href.
257         var re = /^(http|https|ftp|chrome|file):\/\//;
258         if (re.test(text)) {
259           var aCell = document.createElement('a');
260           aCell.textContent = text;
261           aCell.href = text;
262           cell.appendChild(aCell);
263         } else {
264           // All other data types (integer, strings, etc.) display their
265           // normal toString() output.
266           cell.textContent = autocompleteSuggestion[propertyName];
267         }
268       }
269     }  // else: if propertyName is undefined, we leave the cell blank
270     return cell;
271   }
273   /**
274    * Appends some human-readable information about the provided
275    * autocomplete result to the HTML node with id omnibox-debug-text.
276    * The current human-readable form is a few lines about general
277    * autocomplete result statistics followed by a table with one line
278    * for each autocomplete match.  The input parameter is an OmniboxResultMojo.
279    */
280   function addResultToOutput(result) {
281     var output = $('omnibox-debug-text');
282     var inDetailedMode = $('show-details').checked;
283     var showIncompleteResults = $('show-incomplete-results').checked;
284     var showPerProviderResults = $('show-all-providers').checked;
286     // Always output cursor position.
287     var p = document.createElement('p');
288     p.textContent = 'cursor position = ' + cursorPositionUsed;
289     output.appendChild(p);
291     // Output the result-level features in detailed mode and in
292     // show incomplete results mode.  We do the latter because without
293     // these result-level features, one can't make sense of each
294     // batch of results.
295     if (inDetailedMode || showIncompleteResults) {
296       var p1 = document.createElement('p');
297       p1.textContent = 'elapsed time = ' +
298           result.time_since_omnibox_started_ms + 'ms';
299       output.appendChild(p1);
300       var p2 = document.createElement('p');
301       p2.textContent = 'all providers done = ' + result.done;
302       output.appendChild(p2);
303       var p3 = document.createElement('p');
304       p3.textContent = 'host = ' + result.host;
305       if ('is_typed_host' in result) {
306         // Only output the is_typed_host information if available.  (It may
307         // be missing if the history database lookup failed.)
308         p3.textContent = p3.textContent + ' has is_typed_host = ' +
309             result.is_typed_host;
310       }
311       output.appendChild(p3);
312     }
314     // Combined results go after the lines below.
315     var group = document.createElement('a');
316     group.className = 'group-separator';
317     group.textContent = 'Combined results.';
318     output.appendChild(group);
320     // Add combined/merged result table.
321     var p = document.createElement('p');
322     p.appendChild(addResultTableToOutput(result.combined_results));
323     output.appendChild(p);
325     // Move forward only if you want to display per provider results.
326     if (!showPerProviderResults) {
327       return;
328     }
330     // Individual results go after the lines below.
331     var group = document.createElement('a');
332     group.className = 'group-separator';
333     group.textContent = 'Results for individual providers.';
334     output.appendChild(group);
336     // Add the per-provider result tables with labels. We do not append the
337     // combined/merged result table since we already have the per provider
338     // results.
339     for (var i = 0; i < result.results_by_provider.length; i++) {
340       var providerResults = result.results_by_provider[i];
341       // If we have no results we do not display anything.
342       if (providerResults.results.length == 0) {
343         continue;
344       }
345       var p = document.createElement('p');
346       p.appendChild(addResultTableToOutput(providerResults.results));
347       output.appendChild(p);
348     }
349   }
351   /**
352    * @param {Object} result an array of AutocompleteMatchMojos.
353    * @return {HTMLTableCellElement} that is a user-readable HTML
354    *     representation of this object.
355    */
356   function addResultTableToOutput(result) {
357     var inDetailedMode = $('show-details').checked;
358     // Create a table to hold all the autocomplete items.
359     var table = document.createElement('table');
360     table.className = 'autocomplete-results-table';
361     table.appendChild(createAutocompleteResultTableHeader());
362     // Loop over every autocomplete item and add it as a row in the table.
363     for (var i = 0; i < result.length; i++) {
364       var autocompleteSuggestion = result[i];
365       var row = document.createElement('tr');
366       // Loop over all the columns/properties and output either them
367       // all (if we're in detailed mode) or only the ones marked displayAlways.
368       // Keep track of which properties we displayed.
369       var displayedProperties = {};
370       for (var j = 0; j < PROPERTY_OUTPUT_ORDER.length; j++) {
371         if (inDetailedMode || PROPERTY_OUTPUT_ORDER[j].displayAlways) {
372           row.appendChild(createCellForPropertyAndRemoveProperty(
373               autocompleteSuggestion, PROPERTY_OUTPUT_ORDER[j].propertyName));
374           displayedProperties[PROPERTY_OUTPUT_ORDER[j].propertyName] = true;
375         }
376       }
378       // Now, if we're in detailed mode, add all the properties that
379       // haven't already been output.  (We know which properties have
380       // already been output because we delete the property when we output
381       // it.  The only way we have properties left at this point if
382       // we're in detailed mode and we're getting back properties
383       // not listed in PROPERTY_OUTPUT_ORDER.  Perhaps someone added
384       // something to the C++ code but didn't bother to update this
385       // Javascript?  In any case, we want to display them.)
386       if (inDetailedMode) {
387         for (var key in autocompleteSuggestion) {
388           if (!displayedProperties[key] &&
389               typeof autocompleteSuggestion[key] != 'function') {
390             var cell = document.createElement('td');
391             cell.textContent = key + '=' + autocompleteSuggestion[key];
392             row.appendChild(cell);
393           }
394         }
395       }
397       table.appendChild(row);
398     }
399     return table;
400   }
402   /* Repaints the page based on the contents of the array
403    * progressiveAutocompleteResults, which represents consecutive
404    * autocomplete results.  We only display the last (most recent)
405    * entry unless we're asked to display incomplete results.  For an
406    * example of the output, play with chrome://omnibox/
407    */
408   function refresh() {
409     // Erase whatever is currently being displayed.
410     var output = $('omnibox-debug-text');
411     output.innerHTML = '';
413     if (progressiveAutocompleteResults.length > 0) {  // if we have results
414       // Display the results.
415       var showIncompleteResults = $('show-incomplete-results').checked;
416       var startIndex = showIncompleteResults ? 0 :
417           progressiveAutocompleteResults.length - 1;
418       for (var i = startIndex; i < progressiveAutocompleteResults.length; i++) {
419         addResultToOutput(progressiveAutocompleteResults[i]);
420       }
421     }
422   }
424   function OmniboxPageImpl(browser) {
425     this.browser_ = browser;
426     initialize();
427   }
429   OmniboxPageImpl.prototype =
430       Object.create(browser.OmniboxPage.stubClass.prototype);
432   OmniboxPageImpl.prototype.handleNewAutocompleteResult = function(result) {
433     progressiveAutocompleteResults.push(result);
434     refresh();
435   };
437   return function() {
438     var browserProxy = connection.bindHandleToProxy(
439         serviceProvider.connectToService(
440             browser.OmniboxUIHandlerMojo.name),
441         browser.OmniboxUIHandlerMojo);
442     page = new OmniboxPageImpl(browserProxy);
443   };