// Copyright 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview Support for omnibox behavior in offline mode or when API * features are not supported on the server. */ // ========================================================== // Enums. // ========================================================== /** * Possible behaviors for navigateContentWindow. * @enum {number} */ var WindowOpenDisposition = { CURRENT_TAB: 1, NEW_BACKGROUND_TAB: 2 }; /** * The JavaScript button event value for a middle click. * @type {number} * @const */ var MIDDLE_MOUSE_BUTTON = 1; // ============================================================================= // Util functions // ============================================================================= /** * The maximum number of suggestions to show. * @type {number} * @const */ var MAX_SUGGESTIONS_TO_SHOW = 5; /** * The omnibox input value during the last onnativesuggestions event. * @type {string} */ var lastInputValue = ''; /** * The ordered restricted ids of the currently displayed suggestions. Since the * suggestions contain the user's personal data (browser history) the searchBox * API embeds the content of the suggestion in a shadow dom, and assigns a * random restricted id to each suggestion which is accessible to the JS. * @type {Array.} */ var restrictedIds = []; /** * The index of the currently selected suggestion or -1 if none are selected. * @type {number} */ var selectedIndex = -1; /** * Displays a suggestion. * @param {Object} suggestion The suggestion to render. * @param {HTMLElement} box The html element to add the suggestion to. * @param {boolean} select True to select the selection. */ function addSuggestionToBox(suggestion, box, select) { var suggestionDiv = document.createElement('div'); suggestionDiv.classList.add('suggestion'); if (select) suggestionDiv.classList.add('selected'); var contentsContainer = document.createElement('div'); contentsContainer.className = 'contents'; var contents = suggestion.combinedNode; contentsContainer.appendChild(contents); suggestionDiv.appendChild(contentsContainer); var restrictedId = suggestion.rid; suggestionDiv.onclick = function(event) { handleSuggestionClick(restrictedId, event); }; restrictedIds.push(restrictedId); box.appendChild(suggestionDiv); } /** * Renders the input suggestions. * @param {Array} nativeSuggestions An array of native suggestions to render. */ function renderSuggestions(nativeSuggestions) { var box = document.createElement('div'); box.id = 'suggestionsBox'; $('suggestions-box-container').appendChild(box); for (var i = 0, length = nativeSuggestions.length; i < Math.min(MAX_SUGGESTIONS_TO_SHOW, length); ++i) { // Select the first suggestion. addSuggestionToBox(nativeSuggestions[i], box, i == 0); } } /** * Clears the suggestions being displayed. */ function clearSuggestions() { $('suggestions-box-container').innerHTML = ''; restrictedIds = []; selectedIndex = -1; } /** * @return {integer} The height of the dropdown. */ function getDropdownHeight() { return $('suggestions-box-container').offsetHeight; } /** * Updates selectedIndex, bounding it between -1 and the total number of * of suggestions - 1 (looping as necessary), and selects the corresponding * suggestion. * @param {boolean} increment True to increment the selected suggestion, false * to decrement. */ function updateSelectedSuggestion(increment) { var numSuggestions = restrictedIds.length; if (!numSuggestions) return; var oldSelection = $('suggestionsBox').querySelector('.selected'); if (oldSelection) oldSelection.classList.remove('selected'); if (increment) selectedIndex = ++selectedIndex > numSuggestions - 1 ? -1 : selectedIndex; else selectedIndex = --selectedIndex < -1 ? numSuggestions - 1 : selectedIndex; var apiHandle = getApiObjectHandle(); if (selectedIndex == -1) { apiHandle.setValue(lastInputValue); } else { var newSelection = $('suggestionsBox').querySelector( '.suggestion:nth-of-type(' + (selectedIndex + 1) + ')'); newSelection.classList.add('selected'); apiHandle.setRestrictedValue(restrictedIds[selectedIndex]); } } // ============================================================================= // Handlers / API stuff // ============================================================================= /** * @return {Object} the handle to the searchBox API. */ function getApiObjectHandle() { if (window.cideb) return window.cideb; if (window.navigator && window.navigator.embeddedSearch && window.navigator.embeddedSearch.searchBox) return window.navigator.embeddedSearch.searchBox; if (window.chrome && window.chrome.embeddedSearch && window.chrome.embeddedSearch.searchBox) return window.chrome.embeddedSearch.searchBox; return null; } /** * chrome.searchBox.onnativesuggestions implementation. */ function handleNativeSuggestions() { var apiHandle = getApiObjectHandle(); // Used to workaround repeated undesired asynchronous onnativesuggestions // events and the fact that when a suggestion is clicked, the omnibox unfocus // can cause onnativesuggestions to fire, preventing the suggestion onclick // from registering. if (lastInputValue == apiHandle.value && $('suggestionsBox')) { return; } lastInputValue = apiHandle.value; clearSuggestions(); var nativeSuggestions = apiHandle.nativeSuggestions; if (nativeSuggestions.length) { nativeSuggestions.sort(function(a, b) { return b.rankingData.relevance - a.rankingData.relevance; }); renderSuggestions(nativeSuggestions); selectedIndex = 0; apiHandle.setRestrictedAutocompleteText( nativeSuggestions[selectedIndex].rid); } var height = getDropdownHeight(); // TODO(jered): Remove deprecated "reason" argument. apiHandle.showOverlay(2, height); } /** * Appends a style node for suggestion properties that depend on apiHandle. */ function appendSuggestionStyles() { var apiHandle = getApiObjectHandle(); var style = document.createElement('style'); style.type = 'text/css'; style.id = 'suggestionStyle'; style.textContent = '.suggestion {' + ' -webkit-margin-start: ' + apiHandle.startMargin + 'px;' + ' -webkit-margin-end: ' + apiHandle.endMargin + 'px;' + ' font: ' + apiHandle.fontSize + 'px "' + apiHandle.font + '";' + '}'; document.querySelector('head').appendChild(style); } /** * Extract the desired navigation behavior from a click event. * @param {Event} event The click event. * @return {WindowOpenDisposition} The desired behavior for * navigateContentWindow. */ function getDispositionFromClick(event) { if (event.button == MIDDLE_MOUSE_BUTTON) return WindowOpenDisposition.NEW_BACKGROUND_TAB; return WindowOpenDisposition.CURRENT_TAB; } /** * Handles suggestion clicks. * @param {integer} restrictedId The restricted id of the suggestion being * clicked. * @param {Event} event The click event. */ function handleSuggestionClick(restrictedId, event) { clearSuggestions(); getApiObjectHandle().navigateContentWindow(restrictedId, getDispositionFromClick(event)); } /** * chrome.searchBox.onkeypress implementation. * @param {Object} e The key being pressed. */ function handleKeyPress(e) { switch (e.keyCode) { case 38: // Up arrow updateSelectedSuggestion(false); break; case 40: // Down arrow updateSelectedSuggestion(true); break; } } /** * chrome.searchBox.onsubmit implementation. */ function onSubmit() { } /** * Sets up the searchBox API. */ function setUpApi() { var apiHandle = getApiObjectHandle(); apiHandle.onnativesuggestions = handleNativeSuggestions; apiHandle.onkeypress = handleKeyPress; apiHandle.onsubmit = onSubmit; appendSuggestionStyles(); if (apiHandle.nativeSuggestions.length) handleNativeSuggestions(); } document.addEventListener('DOMContentLoaded', setUpApi);