Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(966)

Side by Side Diff: chrome/browser/resources/local_omnibox_popup/local_omnibox_popup.js

Issue 13375003: Fixing iframe jank in the local omnibox popup. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Rebuilding on 14039004. Created 7 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
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 (function() {
10 // ========================================================== 10 <include src="../../../../ui/webui/resources/js/assert.js">
11 // Enums.
12 // ==========================================================
13 11
14 /** 12 /**
15 * Possible behaviors for navigateContentWindow. 13 * Possible behaviors for navigateContentWindow.
16 * @enum {number} 14 * @enum {number}
17 */ 15 */
18 var WindowOpenDisposition = { 16 var WindowOpenDisposition = {
19 CURRENT_TAB: 1, 17 CURRENT_TAB: 1,
20 NEW_BACKGROUND_TAB: 2 18 NEW_BACKGROUND_TAB: 2
21 }; 19 };
22 20
23 /** 21 /**
24 * The JavaScript button event value for a middle click. 22 * The JavaScript button event value for a middle click.
25 * @type {number} 23 * @type {number}
26 * @const 24 * @const
27 */ 25 */
28 var MIDDLE_MOUSE_BUTTON = 1; 26 var MIDDLE_MOUSE_BUTTON = 1;
29 27
30 // =============================================================================
31 // Util functions
32 // =============================================================================
33
34 /** 28 /**
35 * The maximum number of suggestions to show. 29 * The maximum number of suggestions to show.
36 * @type {number} 30 * @type {number}
37 * @const 31 * @const
38 */ 32 */
39 var MAX_SUGGESTIONS_TO_SHOW = 5; 33 var MAX_SUGGESTIONS_TO_SHOW = 5;
40 34
41 /** 35 /**
42 * Assume any native suggestion with a score higher than this value has been 36 * Assume any native suggestion with a score higher than this value has been
43 * inlined by the browser. 37 * inlined by the browser.
44 * @type {number} 38 * @type {number}
45 * @const 39 * @const
46 */ 40 */
47 var INLINE_SUGGESTION_THRESHOLD = 1200; 41 var INLINE_SUGGESTION_THRESHOLD = 1200;
48 42
49 /** 43 /**
44 * The color code for a query.
45 * @type {number}
46 * @const
47 */
48 var QUERY_COLOR = 0x000000;
49
50 /**
51 * The color code for a display URL.
52 * @type {number}
53 * @const
54 */
55 var URL_COLOR = 0x009933;
56
57 /**
58 * The color code for a suggestion title.
59 * @type {number}
60 * @const
61 */
62 var TITLE_COLOR = 0x666666;
63
64 /**
65 * A top position which is off-screen.
66 * @type {string}
67 * @const
68 */
69 var OFF_SCREEN = '-1000px';
70
71 /**
72 * The expected origin of a suggestion iframe.
73 * @type {string}
74 * @const
75 */
76 var SUGGESTION_ORIGIN = 'chrome-search://suggestion';
77
78 /**
50 * Suggestion provider type corresponding to a verbatim URL suggestion. 79 * Suggestion provider type corresponding to a verbatim URL suggestion.
51 * @type {string} 80 * @type {string}
52 * @const 81 * @const
53 */ 82 */
54 var VERBATIM_URL_TYPE = 'url-what-you-typed'; 83 var VERBATIM_URL_TYPE = 'url-what-you-typed';
55 84
56 /** 85 /**
57 * Suggestion provider type corresponding to a verbatim search suggestion. 86 * Suggestion provider type corresponding to a verbatim search suggestion.
58 * @type {string} 87 * @type {string}
59 * @const 88 * @const
60 */ 89 */
61 var VERBATIM_SEARCH_TYPE = 'search-what-you-typed'; 90 var VERBATIM_SEARCH_TYPE = 'search-what-you-typed';
62 91
63 /** 92 /**
64 * The omnibox input value during the last onnativesuggestions event. 93 * "Up" arrow keycode.
65 * @type {string} 94 * @type {number}
95 * @const
66 */ 96 */
67 var lastInputValue = ''; 97 var KEY_UP_ARROW = 38;
68 98
69 /** 99 /**
70 * The ordered restricted ids of the currently displayed suggestions. Since the 100 * "Down" arrow keycode.
71 * suggestions contain the user's personal data (browser history) the searchBox 101 * @type {number}
72 * API embeds the content of the suggestion in a shadow dom, and assigns a 102 * @const
73 * random restricted id to each suggestion which is accessible to the JS.
74 * @type {Array.<number>}
75 */ 103 */
76 104 var KEY_DOWN_ARROW = 40;
77 var restrictedIds = [];
78 105
79 /** 106 /**
80 * The index of the currently selected suggestion or -1 if none are selected. 107 * Pixels of padding inside a suggestion div for displaying its icon.
108 * @type {number}
109 * @const
110 */
111 var SUGGESTION_ICON_PADDING = 26;
112
113 /**
114 * Pixels by which iframes should be moved down relative to their wrapping
115 * suggestion div.
116 */
117 var SUGGESTION_TOP_OFFSET = 4;
118
119 /**
120 * The displayed suggestions.
121 * @type {SuggestionsBox}
122 */
123 var activeBox;
124
125 /**
126 * The suggestions being rendered.
127 * @type {SuggestionsBox}
128 */
129 var pendingBox;
130
131 /**
132 * A pool of iframes to display suggestions.
133 * @type {IframePool}
134 */
135 var iframePool;
136
137 /**
138 * A serial number for the next suggestions rendered.
81 * @type {number} 139 * @type {number}
82 */ 140 */
83 var selectedIndex = -1; 141 var nextRequestId = 0;
84 142
85 /** 143 /**
86 * Shortcut for document.getElementById. 144 * Shortcut for document.querySelector.
87 * @param {string} id of the element. 145 * @param {string} selector A selector to query the desired element.
88 * @return {HTMLElement} with the id. 146 * @return {HTMLElement} The first element to match |selector| or null.
89 */ 147 */
90 function $(id) { 148 function $qs(selector) {
91 return document.getElementById(id); 149 return document.querySelector(selector);
92 } 150 }
93 151
94 /** 152 /**
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. 153 * @param {Object} suggestion A suggestion.
162 * @param {boolean} inVerbatimMode Are we in verbatim mode? 154 * @param {boolean} inVerbatimMode Are we in verbatim mode?
163 * @return {boolean} True if the suggestion should be selected. 155 * @return {boolean} True if the suggestion should be selected.
164 */ 156 */
165 function shouldSelectSuggestion(suggestion, inVerbatimMode) { 157 function shouldSelectSuggestion(suggestion, inVerbatimMode) {
166 var isVerbatimUrl = suggestion.type == VERBATIM_URL_TYPE; 158 var isVerbatimUrl = suggestion.type == VERBATIM_URL_TYPE;
167 var inlinableSuggestion = suggestion.type != VERBATIM_SEARCH_TYPE && 159 var inlineableSuggestion = suggestion.type != VERBATIM_SEARCH_TYPE &&
168 suggestion.rankingData.relevance > INLINE_SUGGESTION_THRESHOLD; 160 suggestion.rankingData.relevance > INLINE_SUGGESTION_THRESHOLD;
169 // Verbatim URLs should always be selected. Otherwise, select suggestions 161 // Verbatim URLs should always be selected. Otherwise, select suggestions
170 // with a high enough score unless we are in verbatim mode (e.g. backspacing 162 // with a high enough score unless we are in verbatim mode (e.g. backspacing
171 // away). 163 // away).
172 return isVerbatimUrl || (!inVerbatimMode && inlinableSuggestion); 164 return isVerbatimUrl || (!inVerbatimMode && inlineableSuggestion);
173 } 165 }
174 166
175 /** 167 /**
176 * Updates selectedIndex, bounding it between -1 and the total number of 168 * Extract the desired navigation behavior from a click button.
177 * of suggestions - 1 (looping as necessary), and selects the corresponding 169 * @param {number} button The Event#button property of a click event.
178 * suggestion. 170 * @return {WindowOpenDisposition} The desired behavior for
179 * @param {boolean} increment True to increment the selected suggestion, false 171 * navigateContentWindow.
180 * to decrement. 172 */
181 */ 173 function getDispositionFromClickButton(button) {
182 function updateSelectedSuggestion(increment) { 174 if (button == MIDDLE_MOUSE_BUTTON)
183 var numSuggestions = restrictedIds.length; 175 return WindowOpenDisposition.NEW_BACKGROUND_TAB;
184 if (!numSuggestions) 176 return WindowOpenDisposition.CURRENT_TAB;
185 return; 177 }
186 178
187 var oldSelection = $('suggestionsBox').querySelector('.selected'); 179
188 if (oldSelection) 180 /**
189 oldSelection.classList.remove('selected'); 181 * Manages a pool of chrome-search suggestion result iframes.
190 182 * @constructor
191 if (increment) 183 */
192 selectedIndex = ++selectedIndex > numSuggestions - 1 ? -1 : selectedIndex; 184 function IframePool() {
193 else 185 }
194 selectedIndex = --selectedIndex < -1 ? numSuggestions - 1 : selectedIndex; 186
195 var apiHandle = getApiObjectHandle(); 187 IframePool.prototype = {
196 if (selectedIndex == -1) { 188 /**
197 apiHandle.setValue(lastInputValue); 189 * HTML iframe elements.
190 * @type {Array.<Element>}
191 * @private
192 */
193 iframes_: [],
194
195 /**
196 * Initializes the pool with blank result template iframes, positioned off
197 * screen.
198 */
199 init: function() {
200 for (var i = 0; i < 2 * MAX_SUGGESTIONS_TO_SHOW; ++i) {
201 var iframe = document.createElement('iframe');
202 iframe.className = 'contents';
203 iframe.id = 'suggestion-text-' + i;
204 iframe.src = 'chrome-search://suggestion/result.html';
205 iframe.style.top = OFF_SCREEN;
206 iframe.addEventListener('mouseover', function(e) {
207 if (activeBox)
208 activeBox.hover(e.currentTarget.id);
209 }, false);
210 iframe.addEventListener('mouseout', function(e) {
211 if (activeBox)
212 activeBox.unhover(e.currentTarget.id);
213 }, false);
214 document.body.appendChild(iframe);
215 this.iframes_.push(iframe);
216 }
217 },
218
219 /**
220 * Reserves a free suggestion iframe from the pool.
221 * @return {Element} An iframe suitable for holding a suggestion.
222 */
223 reserve: function() {
224 return this.iframes_.pop();
225 },
226
227 /**
228 * Releases a suggestion iframe back into the pool.
229 * @param {Element} iframe The iframe to return to the pool.
230 */
231 release: function(iframe) {
232 this.iframes_.push(iframe);
233 iframe.style.top = OFF_SCREEN;
234 },
235 };
236
237
238 /**
239 * An individual suggestion.
240 * @param {!Object} data Autocomplete fields for this suggestion.
241 * @constructor
242 */
243 function Suggestion(data) {
244 assert(data);
245 /**
246 * Autocomplete fields for this suggestion.
247 * @type {!Object}
248 * @private
249 */
250 this.data_ = data;
251 }
252
253 Suggestion.prototype = {
254 /**
255 * Releases the iframe reserved for this suggestion.
256 */
257 destroy: function() {
258 if (this.iframe_)
259 iframePool.release(this.iframe_);
260 },
261
262 /**
263 * Creates and appends the placeholder div for this suggestion to box.
264 * @param {Element} box A suggestions box.
265 * @param {boolean} selected True if the suggestion should be drawn as
266 * selected and false otherwise.
267 */
268 appendToBox: function(box, selected) {
269 var div = document.createElement('div');
270 div.classList.add('suggestion');
271 div.classList.toggle('selected', selected);
272 div.classList.toggle('search', this.data_.is_search);
273 box.appendChild(div);
274 this.div_ = div;
275 },
276
277 /**
278 * Repositions the suggestion iframe to align with its expected dropdown
279 * position.
280 * @param {boolean} isRtl True if rendering right-to-left and false if not.
281 * @param {number} startMargin Leading space before suggestion.
282 * @param {number} totalMargin Total non-content space on suggestion line.
283 */
284 reposition: function(isRtl, startMargin, totalMargin) {
285 // Add in the expected parent offset and the top margin.
286 this.iframe_.style.top = this.div_.offsetTop + SUGGESTION_TOP_OFFSET + 'px';
287 // Call parseInt to enforce that startMargin and totalMargin are really
288 // numbers since we're interpolating CSS.
289 startMargin = parseInt(startMargin, 10);
290 totalMargin = parseInt(totalMargin, 10);
291 if (isFinite(startMargin) && isFinite(totalMargin)) {
292 this.iframe_.style[isRtl ? 'right' : 'left'] = startMargin + 'px';
293 this.iframe_.style.width = '-webkit-calc(100% - ' +
294 (totalMargin + SUGGESTION_ICON_PADDING) + 'px)';
295 }
296 },
297
298 /**
299 * Updates the suggestion selection state.
300 * @param {boolean} selected True if drawn selected or false if not.
301 */
302 select: function(selected) {
303 this.div_.classList.toggle('selected', selected);
304 },
305
306 /**
307 * Updates the suggestion hover state.
308 * @param {boolean} hovered True if drawn hovered or false if not.
309 */
310 hover: function(hovered) {
311 this.div_.classList.toggle('hovered', hovered);
312 },
313
314 /**
315 * @param {Window} iframeWindow The content window of an iframe.
316 * @return {boolean} True if this suggestion's iframe has the specified
317 * window and false if not.
318 */
319 hasIframeWindow: function(iframeWindow) {
320 return this.iframe_.contentWindow == iframeWindow;
321 },
322
323 /**
324 * @param {string} id An element id.
325 * @return {boolean} True if this suggestion's iframe has the specified id
326 * and false if not.
327 */
328 hasIframeId: function(id) {
329 return this.iframe_.id == id;
330 },
331
332 /**
333 * The iframe element for this suggestion.
334 * @type {Element}
335 */
336 set iframe(iframe) {
337 this.iframe_ = iframe;
338 },
339
340 /**
341 * The restricted id associated with this suggestion.
342 * @type {number}
343 */
344 get restrictedId() {
345 return this.data_.rid;
346 },
347 };
348
349
350 /**
351 * Displays a suggestions box.
352 * @param {string} inputValue The user text that prompted these suggestions.
353 * @param {!Array.<!Object>} suggestionData Suggestion data to display.
354 * @param {number} selectedIndex The index of the suggestion selected.
355 * @constructor
356 */
357 function SuggestionsBox(inputValue, suggestionData, selectedIndex) {
358 /**
359 * The user text that prompted these suggestions.
360 * @type {string}
361 * @private
362 */
363 this.inputValue_ = inputValue;
364
365 /**
366 * The index of the suggestion currently selected, whether by default or
367 * because the user arrowed down to it.
368 * @type {number}
369 * @private
370 */
371 this.selectedIndex_ = selectedIndex;
372
373 /**
374 * The index of the suggestion currently under the mouse pointer.
375 * @type {number}
376 * @private
377 */
378 this.hoveredIndex_ = -1;
379
380 /**
381 * A stamp to distinguish this suggestions box from others.
382 * @type {number}
383 * @private
384 */
385 this.requestId_ = nextRequestId++;
386
387 /**
388 * The ordered suggestions this box is displaying.
389 * @type {Array.<Suggestion>}
390 * @private
391 */
392 this.suggestions_ = [];
393 for (var i = 0; i < suggestionData.length; ++i) {
394 this.suggestions_.push(new Suggestion(suggestionData[i]));
395 }
396
397 /**
398 * An embedded search API handle.
399 * @type {Object}
400 * @private
401 */
402 this.apiHandle_ = getApiObjectHandle();
403
404 /**
405 * The container for this suggestions box. div.pending-container if inactive
406 * and div.active-container if active.
407 * @type {Element}
408 * @private
409 */
410 this.container_ = $qs('.pending-container');
411 assert(this.container_);
412 }
413
414 SuggestionsBox.prototype = {
415 /**
416 * Releases suggestion iframes and ignores any load done message for the
417 * current suggestions.
418 */
419 destroy: function() {
420 while (this.suggestions_.length > 0) {
421 this.suggestions_.pop().destroy();
422 }
423 this.responseId = -1;
424 },
425
426 /**
427 * Starts rendering new suggestions.
428 */
429 loadSuggestions: function() {
430 // Create a placeholder DOM in the invisible container.
431 this.container_.innerHTML = '';
432
433 var box = document.createElement('div');
434 box.className = 'suggestions-box';
435 this.container_.appendChild(box);
436
437 var iframesToLoad = {};
438 for (var i = 0; i < this.suggestions_.length; ++i) {
439 var suggestion = this.suggestions_[i];
440 suggestion.appendToBox(box, i == this.selectedIndex_);
441 var iframe = iframePool.reserve();
442 suggestion.iframe = iframe;
443 iframesToLoad[iframe.id] = suggestion.restrictedId;
444 }
445
446 // Ask the loader iframe to populate the iframes just reserved.
447 var loadRequest = {
448 load: iframesToLoad,
449 requestId: this.requestId_,
450 style: {
451 queryColor: QUERY_COLOR,
452 urlColor: URL_COLOR,
453 titleColor: TITLE_COLOR
454 }
455 };
456 $qs('#suggestion-loader').contentWindow.postMessage(loadRequest,
457 SUGGESTION_ORIGIN);
458 },
459
460 /**
461 * @param {number} responseId The id of a request that just finished
462 * rendering.
463 * @return {boolean} Whether the request is for the suggestions in this box.
464 */
465 isResponseCurrent: function(responseId) {
466 return responseId == this.requestId_;
467 },
468
469 /**
470 * Moves suggestion iframes into position.
471 */
472 repositionSuggestions: function() {
473 // Note: This may be called before margins are ready. In that case,
474 // suggestion iframes will initially be too large and then size down
475 // onresize.
476 var startMargin = this.apiHandle_.startMargin;
477 var totalMargin = window.innerWidth - this.apiHandle_.width;
478 var isRtl = this.apiHandle_.isRtl;
479 for (var i = 0; i < this.suggestions_.length; ++i) {
480 this.suggestions_[i].reposition(isRtl, startMargin, totalMargin);
481 }
482 },
483
484 /**
485 * Selects the suggestion before the current selection.
486 */
487 selectPrevious: function() {
488 this.changeSelection_(this.selectedIndex_ - 1);
489 },
490
491 /**
492 * Selects the suggestion after the current selection.
493 */
494 selectNext: function() {
495 this.changeSelection_(this.selectedIndex_ + 1);
496 },
497
498 /**
499 * Changes the current selected suggestion index.
500 * @param {number} index The new selection to suggest.
501 * @private
502 */
503 changeSelection_: function(index) {
504 var numSuggestions = this.suggestions_.length;
505 this.selectedIndex_ = Math.min(numSuggestions - 1, Math.max(-1, index));
506
507 this.redrawSelection_();
508 this.redrawHover_();
509 },
510
511 /**
512 * Redraws the selected suggestion.
513 * @private
514 */
515 redrawSelection_: function() {
516 var oldSelection = this.container_.querySelector('.selected');
517 if (oldSelection)
518 oldSelection.classList.remove('selected');
519 if (this.selectedIndex_ == -1) {
520 this.apiHandle_.setValue(this.inputValue_);
521 } else {
522 this.suggestions_[this.selectedIndex_].select(true);
523 this.apiHandle_.setRestrictedValue(
524 this.suggestions_[this.selectedIndex_].restrcitedId);
palmer 2013/04/22 19:38:37 typo: restrictedId
Jered 2013/04/22 23:02:58 Done. Good catch!
525 }
526 },
527
528 /**
529 * Returns the restricted id of the iframe clicked.
palmer 2013/04/22 19:38:37 This is redundant; you have a @return comment.
Jered 2013/04/22 23:02:58 Done.
530 * @param {!Window} iframeWindow The window of the iframe that was clicked.
531 * @return {?number} The restricted id clicked or null if none.
palmer 2013/04/22 19:38:37 "...ID of the iframe that was clicked, or null if
Jered 2013/04/22 23:02:58 Done.
532 */
533 getClickTarget: function(iframeWindow) {
534 for (var i = 0; i < this.suggestions_.length; ++i) {
535 if (this.suggestions_[i].hasIframeWindow(iframeWindow))
536 return this.suggestions_[i].restrictedId;
537 }
538 return null;
539 },
540
541 /**
542 * Called when the user hovers on the specified iframe.
palmer 2013/04/22 19:38:37 ... for the purpose of... Updating hoveredIndex_,
Jered 2013/04/22 23:02:58 Done.
543 * @param {string} iframeId The id of the iframe hovered.
544 */
545 hover: function(iframeId) {
546 this.hoveredIndex_ = -1;
547 for (var i = 0; i < this.suggestions_.length; ++i) {
548 if (this.suggestions_[i].hasIframeId(iframeId)) {
549 this.hoveredIndex_ = i;
550 break;
551 }
552 }
553 this.redrawHover_();
554 },
555
556 /**
557 * Called when the user unhovers the specified iframe.
palmer 2013/04/22 19:38:37 Same thing: say why. It seems like this function c
Jered 2013/04/22 23:02:58 I'd prefer to keep the check. This method is calle
558 * @param {string} iframeId The id of the iframe hovered.
559 */
560 unhover: function(iframeId) {
561 if (this.suggestions_[this.hoveredIndex_] &&
562 this.suggestions_[this.hoveredIndex_].hasIframeId(iframeId)) {
563 this.clearHover();
564 }
565 },
566
567 /**
568 * Clears the current hover.
569 */
570 clearHover: function() {
571 this.hoveredIndex_ = -1;
572 this.redrawHover_();
573 },
574
575 /**
576 * Redraws the mouse hover background.
577 * @private
578 */
579 redrawHover_: function() {
580 var oldHover = this.container_.querySelector('.hovered');
581 if (oldHover)
582 oldHover.classList.remove('hovered');
583 if (this.hoveredIndex_ != -1 && this.hoveredIndex_ != this.selectedIndex_)
584 this.suggestions_[this.hoveredIndex_].hover(true);
585 },
586
587 /**
588 * Marks the suggestions container as active.
589 */
590 activate: function() {
591 this.container_.className = 'active-container';
592 },
593
594 /**
595 * Marks the suggestions container as inactive.
596 */
597 deactivate: function() {
598 this.container_.className = 'pending-container';
599 this.container_.innerHTML = '';
600 },
601
602 /**
603 * The height of the suggestions container.
604 * @type {number}
605 */
606 get height() {
607 return this.container_.offsetHeight;
608 },
609 };
610
611
612 /**
613 * Clears the currently active suggestions and shows pending suggestions.
614 */
615 function makePendingSuggestionsActive() {
616 if (activeBox) {
617 activeBox.deactivate();
618 activeBox.destroy();
198 } else { 619 } else {
199 var newSelection = $('suggestionsBox').querySelector( 620 // Initially there will be no active suggestions, but we still want to use
200 '.suggestion:nth-of-type(' + (selectedIndex + 1) + ')'); 621 // div.active-container to load the next suggestions.
201 newSelection.classList.add('selected'); 622 $qs('.active-container').className = 'pending-container';
202 apiHandle.setRestrictedValue(restrictedIds[selectedIndex]);
203 } 623 }
204 } 624 pendingBox.activate();
205 625 activeBox = pendingBox;
206 // ============================================================================= 626 pendingBox = null;
207 // Handlers / API stuff 627 activeBox.repositionSuggestions();
208 // ============================================================================= 628 getApiObjectHandle().showOverlay(activeBox.height);
629 }
630
631 /**
632 * Hides the active suggestions box.
633 */
634 function hideActiveSuggestions() {
635 getApiObjectHandle().showOverlay(0);
636 if (activeBox) {
637 $qs('.active-container').innerHTML = '';
638 activeBox.destroy();
639 }
640 activeBox = null;
641 }
209 642
210 /** 643 /**
211 * @return {Object} the handle to the searchBox API. 644 * @return {Object} the handle to the searchBox API.
212 */ 645 */
213 function getApiObjectHandle() { 646 function getApiObjectHandle() {
214 if (window.cideb) 647 if (window.cideb)
215 return window.cideb; 648 return window.cideb;
216 if (window.navigator && window.navigator.embeddedSearch && 649 if (window.navigator && window.navigator.embeddedSearch &&
217 window.navigator.embeddedSearch.searchBox) 650 window.navigator.embeddedSearch.searchBox)
218 return window.navigator.embeddedSearch.searchBox; 651 return window.navigator.embeddedSearch.searchBox;
219 if (window.chrome && window.chrome.embeddedSearch && 652 if (window.chrome && window.chrome.embeddedSearch &&
220 window.chrome.embeddedSearch.searchBox) 653 window.chrome.embeddedSearch.searchBox)
221 return window.chrome.embeddedSearch.searchBox; 654 return window.chrome.embeddedSearch.searchBox;
222 return null; 655 return null;
223 } 656 }
224 657
225 /** 658 /**
226 * Updates suggestions in response to a onchange or onnativesuggestions call. 659 * Updates suggestions in response to a onchange or onnativesuggestions call.
227 */ 660 */
228 function updateSuggestions() { 661 function updateSuggestions() {
662 if (pendingBox)
663 pendingBox.destroy();
664 pendingBox = null;
229 var apiHandle = getApiObjectHandle(); 665 var apiHandle = getApiObjectHandle();
230 lastInputValue = apiHandle.value; 666 var suggestions = apiHandle.nativeSuggestions;
231 667 if (suggestions.length) {
232 clearSuggestions(); 668 suggestions.sort(function(a, b) {
233 var nativeSuggestions = apiHandle.nativeSuggestions;
234 if (nativeSuggestions.length) {
235 nativeSuggestions.sort(function(a, b) {
236 return b.rankingData.relevance - a.rankingData.relevance; 669 return b.rankingData.relevance - a.rankingData.relevance;
237 }); 670 });
238 if (shouldSelectSuggestion(nativeSuggestions[0], apiHandle.verbatim)) 671 var selectedIndex = -1;
672 if (shouldSelectSuggestion(suggestions[0], apiHandle.verbatim))
239 selectedIndex = 0; 673 selectedIndex = 0;
240 renderSuggestions(nativeSuggestions); 674 // Don't display a search-what-you-typed suggestion if it's the top match.
675 if (suggestions[0].type == VERBATIM_SEARCH_TYPE)
676 suggestions.shift();
241 } 677 }
242 678 var inputValue = apiHandle.value;
243 var height = getDropdownHeight(); 679 if (!!inputValue && suggestions.length) {
palmer 2013/04/22 19:38:37 Do you get the same result without the !! ?
Jered 2013/04/22 23:02:58 You get the same truthiness, but not the same resu
244 apiHandle.showOverlay(height); 680 pendingBox = new SuggestionsBox(inputValue,
681 suggestions.slice(0, MAX_SUGGESTIONS_TO_SHOW), selectedIndex);
682 pendingBox.loadSuggestions();
683 } else {
684 hideActiveSuggestions();
685 }
245 } 686 }
246 687
247 /** 688 /**
248 * Appends a style node for suggestion properties that depend on apiHandle. 689 * Appends a style node for suggestion properties that depend on apiHandle.
249 */ 690 */
250 function appendSuggestionStyles() { 691 function appendSuggestionStyles() {
251 var apiHandle = getApiObjectHandle(); 692 var apiHandle = getApiObjectHandle();
252 var isRtl = apiHandle.rtl; 693 var isRtl = apiHandle.rtl;
253 var startMargin = apiHandle.startMargin; 694 var startMargin = apiHandle.startMargin;
254 var style = document.createElement('style'); 695 var style = document.createElement('style');
255 style.type = 'text/css'; 696 style.type = 'text/css';
256 style.id = 'suggestionStyle'; 697 style.id = 'suggestionStyle';
257 style.textContent = 698 style.textContent =
258 '.suggestion, ' + 699 '.suggestion, ' +
259 '.suggestion.search {' + 700 '.suggestion.search {' +
260 ' background-position: ' + 701 ' background-position: ' +
261 (isRtl ? '-webkit-calc(100% - 5px)' : '5px') + ' 4px;' + 702 (isRtl ? '-webkit-calc(100% - 5px)' : '5px') + ' 4px;' +
262 ' -webkit-margin-start: ' + startMargin + 'px;' + 703 ' -webkit-margin-start: ' + startMargin + 'px;' +
263 ' -webkit-margin-end: ' + 704 ' -webkit-margin-end: ' +
264 (window.innerWidth - apiHandle.width - startMargin) + 'px;' + 705 (window.innerWidth - apiHandle.width - startMargin) + 'px;' +
265 ' font: ' + apiHandle.fontSize + 'px "' + apiHandle.font + '";' + 706 ' font: ' + apiHandle.fontSize + 'px "' + apiHandle.font + '";' +
266 '}'; 707 '}';
267 document.querySelector('head').appendChild(style); 708 $qs('head').appendChild(style);
709 if (activeBox)
710 activeBox.repositionSuggestions();
268 window.removeEventListener('resize', appendSuggestionStyles); 711 window.removeEventListener('resize', appendSuggestionStyles);
269 } 712 }
270 713
271 /** 714 /**
272 * Extract the desired navigation behavior from a click button. 715 * Makes keys navigate through suggestions.
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. 716 * @param {Object} e The key being pressed.
299 */ 717 */
300 function handleKeyPress(e) { 718 function handleKeyPress(e) {
719 if (!activeBox)
720 return;
721
301 switch (e.keyCode) { 722 switch (e.keyCode) {
302 case 38: // Up arrow 723 case KEY_UP_ARROW:
303 updateSelectedSuggestion(false); 724 activeBox.selectPrevious();
304 break; 725 break;
305 case 40: // Down arrow 726 case KEY_DOWN_ARROW:
306 updateSelectedSuggestion(true); 727 activeBox.selectNext();
307 break; 728 break;
308 } 729 }
309 } 730 }
310 731
311 /** 732 /**
312 * Handles the postMessage calls from the result iframes. 733 * Handles postMessage calls from suggestion iframes.
313 * @param {Object} message The message containing details of clicks the iframes. 734 * @param {Object} message A notification that all iframes are done loading or
735 * that an iframe was clicked.
314 */ 736 */
315 function handleMessage(message) { 737 function handleMessage(message) {
316 if (message.origin != 'null' || !message.data || 738 if (message.origin != SUGGESTION_ORIGIN)
317 message.data.eventType != 'click') {
318 return; 739 return;
319 }
320 740
321 var iframes = document.getElementsByClassName('contents'); 741 if ('loaded' in message.data) {
322 for (var i = 0; i < iframes.length; ++i) { 742 if (pendingBox && pendingBox.isResponseCurrent(message.data.loaded))
323 if (iframes[i].contentWindow == message.source) { 743 makePendingSuggestionsActive();
324 handleSuggestionClick(parseInt(iframes[i].id, 10), 744 } else if ('click' in message.data) {
325 message.data.button); 745 if (activeBox) {
326 break; 746 var restrictedId = activeBox.getClickTarget(message.source);
747 if (restrictedId != null) {
748 hideActiveSuggestions();
749 getApiObjectHandle().navigateContentWindow(restrictedId,
750 getDispositionFromClickButton(message.data.click));
751 }
327 } 752 }
328 } 753 }
329 } 754 }
330 755
331 /** 756 /**
332 * chrome.searchBox.embeddedSearch.onsubmit implementation. 757 * Sets up the embedded search API and creates suggestion iframes.
333 */ 758 */
334 function onSubmit() { 759 function init() {
335 } 760 iframePool = new IframePool();
336 761 iframePool.init();
337 /**
338 * Sets up the searchBox API.
339 */
340 function setUpApi() {
341 var apiHandle = getApiObjectHandle(); 762 var apiHandle = getApiObjectHandle();
342 apiHandle.onnativesuggestions = updateSuggestions; 763 apiHandle.onnativesuggestions = updateSuggestions;
343 apiHandle.onchange = updateSuggestions; 764 apiHandle.onchange = updateSuggestions;
344 apiHandle.onkeypress = handleKeyPress; 765 apiHandle.onkeypress = handleKeyPress;
345 apiHandle.onsubmit = onSubmit; 766 // Instant checks for this handler to be bound.
346 $('suggestions-box-container').dir = apiHandle.rtl ? 'rtl' : 'ltr'; 767 apiHandle.onsubmit = function() {};
768 $qs('.active-container').dir = apiHandle.rtl ? 'rtl' : 'ltr';
769 $qs('.pending-container').dir = apiHandle.rtl ? 'rtl' : 'ltr';
347 // Delay adding these styles until the window width is available. 770 // Delay adding these styles until the window width is available.
348 window.addEventListener('resize', appendSuggestionStyles); 771 window.addEventListener('resize', appendSuggestionStyles);
349 if (apiHandle.nativeSuggestions.length) 772 if (apiHandle.nativeSuggestions.length)
350 handleNativeSuggestions(); 773 updateSuggestions();
351 } 774 }
352 775
353 document.addEventListener('DOMContentLoaded', setUpApi); 776 document.addEventListener('DOMContentLoaded', init);
354 window.addEventListener('message', handleMessage, false); 777 window.addEventListener('message', handleMessage, false);
778 window.addEventListener('blur', function() {
779 if (activeBox)
780 activeBox.clearHover();
781 }, false);
782 })();
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698