OLD | NEW |
| (Empty) |
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 | |
3 // found in the LICENSE file. | |
4 | |
5 /** | |
6 * @fileoverview Support for omnibox behavior in offline mode or when API | |
7 * features are not supported on the server. | |
8 */ | |
9 | |
10 // ========================================================== | |
11 // Enums. | |
12 // ========================================================== | |
13 | |
14 /** | |
15 * Possible behaviors for navigateContentWindow. | |
16 * @enum {number} | |
17 */ | |
18 var WindowOpenDisposition = { | |
19 CURRENT_TAB: 1, | |
20 NEW_BACKGROUND_TAB: 2 | |
21 }; | |
22 | |
23 /** | |
24 * The JavaScript button event value for a middle click. | |
25 * @type {number} | |
26 * @const | |
27 */ | |
28 var MIDDLE_MOUSE_BUTTON = 1; | |
29 | |
30 // ============================================================================= | |
31 // Util functions | |
32 // ============================================================================= | |
33 | |
34 /** | |
35 * The maximum number of suggestions to show. | |
36 * @type {number} | |
37 * @const | |
38 */ | |
39 var MAX_SUGGESTIONS_TO_SHOW = 5; | |
40 | |
41 /** | |
42 * Assume any native suggestion with a score higher than this value has been | |
43 * inlined by the browser. | |
44 * @type {number} | |
45 * @const | |
46 */ | |
47 var INLINE_SUGGESTION_THRESHOLD = 1200; | |
48 | |
49 /** | |
50 * Suggestion provider type corresponding to a verbatim URL suggestion. | |
51 * @type {string} | |
52 * @const | |
53 */ | |
54 var VERBATIM_URL_TYPE = 'url-what-you-typed'; | |
55 | |
56 /** | |
57 * Suggestion provider type corresponding to a verbatim search suggestion. | |
58 * @type {string} | |
59 * @const | |
60 */ | |
61 var VERBATIM_SEARCH_TYPE = 'search-what-you-typed'; | |
62 | |
63 /** | |
64 * The omnibox input value during the last onnativesuggestions event. | |
65 * @type {string} | |
66 */ | |
67 var lastInputValue = ''; | |
68 | |
69 /** | |
70 * The ordered restricted ids of the currently displayed suggestions. Since the | |
71 * suggestions contain the user's personal data (browser history) the searchBox | |
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 */ | |
76 | |
77 var restrictedIds = []; | |
78 | |
79 /** | |
80 * The index of the currently selected suggestion or -1 if none are selected. | |
81 * @type {number} | |
82 */ | |
83 var selectedIndex = -1; | |
84 | |
85 /** | |
86 * Shortcut for document.getElementById. | |
87 * @param {string} id of the element. | |
88 * @return {HTMLElement} with the id. | |
89 */ | |
90 function $(id) { | |
91 return document.getElementById(id); | |
92 } | |
93 | |
94 /** | |
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 if (suggestion.destination_url) { // iframes. | |
107 var suggestionIframe = document.createElement('iframe'); | |
108 suggestionIframe.className = 'contents'; | |
109 suggestionIframe.src = suggestion.destination_url; | |
110 suggestionIframe.id = suggestion.rid; | |
111 suggestionDiv.appendChild(suggestionIframe); | |
112 } else { | |
113 var contentsContainer = document.createElement('div'); | |
114 var contents = suggestion.combinedNode; | |
115 contents.classList.add('contents'); | |
116 contentsContainer.appendChild(contents); | |
117 suggestionDiv.appendChild(contentsContainer); | |
118 suggestionDiv.onclick = function(event) { | |
119 handleSuggestionClick(suggestion.rid, event.button); | |
120 }; | |
121 } | |
122 | |
123 restrictedIds.push(suggestion.rid); | |
124 box.appendChild(suggestionDiv); | |
125 } | |
126 | |
127 /** | |
128 * Renders the input suggestions. | |
129 * @param {Array} nativeSuggestions An array of native suggestions to render. | |
130 */ | |
131 function renderSuggestions(nativeSuggestions) { | |
132 var box = document.createElement('div'); | |
133 box.id = 'suggestionsBox'; | |
134 $('suggestions-box-container').appendChild(box); | |
135 | |
136 for (var i = 0, length = nativeSuggestions.length; | |
137 i < Math.min(MAX_SUGGESTIONS_TO_SHOW, length); ++i) { | |
138 // Don't add the search-what-you-typed suggestion if it's the top match. | |
139 if (i > 0 || nativeSuggestions[i].type != VERBATIM_SEARCH_TYPE) | |
140 addSuggestionToBox(nativeSuggestions[i], box, i == selectedIndex); | |
141 } | |
142 } | |
143 | |
144 /** | |
145 * Clears the suggestions being displayed. | |
146 */ | |
147 function clearSuggestions() { | |
148 $('suggestions-box-container').innerHTML = ''; | |
149 restrictedIds = []; | |
150 selectedIndex = -1; | |
151 } | |
152 | |
153 /** | |
154 * @return {number} The height of the dropdown. | |
155 */ | |
156 function getDropdownHeight() { | |
157 return $('suggestions-box-container').offsetHeight; | |
158 } | |
159 | |
160 /** | |
161 * @param {Object} suggestion A suggestion. | |
162 * @param {boolean} inVerbatimMode Are we in verbatim mode? | |
163 * @return {boolean} True if the suggestion should be selected. | |
164 */ | |
165 function shouldSelectSuggestion(suggestion, inVerbatimMode) { | |
166 var isVerbatimUrl = suggestion.type == VERBATIM_URL_TYPE; | |
167 var inlinableSuggestion = suggestion.type != VERBATIM_SEARCH_TYPE && | |
168 suggestion.rankingData.relevance > INLINE_SUGGESTION_THRESHOLD; | |
169 // Verbatim URLs should always be selected. Otherwise, select suggestions | |
170 // with a high enough score unless we are in verbatim mode (e.g. backspacing | |
171 // away). | |
172 return isVerbatimUrl || (!inVerbatimMode && inlinableSuggestion); | |
173 } | |
174 | |
175 /** | |
176 * Updates selectedIndex, bounding it between -1 and the total number of | |
177 * of suggestions - 1 (looping as necessary), and selects the corresponding | |
178 * suggestion. | |
179 * @param {boolean} increment True to increment the selected suggestion, false | |
180 * to decrement. | |
181 */ | |
182 function updateSelectedSuggestion(increment) { | |
183 var numSuggestions = restrictedIds.length; | |
184 if (!numSuggestions) | |
185 return; | |
186 | |
187 var oldSelection = $('suggestionsBox').querySelector('.selected'); | |
188 if (oldSelection) | |
189 oldSelection.classList.remove('selected'); | |
190 | |
191 if (increment) | |
192 selectedIndex = ++selectedIndex > numSuggestions - 1 ? -1 : selectedIndex; | |
193 else | |
194 selectedIndex = --selectedIndex < -1 ? numSuggestions - 1 : selectedIndex; | |
195 var apiHandle = getApiObjectHandle(); | |
196 if (selectedIndex == -1) { | |
197 apiHandle.setValue(lastInputValue); | |
198 } else { | |
199 var newSelection = $('suggestionsBox').querySelector( | |
200 '.suggestion:nth-of-type(' + (selectedIndex + 1) + ')'); | |
201 newSelection.classList.add('selected'); | |
202 apiHandle.setRestrictedValue(restrictedIds[selectedIndex]); | |
203 } | |
204 } | |
205 | |
206 // ============================================================================= | |
207 // Handlers / API stuff | |
208 // ============================================================================= | |
209 | |
210 /** | |
211 * @return {Object} the handle to the searchBox API. | |
212 */ | |
213 function getApiObjectHandle() { | |
214 if (window.cideb) | |
215 return window.cideb; | |
216 if (window.navigator && window.navigator.embeddedSearch && | |
217 window.navigator.embeddedSearch.searchBox) | |
218 return window.navigator.embeddedSearch.searchBox; | |
219 if (window.chrome && window.chrome.embeddedSearch && | |
220 window.chrome.embeddedSearch.searchBox) | |
221 return window.chrome.embeddedSearch.searchBox; | |
222 return null; | |
223 } | |
224 | |
225 /** | |
226 * Updates suggestions in response to a onchange or onnativesuggestions call. | |
227 */ | |
228 function updateSuggestions() { | |
229 var apiHandle = getApiObjectHandle(); | |
230 lastInputValue = apiHandle.value; | |
231 | |
232 clearSuggestions(); | |
233 var nativeSuggestions = apiHandle.nativeSuggestions; | |
234 if (nativeSuggestions.length) { | |
235 nativeSuggestions.sort(function(a, b) { | |
236 return b.rankingData.relevance - a.rankingData.relevance; | |
237 }); | |
238 if (shouldSelectSuggestion(nativeSuggestions[0], apiHandle.verbatim)) | |
239 selectedIndex = 0; | |
240 renderSuggestions(nativeSuggestions); | |
241 } | |
242 | |
243 var height = getDropdownHeight(); | |
244 apiHandle.showOverlay(height); | |
245 } | |
246 | |
247 /** | |
248 * Appends a style node for suggestion properties that depend on apiHandle. | |
249 */ | |
250 function appendSuggestionStyles() { | |
251 var apiHandle = getApiObjectHandle(); | |
252 var isRtl = apiHandle.rtl; | |
253 var startMargin = apiHandle.startMargin; | |
254 var style = document.createElement('style'); | |
255 style.type = 'text/css'; | |
256 style.id = 'suggestionStyle'; | |
257 style.textContent = | |
258 '.suggestion, ' + | |
259 '.suggestion.search {' + | |
260 ' background-position: ' + | |
261 (isRtl ? '-webkit-calc(100% - 5px)' : '5px') + ' 4px;' + | |
262 ' -webkit-margin-start: ' + startMargin + 'px;' + | |
263 ' -webkit-margin-end: ' + | |
264 (window.innerWidth - apiHandle.width - startMargin) + 'px;' + | |
265 ' font: ' + apiHandle.fontSize + 'px "' + apiHandle.font + '";' + | |
266 '}'; | |
267 document.querySelector('head').appendChild(style); | |
268 window.removeEventListener('resize', appendSuggestionStyles); | |
269 } | |
270 | |
271 /** | |
272 * Extract the desired navigation behavior from a click button. | |
273 * @param {number} button The Event#button property of a click event. | |
274 * @return {WindowOpenDisposition} The desired behavior for | |
275 * navigateContentWindow. | |
276 */ | |
277 function getDispositionFromClickButton(button) { | |
278 if (button == MIDDLE_MOUSE_BUTTON) | |
279 return WindowOpenDisposition.NEW_BACKGROUND_TAB; | |
280 return WindowOpenDisposition.CURRENT_TAB; | |
281 } | |
282 | |
283 /** | |
284 * Handles suggestion clicks. | |
285 * @param {number} restrictedId The restricted id of the suggestion being | |
286 * clicked. | |
287 * @param {number} button The Event#button property of a click event. | |
288 * | |
289 */ | |
290 function handleSuggestionClick(restrictedId, button) { | |
291 clearSuggestions(); | |
292 getApiObjectHandle().navigateContentWindow( | |
293 restrictedId, getDispositionFromClickButton(button)); | |
294 } | |
295 | |
296 /** | |
297 * chrome.searchBox.onkeypress implementation. | |
298 * @param {Object} e The key being pressed. | |
299 */ | |
300 function handleKeyPress(e) { | |
301 switch (e.keyCode) { | |
302 case 38: // Up arrow | |
303 updateSelectedSuggestion(false); | |
304 break; | |
305 case 40: // Down arrow | |
306 updateSelectedSuggestion(true); | |
307 break; | |
308 } | |
309 } | |
310 | |
311 /** | |
312 * Handles the postMessage calls from the result iframes. | |
313 * @param {Object} message The message containing details of clicks the iframes. | |
314 */ | |
315 function handleMessage(message) { | |
316 if (message.origin != 'null' || !message.data || | |
317 message.data.eventType != 'click') { | |
318 return; | |
319 } | |
320 | |
321 var iframes = document.getElementsByClassName('contents'); | |
322 for (var i = 0; i < iframes.length; ++i) { | |
323 if (iframes[i].contentWindow == message.source) { | |
324 handleSuggestionClick(parseInt(iframes[i].id, 10), | |
325 message.data.button); | |
326 break; | |
327 } | |
328 } | |
329 } | |
330 | |
331 /** | |
332 * chrome.searchBox.embeddedSearch.onsubmit implementation. | |
333 */ | |
334 function onSubmit() { | |
335 } | |
336 | |
337 /** | |
338 * Sets up the searchBox API. | |
339 */ | |
340 function setUpApi() { | |
341 var apiHandle = getApiObjectHandle(); | |
342 apiHandle.onnativesuggestions = updateSuggestions; | |
343 apiHandle.onchange = updateSuggestions; | |
344 apiHandle.onkeypress = handleKeyPress; | |
345 apiHandle.onsubmit = onSubmit; | |
346 $('suggestions-box-container').dir = apiHandle.rtl ? 'rtl' : 'ltr'; | |
347 // Delay adding these styles until the window width is available. | |
348 window.addEventListener('resize', appendSuggestionStyles); | |
349 if (apiHandle.nativeSuggestions.length) | |
350 handleNativeSuggestions(); | |
351 } | |
352 | |
353 document.addEventListener('DOMContentLoaded', setUpApi); | |
354 window.addEventListener('message', handleMessage, false); | |
OLD | NEW |