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 /** 7 * @fileoverview The local InstantExtended NTP. 8 */ 9 10 11 /** 12 * Controls rendering the new tab page for InstantExtended. 13 * @return {Object} A limited interface for testing the local NTP. 14 */ 15 function LocalNTP() { 16 <include src="../../../../ui/webui/resources/js/assert.js"> 17 <include src="local_ntp_design.js"> 18 <include src="local_ntp_util.js"> 19 <include src="window_disposition_util.js"> 20 21 22 /** 23 * Enum for classnames. 24 * @enum {string} 25 * @const 26 */ 27 var CLASSES = { 28 ALTERNATE_LOGO: 'alternate-logo', // Shows white logo if required by theme 29 BLACKLIST: 'mv-blacklist', // triggers tile blacklist animation 30 BLACKLIST_BUTTON: 'mv-x', 31 BLACKLIST_BUTTON_INNER: 'mv-x-inner', 32 DARK: 'dark', 33 DEFAULT_THEME: 'default-theme', 34 DELAYED_HIDE_NOTIFICATION: 'mv-notice-delayed-hide', 35 DOT: 'dot', 36 FAKEBOX_DISABLE: 'fakebox-disable', // Makes fakebox non-interactive 37 FAKEBOX_FOCUS: 'fakebox-focused', // Applies focus styles to the fakebox 38 // Applies drag focus style to the fakebox 39 FAKEBOX_DRAG_FOCUS: 'fakebox-drag-focused', 40 FAVICON: 'mv-favicon', 41 FAVICON_FALLBACK: 'mv-favicon-fallback', 42 FOCUSED: 'mv-focused', 43 HIDE_BLACKLIST_BUTTON: 'mv-x-hide', // hides blacklist button during animation 44 HIDE_FAKEBOX_AND_LOGO: 'hide-fakebox-logo', 45 HIDE_NOTIFICATION: 'mv-notice-hide', 46 // Vertically centers the most visited section for a non-Google provided page. 47 NON_GOOGLE_PAGE: 'non-google-page', 48 PAGE: 'mv-page', // page tiles 49 PAGE_READY: 'mv-page-ready', // page tile when ready 50 RTL: 'rtl', // Right-to-left language text. 51 THUMBNAIL: 'mv-thumb', 52 THUMBNAIL_FALLBACK: 'mv-thumb-fallback', 53 THUMBNAIL_MASK: 'mv-mask', 54 TILE: 'mv-tile', 55 TILE_INNER: 'mv-tile-inner', 56 TITLE: 'mv-title' 57 }; 58 59 60 /** 61 * Enum for HTML element ids. 62 * @enum {string} 63 * @const 64 */ 65 var IDS = { 66 ATTRIBUTION: 'attribution', 67 ATTRIBUTION_TEXT: 'attribution-text', 68 CUSTOM_THEME_STYLE: 'ct-style', 69 FAKEBOX: 'fakebox', 70 FAKEBOX_INPUT: 'fakebox-input', 71 FAKEBOX_TEXT: 'fakebox-text', 72 LOGO: 'logo', 73 NOTIFICATION: 'mv-notice', 74 NOTIFICATION_CLOSE_BUTTON: 'mv-notice-x', 75 NOTIFICATION_MESSAGE: 'mv-msg', 76 NTP_CONTENTS: 'ntp-contents', 77 RESTORE_ALL_LINK: 'mv-restore', 78 TILES: 'mv-tiles', 79 UNDO_LINK: 'mv-undo' 80 }; 81 82 83 /** 84 * Enum for keycodes. 85 * @enum {number} 86 * @const 87 */ 88 var KEYCODE = { 89 ENTER: 13 90 }; 91 92 93 /** 94 * Enum for the state of the NTP when it is disposed. 95 * @enum {number} 96 * @const 97 */ 98 var NTP_DISPOSE_STATE = { 99 NONE: 0, // Preserve the NTP appearance and functionality 100 DISABLE_FAKEBOX: 1, 101 HIDE_FAKEBOX_AND_LOGO: 2 102 }; 103 104 105 /** 106 * The JavaScript button event value for a middle click. 107 * @type {number} 108 * @const 109 */ 110 var MIDDLE_MOUSE_BUTTON = 1; 111 112 113 /** 114 * Specifications for the NTP design. 115 * @const {NtpDesign} 116 */ 117 var NTP_DESIGN = getNtpDesign(configData.ntpDesignName); 118 119 120 /** 121 * The container for the tile elements. 122 * @type {Element} 123 */ 124 var tilesContainer; 125 126 127 /** 128 * The notification displayed when a page is blacklisted. 129 * @type {Element} 130 */ 131 var notification; 132 133 134 /** 135 * The container for the theme attribution. 136 * @type {Element} 137 */ 138 var attribution; 139 140 141 /** 142 * The "fakebox" - an input field that looks like a regular searchbox. When it 143 * is focused, any text the user types goes directly into the omnibox. 144 * @type {Element} 145 */ 146 var fakebox; 147 148 149 /** 150 * The container for NTP elements. 151 * @type {Element} 152 */ 153 var ntpContents; 154 155 156 /** 157 * The array of rendered tiles, ordered by appearance. 158 * @type {!Array.<Tile>} 159 */ 160 var tiles = []; 161 162 163 /** 164 * The last blacklisted tile if any, which by definition should not be filler. 165 * @type {?Tile} 166 */ 167 var lastBlacklistedTile = null; 168 169 170 /** 171 * The iframe element which is currently keyboard focused, or null. 172 * @type {?Element} 173 */ 174 var focusedIframe = null; 175 176 177 /** 178 * True if a page has been blacklisted and we're waiting on the 179 * onmostvisitedchange callback. See onMostVisitedChange() for how this 180 * is used. 181 * @type {boolean} 182 */ 183 var isBlacklisting = false; 184 185 186 /** 187 * Current number of tiles columns shown based on the window width, including 188 * those that just contain filler. 189 * @type {number} 190 */ 191 var numColumnsShown = 0; 192 193 194 /** 195 * A flag to indicate Most Visited changed caused by user action. If true, then 196 * in onMostVisitedChange() tiles remain visible so no flickering occurs. 197 * @type {boolean} 198 */ 199 var userInitiatedMostVisitedChange = false; 200 201 202 /** 203 * The browser embeddedSearch.newTabPage object. 204 * @type {Object} 205 */ 206 var ntpApiHandle; 207 208 209 /** 210 * The browser embeddedSearch.searchBox object. 211 * @type {Object} 212 */ 213 var searchboxApiHandle; 214 215 216 /** 217 * The state of the NTP when a query is entered into the Omnibox. 218 * @type {NTP_DISPOSE_STATE} 219 */ 220 var omniboxInputBehavior = NTP_DISPOSE_STATE.NONE; 221 222 223 /** 224 * The state of the NTP when a query is entered into the Fakebox. 225 * @type {NTP_DISPOSE_STATE} 226 */ 227 var fakeboxInputBehavior = NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO; 228 229 230 /** @type {number} @const */ 231 var MAX_NUM_TILES_TO_SHOW = 8; 232 233 234 /** @type {number} @const */ 235 var MIN_NUM_COLUMNS = 2; 236 237 238 /** @type {number} @const */ 239 var MAX_NUM_COLUMNS = 4; 240 241 242 /** @type {number} @const */ 243 var NUM_ROWS = 2; 244 245 246 /** 247 * Minimum total padding to give to the left and right of the most visited 248 * section. Used to determine how many tiles to show. 249 * @type {number} 250 * @const 251 */ 252 var MIN_TOTAL_HORIZONTAL_PADDING = 200; 253 254 255 /** 256 * The filename for a most visited iframe src which shows a page title. 257 * @type {string} 258 * @const 259 */ 260 var MOST_VISITED_TITLE_IFRAME = 'title.html'; 261 262 263 /** 264 * The filename for a most visited iframe src which shows a thumbnail image. 265 * @type {string} 266 * @const 267 */ 268 var MOST_VISITED_THUMBNAIL_IFRAME = 'thumbnail.html'; 269 270 271 /** 272 * The color of the title in RRGGBBAA format. 273 * @type {?string} 274 */ 275 var titleColor = null; 276 277 278 /** 279 * Hide most visited tiles for at most this many milliseconds while painting. 280 * @type {number} 281 * @const 282 */ 283 var MOST_VISITED_PAINT_TIMEOUT_MSEC = 500; 284 285 286 /** 287 * A Tile is either a rendering of a Most Visited page or "filler" used to 288 * pad out the section when not enough pages exist. 289 * 290 * @param {Element} elem The element for rendering the tile. 291 * @param {Element=} opt_innerElem The element for contents of tile. 292 * @param {Element=} opt_titleElem The element for rendering the title. 293 * @param {Element=} opt_thumbnailElem The element for rendering the thumbnail. 294 * @param {number=} opt_rid The RID for the corresponding Most Visited page. 295 * Should only be left unspecified when creating a filler tile. 296 * @constructor 297 */ 298 function Tile(elem, opt_innerElem, opt_titleElem, opt_thumbnailElem, opt_rid) { 299 /** @type {Element} */ 300 this.elem = elem; 301 302 /** @type {Element|undefined} */ 303 this.innerElem = opt_innerElem; 304 305 /** @type {Element|undefined} */ 306 this.titleElem = opt_titleElem; 307 308 /** @type {Element|undefined} */ 309 this.thumbnailElem = opt_thumbnailElem; 310 311 /** @type {number|undefined} */ 312 this.rid = opt_rid; 313 } 314 315 316 /** 317 * Heuristic to determine whether a theme should be considered to be dark, so 318 * the colors of various UI elements can be adjusted. 319 * @param {ThemeBackgroundInfo|undefined} info Theme background information. 320 * @return {boolean} Whether the theme is dark. 321 * @private 322 */ 323 function getIsThemeDark(info) { 324 if (!info) 325 return false; 326 // Heuristic: light text implies dark theme. 327 var rgba = info.textColorRgba; 328 var luminance = 0.3 * rgba[0] + 0.59 * rgba[1] + 0.11 * rgba[2]; 329 return luminance >= 128; 330 } 331 332 333 /** 334 * Updates the NTP based on the current theme. 335 * @private 336 */ 337 function renderTheme() { 338 var fakeboxText = $(IDS.FAKEBOX_TEXT); 339 if (fakeboxText) { 340 fakeboxText.innerHTML = ''; 341 if (NTP_DESIGN.showFakeboxHint && 342 configData.translatedStrings.searchboxPlaceholder) { 343 fakeboxText.textContent = 344 configData.translatedStrings.searchboxPlaceholder; 345 } 346 } 347 348 var info = ntpApiHandle.themeBackgroundInfo; 349 var isThemeDark = getIsThemeDark(info); 350 ntpContents.classList.toggle(CLASSES.DARK, isThemeDark); 351 if (!info) { 352 titleColor = NTP_DESIGN.titleColor; 353 return; 354 } 355 356 if (!info.usingDefaultTheme && info.textColorRgba) { 357 titleColor = convertToRRGGBBAAColor(info.textColorRgba); 358 } else { 359 titleColor = isThemeDark ? 360 NTP_DESIGN.titleColorAgainstDark : NTP_DESIGN.titleColor; 361 } 362 363 var background = [convertToRGBAColor(info.backgroundColorRgba), 364 info.imageUrl, 365 info.imageTiling, 366 info.imageHorizontalAlignment, 367 info.imageVerticalAlignment].join(' ').trim(); 368 369 document.body.style.background = background; 370 document.body.classList.toggle(CLASSES.ALTERNATE_LOGO, info.alternateLogo); 371 updateThemeAttribution(info.attributionUrl); 372 setCustomThemeStyle(info); 373 } 374 375 376 /** 377 * Updates the NTP based on the current theme, then rerenders all tiles. 378 * @private 379 */ 380 function onThemeChange() { 381 renderTheme(); 382 tilesContainer.innerHTML = ''; 383 renderAndShowTiles(); 384 } 385 386 387 /** 388 * Updates the NTP style according to theme. 389 * @param {Object=} opt_themeInfo The information about the theme. If it is 390 * omitted the style will be reverted to the default. 391 * @private 392 */ 393 function setCustomThemeStyle(opt_themeInfo) { 394 var customStyleElement = $(IDS.CUSTOM_THEME_STYLE); 395 var head = document.head; 396 if (opt_themeInfo && !opt_themeInfo.usingDefaultTheme) { 397 ntpContents.classList.remove(CLASSES.DEFAULT_THEME); 398 var themeStyle = 399 '#attribution {' + 400 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' + 401 '}' + 402 '#mv-msg {' + 403 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorRgba) + ';' + 404 '}' + 405 '#mv-notice-links span {' + 406 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' + 407 '}' + 408 '#mv-notice-x {' + 409 ' -webkit-filter: drop-shadow(0 0 0 ' + 410 convertToRGBAColor(opt_themeInfo.textColorRgba) + ');' + 411 '}' + 412 '.mv-page-ready .mv-mask {' + 413 ' border: 1px solid ' + 414 convertToRGBAColor(opt_themeInfo.sectionBorderColorRgba) + ';' + 415 '}' + 416 '.mv-page-ready:hover .mv-mask, .mv-page-ready .mv-focused ~ .mv-mask {' + 417 ' border-color: ' + 418 convertToRGBAColor(opt_themeInfo.headerColorRgba) + ';' + 419 '}'; 420 421 if (customStyleElement) { 422 customStyleElement.textContent = themeStyle; 423 } else { 424 customStyleElement = document.createElement('style'); 425 customStyleElement.type = 'text/css'; 426 customStyleElement.id = IDS.CUSTOM_THEME_STYLE; 427 customStyleElement.textContent = themeStyle; 428 head.appendChild(customStyleElement); 429 } 430 431 } else { 432 ntpContents.classList.add(CLASSES.DEFAULT_THEME); 433 if (customStyleElement) 434 head.removeChild(customStyleElement); 435 } 436 } 437 438 439 /** 440 * Renders the attribution if the URL is present, otherwise hides it. 441 * @param {string} url The URL of the attribution image, if any. 442 * @private 443 */ 444 function updateThemeAttribution(url) { 445 if (!url) { 446 setAttributionVisibility_(false); 447 return; 448 } 449 450 var attributionImage = attribution.querySelector('img'); 451 if (!attributionImage) { 452 attributionImage = new Image(); 453 attribution.appendChild(attributionImage); 454 } 455 attributionImage.style.content = url; 456 setAttributionVisibility_(true); 457 } 458 459 460 /** 461 * Sets the visibility of the theme attribution. 462 * @param {boolean} show True to show the attribution. 463 * @private 464 */ 465 function setAttributionVisibility_(show) { 466 if (attribution) { 467 attribution.style.display = show ? '' : 'none'; 468 } 469 } 470 471 472 /** 473 * Converts an Array of color components into RRGGBBAA format. 474 * @param {Array.<number>} color Array of rgba color components. 475 * @return {string} Color string in RRGGBBAA format. 476 * @private 477 */ 478 function convertToRRGGBBAAColor(color) { 479 return color.map(function(t) { 480 return ('0' + t.toString(16)).slice(-2); // To 2-digit, 0-padded hex. 481 }).join(''); 482 } 483 484 485 /** 486 * Converts an Array of color components into RGBA format "rgba(R,G,B,A)". 487 * @param {Array.<number>} color Array of rgba color components. 488 * @return {string} CSS color in RGBA format. 489 * @private 490 */ 491 function convertToRGBAColor(color) { 492 return 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' + 493 color[3] / 255 + ')'; 494 } 495 496 497 /** 498 * Handles a new set of Most Visited page data. 499 */ 500 function onMostVisitedChange() { 501 if (isBlacklisting) { 502 // Trigger the blacklist animation, which then triggers reloadAllTiles(). 503 var lastBlacklistedTileElem = lastBlacklistedTile.elem; 504 lastBlacklistedTileElem.addEventListener( 505 'webkitTransitionEnd', blacklistAnimationDone); 506 lastBlacklistedTileElem.classList.add(CLASSES.BLACKLIST); 507 } else { 508 reloadAllTiles(); 509 } 510 } 511 512 513 /** 514 * Handles the end of the blacklist animation by showing the notification and 515 * re-rendering the new set of tiles. 516 */ 517 function blacklistAnimationDone() { 518 showNotification(); 519 isBlacklisting = false; 520 tilesContainer.classList.remove(CLASSES.HIDE_BLACKLIST_BUTTON); 521 lastBlacklistedTile.elem.removeEventListener( 522 'webkitTransitionEnd', blacklistAnimationDone); 523 // Need to call explicitly to re-render the tiles, since the initial 524 // onmostvisitedchange issued by the blacklist function only triggered 525 // the animation. 526 reloadAllTiles(); 527 } 528 529 530 /** 531 * Fetches new data, creates, and renders tiles. 532 */ 533 function reloadAllTiles() { 534 var pages = ntpApiHandle.mostVisited; 535 536 tiles = []; 537 for (var i = 0; i < MAX_NUM_TILES_TO_SHOW; ++i) 538 tiles.push(createTile(pages[i], i)); 539 540 tilesContainer.innerHTML = ''; 541 renderAndShowTiles(); 542 } 543 544 545 /** 546 * Binds onload events for a tile's internal <iframe> elements. 547 * @param {Tile} tile The main tile to bind events to. 548 * @param {Barrier} tileVisibilityBarrier A barrier to make all tiles visible 549 * the moment all tiles are loaded. 550 */ 551 function bindTileOnloadEvents(tile, tileVisibilityBarrier) { 552 if (tile.titleElem) { 553 tileVisibilityBarrier.add(); 554 tile.titleElem.onload = function() { 555 tileVisibilityBarrier.remove(); 556 }; 557 } 558 if (tile.thumbnailElem) { 559 tileVisibilityBarrier.add(); 560 tile.thumbnailElem.onload = function() { 561 tile.elem.classList.add(CLASSES.PAGE_READY); 562 tileVisibilityBarrier.remove(); 563 }; 564 } 565 } 566 567 568 /** 569 * Renders the current list of visible tiles to DOM, and hides tiles that are 570 * already in the DOM but should not be seen. 571 */ 572 function renderAndShowTiles() { 573 var numExisting = tilesContainer.querySelectorAll('.' + CLASSES.TILE).length; 574 // Only add visible tiles to the DOM, to avoid creating invisible tiles that 575 // produce meaningless impression metrics. However, if a tile becomes 576 // invisible then we leave it in DOM to prevent reload if it's shown again. 577 var numDesired = Math.min(tiles.length, numColumnsShown * NUM_ROWS); 578 579 // If we need to render new tiles, manage the visibility to hide intermediate 580 // load states of the <iframe>s. 581 if (numExisting < numDesired) { 582 var showAll = function() { 583 for (var i = 0; i < numDesired; ++i) { 584 if (tiles[i].titleElem || tiles[i].thumbnailElem) 585 tiles[i].elem.classList.add(CLASSES.PAGE_READY); 586 } 587 }; 588 var tileVisibilityBarrier = new Barrier(showAll); 589 590 if (!userInitiatedMostVisitedChange) { 591 // Make titleContainer invisible, but still taking up space. 592 // titleContainer becomes visible again (1) on timeout, or (2) when all 593 // tiles finish loading (using tileVisibilityBarrier). 594 window.setTimeout(function() { 595 tileVisibilityBarrier.cancel(); 596 showAll(); 597 }, MOST_VISITED_PAINT_TIMEOUT_MSEC); 598 } 599 userInitiatedMostVisitedChange = false; 600 601 for (var i = numExisting; i < numDesired; ++i) { 602 bindTileOnloadEvents(tiles[i], tileVisibilityBarrier); 603 tilesContainer.appendChild(tiles[i].elem); 604 } 605 } 606 607 // Show only the desired tiles. Note that .hidden does not work for 608 // inline-block elements like tiles[i].elem. 609 for (var i = 0; i < numDesired; ++i) 610 tiles[i].elem.style.display = 'inline-block'; 611 // If |numDesired| < |numExisting| then hide extra tiles (e.g., this occurs 612 // when window is downsized). 613 for (; i < numExisting; ++i) 614 tiles[i].elem.style.display = 'none'; 615 } 616 617 618 /** 619 * Builds a URL to display a most visited tile title in an iframe. 620 * @param {number} rid The restricted ID. 621 * @param {number} position The position of the iframe in the UI. 622 * @return {string} An URL to display the most visited title in an iframe. 623 */ 624 function getMostVisitedTitleIframeUrl(rid, position) { 625 var url = 'chrome-search://most-visited/' + 626 encodeURIComponent(MOST_VISITED_TITLE_IFRAME); 627 var params = [ 628 'rid=' + encodeURIComponent(rid), 629 'f=' + encodeURIComponent(NTP_DESIGN.fontFamily), 630 'fs=' + encodeURIComponent(NTP_DESIGN.fontSize), 631 'c=' + encodeURIComponent(titleColor), 632 'pos=' + encodeURIComponent(position)]; 633 if (NTP_DESIGN.titleTextAlign) 634 params.push('ta=' + encodeURIComponent(NTP_DESIGN.titleTextAlign)); 635 if (NTP_DESIGN.titleTextFade) 636 params.push('tf=' + encodeURIComponent(NTP_DESIGN.titleTextFade)); 637 return url + '?' + params.join('&'); 638 } 639 640 641 /** 642 * Builds a URL to display a most visited tile thumbnail in an iframe. 643 * @param {number} rid The restricted ID. 644 * @param {number} position The position of the iframe in the UI. 645 * @return {string} An URL to display the most visited thumbnail in an iframe. 646 */ 647 function getMostVisitedThumbnailIframeUrl(rid, position) { 648 var url = 'chrome-search://most-visited/' + 649 encodeURIComponent(MOST_VISITED_THUMBNAIL_IFRAME); 650 var params = [ 651 'rid=' + encodeURIComponent(rid), 652 'f=' + encodeURIComponent(NTP_DESIGN.fontFamily), 653 'fs=' + encodeURIComponent(NTP_DESIGN.fontSize), 654 'c=' + encodeURIComponent(NTP_DESIGN.thumbnailTextColor), 655 'pos=' + encodeURIComponent(position)]; 656 if (NTP_DESIGN.thumbnailFallback) 657 params.push('etfb=1'); 658 return url + '?' + params.join('&'); 659 } 660 661 662 /** 663 * Creates a Tile with the specified page data. If no data is provided, a 664 * filler Tile is created. 665 * @param {Object} page The page data. 666 * @param {number} position The position of the tile. 667 * @return {Tile} The new Tile. 668 */ 669 function createTile(page, position) { 670 var tileElem = document.createElement('div'); 671 tileElem.classList.add(CLASSES.TILE); 672 // Prevent tile from being selected (and highlighted) when areas outside the 673 // <iframe>s are clicked. 674 tileElem.addEventListener('mousedown', function(e) { 675 e.preventDefault(); 676 }); 677 var innerElem = createAndAppendElement(tileElem, 'div', CLASSES.TILE_INNER); 678 679 if (page) { 680 var rid = page.rid; 681 tileElem.classList.add(CLASSES.PAGE); 682 683 var navigateFunction = function(e) { 684 e.preventDefault(); 685 ntpApiHandle.navigateContentWindow(rid, getDispositionFromEvent(e)); 686 }; 687 688 // The click handler for navigating to the page identified by the RID. 689 tileElem.addEventListener('click', navigateFunction); 690 691 // The iframe which renders the page title. 692 var titleElem = document.createElement('iframe'); 693 // Enable tab navigation on the iframe, which will move the selection to the 694 // link element (which also has a tabindex). 695 titleElem.tabIndex = '0'; 696 697 // Why iframes have IDs: 698 // 699 // On navigating back to the NTP we see several onmostvisitedchange() events 700 // in series with incrementing RIDs. After the first event, a set of iframes 701 // begins loading RIDs n, n+1, ..., n+k-1; after the second event, these get 702 // destroyed and a new set begins loading RIDs n+k, n+k+1, ..., n+2k-1. 703 // Now due to crbug.com/68841, Chrome incorrectly loads the content for the 704 // first set of iframes into the most recent set of iframes. 705 // 706 // Giving iframes distinct ids seems to cause some invalidation and prevent 707 // associating the incorrect data. 708 // 709 // TODO(jered): Find and fix the root (probably Blink) bug. 710 711 // Keep this ID here. See comment above. 712 titleElem.id = 'title-' + rid; 713 titleElem.className = CLASSES.TITLE; 714 titleElem.src = getMostVisitedTitleIframeUrl(rid, position); 715 innerElem.appendChild(titleElem); 716 717 // A fallback element for missing thumbnails. 718 if (NTP_DESIGN.thumbnailFallback) { 719 var fallbackElem = createAndAppendElement( 720 innerElem, 'div', CLASSES.THUMBNAIL_FALLBACK); 721 if (NTP_DESIGN.thumbnailFallback === THUMBNAIL_FALLBACK.DOT) 722 createAndAppendElement(fallbackElem, 'div', CLASSES.DOT); 723 } 724 725 // The iframe which renders either a thumbnail or domain element. 726 var thumbnailElem = document.createElement('iframe'); 727 thumbnailElem.tabIndex = '-1'; 728 thumbnailElem.setAttribute('aria-hidden', 'true'); 729 // Keep this ID here. See comment above. 730 thumbnailElem.id = 'thumb-' + rid; 731 thumbnailElem.className = CLASSES.THUMBNAIL; 732 thumbnailElem.src = getMostVisitedThumbnailIframeUrl(rid, position); 733 innerElem.appendChild(thumbnailElem); 734 735 // The button used to blacklist this page. 736 var blacklistButton = createAndAppendElement( 737 innerElem, 'div', CLASSES.BLACKLIST_BUTTON); 738 createAndAppendElement( 739 blacklistButton, 'div', CLASSES.BLACKLIST_BUTTON_INNER); 740 var blacklistFunction = generateBlacklistFunction(rid); 741 blacklistButton.addEventListener('click', blacklistFunction); 742 blacklistButton.title = configData.translatedStrings.removeThumbnailTooltip; 743 744 // A helper mask on top of the tile that is used to create hover border 745 // and/or to darken the thumbnail on focus. 746 var maskElement = createAndAppendElement( 747 innerElem, 'div', CLASSES.THUMBNAIL_MASK); 748 749 // The page favicon, or a fallback. 750 var favicon = createAndAppendElement(innerElem, 'div', CLASSES.FAVICON); 751 if (page.faviconUrl) { 752 favicon.style.backgroundImage = 'url(' + page.faviconUrl + ')'; 753 } else { 754 favicon.classList.add(CLASSES.FAVICON_FALLBACK); 755 } 756 return new Tile(tileElem, innerElem, titleElem, thumbnailElem, rid); 757 } else { 758 return new Tile(tileElem); 759 } 760 } 761 762 763 /** 764 * Generates a function to be called when the page with the corresponding RID 765 * is blacklisted. 766 * @param {number} rid The RID of the page being blacklisted. 767 * @return {function(Event=)} A function which handles the blacklisting of the 768 * page by updating state variables and notifying Chrome. 769 */ 770 function generateBlacklistFunction(rid) { 771 return function(e) { 772 // Prevent navigation when the page is being blacklisted. 773 if (e) 774 e.stopPropagation(); 775 776 userInitiatedMostVisitedChange = true; 777 isBlacklisting = true; 778 tilesContainer.classList.add(CLASSES.HIDE_BLACKLIST_BUTTON); 779 lastBlacklistedTile = getTileByRid(rid); 780 ntpApiHandle.deleteMostVisitedItem(rid); 781 }; 782 } 783 784 785 /** 786 * Shows the blacklist notification and triggers a delay to hide it. 787 */ 788 function showNotification() { 789 notification.classList.remove(CLASSES.HIDE_NOTIFICATION); 790 notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION); 791 notification.scrollTop; 792 notification.classList.add(CLASSES.DELAYED_HIDE_NOTIFICATION); 793 } 794 795 796 /** 797 * Hides the blacklist notification. 798 */ 799 function hideNotification() { 800 notification.classList.add(CLASSES.HIDE_NOTIFICATION); 801 notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION); 802 } 803 804 805 /** 806 * Handles a click on the notification undo link by hiding the notification and 807 * informing Chrome. 808 */ 809 function onUndo() { 810 userInitiatedMostVisitedChange = true; 811 hideNotification(); 812 var lastBlacklistedRID = lastBlacklistedTile.rid; 813 if (typeof lastBlacklistedRID != 'undefined') 814 ntpApiHandle.undoMostVisitedDeletion(lastBlacklistedRID); 815 } 816 817 818 /** 819 * Handles a click on the restore all notification link by hiding the 820 * notification and informing Chrome. 821 */ 822 function onRestoreAll() { 823 userInitiatedMostVisitedChange = true; 824 hideNotification(); 825 ntpApiHandle.undoAllMostVisitedDeletions(); 826 } 827 828 829 /** 830 * Recomputes the number of tile columns, and width of various contents based 831 * on the width of the window. 832 * @return {boolean} Whether the number of tile columns has changed. 833 */ 834 function updateContentWidth() { 835 var tileRequiredWidth = NTP_DESIGN.tileWidth + NTP_DESIGN.tileMargin; 836 // If innerWidth is zero, then use the maximum snap size. 837 var maxSnapSize = MAX_NUM_COLUMNS * tileRequiredWidth - 838 NTP_DESIGN.tileMargin + MIN_TOTAL_HORIZONTAL_PADDING; 839 var innerWidth = window.innerWidth || maxSnapSize; 840 // Each tile has left and right margins that sum to NTP_DESIGN.tileMargin. 841 var availableWidth = innerWidth + NTP_DESIGN.tileMargin - 842 MIN_TOTAL_HORIZONTAL_PADDING; 843 var newNumColumns = Math.floor(availableWidth / tileRequiredWidth); 844 if (newNumColumns < MIN_NUM_COLUMNS) 845 newNumColumns = MIN_NUM_COLUMNS; 846 else if (newNumColumns > MAX_NUM_COLUMNS) 847 newNumColumns = MAX_NUM_COLUMNS; 848 849 if (numColumnsShown === newNumColumns) 850 return false; 851 852 numColumnsShown = newNumColumns; 853 var tilesContainerWidth = numColumnsShown * tileRequiredWidth; 854 tilesContainer.style.width = tilesContainerWidth + 'px'; 855 if (fakebox) { 856 // -2 to account for border. 857 var fakeboxWidth = (tilesContainerWidth - NTP_DESIGN.tileMargin - 2); 858 fakebox.style.width = fakeboxWidth + 'px'; 859 } 860 return true; 861 } 862 863 864 /** 865 * Resizes elements because the number of tile columns may need to change in 866 * response to resizing. Also shows or hides extra tiles tiles according to the 867 * new width of the page. 868 */ 869 function onResize() { 870 if (updateContentWidth()) { 871 // Render without clearing tiles. 872 renderAndShowTiles(); 873 } 874 } 875 876 877 /** 878 * Returns the tile corresponding to the specified page RID. 879 * @param {number} rid The page RID being looked up. 880 * @return {Tile} The corresponding tile. 881 */ 882 function getTileByRid(rid) { 883 for (var i = 0, length = tiles.length; i < length; ++i) { 884 var tile = tiles[i]; 885 if (tile.rid == rid) 886 return tile; 887 } 888 return null; 889 } 890 891 892 /** 893 * Handles new input by disposing the NTP, according to where the input was 894 * entered. 895 */ 896 function onInputStart() { 897 if (fakebox && isFakeboxFocused()) { 898 setFakeboxFocus(false); 899 setFakeboxDragFocus(false); 900 disposeNtp(true); 901 } else if (!isFakeboxFocused()) { 902 disposeNtp(false); 903 } 904 } 905 906 907 /** 908 * Disposes the NTP, according to where the input was entered. 909 * @param {boolean} wasFakeboxInput True if the input was in the fakebox. 910 */ 911 function disposeNtp(wasFakeboxInput) { 912 var behavior = wasFakeboxInput ? fakeboxInputBehavior : omniboxInputBehavior; 913 if (behavior == NTP_DISPOSE_STATE.DISABLE_FAKEBOX) 914 setFakeboxActive(false); 915 else if (behavior == NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO) 916 setFakeboxAndLogoVisibility(false); 917 } 918 919 920 /** 921 * Restores the NTP (re-enables the fakebox and unhides the logo.) 922 */ 923 function restoreNtp() { 924 setFakeboxActive(true); 925 setFakeboxAndLogoVisibility(true); 926 } 927 928 929 /** 930 * @param {boolean} focus True to focus the fakebox. 931 */ 932 function setFakeboxFocus(focus) { 933 document.body.classList.toggle(CLASSES.FAKEBOX_FOCUS, focus); 934 } 935 936 /** 937 * @param {boolean} focus True to show a dragging focus to the fakebox. 938 */ 939 function setFakeboxDragFocus(focus) { 940 document.body.classList.toggle(CLASSES.FAKEBOX_DRAG_FOCUS, focus); 941 } 942 943 /** 944 * @return {boolean} True if the fakebox has focus. 945 */ 946 function isFakeboxFocused() { 947 return document.body.classList.contains(CLASSES.FAKEBOX_FOCUS) || 948 document.body.classList.contains(CLASSES.FAKEBOX_DRAG_FOCUS); 949 } 950 951 952 /** 953 * @param {boolean} enable True to enable the fakebox. 954 */ 955 function setFakeboxActive(enable) { 956 document.body.classList.toggle(CLASSES.FAKEBOX_DISABLE, !enable); 957 } 958 959 960 /** 961 * @param {!Event} event The click event. 962 * @return {boolean} True if the click occurred in an enabled fakebox. 963 */ 964 function isFakeboxClick(event) { 965 return fakebox.contains(event.target) && 966 !document.body.classList.contains(CLASSES.FAKEBOX_DISABLE); 967 } 968 969 970 /** 971 * @param {boolean} show True to show the fakebox and logo. 972 */ 973 function setFakeboxAndLogoVisibility(show) { 974 document.body.classList.toggle(CLASSES.HIDE_FAKEBOX_AND_LOGO, !show); 975 } 976 977 978 /** 979 * Shortcut for document.getElementById. 980 * @param {string} id of the element. 981 * @return {HTMLElement} with the id. 982 */ 983 function $(id) { 984 return document.getElementById(id); 985 } 986 987 988 /** 989 * Utility function which creates an element with an optional classname and 990 * appends it to the specified parent. 991 * @param {Element} parent The parent to append the new element. 992 * @param {string} name The name of the new element. 993 * @param {string=} opt_class The optional classname of the new element. 994 * @return {Element} The new element. 995 */ 996 function createAndAppendElement(parent, name, opt_class) { 997 var child = document.createElement(name); 998 if (opt_class) 999 child.classList.add(opt_class); 1000 parent.appendChild(child); 1001 return child; 1002 } 1003 1004 1005 /** 1006 * Removes a node from its parent. 1007 * @param {Node} node The node to remove. 1008 */ 1009 function removeNode(node) { 1010 node.parentNode.removeChild(node); 1011 } 1012 1013 1014 /** 1015 * @param {!Element} element The element to register the handler for. 1016 * @param {number} keycode The keycode of the key to register. 1017 * @param {!Function} handler The key handler to register. 1018 */ 1019 function registerKeyHandler(element, keycode, handler) { 1020 element.addEventListener('keydown', function(event) { 1021 if (event.keyCode == keycode) 1022 handler(event); 1023 }); 1024 } 1025 1026 1027 /** 1028 * @return {Object} the handle to the embeddedSearch API. 1029 */ 1030 function getEmbeddedSearchApiHandle() { 1031 if (window.cideb) 1032 return window.cideb; 1033 if (window.chrome && window.chrome.embeddedSearch) 1034 return window.chrome.embeddedSearch; 1035 return null; 1036 } 1037 1038 1039 /** 1040 * Event handler for the focus changed and blacklist messages on link elements. 1041 * Used to toggle visual treatment on the tiles (depending on the message). 1042 * @param {Event} event Event received. 1043 */ 1044 function handlePostMessage(event) { 1045 if (event.origin !== 'chrome-search://most-visited') 1046 return; 1047 1048 if (event.data === 'linkFocused') { 1049 var activeElement = document.activeElement; 1050 if (activeElement.classList.contains(CLASSES.TITLE)) { 1051 activeElement.classList.add(CLASSES.FOCUSED); 1052 focusedIframe = activeElement; 1053 } 1054 } else if (event.data === 'linkBlurred') { 1055 if (focusedIframe) 1056 focusedIframe.classList.remove(CLASSES.FOCUSED); 1057 focusedIframe = null; 1058 } else if (event.data.indexOf('tileBlacklisted') === 0) { 1059 var tilePosition = event.data.split(',')[1]; 1060 if (tilePosition) 1061 generateBlacklistFunction(tiles[parseInt(tilePosition, 10)].rid)(); 1062 } 1063 } 1064 1065 1066 /** 1067 * Prepares the New Tab Page by adding listeners, rendering the current 1068 * theme, the most visited pages section, and Google-specific elements for a 1069 * Google-provided page. 1070 */ 1071 function init() { 1072 tilesContainer = $(IDS.TILES); 1073 notification = $(IDS.NOTIFICATION); 1074 attribution = $(IDS.ATTRIBUTION); 1075 ntpContents = $(IDS.NTP_CONTENTS); 1076 1077 if (configData.isGooglePage) { 1078 var logo = document.createElement('div'); 1079 logo.id = IDS.LOGO; 1080 1081 fakebox = document.createElement('div'); 1082 fakebox.id = IDS.FAKEBOX; 1083 var fakeboxHtml = []; 1084 fakeboxHtml.push('<input id="' + IDS.FAKEBOX_INPUT + 1085 '" autocomplete="off" tabindex="-1" type="url" aria-hidden="true">'); 1086 fakeboxHtml.push('<div id="' + IDS.FAKEBOX_TEXT + '"></div>'); 1087 fakeboxHtml.push('<div id="cursor"></div>'); 1088 fakebox.innerHTML = fakeboxHtml.join(''); 1089 1090 ntpContents.insertBefore(fakebox, ntpContents.firstChild); 1091 ntpContents.insertBefore(logo, ntpContents.firstChild); 1092 } else { 1093 document.body.classList.add(CLASSES.NON_GOOGLE_PAGE); 1094 } 1095 1096 // Hide notifications after fade out, so we can't focus on links via keyboard. 1097 notification.addEventListener('webkitTransitionEnd', hideNotification); 1098 1099 var notificationMessage = $(IDS.NOTIFICATION_MESSAGE); 1100 notificationMessage.textContent = 1101 configData.translatedStrings.thumbnailRemovedNotification; 1102 1103 var undoLink = $(IDS.UNDO_LINK); 1104 undoLink.addEventListener('click', onUndo); 1105 registerKeyHandler(undoLink, KEYCODE.ENTER, onUndo); 1106 undoLink.textContent = configData.translatedStrings.undoThumbnailRemove; 1107 1108 var restoreAllLink = $(IDS.RESTORE_ALL_LINK); 1109 restoreAllLink.addEventListener('click', onRestoreAll); 1110 registerKeyHandler(restoreAllLink, KEYCODE.ENTER, onUndo); 1111 restoreAllLink.textContent = 1112 configData.translatedStrings.restoreThumbnailsShort; 1113 1114 $(IDS.ATTRIBUTION_TEXT).textContent = 1115 configData.translatedStrings.attributionIntro; 1116 1117 var notificationCloseButton = $(IDS.NOTIFICATION_CLOSE_BUTTON); 1118 createAndAppendElement( 1119 notificationCloseButton, 'div', CLASSES.BLACKLIST_BUTTON_INNER); 1120 notificationCloseButton.addEventListener('click', hideNotification); 1121 1122 window.addEventListener('resize', onResize); 1123 updateContentWidth(); 1124 1125 var topLevelHandle = getEmbeddedSearchApiHandle(); 1126 1127 ntpApiHandle = topLevelHandle.newTabPage; 1128 ntpApiHandle.onthemechange = onThemeChange; 1129 ntpApiHandle.onmostvisitedchange = onMostVisitedChange; 1130 1131 ntpApiHandle.oninputstart = onInputStart; 1132 ntpApiHandle.oninputcancel = restoreNtp; 1133 1134 if (ntpApiHandle.isInputInProgress) 1135 onInputStart(); 1136 1137 renderTheme(); 1138 onMostVisitedChange(); 1139 1140 searchboxApiHandle = topLevelHandle.searchBox; 1141 1142 if (fakebox) { 1143 // Listener for updating the key capture state. 1144 document.body.onmousedown = function(event) { 1145 if (isFakeboxClick(event)) 1146 searchboxApiHandle.startCapturingKeyStrokes(); 1147 else if (isFakeboxFocused()) 1148 searchboxApiHandle.stopCapturingKeyStrokes(); 1149 }; 1150 searchboxApiHandle.onkeycapturechange = function() { 1151 setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled); 1152 }; 1153 var inputbox = $(IDS.FAKEBOX_INPUT); 1154 if (inputbox) { 1155 inputbox.onpaste = function(event) { 1156 event.preventDefault(); 1157 searchboxApiHandle.paste(); 1158 }; 1159 inputbox.ondrop = function(event) { 1160 event.preventDefault(); 1161 var text = event.dataTransfer.getData('text/plain'); 1162 if (text) { 1163 searchboxApiHandle.paste(text); 1164 } 1165 }; 1166 inputbox.ondragenter = function() { 1167 setFakeboxDragFocus(true); 1168 }; 1169 inputbox.ondragleave = function() { 1170 setFakeboxDragFocus(false); 1171 }; 1172 } 1173 1174 // Update the fakebox style to match the current key capturing state. 1175 setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled); 1176 } 1177 1178 if (searchboxApiHandle.rtl) { 1179 $(IDS.NOTIFICATION).dir = 'rtl'; 1180 document.body.setAttribute('dir', 'rtl'); 1181 // Add class for setting alignments based on language directionality. 1182 document.body.classList.add(CLASSES.RTL); 1183 $(IDS.TILES).dir = 'rtl'; 1184 } 1185 1186 window.addEventListener('message', handlePostMessage); 1187 } 1188 1189 1190 /** 1191 * Binds event listeners. 1192 */ 1193 function listen() { 1194 document.addEventListener('DOMContentLoaded', init); 1195 } 1196 1197 return { 1198 init: init, 1199 listen: listen 1200 }; 1201 } 1202 1203 if (!window.localNTPUnitTest) { 1204 LocalNTP().listen(); 1205 } 1206