OLD | NEW |
---|---|
1 // Copyright 2013 The Chromium Authors. All rights reserved. | 1 // Copyright 2013 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 (function() { | 5 (function() { |
6 | |
7 /** | |
8 * True if this a Google page and not some other search provider. Used to | |
9 * determine whether to show the logo and fakebox. | |
10 * @type {boolean} | |
11 */ | |
12 var isGooglePage; | |
13 | |
14 // ========================================================== | |
15 // Enums | |
16 // ========================================================== | |
17 | |
18 /** | |
19 * Enum for classnames. | |
20 * @enum {string} | |
21 * @const | |
22 */ | |
23 var CLASSES = { | |
24 BLACKLIST: 'mv-blacklist', // triggers tile blacklist animation | |
25 BLACKLIST_BUTTON: 'mv-x', | |
26 DELAYED_HIDE_NOTIFICATION: 'mv-notice-delayed-hide', | |
27 DOMAIN: 'mv-domain', | |
28 FAKEBOX_ANIMATE: 'fakebox-animate', // triggers fakebox animation | |
29 FAKEBOX_FOCUS: 'fakebox-focused', // Applies focus styles to the fakebox | |
30 FAVICON: 'mv-favicon', | |
31 FILLER: 'mv-filler', // filler tiles | |
32 GOOGLE_PAGE: 'google-page', // shows the Google logo and fakebox | |
33 HIDE_BLACKLIST_BUTTON: 'mv-x-hide', // hides blacklist button during animation | |
34 HIDE_NOTIFICATION: 'mv-notice-hide', | |
35 HIDE_TILE: 'mv-tile-hide', // hides tiles on small browser width | |
36 PAGE: 'mv-page', // page tiles | |
37 THUMBNAIL: 'mv-thumb', | |
38 TILE: 'mv-tile', | |
39 TITLE: 'mv-title' | |
40 }; | |
41 | |
42 /** | |
43 * Enum for HTML element ids. | |
44 * @enum {string} | |
45 * @const | |
46 */ | |
47 var IDS = { | |
48 ATTRIBUTION: 'attribution', | |
49 CURSOR: 'cursor', | |
50 FAKEBOX: 'fakebox', | |
51 LOGO: 'logo', | |
52 NOTIFICATION: 'mv-notice', | |
53 NOTIFICATION_CLOSE_BUTTON: 'mv-notice-x', | |
54 NOTIFICATION_MESSAGE: 'mv-msg', | |
55 NTP_CONTENTS: 'ntp-contents', | |
56 RESTORE_ALL_LINK: 'mv-restore', | |
57 SUGGESTIONS_BOX: 'suggestions-box', | |
58 SUGGESTIONS_CONTAINER: 'suggestions-box-container', | |
59 TILES: 'mv-tiles', | |
60 TOP_MARGIN: 'mv-top-margin', | |
61 UNDO_LINK: 'mv-undo' | |
62 }; | |
63 | |
64 // ============================================================================= | |
65 // NTP implementation | |
66 // ============================================================================= | |
Dan Beam
2013/04/16 03:01:44
why isn't this defined as a LocalNtp class or some
jeremycho
2013/04/16 16:18:33
This code is only used here and initialized immedi
| |
67 | |
6 /** | 68 /** |
7 * The element used to vertically position the most visited section on | 69 * The element used to vertically position the most visited section on |
8 * window resize. | 70 * window resize. |
9 * @type {Element} | 71 * @type {Element} |
10 */ | 72 */ |
11 var topMarginElement; | 73 var topMarginElement; |
12 | 74 |
13 /** | 75 /** |
14 * The container for the tile elements. | 76 * The container for the tile elements. |
15 * @type {Element} | 77 * @type {Element} |
16 */ | 78 */ |
17 var tilesContainer; | 79 var tilesContainer; |
18 | 80 |
19 /** | 81 /** |
20 * The notification displayed when a page is blacklisted. | 82 * The notification displayed when a page is blacklisted. |
21 * @type {Element} | 83 * @type {Element} |
22 */ | 84 */ |
23 var notification; | 85 var notification; |
24 | 86 |
25 /** | 87 /** |
26 * The container for the theme attribution. | 88 * The container for the theme attribution. |
27 * @type {Element} | 89 * @type {Element} |
28 */ | 90 */ |
29 var attribution; | 91 var attribution; |
30 | 92 |
31 /** | 93 /** |
94 * The "fakebox" - an input field that looks like a regular searchbox. When it | |
95 * is focused, any text the user types goes directly into the omnibox. | |
96 * @type {Element} | |
97 */ | |
98 var fakebox; | |
99 | |
100 /** | |
101 * The container for NTP elements that should be hidden when suggestions are | |
102 * visible. | |
103 * @type {Element} | |
104 */ | |
105 var ntpContents; | |
106 | |
107 /** | |
32 * The array of rendered tiles, ordered by appearance. | 108 * The array of rendered tiles, ordered by appearance. |
33 * @type {Array.<Tile>} | 109 * @type {Array.<Tile>} |
34 */ | 110 */ |
35 var tiles = []; | 111 var tiles = []; |
36 | 112 |
37 /** | 113 /** |
38 * The last blacklisted tile if any, which by definition should not be filler. | 114 * The last blacklisted tile if any, which by definition should not be filler. |
39 * @type {?Tile} | 115 * @type {?Tile} |
40 */ | 116 */ |
41 var lastBlacklistedTile = null; | 117 var lastBlacklistedTile = null; |
(...skipping 24 matching lines...) Expand all Loading... | |
66 /** | 142 /** |
67 * Current number of tiles shown based on the window width, including filler. | 143 * Current number of tiles shown based on the window width, including filler. |
68 * @type {number} | 144 * @type {number} |
69 */ | 145 */ |
70 var numTilesShown = 0; | 146 var numTilesShown = 0; |
71 | 147 |
72 /** | 148 /** |
73 * The browser embeddedSearch.newTabPage object. | 149 * The browser embeddedSearch.newTabPage object. |
74 * @type {Object} | 150 * @type {Object} |
75 */ | 151 */ |
76 var apiHandle; | 152 var ntpApiHandle; |
77 | 153 |
78 /** | 154 /** |
79 * Possible background-colors of a non-custom theme. Used to determine whether | 155 * Possible background-colors of a non-custom theme. Used to determine whether |
80 * the homepage should be updated to support custom or non-custom themes. | 156 * the homepage should be updated to support custom or non-custom themes. |
81 * @type {!Array.<string>} | 157 * @type {!Array.<string>} |
82 * @const | 158 * @const |
83 */ | 159 */ |
84 var WHITE = ['rgba(255,255,255,1)', 'rgba(0,0,0,0)']; | 160 var WHITE = ['rgba(255,255,255,1)', 'rgba(0,0,0,0)']; |
85 | 161 |
86 /** | 162 /** |
(...skipping 18 matching lines...) Expand all Loading... | |
105 | 181 |
106 /** | 182 /** |
107 * Minimum total padding to give to the left and right of the most visited | 183 * Minimum total padding to give to the left and right of the most visited |
108 * section. Used to determine how many tiles to show. | 184 * section. Used to determine how many tiles to show. |
109 * @type {number} | 185 * @type {number} |
110 * @const | 186 * @const |
111 */ | 187 */ |
112 var MIN_TOTAL_HORIZONTAL_PADDING = 188; | 188 var MIN_TOTAL_HORIZONTAL_PADDING = 188; |
113 | 189 |
114 /** | 190 /** |
115 * Enum for classnames. | |
116 * @enum {string} | |
117 * @const | |
118 */ | |
119 var CLASSES = { | |
120 BLACKLIST: 'mv-blacklist', // triggers tile blacklist animation | |
121 BLACKLIST_BUTTON: 'mv-x', | |
122 DELAYED_HIDE_NOTIFICATION: 'mv-notice-delayed-hide', | |
123 DOMAIN: 'mv-domain', | |
124 FAVICON: 'mv-favicon', | |
125 FILLER: 'mv-filler', // filler tiles | |
126 HIDE_BLACKLIST_BUTTON: 'mv-x-hide', // hides blacklist button during animation | |
127 HIDE_NOTIFICATION: 'mv-notice-hide', | |
128 HIDE_TILE: 'mv-tile-hide', // hides tiles on small browser width | |
129 PAGE: 'mv-page', // page tiles | |
130 THUMBNAIL: 'mv-thumb', | |
131 TILE: 'mv-tile', | |
132 TITLE: 'mv-title' | |
133 }; | |
134 | |
135 /** | |
136 * Enum for HTML element ids. | |
137 * @enum {string} | |
138 * @const | |
139 */ | |
140 var IDS = { | |
141 ATTRIBUTION: 'attribution', | |
142 NOTIFICATION: 'mv-notice', | |
143 NOTIFICATION_CLOSE_BUTTON: 'mv-notice-x', | |
144 NOTIFICATION_MESSAGE: 'mv-msg', | |
145 RESTORE_ALL_LINK: 'mv-restore', | |
146 TILES: 'mv-tiles', | |
147 TOP_MARGIN: 'mv-top-margin', | |
148 UNDO_LINK: 'mv-undo' | |
149 }; | |
150 | |
151 /** | |
152 * A Tile is either a rendering of a Most Visited page or "filler" used to | 191 * A Tile is either a rendering of a Most Visited page or "filler" used to |
153 * pad out the section when not enough pages exist. | 192 * pad out the section when not enough pages exist. |
154 * | 193 * |
155 * @param {Element} elem The element for rendering the tile. | 194 * @param {Element} elem The element for rendering the tile. |
156 * @param {number=} opt_rid The RID for the corresponding Most Visited page. | 195 * @param {number=} opt_rid The RID for the corresponding Most Visited page. |
157 * Should only be left unspecified when creating a filler tile. | 196 * Should only be left unspecified when creating a filler tile. |
158 * @constructor | 197 * @constructor |
159 */ | 198 */ |
160 function Tile(elem, opt_rid) { | 199 function Tile(elem, opt_rid) { |
161 /** @type {Element} */ | 200 /** @type {Element} */ |
162 this.elem = elem; | 201 this.elem = elem; |
163 | 202 |
164 /** @type {number|undefined} */ | 203 /** @type {number|undefined} */ |
165 this.rid = opt_rid; | 204 this.rid = opt_rid; |
166 } | 205 } |
167 | 206 |
168 /** | 207 /** |
169 * Updates the NTP based on the current theme. | 208 * Updates the NTP based on the current theme. |
170 * @private | 209 * @private |
171 */ | 210 */ |
172 function onThemeChange() { | 211 function onThemeChange() { |
173 var info = apiHandle.themeBackgroundInfo; | 212 if (!isNtpVisible()) |
213 return; | |
214 | |
215 var info = ntpApiHandle.themeBackgroundInfo; | |
174 if (!info) | 216 if (!info) |
175 return; | 217 return; |
176 var background = [info.colorRgba, | 218 var background = [info.colorRgba, |
177 info.imageUrl, | 219 info.imageUrl, |
178 info.imageTiling, | 220 info.imageTiling, |
179 info.imageHorizontalAlignment, | 221 info.imageHorizontalAlignment, |
180 info.imageVerticalAlignment].join(' ').trim(); | 222 info.imageVerticalAlignment].join(' ').trim(); |
181 document.body.style.background = background; | 223 document.body.style.background = background; |
182 var isCustom = !!background && WHITE.indexOf(background) == -1; | 224 var isCustom = !!background && WHITE.indexOf(background) == -1; |
183 document.body.classList.toggle('custom-theme', isCustom); | 225 document.body.classList.toggle('custom-theme', isCustom); |
(...skipping 22 matching lines...) Expand all Loading... | |
206 attributionImage.onerror = function() { | 248 attributionImage.onerror = function() { |
207 attribution.hidden = true; | 249 attribution.hidden = true; |
208 }; | 250 }; |
209 attributionImage.src = url; | 251 attributionImage.src = url; |
210 } | 252 } |
211 | 253 |
212 /** | 254 /** |
213 * Handles a new set of Most Visited page data. | 255 * Handles a new set of Most Visited page data. |
214 */ | 256 */ |
215 function onMostVisitedChange() { | 257 function onMostVisitedChange() { |
216 var pages = apiHandle.mostVisited; | 258 var pages = ntpApiHandle.mostVisited; |
217 | 259 |
218 if (isBlacklisting) { | 260 if (isBlacklisting) { |
219 // If this was called as a result of a blacklist, add a new replacement | 261 // If this was called as a result of a blacklist, add a new replacement |
220 // (possibly filler) tile at the end and trigger the blacklist animation. | 262 // (possibly filler) tile at the end and trigger the blacklist animation. |
221 var replacementTile = createTile(pages[MAX_NUM_TILES_TO_SHOW - 1]); | 263 var replacementTile = createTile(pages[MAX_NUM_TILES_TO_SHOW - 1]); |
222 | 264 |
223 tiles.push(replacementTile); | 265 tiles.push(replacementTile); |
224 tilesContainer.appendChild(replacementTile.elem); | 266 tilesContainer.appendChild(replacementTile.elem); |
225 | 267 |
226 var lastBlacklistedTileElement = lastBlacklistedTile.elem; | 268 var lastBlacklistedTileElement = lastBlacklistedTile.elem; |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
274 function createTile(page) { | 316 function createTile(page) { |
275 var tileElement = document.createElement('div'); | 317 var tileElement = document.createElement('div'); |
276 tileElement.classList.add(CLASSES.TILE); | 318 tileElement.classList.add(CLASSES.TILE); |
277 | 319 |
278 if (page) { | 320 if (page) { |
279 var rid = page.rid; | 321 var rid = page.rid; |
280 tileElement.classList.add(CLASSES.PAGE); | 322 tileElement.classList.add(CLASSES.PAGE); |
281 | 323 |
282 // The click handler for navigating to the page identified by the RID. | 324 // The click handler for navigating to the page identified by the RID. |
283 tileElement.addEventListener('click', function() { | 325 tileElement.addEventListener('click', function() { |
284 apiHandle.navigateContentWindow(rid); | 326 ntpApiHandle.navigateContentWindow(rid); |
285 }); | 327 }); |
286 | 328 |
287 // The shadow DOM which renders the page title. | 329 // The shadow DOM which renders the page title. |
288 var titleElement = page.titleElement; | 330 var titleElement = page.titleElement; |
289 if (titleElement) { | 331 if (titleElement) { |
290 titleElement.classList.add(CLASSES.TITLE); | 332 titleElement.classList.add(CLASSES.TITLE); |
291 tileElement.appendChild(titleElement); | 333 tileElement.appendChild(titleElement); |
292 } | 334 } |
293 | 335 |
294 // Render the thumbnail if present. Otherwise, fall back to a shadow DOM | 336 // Render the thumbnail if present. Otherwise, fall back to a shadow DOM |
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
348 function generateBlacklistFunction(rid) { | 390 function generateBlacklistFunction(rid) { |
349 return function(e) { | 391 return function(e) { |
350 // Prevent navigation when the page is being blacklisted. | 392 // Prevent navigation when the page is being blacklisted. |
351 e.stopPropagation(); | 393 e.stopPropagation(); |
352 | 394 |
353 showNotification(); | 395 showNotification(); |
354 isBlacklisting = true; | 396 isBlacklisting = true; |
355 tilesContainer.classList.add(CLASSES.HIDE_BLACKLIST_BUTTON); | 397 tilesContainer.classList.add(CLASSES.HIDE_BLACKLIST_BUTTON); |
356 lastBlacklistedTile = getTileByRid(rid); | 398 lastBlacklistedTile = getTileByRid(rid); |
357 lastBlacklistedIndex = tiles.indexOf(lastBlacklistedTile); | 399 lastBlacklistedIndex = tiles.indexOf(lastBlacklistedTile); |
358 apiHandle.deleteMostVisitedItem(rid); | 400 ntpApiHandle.deleteMostVisitedItem(rid); |
359 }; | 401 }; |
360 } | 402 } |
361 | 403 |
362 /** | 404 /** |
363 * Shows the blacklist notification and triggers a delay to hide it. | 405 * Shows the blacklist notification and triggers a delay to hide it. |
364 */ | 406 */ |
365 function showNotification() { | 407 function showNotification() { |
366 notification.classList.remove(CLASSES.HIDE_NOTIFICATION); | 408 notification.classList.remove(CLASSES.HIDE_NOTIFICATION); |
367 notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION); | 409 notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION); |
368 notification.scrollTop; | 410 notification.scrollTop; |
(...skipping 22 matching lines...) Expand all Loading... | |
391 | 433 |
392 /** | 434 /** |
393 * Handles a click on the notification undo link by hiding the notification and | 435 * Handles a click on the notification undo link by hiding the notification and |
394 * informing Chrome. | 436 * informing Chrome. |
395 */ | 437 */ |
396 function onUndo() { | 438 function onUndo() { |
397 hideNotification(); | 439 hideNotification(); |
398 var lastBlacklistedRID = lastBlacklistedTile.rid; | 440 var lastBlacklistedRID = lastBlacklistedTile.rid; |
399 if (typeof lastBlacklistedRID != 'undefined') { | 441 if (typeof lastBlacklistedRID != 'undefined') { |
400 isUndoing = true; | 442 isUndoing = true; |
401 apiHandle.undoMostVisitedDeletion(lastBlacklistedRID); | 443 ntpApiHandle.undoMostVisitedDeletion(lastBlacklistedRID); |
402 } | 444 } |
403 } | 445 } |
404 | 446 |
405 /** | 447 /** |
406 * Handles the end of the undo animation by removing the extraneous end tile. | 448 * Handles the end of the undo animation by removing the extraneous end tile. |
407 */ | 449 */ |
408 function undoAnimationDone() { | 450 function undoAnimationDone() { |
409 isUndoing = false; | 451 isUndoing = false; |
410 tiles.splice(tiles.length - 1, 1); | 452 tiles.splice(tiles.length - 1, 1); |
411 removeNode(tilesContainer.lastElementChild); | 453 removeNode(tilesContainer.lastElementChild); |
412 updateTileVisibility(numTilesShown); | 454 updateTileVisibility(numTilesShown); |
413 lastBlacklistedTile.elem.removeEventListener( | 455 lastBlacklistedTile.elem.removeEventListener( |
414 'webkitTransitionEnd', undoAnimationDone); | 456 'webkitTransitionEnd', undoAnimationDone); |
415 } | 457 } |
416 | 458 |
417 /** | 459 /** |
418 * Handles a click on the restore all notification link by hiding the | 460 * Handles a click on the restore all notification link by hiding the |
419 * notification and informing Chrome. | 461 * notification and informing Chrome. |
420 */ | 462 */ |
421 function onRestoreAll() { | 463 function onRestoreAll() { |
422 hideNotification(); | 464 hideNotification(); |
423 apiHandle.undoAllMostVisitedDeletions(); | 465 ntpApiHandle.undoAllMostVisitedDeletions(); |
424 } | 466 } |
425 | 467 |
426 /** | 468 /** |
427 * Handles a resize by vertically centering the most visited section | 469 * Handles a resize by vertically centering the most visited section |
428 * and triggering the tile show/hide animation if necessary. | 470 * and triggering the tile show/hide animation if necessary. |
429 */ | 471 */ |
430 function onResize() { | 472 function onResize() { |
431 var clientHeight = document.documentElement.clientHeight; | 473 // The Google page uses a fixed layout instead. |
432 topMarginElement.style.marginTop = | 474 if (!isGooglePage) { |
433 Math.max(0, (clientHeight - MOST_VISITED_HEIGHT) / 2) + 'px'; | 475 var clientHeight = document.documentElement.clientHeight; |
434 | 476 topMarginElement.style.marginTop = |
477 Math.max(0, (clientHeight - MOST_VISITED_HEIGHT) / 2) + 'px'; | |
478 } | |
435 var clientWidth = document.documentElement.clientWidth; | 479 var clientWidth = document.documentElement.clientWidth; |
436 var numTilesToShow = Math.floor( | 480 var numTilesToShow = Math.floor( |
437 (clientWidth - MIN_TOTAL_HORIZONTAL_PADDING) / TILE_WIDTH); | 481 (clientWidth - MIN_TOTAL_HORIZONTAL_PADDING) / TILE_WIDTH); |
438 numTilesToShow = Math.max(MIN_NUM_TILES_TO_SHOW, numTilesToShow); | 482 numTilesToShow = Math.max(MIN_NUM_TILES_TO_SHOW, numTilesToShow); |
439 if (numTilesToShow != numTilesShown) { | 483 if (numTilesToShow != numTilesShown) { |
440 updateTileVisibility(numTilesToShow); | 484 updateTileVisibility(numTilesToShow); |
441 numTilesShown = numTilesToShow; | 485 numTilesShown = numTilesToShow; |
442 } | 486 } |
443 } | 487 } |
444 | 488 |
(...skipping 16 matching lines...) Expand all Loading... | |
461 function getTileByRid(rid) { | 505 function getTileByRid(rid) { |
462 for (var i = 0, length = tiles.length; i < length; ++i) { | 506 for (var i = 0, length = tiles.length; i < length; ++i) { |
463 var tile = tiles[i]; | 507 var tile = tiles[i]; |
464 if (tile.rid == rid) | 508 if (tile.rid == rid) |
465 return tile; | 509 return tile; |
466 } | 510 } |
467 return null; | 511 return null; |
468 } | 512 } |
469 | 513 |
470 /** | 514 /** |
515 * @param {boolean} visible True to show the NTP. | |
516 */ | |
517 function updateNtpVisibility(visible) { | |
518 if (visible && !isNtpVisible()) { | |
519 if (fakebox) { | |
520 // Stop any fakebox animation animation in progress and restore the NTP. | |
521 fakebox.removeEventListener('webkitTransitionEnd', fakeboxAnimationDone); | |
522 document.body.classList.remove(CLASSES.FAKEBOX_ANIMATE); | |
523 } | |
524 ntpContents.hidden = false; | |
525 onThemeChange(); | |
526 } else if (!visible && isNtpVisible()) { | |
527 if (fakebox && isFakeboxFocused()) { | |
528 // The user has typed in the fakebox - initiate the fakebox animation, | |
529 // which upon termination will hide the NTP. | |
530 document.body.classList.remove(CLASSES.FAKEBOX_FOCUS); | |
531 // Don't show the suggestions until the animation termintes. | |
532 $(IDS.SUGGESTIONS_CONTAINER).hidden = true; | |
533 fakebox.addEventListener('webkitTransitionEnd', fakeboxAnimationDone); | |
534 document.body.classList.add(CLASSES.FAKEBOX_ANIMATE); | |
535 } else if (!fakebox || | |
536 !document.body.classList.contains(CLASSES.FAKEBOX_ANIMATE)) { | |
537 // The user has typed in the omnibox - hide the NTP immediately. | |
538 ntpContents.hidden = true; | |
539 clearCustomTheme(); | |
540 } | |
541 } | |
542 } | |
543 | |
544 /** | |
545 * Clears the custom theme (if any). | |
546 */ | |
547 function clearCustomTheme() { | |
548 document.body.style.background = ''; | |
549 document.body.classList.remove('custom-theme'); | |
550 } | |
551 | |
552 /** | |
553 * @return {boolean} True if the NTP is visible. | |
554 */ | |
555 function isNtpVisible() { | |
556 return !ntpContents.hidden; | |
557 } | |
558 | |
559 /** | |
560 * @return {boolean} True if the fakebox has focus. | |
561 */ | |
562 function isFakeboxFocused() { | |
563 return document.body.classList.contains(CLASSES.FAKEBOX_FOCUS); | |
564 } | |
565 | |
566 /** | |
567 * @param {Event} event The click event. | |
568 * @return {boolean} True if the click occurred in the fakebox. | |
569 */ | |
570 function isFakeboxClick(event) { | |
571 var element = event.target; | |
572 while (element) { | |
573 if (element == fakebox) | |
574 return true; | |
575 element = element.parentNode; | |
576 } | |
577 return false; | |
578 } | |
579 | |
580 /** | |
581 * Cleans up the fakebox animation, hides the NTP, and shows suggestions. | |
582 */ | |
583 function fakeboxAnimationDone() { | |
584 ntpContents.hidden = true; | |
585 $(IDS.SUGGESTIONS_CONTAINER).hidden = false; | |
586 clearCustomTheme(); | |
587 document.body.classList.remove(CLASSES.FAKEBOX_ANIMATE); | |
588 fakebox.removeEventListener('webkitTransitionEnd', fakeboxAnimationDone); | |
589 } | |
590 | |
591 // ============================================================================= | |
592 // Dropdown Implementation | |
593 // ============================================================================= | |
594 | |
595 /** | |
596 * Possible behaviors for navigateContentWindow. | |
597 * @enum {number} | |
598 */ | |
599 var WindowOpenDisposition = { | |
600 CURRENT_TAB: 1, | |
601 NEW_BACKGROUND_TAB: 2 | |
602 }; | |
603 | |
604 /** | |
605 * The JavaScript button event value for a middle click. | |
606 * @type {number} | |
607 * @const | |
608 */ | |
609 var MIDDLE_MOUSE_BUTTON = 1; | |
610 | |
611 /** | |
612 * The maximum number of suggestions to show. | |
613 * @type {number} | |
614 * @const | |
615 */ | |
616 var MAX_SUGGESTIONS_TO_SHOW = 5; | |
617 | |
618 /** | |
619 * Assume any native suggestion with a score higher than this value has been | |
620 * inlined by the browser. | |
621 * @type {number} | |
622 * @const | |
623 */ | |
624 var INLINE_SUGGESTION_THRESHOLD = 1200; | |
625 | |
626 /** | |
627 * Suggestion provider type corresponding to a verbatim URL suggestion. | |
628 * @type {string} | |
629 * @const | |
630 */ | |
631 var VERBATIM_URL_TYPE = 'url-what-you-typed'; | |
632 | |
633 /** | |
634 * Suggestion provider type corresponding to a verbatim search suggestion. | |
635 * @type {string} | |
636 * @const | |
637 */ | |
638 var VERBATIM_SEARCH_TYPE = 'search-what-you-typed'; | |
639 | |
640 /** | |
641 * The omnibox input value during the last onnativesuggestions event. | |
642 * @type {string} | |
643 */ | |
644 var lastInputValue = ''; | |
645 | |
646 /** | |
647 * The ordered restricted ids of the currently displayed suggestions. Since the | |
648 * suggestions contain the user's personal data (browser history) the searchBox | |
649 * API embeds the content of the suggestion in a shadow dom, and assigns a | |
650 * random restricted id to each suggestion which is accessible to the JS. | |
651 * @type {Array.<number>} | |
652 */ | |
653 | |
654 var restrictedIds = []; | |
655 | |
656 /** | |
657 * The index of the currently selected suggestion or -1 if none are selected. | |
658 * @type {number} | |
659 */ | |
660 var selectedIndex = -1; | |
661 | |
662 /** | |
663 * The browser embeddedSearch.searchBox object. | |
664 * @type {Object} | |
665 */ | |
666 var searchboxApiHandle; | |
667 | |
668 /** | |
669 * Displays a suggestion. | |
670 * @param {Object} suggestion The suggestion to render. | |
671 * @param {HTMLElement} box The html element to add the suggestion to. | |
672 * @param {boolean} select True to select the selection. | |
673 */ | |
674 function addSuggestionToBox(suggestion, box, select) { | |
675 var suggestionDiv = document.createElement('div'); | |
676 suggestionDiv.classList.add('suggestion'); | |
677 suggestionDiv.classList.toggle('selected', select); | |
678 suggestionDiv.classList.toggle('search', suggestion.is_search); | |
679 | |
680 if (suggestion.destination_url) { // iframes. | |
681 var suggestionIframe = document.createElement('iframe'); | |
682 suggestionIframe.className = 'contents'; | |
683 suggestionIframe.src = suggestion.destination_url; | |
684 suggestionIframe.id = suggestion.rid; | |
685 suggestionDiv.appendChild(suggestionIframe); | |
686 } else { | |
687 var contentsContainer = document.createElement('div'); | |
688 var contents = suggestion.combinedNode; | |
689 contents.classList.add('contents'); | |
690 contentsContainer.appendChild(contents); | |
691 suggestionDiv.appendChild(contentsContainer); | |
692 suggestionDiv.onclick = function(event) { | |
693 handleSuggestionClick(suggestion.rid, event.button); | |
694 }; | |
695 } | |
696 | |
697 restrictedIds.push(suggestion.rid); | |
698 box.appendChild(suggestionDiv); | |
699 } | |
700 | |
701 /** | |
702 * Renders the input suggestions. | |
703 * @param {Array} nativeSuggestions An array of native suggestions to render. | |
704 */ | |
705 function renderSuggestions(nativeSuggestions) { | |
706 var box = document.createElement('div'); | |
707 box.id = IDS.SUGGESTIONS_BOX; | |
708 $(IDS.SUGGESTIONS_CONTAINER).appendChild(box); | |
709 | |
710 for (var i = 0, length = nativeSuggestions.length; | |
711 i < Math.min(MAX_SUGGESTIONS_TO_SHOW, length); ++i) { | |
712 // Don't add the search-what-you-typed suggestion if it's the top match. | |
713 if (i > 0 || nativeSuggestions[i].type != VERBATIM_SEARCH_TYPE) | |
714 addSuggestionToBox(nativeSuggestions[i], box, i == selectedIndex); | |
715 } | |
716 } | |
717 | |
718 /** | |
719 * Clears the suggestions being displayed. | |
720 */ | |
721 function clearSuggestions() { | |
722 $(IDS.SUGGESTIONS_CONTAINER).innerHTML = ''; | |
723 restrictedIds = []; | |
724 selectedIndex = -1; | |
725 } | |
726 | |
727 /** | |
728 * @return {number} The height of the dropdown. | |
729 */ | |
730 function getDropdownHeight() { | |
731 return $(IDS.SUGGESTIONS_CONTAINER).offsetHeight; | |
732 } | |
733 | |
734 /** | |
735 * @param {Object} suggestion A suggestion. | |
736 * @param {boolean} inVerbatimMode Are we in verbatim mode? | |
737 * @return {boolean} True if the suggestion should be selected. | |
738 */ | |
739 function shouldSelectSuggestion(suggestion, inVerbatimMode) { | |
740 var isVerbatimUrl = suggestion.type == VERBATIM_URL_TYPE; | |
741 var inlinableSuggestion = suggestion.type != VERBATIM_SEARCH_TYPE && | |
742 suggestion.rankingData.relevance > INLINE_SUGGESTION_THRESHOLD; | |
743 // Verbatim URLs should always be selected. Otherwise, select suggestions | |
744 // with a high enough score unless we are in verbatim mode (e.g. backspacing | |
745 // away). | |
746 return isVerbatimUrl || (!inVerbatimMode && inlinableSuggestion); | |
747 } | |
748 | |
749 /** | |
750 * Updates selectedIndex, bounding it between -1 and the total number of | |
751 * of suggestions - 1 (looping as necessary), and selects the corresponding | |
752 * suggestion. | |
753 * @param {boolean} increment True to increment the selected suggestion, false | |
754 * to decrement. | |
755 */ | |
756 function updateSelectedSuggestion(increment) { | |
757 var numSuggestions = restrictedIds.length; | |
758 if (!numSuggestions) | |
759 return; | |
760 | |
761 var oldSelection = $(IDS.SUGGESTIONS_BOX).querySelector('.selected'); | |
762 if (oldSelection) | |
763 oldSelection.classList.remove('selected'); | |
764 | |
765 if (increment) | |
766 selectedIndex = ++selectedIndex > numSuggestions - 1 ? -1 : selectedIndex; | |
767 else | |
768 selectedIndex = --selectedIndex < -1 ? numSuggestions - 1 : selectedIndex; | |
769 if (selectedIndex == -1) { | |
770 searchboxApiHandle.setValue(lastInputValue); | |
771 } else { | |
772 var newSelection = $(IDS.SUGGESTIONS_BOX).querySelector( | |
773 '.suggestion:nth-of-type(' + (selectedIndex + 1) + ')'); | |
774 newSelection.classList.add('selected'); | |
775 searchboxApiHandle.setRestrictedValue(restrictedIds[selectedIndex]); | |
776 } | |
777 } | |
778 | |
779 /** | |
780 * Updates suggestions in response to a onchange or onnativesuggestions call. | |
781 */ | |
782 function updateSuggestions() { | |
783 appendSuggestionStyles(); | |
784 lastInputValue = searchboxApiHandle.value; | |
785 | |
786 // Hide the NTP if input has made it into the omnibox. | |
787 var show = lastInputValue == ''; | |
788 updateNtpVisibility(show); | |
789 | |
790 clearSuggestions(); | |
791 if (show) { | |
792 searchboxApiHandle.showBars(); | |
793 return; | |
794 } | |
795 | |
796 var nativeSuggestions = searchboxApiHandle.nativeSuggestions; | |
797 if (nativeSuggestions.length) { | |
798 nativeSuggestions.sort(function(a, b) { | |
799 return b.rankingData.relevance - a.rankingData.relevance; | |
800 }); | |
801 if (shouldSelectSuggestion( | |
802 nativeSuggestions[0], searchboxApiHandle.verbatim)) | |
803 selectedIndex = 0; | |
804 renderSuggestions(nativeSuggestions); | |
805 searchboxApiHandle.hideBars(); | |
806 } else { | |
807 searchboxApiHandle.showBars(); | |
808 } | |
809 | |
810 var height = getDropdownHeight(); | |
811 searchboxApiHandle.showOverlay(height); | |
812 } | |
813 | |
814 /** | |
815 * Appends a style node for suggestion properties that depend on apiHandle. | |
816 */ | |
817 function appendSuggestionStyles() { | |
818 if ($('suggestionStyle')) | |
819 return; | |
820 | |
821 var isRtl = searchboxApiHandle.rtl; | |
822 var startMargin = searchboxApiHandle.startMargin; | |
823 var style = document.createElement('style'); | |
824 style.type = 'text/css'; | |
825 style.id = 'suggestionStyle'; | |
826 style.textContent = | |
827 '.suggestion, ' + | |
828 '.suggestion.search {' + | |
829 ' background-position: ' + | |
830 (isRtl ? '-webkit-calc(100% - 5px)' : '5px') + ' 4px;' + | |
831 ' -webkit-margin-start: ' + startMargin + 'px;' + | |
832 ' -webkit-margin-end: ' + | |
833 (window.innerWidth - searchboxApiHandle.width - startMargin) + 'px;' + | |
834 ' font: ' + searchboxApiHandle.fontSize + 'px "' + | |
835 searchboxApiHandle.font + '";' + | |
836 '}'; | |
837 document.querySelector('head').appendChild(style); | |
838 } | |
839 | |
840 /** | |
841 * Extract the desired navigation behavior from a click button. | |
842 * @param {number} button The Event#button property of a click event. | |
843 * @return {WindowOpenDisposition} The desired behavior for | |
844 * navigateContentWindow. | |
845 */ | |
846 function getDispositionFromClickButton(button) { | |
847 if (button == MIDDLE_MOUSE_BUTTON) | |
848 return WindowOpenDisposition.NEW_BACKGROUND_TAB; | |
849 return WindowOpenDisposition.CURRENT_TAB; | |
850 } | |
851 | |
852 /** | |
853 * Handles suggestion clicks. | |
854 * @param {number} restrictedId The restricted id of the suggestion being | |
855 * clicked. | |
856 * @param {number} button The Event#button property of a click event. | |
857 * | |
858 */ | |
859 function handleSuggestionClick(restrictedId, button) { | |
860 clearSuggestions(); | |
861 searchboxApiHandle.navigateContentWindow( | |
862 restrictedId, getDispositionFromClickButton(button)); | |
863 } | |
864 | |
865 /** | |
866 * chrome.searchBox.onkeypress implementation. | |
867 * @param {Object} e The key being pressed. | |
868 */ | |
869 function handleKeyPress(e) { | |
870 switch (e.keyCode) { | |
871 case 38: // Up arrow | |
872 updateSelectedSuggestion(false); | |
873 break; | |
874 case 40: // Down arrow | |
875 updateSelectedSuggestion(true); | |
876 break; | |
877 } | |
878 } | |
879 | |
880 /** | |
881 * Handles the postMessage calls from the result iframes. | |
882 * @param {Object} message The message containing details of clicks the iframes. | |
883 */ | |
884 function handleMessage(message) { | |
885 if (message.origin != 'null' || !message.data || | |
886 message.data.eventType != 'click') { | |
887 return; | |
888 } | |
889 | |
890 var iframes = document.getElementsByClassName('contents'); | |
891 for (var i = 0; i < iframes.length; ++i) { | |
892 if (iframes[i].contentWindow == message.source) { | |
893 handleSuggestionClick(parseInt(iframes[i].id, 10), | |
894 message.data.button); | |
895 break; | |
896 } | |
897 } | |
898 } | |
899 | |
900 // ============================================================================= | |
901 // Utils | |
902 // ============================================================================= | |
903 | |
904 /** | |
905 * Shortcut for document.getElementById. | |
906 * @param {string} id of the element. | |
907 * @return {HTMLElement} with the id. | |
908 */ | |
909 function $(id) { | |
910 return document.getElementById(id); | |
911 } | |
912 | |
913 /** | |
471 * Utility function which creates an element with an optional classname and | 914 * Utility function which creates an element with an optional classname and |
472 * appends it to the specified parent. | 915 * appends it to the specified parent. |
473 * @param {Element} parent The parent to append the new element. | 916 * @param {Element} parent The parent to append the new element. |
474 * @param {string} name The name of the new element. | 917 * @param {string} name The name of the new element. |
475 * @param {string=} opt_class The optional classname of the new element. | 918 * @param {string=} opt_class The optional classname of the new element. |
476 * @return {Element} The new element. | 919 * @return {Element} The new element. |
477 */ | 920 */ |
478 function createAndAppendElement(parent, name, opt_class) { | 921 function createAndAppendElement(parent, name, opt_class) { |
479 var child = document.createElement(name); | 922 var child = document.createElement(name); |
480 if (opt_class) | 923 if (opt_class) |
(...skipping 22 matching lines...) Expand all Loading... | |
503 * @return {Object} the handle to the embeddedSearch API. | 946 * @return {Object} the handle to the embeddedSearch API. |
504 */ | 947 */ |
505 function getEmbeddedSearchApiHandle() { | 948 function getEmbeddedSearchApiHandle() { |
506 if (window.cideb) | 949 if (window.cideb) |
507 return window.cideb; | 950 return window.cideb; |
508 if (window.chrome && window.chrome.embeddedSearch) | 951 if (window.chrome && window.chrome.embeddedSearch) |
509 return window.chrome.embeddedSearch; | 952 return window.chrome.embeddedSearch; |
510 return null; | 953 return null; |
511 } | 954 } |
512 | 955 |
956 // ============================================================================= | |
957 // Initialization | |
958 // ============================================================================= | |
959 | |
513 /** | 960 /** |
514 * Prepares the New Tab Page by adding listeners, rendering the current | 961 * Prepares the New Tab Page by adding listeners, rendering the current |
515 * theme, and the most visited pages section. | 962 * theme, the most visited pages section, and Google-specific elements for a |
963 * Google-provided page. | |
516 */ | 964 */ |
517 function init() { | 965 function init() { |
518 topMarginElement = document.getElementById(IDS.TOP_MARGIN); | 966 isGooglePage = location.href.indexOf('isGoogle' != -1); |
519 tilesContainer = document.getElementById(IDS.TILES); | 967 |
520 notification = document.getElementById(IDS.NOTIFICATION); | 968 topMarginElement = $(IDS.TOP_MARGIN); |
521 attribution = document.getElementById(IDS.ATTRIBUTION); | 969 tilesContainer = $(IDS.TILES); |
970 notification = $(IDS.NOTIFICATION); | |
971 attribution = $(IDS.ATTRIBUTION); | |
972 ntpContents = $(IDS.NTP_CONTENTS); | |
973 | |
974 if (isGooglePage) { | |
975 document.body.classList.add(CLASSES.GOOGLE_PAGE); | |
976 var logo = document.createElement('div'); | |
977 logo.id = IDS.LOGO; | |
978 logo.alt = 'Google'; | |
979 | |
980 fakebox = document.createElement('div'); | |
981 fakebox.id = IDS.FAKEBOX; | |
982 fakebox.innerHTML = | |
983 '<input autocomplete="off" tabindex="-1" aria-hidden="true">' + | |
984 '<div id=cursor></div>'; | |
985 | |
986 ntpContents.insertBefore(fakebox, ntpContents.firstChild); | |
987 ntpContents.insertBefore(logo, ntpContents.firstChild); | |
988 } | |
989 | |
522 | 990 |
523 // TODO(jeremycho): i18n. | 991 // TODO(jeremycho): i18n. |
524 var notificationMessage = document.getElementById(IDS.NOTIFICATION_MESSAGE); | 992 var notificationMessage = $(IDS.NOTIFICATION_MESSAGE); |
525 notificationMessage.innerText = 'Thumbnail removed.'; | 993 notificationMessage.innerText = 'Thumbnail removed.'; |
526 var undoLink = document.getElementById(IDS.UNDO_LINK); | 994 var undoLink = $(IDS.UNDO_LINK); |
527 undoLink.addEventListener('click', onUndo); | 995 undoLink.addEventListener('click', onUndo); |
528 undoLink.innerText = 'Undo'; | 996 undoLink.innerText = 'Undo'; |
529 var restoreAllLink = document.getElementById(IDS.RESTORE_ALL_LINK); | 997 var restoreAllLink = $(IDS.RESTORE_ALL_LINK); |
530 restoreAllLink.addEventListener('click', onRestoreAll); | 998 restoreAllLink.addEventListener('click', onRestoreAll); |
531 restoreAllLink.innerText = 'Restore all'; | 999 restoreAllLink.innerText = 'Restore all'; |
532 attribution.innerText = 'Theme created by'; | 1000 attribution.innerText = 'Theme created by'; |
533 | 1001 |
534 var notificationCloseButton = | 1002 var notificationCloseButton = $(IDS.NOTIFICATION_CLOSE_BUTTON); |
535 document.getElementById(IDS.NOTIFICATION_CLOSE_BUTTON); | |
536 notificationCloseButton.addEventListener('click', hideNotification); | 1003 notificationCloseButton.addEventListener('click', hideNotification); |
537 | 1004 |
538 window.addEventListener('resize', onResize); | 1005 window.addEventListener('resize', onResize); |
539 onResize(); | 1006 onResize(); |
540 | 1007 |
541 var topLevelHandle = getEmbeddedSearchApiHandle(); | 1008 var topLevelHandle = getEmbeddedSearchApiHandle(); |
542 // This is to inform Chrome that the NTP is instant-extended capable i.e. | |
543 // it should fire events like onmostvisitedchange. | |
544 topLevelHandle.searchBox.onsubmit = function() {}; | |
545 | 1009 |
546 apiHandle = topLevelHandle.newTabPage; | 1010 ntpApiHandle = topLevelHandle.newTabPage; |
547 apiHandle.onthemechange = onThemeChange; | 1011 ntpApiHandle.onthemechange = onThemeChange; |
548 apiHandle.onmostvisitedchange = onMostVisitedChange; | 1012 ntpApiHandle.onmostvisitedchange = onMostVisitedChange; |
549 | 1013 |
550 onThemeChange(); | 1014 onThemeChange(); |
551 onMostVisitedChange(); | 1015 onMostVisitedChange(); |
1016 | |
1017 searchboxApiHandle = topLevelHandle.searchBox; | |
1018 searchboxApiHandle.onnativesuggestions = updateSuggestions; | |
1019 searchboxApiHandle.onchange = updateSuggestions; | |
1020 searchboxApiHandle.onkeypress = handleKeyPress; | |
1021 searchboxApiHandle.onsubmit = function() { | |
1022 var value = searchboxApiHandle.value; | |
1023 if (!value) { | |
1024 // Interpret onsubmit with an empty query as an ESC key press. | |
1025 clearSuggestions(); | |
1026 updateNtpVisibility(true); | |
1027 } | |
1028 }; | |
1029 | |
1030 $(IDS.SUGGESTIONS_CONTAINER).dir = searchboxApiHandle.rtl ? 'rtl' : 'ltr'; | |
1031 if (searchboxApiHandle.nativeSuggestions.length) | |
1032 updateSuggestions(); | |
1033 | |
1034 if (!document.webkitHidden) | |
1035 window.addEventListener('resize', addDelayedTransitions); | |
1036 else | |
1037 document.addEventListener('webkitvisibilitychange', addDelayedTransitions); | |
1038 | |
1039 | |
1040 if (fakebox) { | |
1041 // Listener for updating the fakebox focus. | |
1042 document.body.onclick = function(event) { | |
1043 if (isFakeboxClick(event)) { | |
1044 document.body.classList.add(CLASSES.FAKEBOX_FOCUS); | |
1045 searchboxApiHandle.startCapturingKeyStrokes(); | |
1046 } else { | |
1047 document.body.classList.remove(CLASSES.FAKEBOX_FOCUS); | |
1048 searchboxApiHandle.stopCapturingKeyStrokes(); | |
1049 } | |
1050 }; | |
1051 | |
1052 // Set the cursor alignment based on language directionality. | |
1053 $(IDS.CURSOR).style[searchboxApiHandle.rtl ? 'right' : 'left'] = '9px'; | |
1054 } | |
1055 } | |
1056 | |
1057 /** | |
1058 * Applies webkit transitions to NTP elements which need to be delayed until | |
1059 * after the page is made visible and any initial resize has occurred. This is | |
1060 * to prevent animations from triggering when the NTP is shown. | |
1061 */ | |
1062 function addDelayedTransitions() { | |
1063 if (fakebox) { | |
1064 fakebox.style.webkitTransition = | |
1065 '-webkit-transform 100ms linear, width 200ms ease'; | |
1066 } | |
1067 tilesContainer.style.webkitTransition = 'width 200ms'; | |
1068 window.removeEventListener('resize', addDelayedTransitions); | |
1069 document.removeEventListener('webkitvisibilitychange', addDelayedTransitions); | |
552 } | 1070 } |
553 | 1071 |
554 document.addEventListener('DOMContentLoaded', init); | 1072 document.addEventListener('DOMContentLoaded', init); |
1073 window.addEventListener('message', handleMessage, false); | |
555 })(); | 1074 })(); |
OLD | NEW |