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