| OLD | NEW |
| 1 // Copyright 2012 The Chromium Authors. All rights reserved. | 1 // Copyright 2012 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * @fileoverview Support for omnibox behavior in offline mode or when API | 6 * @fileoverview Support for omnibox behavior in offline mode or when API |
| 7 * features are not supported on the server. | 7 * features are not supported on the server. |
| 8 */ | 8 */ |
| 9 | 9 |
| 10 // ========================================================== | |
| 11 // Enums. | |
| 12 // ========================================================== | |
| 13 | |
| 14 /** | 10 /** |
| 15 * Possible behaviors for navigateContentWindow. | 11 * Possible behaviors for navigateContentWindow. |
| 16 * @enum {number} | 12 * @enum {number} |
| 17 */ | 13 */ |
| 18 var WindowOpenDisposition = { | 14 var WindowOpenDisposition = { |
| 19 CURRENT_TAB: 1, | 15 CURRENT_TAB: 1, |
| 20 NEW_BACKGROUND_TAB: 2 | 16 NEW_BACKGROUND_TAB: 2 |
| 21 }; | 17 }; |
| 22 | 18 |
| 23 /** | 19 /** |
| 24 * The JavaScript button event value for a middle click. | 20 * The JavaScript button event value for a middle click. |
| 25 * @type {number} | 21 * @type {number} |
| 26 * @const | 22 * @const |
| 27 */ | 23 */ |
| 28 var MIDDLE_MOUSE_BUTTON = 1; | 24 var MIDDLE_MOUSE_BUTTON = 1; |
| 29 | 25 |
| 30 // ============================================================================= | |
| 31 // Util functions | |
| 32 // ============================================================================= | |
| 33 | |
| 34 /** | 26 /** |
| 35 * The maximum number of suggestions to show. | 27 * The maximum number of suggestions to show. |
| 36 * @type {number} | 28 * @type {number} |
| 37 * @const | 29 * @const |
| 38 */ | 30 */ |
| 39 var MAX_SUGGESTIONS_TO_SHOW = 5; | 31 var MAX_SUGGESTIONS_TO_SHOW = 5; |
| 40 | 32 |
| 41 /** | 33 /** |
| 42 * Assume any native suggestion with a score higher than this value has been | 34 * Assume any native suggestion with a score higher than this value has been |
| 43 * inlined by the browser. | 35 * inlined by the browser. |
| 44 * @type {number} | 36 * @type {number} |
| 45 * @const | 37 * @const |
| 46 */ | 38 */ |
| 47 var INLINE_SUGGESTION_THRESHOLD = 1200; | 39 var INLINE_SUGGESTION_THRESHOLD = 1200; |
| 48 | 40 |
| 49 /** | 41 /** |
| 42 * The color code for a display URL. |
| 43 * @type {string} |
| 44 * @const |
| 45 */ |
| 46 var URL_COLOR = '#093'; |
| 47 |
| 48 /** |
| 49 * The color code for a suggestion title. |
| 50 * @type {string} |
| 51 * @const |
| 52 */ |
| 53 var TITLE_COLOR = '#666'; |
| 54 |
| 55 /** |
| 56 * A top position which is off-screen. |
| 57 * @type {string} |
| 58 * @const |
| 59 */ |
| 60 var OFF_SCREEN = '-1000px'; |
| 61 |
| 62 /** |
| 63 * The expected origin of a suggestion iframe. |
| 64 * @type {string} |
| 65 * @const |
| 66 */ |
| 67 var SUGGESTION_ORIGIN = 'chrome-search://suggestion'; |
| 68 |
| 69 /** |
| 50 * Suggestion provider type corresponding to a verbatim URL suggestion. | 70 * Suggestion provider type corresponding to a verbatim URL suggestion. |
| 51 * @type {string} | 71 * @type {string} |
| 52 * @const | 72 * @const |
| 53 */ | 73 */ |
| 54 var VERBATIM_URL_TYPE = 'url-what-you-typed'; | 74 var VERBATIM_URL_TYPE = 'url-what-you-typed'; |
| 55 | 75 |
| 56 /** | 76 /** |
| 57 * Suggestion provider type corresponding to a verbatim search suggestion. | 77 * Suggestion provider type corresponding to a verbatim search suggestion. |
| 58 * @type {string} | 78 * @type {string} |
| 59 * @const | 79 * @const |
| 60 */ | 80 */ |
| 61 var VERBATIM_SEARCH_TYPE = 'search-what-you-typed'; | 81 var VERBATIM_SEARCH_TYPE = 'search-what-you-typed'; |
| 62 | 82 |
| 63 /** | 83 /** |
| 64 * The omnibox input value during the last onnativesuggestions event. | 84 * The displayed suggestions. |
| 65 * @type {string} | 85 * @type {SuggestionsBox} |
| 66 */ | 86 */ |
| 67 var lastInputValue = ''; | 87 var activeSuggestionsBox; |
| 68 | 88 |
| 69 /** | 89 /** |
| 70 * The ordered restricted ids of the currently displayed suggestions. Since the | 90 * The suggestions being rendered. |
| 71 * suggestions contain the user's personal data (browser history) the searchBox | 91 * @type {SuggestionsBox} |
| 72 * API embeds the content of the suggestion in a shadow dom, and assigns a | |
| 73 * random restricted id to each suggestion which is accessible to the JS. | |
| 74 * @type {Array.<number>} | |
| 75 */ | 92 */ |
| 76 | 93 var pendingSuggestionsBox; |
| 77 var restrictedIds = []; | |
| 78 | 94 |
| 79 /** | 95 /** |
| 80 * The index of the currently selected suggestion or -1 if none are selected. | 96 * A pool of iframes to display suggestions. |
| 97 * @type {IframePool} |
| 98 */ |
| 99 var iframePool; |
| 100 |
| 101 /** |
| 102 * A serial number for the next suggestions rendered. |
| 81 * @type {number} | 103 * @type {number} |
| 82 */ | 104 */ |
| 83 var selectedIndex = -1; | 105 var nextRequestId = 0; |
| 84 | 106 |
| 85 /** | 107 /** |
| 86 * Shortcut for document.getElementById. | 108 * Shortcut for document.querySelector. |
| 87 * @param {string} id of the element. | 109 * @param {string} selector A selector to query the desired element. |
| 88 * @return {HTMLElement} with the id. | 110 * @return {HTMLElement} matching selector. |
| 89 */ | 111 */ |
| 90 function $(id) { | 112 function $(selector) { |
| 91 return document.getElementById(id); | 113 return document.querySelector(selector); |
| 92 } | 114 } |
| 93 | 115 |
| 94 /** | 116 /** |
| 95 * Displays a suggestion. | |
| 96 * @param {Object} suggestion The suggestion to render. | |
| 97 * @param {HTMLElement} box The html element to add the suggestion to. | |
| 98 * @param {boolean} select True to select the selection. | |
| 99 */ | |
| 100 function addSuggestionToBox(suggestion, box, select) { | |
| 101 var suggestionDiv = document.createElement('div'); | |
| 102 suggestionDiv.classList.add('suggestion'); | |
| 103 suggestionDiv.classList.toggle('selected', select); | |
| 104 suggestionDiv.classList.toggle('search', suggestion.is_search); | |
| 105 | |
| 106 var suggestionIframe = document.createElement('iframe'); | |
| 107 suggestionIframe.className = 'contents'; | |
| 108 suggestionIframe.src = suggestion.destination_url; | |
| 109 suggestionIframe.id = suggestion.rid; | |
| 110 suggestionDiv.appendChild(suggestionIframe); | |
| 111 | |
| 112 restrictedIds.push(suggestion.rid); | |
| 113 box.appendChild(suggestionDiv); | |
| 114 } | |
| 115 | |
| 116 /** | |
| 117 * Renders the input suggestions. | |
| 118 * @param {Array} nativeSuggestions An array of native suggestions to render. | |
| 119 */ | |
| 120 function renderSuggestions(nativeSuggestions) { | |
| 121 var box = document.createElement('div'); | |
| 122 box.id = 'suggestionsBox'; | |
| 123 $('suggestions-box-container').appendChild(box); | |
| 124 | |
| 125 for (var i = 0, length = nativeSuggestions.length; | |
| 126 i < Math.min(MAX_SUGGESTIONS_TO_SHOW, length); ++i) { | |
| 127 addSuggestionToBox(nativeSuggestions[i], box, i == selectedIndex); | |
| 128 } | |
| 129 } | |
| 130 | |
| 131 /** | |
| 132 * Clears the suggestions being displayed. | |
| 133 */ | |
| 134 function clearSuggestions() { | |
| 135 $('suggestions-box-container').innerHTML = ''; | |
| 136 restrictedIds = []; | |
| 137 selectedIndex = -1; | |
| 138 } | |
| 139 | |
| 140 /** | |
| 141 * @return {number} The height of the dropdown. | |
| 142 */ | |
| 143 function getDropdownHeight() { | |
| 144 return $('suggestions-box-container').offsetHeight; | |
| 145 } | |
| 146 | |
| 147 /** | |
| 148 * @param {Object} suggestion A suggestion. | 117 * @param {Object} suggestion A suggestion. |
| 149 * @param {boolean} inVerbatimMode Are we in verbatim mode? | 118 * @param {boolean} inVerbatimMode Are we in verbatim mode? |
| 150 * @return {boolean} True if the suggestion should be selected. | 119 * @return {boolean} True if the suggestion should be selected. |
| 151 */ | 120 */ |
| 152 function shouldSelectSuggestion(suggestion, inVerbatimMode) { | 121 function shouldSelectSuggestion(suggestion, inVerbatimMode) { |
| 153 var isVerbatimUrl = suggestion.type == VERBATIM_URL_TYPE; | 122 var isVerbatimUrl = suggestion.type == VERBATIM_URL_TYPE; |
| 154 var inlinableSuggestion = suggestion.type != VERBATIM_SEARCH_TYPE && | 123 var inlinableSuggestion = suggestion.type != VERBATIM_SEARCH_TYPE && |
| 155 suggestion.rankingData.relevance > INLINE_SUGGESTION_THRESHOLD; | 124 suggestion.rankingData.relevance > INLINE_SUGGESTION_THRESHOLD; |
| 156 // Verbatim URLs should always be selected. Otherwise, select suggestions | 125 // Verbatim URLs should always be selected. Otherwise, select suggestions |
| 157 // with a high enough score unless we are in verbatim mode (e.g. backspacing | 126 // with a high enough score unless we are in verbatim mode (e.g. backspacing |
| 158 // away). | 127 // away). |
| 159 return isVerbatimUrl || (!inVerbatimMode && inlinableSuggestion); | 128 return isVerbatimUrl || (!inVerbatimMode && inlinableSuggestion); |
| 160 } | 129 } |
| 161 | 130 |
| 162 /** | 131 /** |
| 163 * Updates selectedIndex, bounding it between -1 and the total number of | 132 * Extract the desired navigation behavior from a click button. |
| 164 * of suggestions - 1 (looping as necessary), and selects the corresponding | 133 * @param {number} button The Event#button property of a click event. |
| 165 * suggestion. | 134 * @return {WindowOpenDisposition} The desired behavior for |
| 166 * @param {boolean} increment True to increment the selected suggestion, false | 135 * navigateContentWindow. |
| 167 * to decrement. | 136 */ |
| 168 */ | 137 function getDispositionFromClickButton(button) { |
| 169 function updateSelectedSuggestion(increment) { | 138 if (button == MIDDLE_MOUSE_BUTTON) |
| 170 var numSuggestions = restrictedIds.length; | 139 return WindowOpenDisposition.NEW_BACKGROUND_TAB; |
| 171 if (!numSuggestions) | 140 return WindowOpenDisposition.CURRENT_TAB; |
| 172 return; | 141 } |
| 173 | 142 |
| 174 var oldSelection = $('suggestionsBox').querySelector('.selected'); | 143 /** |
| 144 * Manages a pool of chrome-search iframes. |
| 145 * @constructor |
| 146 */ |
| 147 function IframePool() { |
| 148 this.iframes_ = []; |
| 149 } |
| 150 |
| 151 /** |
| 152 * Initializes the pool with blank result template iframes, positioned off |
| 153 * screen. |
| 154 */ |
| 155 IframePool.prototype.init = function() { |
| 156 for (var i = 0; i < 2 * MAX_SUGGESTIONS_TO_SHOW; i++) { |
| 157 var iframe = document.createElement('iframe'); |
| 158 iframe.className = 'contents'; |
| 159 iframe.id = 'suggestion-text-' + i; |
| 160 iframe.src = 'chrome-search://suggestion/result.html'; |
| 161 iframe.style.top = OFF_SCREEN; |
| 162 iframe.addEventListener('mouseover', hover(iframe.id), false); |
| 163 iframe.addEventListener('mouseout', unhover(iframe.id), false); |
| 164 document.body.appendChild(iframe); |
| 165 this.iframes_[i] = iframe; |
| 166 } |
| 167 }; |
| 168 |
| 169 /** |
| 170 * Retrieves a free suggestion iframe from the pool. |
| 171 * @return {Element} An iframe suitable for holding a suggestion. |
| 172 */ |
| 173 IframePool.prototype.get = function() { |
| 174 return this.iframes_.pop(); |
| 175 }; |
| 176 |
| 177 /** |
| 178 * Releases a suggestion iframe back into the pool. |
| 179 * @param {Element} iframe The iframe to return to the pool. |
| 180 */ |
| 181 IframePool.prototype.release = function(iframe) { |
| 182 this.iframes_.push(iframe); |
| 183 iframe.style.top = OFF_SCREEN; |
| 184 }; |
| 185 |
| 186 /** |
| 187 * Displays a suggestions box. |
| 188 * @param {string} inputValue The user text which prompted these suggestions. |
| 189 * @param {Array.<Object>} suggestions Suggestions to display. |
| 190 * @param {number} selectedIndex The index of the suggestion selected. |
| 191 * @constructor |
| 192 */ |
| 193 function SuggestionsBox(inputValue, suggestions, selectedIndex) { |
| 194 this.inputValue_ = inputValue; |
| 195 this.suggestions_ = suggestions; |
| 196 this.selectedIndex_ = selectedIndex; |
| 197 |
| 198 /** |
| 199 * The index of the suggestion currently under the mouse pointer. |
| 200 * @type {number} |
| 201 * @private |
| 202 */ |
| 203 this.hoveredIndex_ = -1; |
| 204 |
| 205 /** |
| 206 * A stamp to distinguish this suggestions box from others. |
| 207 * @type {number} |
| 208 * @private |
| 209 */ |
| 210 this.requestId_ = nextRequestId++; |
| 211 |
| 212 /** |
| 213 * The ordered iframes showing suggestions in this suggestions box. |
| 214 * @type {Array.<Element>} |
| 215 * @private |
| 216 */ |
| 217 this.iframes_ = []; |
| 218 |
| 219 /** |
| 220 * The ordered restricted ids for suggestions in this suggestions box. |
| 221 * @type {Array.<number>} |
| 222 * @private |
| 223 */ |
| 224 this.restrictedIds_ = []; |
| 225 |
| 226 /** |
| 227 * An embedded search API handle. |
| 228 * @type {Object} |
| 229 * @private |
| 230 */ |
| 231 this.apiHandle_ = getApiObjectHandle(); |
| 232 |
| 233 /** |
| 234 * The CSS class of the container for these suggestions. Initially pending, |
| 235 * then active once shown. |
| 236 * @type {string} |
| 237 */ |
| 238 this.containerClass = 'pending-container'; |
| 239 } |
| 240 |
| 241 /** |
| 242 * Starts rendering new suggestions. |
| 243 */ |
| 244 SuggestionsBox.prototype.load = function() { |
| 245 // Create a placeholder DOM in the invisible container. |
| 246 $('.' + this.containerClass).innerHTML = ''; |
| 247 var box = document.createElement('div'); |
| 248 box.className = 'suggestionsBox'; |
| 249 $('.' + this.containerClass).appendChild(box); |
| 250 var framesToLoad = {}; |
| 251 for (var i = 0, suggestion; suggestion = this.suggestions_[i]; i++) { |
| 252 var div = document.createElement('div'); |
| 253 div.classList.add('suggestion'); |
| 254 div.classList.toggle('selected', i == this.selectedIndex_); |
| 255 div.classList.toggle('search', suggestion.is_search); |
| 256 box.appendChild(div); |
| 257 // Reserve an iframe for each suggestion. |
| 258 this.iframes_[i] = iframePool.get(); |
| 259 this.restrictedIds_[i] = suggestion.rid; |
| 260 framesToLoad[this.iframes_[i].id] = suggestion.rid; |
| 261 } |
| 262 // Ask the loader iframe to populate the iframes just reserved. |
| 263 var loadRequest = { |
| 264 'load': framesToLoad, |
| 265 'requestId': this.requestId_, |
| 266 'style': { |
| 267 'urlColor': URL_COLOR, |
| 268 'titleColor': TITLE_COLOR |
| 269 } |
| 270 }; |
| 271 $('#suggestion-loader').contentWindow.postMessage(loadRequest, |
| 272 SUGGESTION_ORIGIN); |
| 273 }; |
| 274 |
| 275 /** |
| 276 * Releases suggestion iframes and ignores a load done message for the current |
| 277 * suggestions. |
| 278 */ |
| 279 SuggestionsBox.prototype.releaseIframes = function() { |
| 280 for (var i = 0; i < this.iframes_.length; i++) { |
| 281 iframePool.release(this.iframes_[i]); |
| 282 } |
| 283 this.responseId = -1; |
| 284 }; |
| 285 |
| 286 /** |
| 287 * Returns whether the given request should be displayed. |
| 288 * @param {number} requestId The number of the request that finished rendering. |
| 289 * @return {boolean} True if should display, false if not. |
| 290 */ |
| 291 SuggestionsBox.prototype.shouldShow = function(requestId) { |
| 292 return requestId == this.requestId_; |
| 293 }; |
| 294 |
| 295 /** |
| 296 * Moves iframes into position. |
| 297 */ |
| 298 SuggestionsBox.prototype.showIframes = function() { |
| 299 var divs = document.querySelectorAll('.' + this.containerClass + |
| 300 ' .suggestion'); |
| 301 // Note: This may be called before margins are ready. In that case, |
| 302 // suggestion iframes will initially be too large and then size down |
| 303 // onresize. |
| 304 var startMargin = this.apiHandle_.startMargin; |
| 305 var totalMargin = window.innerWidth - this.apiHandle_.width; |
| 306 for (var i = 0; i < divs.length && i < this.iframes_.length; i++) { |
| 307 // Add in the expected parent offset and the top margin. |
| 308 this.iframes_[i].style.top = (divs[i].offsetTop + 4) + 'px'; |
| 309 this.iframes_[i].style[this.apiHandle_.isRtl ? 'right' : 'left'] = |
| 310 startMargin + 'px'; |
| 311 this.iframes_[i].style.width = '-webkit-calc(100% - ' + |
| 312 (totalMargin + 26) + 'px)'; |
| 313 } |
| 314 }; |
| 315 |
| 316 /** |
| 317 * Selects the suggestion before the current selection. |
| 318 */ |
| 319 SuggestionsBox.prototype.selectPrevious = function() { |
| 320 var numSuggestions = this.suggestions_.length; |
| 321 this.selectedIndex_--; |
| 322 if (this.selectedIndex_ < -1) |
| 323 this.selectedIndex_ = -1; |
| 324 this.redrawSelection_(); |
| 325 this.redrawHover_(); |
| 326 }; |
| 327 |
| 328 /** |
| 329 * Selects the suggestion after the current selection. |
| 330 */ |
| 331 SuggestionsBox.prototype.selectNext = function() { |
| 332 var numSuggestions = this.suggestions_.length; |
| 333 this.selectedIndex_++; |
| 334 if (this.selectedIndex_ > numSuggestions - 1) |
| 335 this.selectedIndex_ = numSuggestions - 1; |
| 336 this.redrawSelection_(); |
| 337 this.redrawHover_(); |
| 338 }; |
| 339 |
| 340 /** |
| 341 * Redraws the selected suggestion. |
| 342 * @private |
| 343 */ |
| 344 SuggestionsBox.prototype.redrawSelection_ = function() { |
| 345 var oldSelection = $('.' + this.containerClass + ' .selected'); |
| 175 if (oldSelection) | 346 if (oldSelection) |
| 176 oldSelection.classList.remove('selected'); | 347 oldSelection.classList.remove('selected'); |
| 177 | 348 if (this.selectedIndex_ == -1) { |
| 178 if (increment) | 349 this.apiHandle_.setValue(this.inputValue_); |
| 179 selectedIndex = ++selectedIndex > numSuggestions - 1 ? -1 : selectedIndex; | |
| 180 else | |
| 181 selectedIndex = --selectedIndex < -1 ? numSuggestions - 1 : selectedIndex; | |
| 182 var apiHandle = getApiObjectHandle(); | |
| 183 if (selectedIndex == -1) { | |
| 184 apiHandle.setValue(lastInputValue); | |
| 185 } else { | 350 } else { |
| 186 var newSelection = $('suggestionsBox').querySelector( | 351 var newSelection = $('.' + this.containerClass + |
| 187 '.suggestion:nth-of-type(' + (selectedIndex + 1) + ')'); | 352 ' .suggestion:nth-of-type(' + (this.selectedIndex_ + 1) + ')'); |
| 188 newSelection.classList.add('selected'); | 353 newSelection.classList.add('selected'); |
| 189 apiHandle.setRestrictedValue(restrictedIds[selectedIndex]); | 354 this.apiHandle_.setRestrictedValue( |
| 190 } | 355 this.suggestions_[this.selectedIndex_].rid); |
| 191 } | 356 } |
| 192 | 357 }; |
| 193 // ============================================================================= | 358 |
| 194 // Handlers / API stuff | 359 /** |
| 195 // ============================================================================= | 360 * Returns the restricted id of the iframe clicked. |
| 361 * @param {Window} iframeWindow The window of the iframe that was clicked. |
| 362 * @return {number?} The restricted id clicked or null if none. |
| 363 */ |
| 364 SuggestionsBox.prototype.getClickTarget = function(iframeWindow) { |
| 365 for (var i = 0; i < this.iframes_.length; ++i) |
| 366 if (this.iframes_[i].contentWindow == iframeWindow) |
| 367 return this.restrictedIds_[i]; |
| 368 return null; |
| 369 }; |
| 370 |
| 371 /** |
| 372 * Called when the user hovers on the specified iframe. |
| 373 * @param {string} iframeId The id of the iframe hovered. |
| 374 */ |
| 375 SuggestionsBox.prototype.hover = function(iframeId) { |
| 376 this.hoveredIndex_ = -1; |
| 377 for (var i = 0; i < this.iframes_.length; ++i) { |
| 378 if (this.iframes_[i].id == iframeId) { |
| 379 this.hoveredIndex_ = i; |
| 380 break; |
| 381 } |
| 382 } |
| 383 this.redrawHover_(); |
| 384 }; |
| 385 |
| 386 /** |
| 387 * Called when the user unhovers the specified iframe. |
| 388 * @param {string} iframeId The id of the iframe hovered. |
| 389 */ |
| 390 SuggestionsBox.prototype.unhover = function(iframeId) { |
| 391 for (var i = 0; i < this.iframes_.length; ++i) { |
| 392 if (this.iframes_[i].id == iframeId && this.hoveredIndex_ == i) { |
| 393 this.hoveredIndex_ = -1; |
| 394 break; |
| 395 } |
| 396 } |
| 397 this.redrawHover_(); |
| 398 }; |
| 399 |
| 400 /** |
| 401 * Clears the current hover. |
| 402 */ |
| 403 SuggestionsBox.prototype.clearHover = function() { |
| 404 this.hoveredIndex_ = -1; |
| 405 this.redrawHover_(); |
| 406 }; |
| 407 |
| 408 /** |
| 409 * Redraws the mouse hover background. |
| 410 * @private |
| 411 */ |
| 412 SuggestionsBox.prototype.redrawHover_ = function() { |
| 413 if (this.hoveredIndex_ == -1) { |
| 414 var divs = document.querySelectorAll('.' + this.containerClass + |
| 415 ' .suggestion'); |
| 416 for (var i = 0; i < divs.length; i++) |
| 417 divs[i].classList.remove('hovered'); |
| 418 } else if (this.hoveredIndex_ != this.selectedIndex_) { |
| 419 var newHover = $('.' + this.containerClass + |
| 420 ' .suggestion:nth-of-type(' + (this.hoveredIndex_ + 1) + ')'); |
| 421 newHover.classList.add('hovered'); |
| 422 } |
| 423 }; |
| 424 |
| 425 /** |
| 426 * Clears the currently active suggestions and shows pending suggestions. |
| 427 */ |
| 428 function makePendingSuggestionsActive() { |
| 429 if (activeSuggestionsBox) |
| 430 activeSuggestionsBox.releaseIframes(); |
| 431 activeSuggestionsBox = pendingSuggestionsBox; |
| 432 pendingSuggestionsBox = null; |
| 433 var oldActiveContainer = $('.active-container'); |
| 434 $('.pending-container').className = 'active-container'; |
| 435 oldActiveContainer.className = 'pending-container'; |
| 436 activeSuggestionsBox.containerClass = 'active-container'; |
| 437 activeSuggestionsBox.showIframes(); |
| 438 var height = $('.active-container').offsetHeight; |
| 439 getApiObjectHandle().showOverlay(height); |
| 440 } |
| 441 |
| 442 /** |
| 443 * Hides the active suggestions box. |
| 444 */ |
| 445 function hideActiveSuggestions() { |
| 446 getApiObjectHandle().showOverlay(0); |
| 447 $('.active-container').innerHTML = ''; |
| 448 if (activeSuggestionsBox) |
| 449 activeSuggestionsBox.releaseIframes(); |
| 450 activeSuggestionsBox = null; |
| 451 } |
| 196 | 452 |
| 197 /** | 453 /** |
| 198 * @return {Object} the handle to the searchBox API. | 454 * @return {Object} the handle to the searchBox API. |
| 199 */ | 455 */ |
| 200 function getApiObjectHandle() { | 456 function getApiObjectHandle() { |
| 201 if (window.cideb) | 457 if (window.cideb) |
| 202 return window.cideb; | 458 return window.cideb; |
| 203 if (window.navigator && window.navigator.embeddedSearch && | 459 if (window.navigator && window.navigator.embeddedSearch && |
| 204 window.navigator.embeddedSearch.searchBox) | 460 window.navigator.embeddedSearch.searchBox) |
| 205 return window.navigator.embeddedSearch.searchBox; | 461 return window.navigator.embeddedSearch.searchBox; |
| 206 if (window.chrome && window.chrome.embeddedSearch && | 462 if (window.chrome && window.chrome.embeddedSearch && |
| 207 window.chrome.embeddedSearch.searchBox) | 463 window.chrome.embeddedSearch.searchBox) |
| 208 return window.chrome.embeddedSearch.searchBox; | 464 return window.chrome.embeddedSearch.searchBox; |
| 209 return null; | 465 return null; |
| 210 } | 466 } |
| 211 | 467 |
| 212 /** | 468 /** |
| 213 * Updates suggestions in response to a onchange or onnativesuggestions call. | 469 * Updates suggestions in response to a onchange or onnativesuggestions call. |
| 214 */ | 470 */ |
| 215 function updateSuggestions() { | 471 function updateSuggestions() { |
| 472 if (pendingSuggestionsBox) { |
| 473 pendingSuggestionsBox.releaseIframes(); |
| 474 pendingSuggestionsBox = null; |
| 475 } |
| 216 var apiHandle = getApiObjectHandle(); | 476 var apiHandle = getApiObjectHandle(); |
| 217 lastInputValue = apiHandle.value; | 477 var inputValue = apiHandle.value; |
| 218 | 478 var suggestions = apiHandle.nativeSuggestions; |
| 219 clearSuggestions(); | 479 if (!inputValue || !suggestions.length) { |
| 220 var nativeSuggestions = apiHandle.nativeSuggestions; | 480 hideActiveSuggestions(); |
| 221 if (nativeSuggestions.length) { | 481 return; |
| 222 nativeSuggestions.sort(function(a, b) { | 482 } |
| 483 if (suggestions.length) { |
| 484 suggestions.sort(function(a, b) { |
| 223 return b.rankingData.relevance - a.rankingData.relevance; | 485 return b.rankingData.relevance - a.rankingData.relevance; |
| 224 }); | 486 }); |
| 225 if (shouldSelectSuggestion(nativeSuggestions[0], apiHandle.verbatim)) | 487 var selectedIndex = -1; |
| 488 if (shouldSelectSuggestion(suggestions[0], apiHandle.verbatim)) |
| 226 selectedIndex = 0; | 489 selectedIndex = 0; |
| 227 renderSuggestions(nativeSuggestions); | 490 pendingSuggestionsBox = new SuggestionsBox(inputValue, |
| 491 suggestions.slice(0, MAX_SUGGESTIONS_TO_SHOW), selectedIndex); |
| 492 pendingSuggestionsBox.load(); |
| 228 } | 493 } |
| 229 | |
| 230 var height = getDropdownHeight(); | |
| 231 apiHandle.showOverlay(height); | |
| 232 } | 494 } |
| 233 | 495 |
| 234 /** | 496 /** |
| 235 * Appends a style node for suggestion properties that depend on apiHandle. | 497 * Appends a style node for suggestion properties that depend on apiHandle. |
| 236 */ | 498 */ |
| 237 function appendSuggestionStyles() { | 499 function appendSuggestionStyles() { |
| 238 var apiHandle = getApiObjectHandle(); | 500 var apiHandle = getApiObjectHandle(); |
| 239 var isRtl = apiHandle.rtl; | 501 var isRtl = apiHandle.rtl; |
| 240 var startMargin = apiHandle.startMargin; | 502 var startMargin = apiHandle.startMargin; |
| 241 var style = document.createElement('style'); | 503 var style = document.createElement('style'); |
| 242 style.type = 'text/css'; | 504 style.type = 'text/css'; |
| 243 style.id = 'suggestionStyle'; | 505 style.id = 'suggestionStyle'; |
| 244 style.textContent = | 506 style.textContent = |
| 245 '.suggestion, ' + | 507 '.suggestion, ' + |
| 246 '.suggestion.search {' + | 508 '.suggestion.search {' + |
| 247 ' background-position: ' + | 509 ' background-position: ' + |
| 248 (isRtl ? '-webkit-calc(100% - 5px)' : '5px') + ' 4px;' + | 510 (isRtl ? '-webkit-calc(100% - 5px)' : '5px') + ' 4px;' + |
| 249 ' -webkit-margin-start: ' + startMargin + 'px;' + | 511 ' -webkit-margin-start: ' + startMargin + 'px;' + |
| 250 ' -webkit-margin-end: ' + | 512 ' -webkit-margin-end: ' + |
| 251 (window.innerWidth - apiHandle.width - startMargin) + 'px;' + | 513 (window.innerWidth - apiHandle.width - startMargin) + 'px;' + |
| 252 ' font: ' + apiHandle.fontSize + 'px "' + apiHandle.font + '";' + | 514 ' font: ' + apiHandle.fontSize + 'px "' + apiHandle.font + '";' + |
| 253 '}'; | 515 '}'; |
| 254 document.querySelector('head').appendChild(style); | 516 $('head').appendChild(style); |
| 517 if (activeSuggestionsBox) |
| 518 activeSuggestionsBox.showIframes(); |
| 255 window.removeEventListener('resize', appendSuggestionStyles); | 519 window.removeEventListener('resize', appendSuggestionStyles); |
| 256 } | 520 } |
| 257 | 521 |
| 258 /** | 522 /** |
| 259 * Extract the desired navigation behavior from a click button. | 523 * Makes keys navigate through suggestions. |
| 260 * @param {number} button The Event#button property of a click event. | |
| 261 * @return {WindowOpenDisposition} The desired behavior for | |
| 262 * navigateContentWindow. | |
| 263 */ | |
| 264 function getDispositionFromClickButton(button) { | |
| 265 if (button == MIDDLE_MOUSE_BUTTON) | |
| 266 return WindowOpenDisposition.NEW_BACKGROUND_TAB; | |
| 267 return WindowOpenDisposition.CURRENT_TAB; | |
| 268 } | |
| 269 | |
| 270 /** | |
| 271 * Handles suggestion clicks. | |
| 272 * @param {number} restrictedId The restricted id of the suggestion being | |
| 273 * clicked. | |
| 274 * @param {number} button The Event#button property of a click event. | |
| 275 * | |
| 276 */ | |
| 277 function handleSuggestionClick(restrictedId, button) { | |
| 278 clearSuggestions(); | |
| 279 getApiObjectHandle().navigateContentWindow( | |
| 280 restrictedId, getDispositionFromClickButton(button)); | |
| 281 } | |
| 282 | |
| 283 /** | |
| 284 * chrome.searchBox.onkeypress implementation. | |
| 285 * @param {Object} e The key being pressed. | 524 * @param {Object} e The key being pressed. |
| 286 */ | 525 */ |
| 287 function handleKeyPress(e) { | 526 function handleKeyPress(e) { |
| 288 switch (e.keyCode) { | 527 if (activeSuggestionsBox) { |
| 289 case 38: // Up arrow | 528 switch (e.keyCode) { |
| 290 updateSelectedSuggestion(false); | 529 case 38: // Up arrow |
| 291 break; | 530 activeSuggestionsBox.selectPrevious(); |
| 292 case 40: // Down arrow | 531 break; |
| 293 updateSelectedSuggestion(true); | 532 case 40: // Down arrow |
| 294 break; | 533 activeSuggestionsBox.selectNext(); |
| 295 } | 534 break; |
| 296 } | |
| 297 | |
| 298 /** | |
| 299 * Handles the postMessage calls from the result iframes. | |
| 300 * @param {Object} message The message containing details of clicks the iframes. | |
| 301 */ | |
| 302 function handleMessage(message) { | |
| 303 if (message.origin != 'null' || !message.data || | |
| 304 message.data.eventType != 'click') { | |
| 305 return; | |
| 306 } | |
| 307 | |
| 308 var iframes = document.getElementsByClassName('contents'); | |
| 309 for (var i = 0; i < iframes.length; ++i) { | |
| 310 if (iframes[i].contentWindow == message.source) { | |
| 311 handleSuggestionClick(parseInt(iframes[i].id, 10), | |
| 312 message.data.button); | |
| 313 break; | |
| 314 } | 535 } |
| 315 } | 536 } |
| 316 } | 537 } |
| 317 | 538 |
| 318 /** | 539 /** |
| 319 * chrome.searchBox.embeddedSearch.onsubmit implementation. | 540 * Gets a function to call when an iframe is hovered. |
| 541 * @param {string} id The iframe id. |
| 542 * @return {function()} A function to call when an iframe is hovered. |
| 320 */ | 543 */ |
| 321 function onSubmit() { | 544 function hover(id) { |
| 545 return function() { |
| 546 if (activeSuggestionsBox) |
| 547 activeSuggestionsBox.hover(id); |
| 548 }; |
| 322 } | 549 } |
| 323 | 550 |
| 324 /** | 551 /** |
| 325 * Sets up the searchBox API. | 552 * Gets a function to call when an iframe is unhovered. |
| 553 * @param {string} id The iframe id. |
| 554 * @return {function()} A function to call when an iframe is unhovered. |
| 326 */ | 555 */ |
| 327 function setUpApi() { | 556 function unhover(id) { |
| 557 return function() { |
| 558 if (activeSuggestionsBox) |
| 559 activeSuggestionsBox.unhover(id); |
| 560 } |
| 561 } |
| 562 |
| 563 /** |
| 564 * Handles postMessage calls from suggestion iframes. |
| 565 * @param {Object} message A notification that all iframes are done loading or |
| 566 * that an iframe was clicked. |
| 567 */ |
| 568 function handleMessage(message) { |
| 569 if (message.origin != SUGGESTION_ORIGIN) |
| 570 return; |
| 571 |
| 572 if ('loaded' in message.data) { |
| 573 if (pendingSuggestionsBox && |
| 574 pendingSuggestionsBox.shouldShow(message.data.loaded)) { |
| 575 makePendingSuggestionsActive(); |
| 576 } |
| 577 } else if ('click' in message.data) { |
| 578 if (activeSuggestionsBox) { |
| 579 var targetId = activeSuggestionsBox.getClickTarget(message.source); |
| 580 if (targetId != null) { |
| 581 hideActiveSuggestions(); |
| 582 getApiObjectHandle().navigateContentWindow(targetId, |
| 583 getDispositionFromClickButton(message.data.click)); |
| 584 } |
| 585 } |
| 586 } |
| 587 } |
| 588 |
| 589 /** |
| 590 * Clears hover when window is blurred. |
| 591 */ |
| 592 function clearHover() { |
| 593 if (activeSuggestionsBox) |
| 594 activeSuggestionsBox.clearHover(); |
| 595 } |
| 596 |
| 597 /** |
| 598 * Sets up the embedded search API and creates suggestion iframes. |
| 599 */ |
| 600 function init() { |
| 601 iframePool = new IframePool(); |
| 602 iframePool.init(); |
| 328 var apiHandle = getApiObjectHandle(); | 603 var apiHandle = getApiObjectHandle(); |
| 329 apiHandle.onnativesuggestions = updateSuggestions; | 604 apiHandle.onnativesuggestions = updateSuggestions; |
| 330 apiHandle.onchange = updateSuggestions; | 605 apiHandle.onchange = updateSuggestions; |
| 331 apiHandle.onkeypress = handleKeyPress; | 606 apiHandle.onkeypress = handleKeyPress; |
| 332 apiHandle.onsubmit = onSubmit; | 607 // Instant checks for this handler to be bound. |
| 333 $('suggestions-box-container').dir = apiHandle.rtl ? 'rtl' : 'ltr'; | 608 apiHandle.onsubmit = function() {}; |
| 609 $('.active-container').dir = apiHandle.rtl ? 'rtl' : 'ltr'; |
| 610 $('.pending-container').dir = apiHandle.rtl ? 'rtl' : 'ltr'; |
| 334 // Delay adding these styles until the window width is available. | 611 // Delay adding these styles until the window width is available. |
| 335 window.addEventListener('resize', appendSuggestionStyles); | 612 window.addEventListener('resize', appendSuggestionStyles); |
| 336 if (apiHandle.nativeSuggestions.length) | 613 if (apiHandle.nativeSuggestions.length) |
| 337 handleNativeSuggestions(); | 614 updateSuggestions(); |
| 338 } | 615 } |
| 339 | 616 |
| 340 document.addEventListener('DOMContentLoaded', setUpApi); | 617 document.addEventListener('DOMContentLoaded', init); |
| 341 window.addEventListener('message', handleMessage, false); | 618 window.addEventListener('message', handleMessage, false); |
| 619 window.addEventListener('blur', clearHover, false); |
| OLD | NEW |