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