OLD | NEW |
(Empty) | |
| 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 |
| 3 // found in the LICENSE file. |
| 4 |
| 5 /** |
| 6 * The element used to vertically position the most visited section on |
| 7 * window resize. |
| 8 * @type {Element} |
| 9 */ |
| 10 var topMarginElement; |
| 11 |
| 12 /** |
| 13 * The container for the tile elements. |
| 14 * @type {Element} |
| 15 */ |
| 16 var tilesContainer; |
| 17 |
| 18 /** |
| 19 * The notification displayed when a page is blacklisted. |
| 20 * @type {Element} |
| 21 */ |
| 22 var notification; |
| 23 |
| 24 /** |
| 25 * The handle for the timer used to hide the notification. |
| 26 * @type {?number} |
| 27 */ |
| 28 var notificationTimer = null; |
| 29 |
| 30 /** |
| 31 * The array of rendered tiles, ordered by appearance. |
| 32 * @type {Array.<Tile>} |
| 33 */ |
| 34 var tiles = []; |
| 35 |
| 36 /** |
| 37 * The last blacklisted tile if any, which by definition should not be filler. |
| 38 * @type {?Tile} |
| 39 */ |
| 40 var lastBlacklistedTile = null; |
| 41 |
| 42 /** |
| 43 * The index of the last blacklisted tile, if any. Used to determine where to |
| 44 * re-insert a tile on undo. |
| 45 * @type {number} |
| 46 */ |
| 47 var lastBlacklistedIndex = -1; |
| 48 |
| 49 /** |
| 50 * True if a page has been blacklisted and we're waiting on the |
| 51 * onmostvisitedchange callback. See onMostVisitedChange() for how this |
| 52 * is used. |
| 53 * @type {boolean} |
| 54 */ |
| 55 var isBlacklisting = false; |
| 56 |
| 57 /** |
| 58 * True if a blacklist has been undone and we're waiting on the |
| 59 * onmostvisitedchange callback. See onMostVisitedChange() for how this |
| 60 * is used. |
| 61 * @type {boolean} |
| 62 */ |
| 63 var isUndoing = false; |
| 64 |
| 65 /** |
| 66 * Current number of tiles shown based on the window width, including filler. |
| 67 * @type {number} |
| 68 */ |
| 69 var numTilesShown = 0; |
| 70 |
| 71 /** |
| 72 * The browser embeddedSearch.newTabPage object. |
| 73 * @type {Object} |
| 74 */ |
| 75 var apiHandle; |
| 76 |
| 77 /** |
| 78 * Possible background-colors of a non-custom theme. Used to determine whether |
| 79 * the homepage should be updated to support custom or non-custom themes. |
| 80 * @type {Array.<string>} |
| 81 * @const |
| 82 */ |
| 83 var WHITE = ['rgba(255,255,255,1)', 'rgba(0,0,0,0)']; |
| 84 |
| 85 /** |
| 86 * Should be equal to mv-tile's -webkit-margin-start + width. |
| 87 * @type {number} |
| 88 * @const |
| 89 */ |
| 90 var TILE_WIDTH = 160; |
| 91 |
| 92 /** |
| 93 * The height of the most visited section. |
| 94 * @type {number} |
| 95 */ |
| 96 var MOST_VISITED_HEIGHT = 156; |
| 97 |
| 98 /** @type {number} @const */ |
| 99 var MAX_NUM_TILES_TO_SHOW = 4; |
| 100 |
| 101 /** @type {number} @const */ |
| 102 var MIN_NUM_TILES_TO_SHOW = 2; |
| 103 |
| 104 /** |
| 105 * Minimum total padding to give to the left and right of the most visited |
| 106 * section. Used to determine how many tiles to show. |
| 107 * @type {number} @const |
| 108 */ |
| 109 var MIN_TOTAL_HORIZONTAL_PADDING = 188; |
| 110 |
| 111 /** @type {string} @const */ |
| 112 var TILE_CLASS = 'mv-tile'; |
| 113 |
| 114 /** |
| 115 * Class applied to page tiles. |
| 116 * @type {string} @const |
| 117 */ |
| 118 var PAGE_CLASS = 'mv-page'; |
| 119 |
| 120 /** |
| 121 * Class applied to a page tile's title. |
| 122 * @type {string} @const |
| 123 */ |
| 124 var TITLE_CLASS = 'mv-title'; |
| 125 |
| 126 /** |
| 127 * Class applied to a page tile's thumbnail. |
| 128 * @type {string} @const |
| 129 */ |
| 130 var THUMBNAIL_CLASS = 'mv-thumb'; |
| 131 |
| 132 /** |
| 133 * Class applied to a page tile's domain. |
| 134 * @type {string} @const |
| 135 */ |
| 136 var DOMAIN_CLASS = 'mv-domain'; |
| 137 |
| 138 /** |
| 139 * Class applied to a page tile's blacklist button. |
| 140 * @type {string} @const |
| 141 */ |
| 142 var BLACKLIST_BUTTON_CLASS = 'mv-x'; |
| 143 |
| 144 /** |
| 145 * Class applied to a page tile's favicon. |
| 146 * @type {string} @const |
| 147 */ |
| 148 var FAVICON_CLASS = 'mv-fav'; |
| 149 |
| 150 /** |
| 151 * Class applied to filler tiles. |
| 152 * @type {string} @const |
| 153 */ |
| 154 var FILLER_CLASS = 'mv-filler'; |
| 155 |
| 156 /** |
| 157 * Class applied to tiles to trigger the blacklist animation. |
| 158 * @type {string} @const |
| 159 */ |
| 160 var BLACKLIST_CLASS = 'mv-bl'; |
| 161 |
| 162 /** |
| 163 * Class applied to tiles to hide them on small browser width. |
| 164 * @type {string} @const |
| 165 */ |
| 166 var HIDE_TILE_CLASS = 'mv-tile-hide'; |
| 167 |
| 168 /** |
| 169 * Class applied to hide the blacklist buttons during the blacklist animation. |
| 170 * @type {string} @const |
| 171 */ |
| 172 var HIDE_BLACKLIST_BUTTON_CLASS = 'mv-x-hide'; |
| 173 |
| 174 /** |
| 175 * Class applied to the notification to hide it. |
| 176 * @type {string} @const |
| 177 */ |
| 178 var HIDE_NOTIFICATION_CLASS = 'mv-noti-hide'; |
| 179 |
| 180 /** |
| 181 * Time (in milliseconds) to show the notification. |
| 182 * @type {number} @const |
| 183 */ |
| 184 var NOTIFICATION_TIMEOUT = 10000; |
| 185 |
| 186 /** |
| 187 * A Tile is either a rendering of a Most Visited page or "filler" used to |
| 188 * pad out the section when not enough pages exist. |
| 189 * |
| 190 * @param {Element} elem The element for rendering the tile. |
| 191 * @param {number=} opt_rid The RID for the corresponding Most Visited page. |
| 192 * Should only be left unspecified when creating a filler tile. |
| 193 * @constructor |
| 194 */ |
| 195 Tile = function(elem, opt_rid) { |
| 196 /** @type {Element} */ |
| 197 this.elem = elem; |
| 198 |
| 199 /** @type {number|undefined} */ |
| 200 this.rid = opt_rid; |
| 201 }; |
| 202 |
| 203 /** |
| 204 * Updates the NTP based on the current theme. |
| 205 * @private |
| 206 */ |
| 207 function onThemeChange() { |
| 208 var info = apiHandle.themeBackgroundInfo; |
| 209 if (!info) |
| 210 return; |
| 211 var background = [info.colorRgba, |
| 212 info.imageUrl, |
| 213 info.imageTiling, |
| 214 info.imageHorizontalAlignment, |
| 215 info.imageVerticalAlignment].join(' ').trim(); |
| 216 document.body.style.background = background; |
| 217 var isCustom = !!background && WHITE.indexOf(background) == -1; |
| 218 enable(document.body, 'custom-theme', isCustom); |
| 219 } |
| 220 |
| 221 /** |
| 222 * Handles a new set of Most Visited page data. |
| 223 */ |
| 224 function onMostVisitedChange() { |
| 225 var pages = apiHandle.mostVisited; |
| 226 |
| 227 // If this was called as a result of a blacklist, add a new replacement |
| 228 // (possibly filler) tile at the end and trigger the blacklist animation. |
| 229 if (isBlacklisting) { |
| 230 var replacementTile = createTile(pages[MAX_NUM_TILES_TO_SHOW - 1]); |
| 231 |
| 232 tiles.push(replacementTile); |
| 233 tilesContainer.appendChild(replacementTile.elem); |
| 234 |
| 235 var lastBlacklistedTileElement = lastBlacklistedTile.elem; |
| 236 lastBlacklistedTileElement.addEventListener( |
| 237 'webkitTransitionEnd', blacklistAnimationDone); |
| 238 lastBlacklistedTileElement.classList.add(BLACKLIST_CLASS); |
| 239 // In order to animate the replacement tile sliding into place, it must |
| 240 // be made visible. |
| 241 updateTileVisibility(numTilesShown + 1); |
| 242 |
| 243 // If this was called as a result of an undo, re-insert the last blacklisted |
| 244 // tile in its old location and trigger the undo animation. |
| 245 } else if (isUndoing) { |
| 246 tiles.splice( |
| 247 lastBlacklistedIndex, 0, lastBlacklistedTile); |
| 248 var lastBlacklistedTileElement = lastBlacklistedTile.elem; |
| 249 tilesContainer.insertBefore( |
| 250 lastBlacklistedTileElement, |
| 251 tilesContainer.childNodes[lastBlacklistedIndex]); |
| 252 lastBlacklistedTileElement.addEventListener( |
| 253 'webkitTransitionEnd', undoAnimationDone); |
| 254 // Yield to ensure the tile is added to the DOM before removing the class. |
| 255 // Without this, the webkit transition doesn't reliably trigger. |
| 256 window.setTimeout(function() { |
| 257 lastBlacklistedTileElement.classList.remove(BLACKLIST_CLASS); |
| 258 }, 0); |
| 259 // Otherwise render the tiles using the new data without animation. |
| 260 } else { |
| 261 tiles = []; |
| 262 for (var i = 0; i < MAX_NUM_TILES_TO_SHOW; ++i) { |
| 263 tiles.push(createTile(pages[i])); |
| 264 } |
| 265 renderTiles(); |
| 266 } |
| 267 } |
| 268 |
| 269 /** |
| 270 * Renders the current set of tiles without animation. |
| 271 */ |
| 272 function renderTiles() { |
| 273 removeChildren(tilesContainer); |
| 274 for (var i = 0, length = tiles.length; i < length; ++i) { |
| 275 tilesContainer.appendChild(tiles[i].elem); |
| 276 } |
| 277 } |
| 278 |
| 279 /** |
| 280 * Creates a Tile with the specified page data. If no data is provided, a |
| 281 * filler Tile is created. |
| 282 * @param {Object} page The page data. |
| 283 * @return {Tile} The new Tile_. |
| 284 */ |
| 285 function createTile(page) { |
| 286 var tileElement = document.createElement('div'); |
| 287 tileElement.classList.add(TILE_CLASS); |
| 288 |
| 289 if (page) { |
| 290 var rid = page.rid; |
| 291 tileElement.classList.add(PAGE_CLASS); |
| 292 |
| 293 // The click handler for navigating to the page identified by the RID. |
| 294 tileElement.addEventListener('click', function() { |
| 295 apiHandle.navigateContentWindow(rid); |
| 296 }); |
| 297 |
| 298 // The shadow DOM which renders the page title. |
| 299 var titleElement = page.titleElement; |
| 300 if (titleElement) { |
| 301 titleElement.classList.add(TITLE_CLASS); |
| 302 tileElement.appendChild(titleElement); |
| 303 } |
| 304 |
| 305 // Render the thumbnail if present. Otherwise, fall back to a shadow DOM |
| 306 // which renders the domain. |
| 307 var thumbnailUrl = page.thumbnailUrl; |
| 308 |
| 309 var showDomainElement = function() { |
| 310 var domainElement = page.domainElement; |
| 311 if (domainElement) { |
| 312 domainElement.classList.add(DOMAIN_CLASS); |
| 313 tileElement.appendChild(domainElement); |
| 314 } |
| 315 }; |
| 316 if (thumbnailUrl) { |
| 317 var image = new Image(); |
| 318 image.onload = function() { |
| 319 var thumbnailElement = createAndAppendElement( |
| 320 tileElement, 'div', THUMBNAIL_CLASS); |
| 321 thumbnailElement.style['background-image'] = |
| 322 'url(' + thumbnailUrl + ')'; |
| 323 }; |
| 324 |
| 325 image.onerror = showDomainElement; |
| 326 image.src = thumbnailUrl; |
| 327 } else { |
| 328 showDomainElement(); |
| 329 } |
| 330 |
| 331 // The button used to blacklist this page. |
| 332 var blacklistButton = createAndAppendElement( |
| 333 tileElement, 'div', BLACKLIST_BUTTON_CLASS); |
| 334 blacklistButton.addEventListener('click', generateBlacklistFunction(rid)); |
| 335 // TODO(jeremycho): i18n. |
| 336 blacklistButton.title = "Don't show on this page"; |
| 337 |
| 338 // The page favicon, if any. |
| 339 var faviconUrl = page.faviconUrl; |
| 340 if (faviconUrl) { |
| 341 var favicon = createAndAppendElement( |
| 342 tileElement, 'div', FAVICON_CLASS); |
| 343 favicon.style['background-image'] = 'url(' + faviconUrl + ')'; |
| 344 } |
| 345 return new Tile(tileElement, rid); |
| 346 } else { |
| 347 tileElement.classList.add(FILLER_CLASS); |
| 348 return new Tile(tileElement); |
| 349 } |
| 350 } |
| 351 |
| 352 /** |
| 353 * Generates a function to be called when the page with the corresponding RID |
| 354 * is blacklisted. |
| 355 * @param {number} rid The RID of the page being blacklisted. |
| 356 * @return {function(Event)} A function which handles the blacklisting of the |
| 357 * page by displaying the notification, updating state variables, and |
| 358 * notifying Chrome. |
| 359 */ |
| 360 function generateBlacklistFunction(rid) { |
| 361 return function(e) { |
| 362 // Prevent navigation when the page is being blacklisted. |
| 363 e.stopPropagation(); |
| 364 |
| 365 showNotification(); |
| 366 isBlacklisting = true; |
| 367 tilesContainer.classList.add(HIDE_BLACKLIST_BUTTON_CLASS); |
| 368 lastBlacklistedTile = getTileByRid(rid); |
| 369 lastBlacklistedIndex = tiles.indexOf(lastBlacklistedTile); |
| 370 apiHandle.deleteMostVisitedItem(rid); |
| 371 }; |
| 372 } |
| 373 |
| 374 /** |
| 375 * Shows the blacklist notification and refreshes the timer to hide it. |
| 376 */ |
| 377 function showNotification() { |
| 378 notification.classList.remove(HIDE_NOTIFICATION_CLASS); |
| 379 if (notificationTimer) |
| 380 window.clearTimeout(notificationTimer); |
| 381 notificationTimer = window.setTimeout( |
| 382 hideNotification, NOTIFICATION_TIMEOUT); |
| 383 } |
| 384 |
| 385 |
| 386 /** |
| 387 * Hides the blacklist notification. |
| 388 */ |
| 389 function hideNotification() { |
| 390 notification.classList.add(HIDE_NOTIFICATION_CLASS); |
| 391 } |
| 392 |
| 393 /** |
| 394 * Handles the end of the blacklist animation by removing the blacklisted tile. |
| 395 */ |
| 396 function blacklistAnimationDone() { |
| 397 tiles.splice(lastBlacklistedIndex, 1); |
| 398 removeNode(lastBlacklistedTile.elem); |
| 399 updateTileVisibility(numTilesShown); |
| 400 isBlacklisting = false; |
| 401 tilesContainer.classList.remove(HIDE_BLACKLIST_BUTTON_CLASS); |
| 402 lastBlacklistedTile.elem.removeEventListener( |
| 403 'webkitTransitionEnd', blacklistAnimationDone); |
| 404 } |
| 405 |
| 406 /** |
| 407 * Handles a click on the notification undo link by hiding the notification and |
| 408 * informing Chrome. |
| 409 */ |
| 410 function onUndo() { |
| 411 hideNotification(); |
| 412 var lastBlacklistedRID = lastBlacklistedTile.rid; |
| 413 if (lastBlacklistedRID != null) { |
| 414 isUndoing = true; |
| 415 apiHandle.undoMostVisitedDeletion(lastBlacklistedRID); |
| 416 } |
| 417 } |
| 418 |
| 419 /** |
| 420 * Handles the end of the undo animation by removing the extraneous end tile. |
| 421 */ |
| 422 function undoAnimationDone() { |
| 423 isUndoing = false; |
| 424 tiles.splice(tiles.length - 1, 1); |
| 425 removeNode(tilesContainer.lastElementChild); |
| 426 updateTileVisibility(numTilesShown); |
| 427 lastBlacklistedTile.elem.removeEventListener( |
| 428 'webkitTransitionEnd', undoAnimationDone); |
| 429 } |
| 430 |
| 431 /** |
| 432 * Handles a click on the restore all notification link by hiding the |
| 433 * notification and informing Chrome. |
| 434 */ |
| 435 function onRestoreAll() { |
| 436 hideNotification(); |
| 437 apiHandle.undoAllMostVisitedDeletions(); |
| 438 } |
| 439 |
| 440 /** |
| 441 * Handles a resize by vertically centering the most visited section |
| 442 * and triggering the tile show/hide animation if necessary. |
| 443 */ |
| 444 function onResize() { |
| 445 var clientHeight = document.documentElement.clientHeight; |
| 446 topMarginElement.style['margin-top'] = |
| 447 Math.max(0, (clientHeight - MOST_VISITED_HEIGHT) / 2) + 'px'; |
| 448 |
| 449 var clientWidth = document.documentElement.clientWidth; |
| 450 var numTilesToShow = Math.floor( |
| 451 (clientWidth - MIN_TOTAL_HORIZONTAL_PADDING) / TILE_WIDTH); |
| 452 numTilesToShow = Math.max(MIN_NUM_TILES_TO_SHOW, numTilesToShow); |
| 453 if (numTilesToShow != numTilesShown) { |
| 454 updateTileVisibility(numTilesToShow); |
| 455 numTilesShown = numTilesToShow; |
| 456 } |
| 457 } |
| 458 |
| 459 |
| 460 /** |
| 461 * Triggers an animation to show the first numTilesToShow tiles and hide the |
| 462 * remaining. |
| 463 * @param {number} numTilesToShow The number of tiles to show. |
| 464 */ |
| 465 function updateTileVisibility(numTilesToShow) { |
| 466 for (var i = 0, length = tiles.length; i < length; ++i) { |
| 467 enable(tiles[i].elem, HIDE_TILE_CLASS, i >= numTilesToShow); |
| 468 } |
| 469 } |
| 470 |
| 471 /** |
| 472 * Returns the tile corresponding to the specified page RID. |
| 473 * @param {number} rid The page RID being looked up. |
| 474 * @return {Tile} The corresponding tile. |
| 475 */ |
| 476 function getTileByRid(rid) { |
| 477 for (var i = 0, length = tiles.length; i < length; ++i) { |
| 478 var tile = tiles[i]; |
| 479 if (tile.rid == rid) |
| 480 return tile; |
| 481 } |
| 482 return null; |
| 483 } |
| 484 |
| 485 /** |
| 486 * Utility function which creates an element with an optional classname and |
| 487 * appends it to the specified parent. |
| 488 * @param {Element} parent The parent to append the new element. |
| 489 * @param {string} name The name of the new element. |
| 490 * @param {string=} opt_class The optional classname of the new element. |
| 491 * @return {Element} The new element. |
| 492 */ |
| 493 function createAndAppendElement(parent, name, opt_class) { |
| 494 var child = document.createElement(name); |
| 495 if (opt_class) |
| 496 child.classList.add(opt_class); |
| 497 parent.appendChild(child); |
| 498 return child; |
| 499 } |
| 500 |
| 501 /** |
| 502 * Removes a node from its parent. |
| 503 * @param {Node} node The node to remove. |
| 504 */ |
| 505 function removeNode(node) { |
| 506 node && node.parentNode && node.parentNode.removeChild(node); |
| 507 } |
| 508 |
| 509 /** |
| 510 * Removes all the child nodes on a DOM node. |
| 511 * @param {Node} node Node to remove children from. |
| 512 */ |
| 513 function removeChildren(node) { |
| 514 var child; |
| 515 while ((child = node.firstChild)) { |
| 516 node.removeChild(child); |
| 517 } |
| 518 } |
| 519 |
| 520 /** |
| 521 * Adds or removes a class depending on the enabled argument. |
| 522 * @param {Element} element DOM node to add or remove the class on. |
| 523 * @param {string} className Class name to add or remove. |
| 524 * @param {boolean} enabled Whether to add or remove the class (true adds, |
| 525 * false removes). |
| 526 */ |
| 527 function enable(element, className, enabled) { |
| 528 if (enabled) |
| 529 element.classList.add(className); |
| 530 else |
| 531 element.classList.remove(className); |
| 532 } |
| 533 |
| 534 /** |
| 535 * @return {Object} the handle to the embeddedSearch API. |
| 536 */ |
| 537 function getEmbeddedSearchApiHandle() { |
| 538 if (window.cideb) |
| 539 return window.cideb; |
| 540 if (window.navigator && window.navigator.embeddedSearch) |
| 541 return window.navigator.embeddedSearch; |
| 542 if (window.chrome && window.chrome.embeddedSearch) |
| 543 return window.chrome.embeddedSearch; |
| 544 return null; |
| 545 } |
| 546 |
| 547 /** |
| 548 * Prepares the New Tab Page by adding listeners, rendering the current |
| 549 * theme, and the most visited pages section. |
| 550 */ |
| 551 function init() { |
| 552 topMarginElement = document.getElementById('top-margin'); |
| 553 tilesContainer = document.getElementById('mv-tiles'); |
| 554 notification = document.getElementById('mv-noti'); |
| 555 |
| 556 // TODO(jeremycho): i18n. |
| 557 var notificationMessage = document.getElementById('mv-msg'); |
| 558 notificationMessage.innerText = 'Thumbnail removed.'; |
| 559 var undoLink = document.getElementById('mv-undo'); |
| 560 undoLink.addEventListener('click', onUndo); |
| 561 undoLink.innerText = 'Undo'; |
| 562 var restoreAllLink = document.getElementById('mv-restore'); |
| 563 restoreAllLink.addEventListener('click', onRestoreAll); |
| 564 restoreAllLink.innerText = 'Restore all'; |
| 565 var notificationCloseButton = document.getElementById('mv-noti-x'); |
| 566 notificationCloseButton.addEventListener('click', hideNotification); |
| 567 |
| 568 window.addEventListener('resize', onResize); |
| 569 onResize(); |
| 570 |
| 571 var topLevelHandle = getEmbeddedSearchApiHandle(); |
| 572 // This is to inform Chrome that the NTP is instant-extended capable. |
| 573 topLevelHandle.searchBox.onsubmit = function() {}; |
| 574 |
| 575 apiHandle = topLevelHandle.newTabPage; |
| 576 apiHandle.onthemechange = onThemeChange; |
| 577 apiHandle.onmostvisitedchange = onMostVisitedChange; |
| 578 |
| 579 onThemeChange(); |
| 580 onMostVisitedChange(); |
| 581 } |
| 582 |
| 583 document.addEventListener('DOMContentLoaded', init); |
OLD | NEW |