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