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 */ 14 function LocalNTP() { 15 <include src="../../../../ui/webui/resources/js/assert.js"> 16 17 18 19 /** 20 * Enum for classnames. 21 * @enum {string} 22 * @const 23 */ 24 var 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 */ 55 var 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 */ 77 var 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 */ 88 var 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 */ 100 var MIDDLE_MOUSE_BUTTON = 1; 101 102 103 /** 104 * Possible behaviors for navigateContentWindow. 105 * @enum {number} 106 */ 107 var 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 */ 117 var tilesContainer; 118 119 120 /** 121 * The notification displayed when a page is blacklisted. 122 * @type {Element} 123 */ 124 var notification; 125 126 127 /** 128 * The container for the theme attribution. 129 * @type {Element} 130 */ 131 var 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 */ 139 var fakebox; 140 141 142 /** 143 * The container for NTP elements. 144 * @type {Element} 145 */ 146 var ntpContents; 147 148 149 /** 150 * The array of rendered tiles, ordered by appearance. 151 * @type {!Array.<Tile>} 152 */ 153 var tiles = []; 154 155 156 /** 157 * The last blacklisted tile if any, which by definition should not be filler. 158 * @type {?Tile} 159 */ 160 var 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 */ 169 var 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 */ 177 var numColumnsShown = 0; 178 179 180 /** 181 * True if the user initiated the current most visited change and false 182 * otherwise. 183 * @type {boolean} 184 */ 185 var userInitiatedMostVisitedChange = false; 186 187 188 /** 189 * The browser embeddedSearch.newTabPage object. 190 * @type {Object} 191 */ 192 var ntpApiHandle; 193 194 195 /** 196 * The browser embeddedSearch.searchBox object. 197 * @type {Object} 198 */ 199 var searchboxApiHandle; 200 201 202 /** 203 * The state of the NTP when a query is entered into the Omnibox. 204 * @type {NTP_DISPOSE_STATE} 205 */ 206 var 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 */ 213 var 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 */ 221 var 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 */ 229 var TILE_MARGIN_START = 20; 230 231 232 /** @type {number} @const */ 233 var MAX_NUM_TILES_TO_SHOW = 8; 234 235 236 /** @type {number} @const */ 237 var MIN_NUM_COLUMNS = 2; 238 239 240 /** @type {number} @const */ 241 var MAX_NUM_COLUMNS = 4; 242 243 244 /** @type {number} @const */ 245 var 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 */ 254 var 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 */ 262 var 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 */ 270 var MOST_VISITED_THUMBNAIL_IFRAME = 'thumbnail.html'; 271 272 273 /** 274 * The hex color for most visited tile elements. 275 * @type {string} 276 * @const 277 */ 278 var MOST_VISITED_COLOR = '777777'; 279 280 281 /** 282 * The font family for most visited tile elements. 283 * @type {string} 284 * @const 285 */ 286 var 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 */ 294 var 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 */ 302 var 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 */ 314 function 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 */ 327 function 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 */ 351 function 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 */ 400 function 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 */ 421 function 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 */ 434 function 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 */ 443 function 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 */ 476 function 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 */ 493 function 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 */ 519 function 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 */ 537 function 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 */ 641 function 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 */ 658 function 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 */ 669 function 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 */ 678 function 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 */ 695 function 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 */ 708 function 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 */ 720 function 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 */ 758 function 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 */ 772 function 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 */ 787 function 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 */ 799 function restoreNtp() { 800 setFakeboxActive(true); 801 setFakeboxAndLogoVisibility(true); 802 } 803 804 805 /** 806 * @param {boolean} focus True to focus the fakebox. 807 */ 808 function 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 */ 815 function 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 */ 822 function 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 */ 831 function 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 */ 840 function 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 */ 849 function 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 */ 859 function $(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 */ 872 function 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 */ 885 function 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 */ 894 function 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 */ 904 function 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 */ 915 function 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 */ 929 function 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 */ 941 function 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 */ 1057 function listen() { 1058 document.addEventListener('DOMContentLoaded', init); 1059 } 1060 1061 return { 1062 init: init, 1063 listen: listen 1064 }; 1065 } 1066 1067 if (!window.localNTPUnitTest) { 1068 LocalNTP().listen(); 1069 } 1070