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