1// Copyright (c) 2011 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 * @fileoverview New tab page 7 * This is the main code for the new tab page used by touch-enabled Chrome 8 * browsers. For now this is still a prototype. 9 */ 10 11// Use an anonymous function to enable strict mode just for this file (which 12// will be concatenated with other files when embedded in Chrome 13cr.define('ntp4', function() { 14 'use strict'; 15 16 /** 17 * The CardSlider object to use for changing app pages. 18 * @type {CardSlider|undefined} 19 */ 20 var cardSlider; 21 22 /** 23 * Template to use for creating new 'dot' elements 24 * @type {!Element|undefined} 25 */ 26 var dotTemplate; 27 28 /** 29 * The 'page-list' element. 30 * @type {!Element|undefined} 31 */ 32 var pageList; 33 34 /** 35 * A list of all 'tile-page' elements. 36 * @type {!NodeList|undefined} 37 */ 38 var tilePages; 39 40 /** 41 * The Most Visited page. 42 * @type {!Element|undefined} 43 */ 44 var mostVisitedPage; 45 46 /** 47 * A list of all 'apps-page' elements. 48 * @type {!NodeList|undefined} 49 */ 50 var appsPages; 51 52 /** 53 * The 'dots-list' element. 54 * @type {!Element|undefined} 55 */ 56 var dotList; 57 58 /** 59 * A list of all 'dots' elements. 60 * @type {!NodeList|undefined} 61 */ 62 var dots; 63 64 /** 65 * The 'trash' element. Note that technically this is unnecessary, 66 * JavaScript creates the object for us based on the id. But I don't want 67 * to rely on the ID being the same, and JSCompiler doesn't know about it. 68 * @type {!Element|undefined} 69 */ 70 var trash; 71 72 /** 73 * The time in milliseconds for most transitions. This should match what's 74 * in new_tab.css. Unfortunately there's no better way to try to time 75 * something to occur until after a transition has completed. 76 * @type {number} 77 * @const 78 */ 79 var DEFAULT_TRANSITION_TIME = 500; 80 81 /** 82 * All the Grabber objects currently in use on the page 83 * @type {Array.<Grabber>} 84 */ 85 var grabbers = []; 86 87 /** 88 * Invoked at startup once the DOM is available to initialize the app. 89 */ 90 function initialize() { 91 // Load the current theme colors. 92 themeChanged(false); 93 94 dotList = getRequiredElement('dot-list'); 95 pageList = getRequiredElement('page-list'); 96 trash = getRequiredElement('trash'); 97 trash.hidden = true; 98 99 // Request data on the apps so we can fill them in. 100 // Note that this is kicked off asynchronously. 'getAppsCallback' will be 101 // invoked at some point after this function returns. 102 chrome.send('getApps'); 103 104 // Prevent touch events from triggering any sort of native scrolling 105 document.addEventListener('touchmove', function(e) { 106 e.preventDefault(); 107 }, true); 108 109 // Get the template elements and remove them from the DOM. Things are 110 // simpler if we start with 0 pages and 0 apps and don't leave hidden 111 // template elements behind in the DOM. 112 dots = dotList.getElementsByClassName('dot'); 113 assert(dots.length == 1, 114 'Expected exactly one dot in the dots-list.'); 115 dotTemplate = dots[0]; 116 dotList.removeChild(dots[0]); 117 118 tilePages = pageList.getElementsByClassName('tile-page'); 119 appsPages = pageList.getElementsByClassName('apps-page'); 120 121 // Initialize the cardSlider without any cards at the moment 122 var sliderFrame = getRequiredElement('card-slider-frame'); 123 cardSlider = new CardSlider(sliderFrame, pageList, [], 0, 124 sliderFrame.offsetWidth); 125 cardSlider.initialize(); 126 127 // Ensure the slider is resized appropriately with the window 128 window.addEventListener('resize', function() { 129 cardSlider.resize(sliderFrame.offsetWidth); 130 }); 131 132 // Handle the page being changed 133 pageList.addEventListener( 134 CardSlider.EventType.CARD_CHANGED, 135 function(e) { 136 // Update the active dot 137 var curDot = dotList.getElementsByClassName('selected')[0]; 138 if (curDot) 139 curDot.classList.remove('selected'); 140 var newPageIndex = e.cardSlider.currentCard; 141 dots[newPageIndex].classList.add('selected'); 142 // If an app was being dragged, move it to the end of the new page 143 if (draggingAppContainer) 144 appsPages[newPageIndex].appendChild(draggingAppContainer); 145 }); 146 147 // Add a drag handler to the body (for drags that don't land on an existing 148 // app) 149 document.addEventListener(Grabber.EventType.DRAG_ENTER, appDragEnter); 150 151 // Handle dropping an app anywhere other than on the trash 152 document.addEventListener(Grabber.EventType.DROP, appDrop); 153 154 // Add handles to manage the transition into/out-of rearrange mode 155 // Note that we assume here that we only use a Grabber for moving apps, 156 // so ANY GRAB event means we're enterring rearrange mode. 157 sliderFrame.addEventListener(Grabber.EventType.GRAB, enterRearrangeMode); 158 sliderFrame.addEventListener(Grabber.EventType.RELEASE, leaveRearrangeMode); 159 160 // Add handlers for the tash can 161 trash.addEventListener(Grabber.EventType.DRAG_ENTER, function(e) { 162 trash.classList.add('hover'); 163 e.grabbedElement.classList.add('trashing'); 164 e.stopPropagation(); 165 }); 166 trash.addEventListener(Grabber.EventType.DRAG_LEAVE, function(e) { 167 e.grabbedElement.classList.remove('trashing'); 168 trash.classList.remove('hover'); 169 }); 170 trash.addEventListener(Grabber.EventType.DROP, appTrash); 171 172 cr.ui.decorate($('recently-closed-menu-button'), ntp4.RecentMenuButton); 173 chrome.send('getRecentlyClosedTabs'); 174 175 mostVisitedPage = new ntp4.MostVisitedPage('Most Visited'); 176 appendTilePage(mostVisitedPage); 177 chrome.send('getMostVisited'); 178 } 179 180 /** 181 * Simple common assertion API 182 * @param {*} condition The condition to test. Note that this may be used to 183 * test whether a value is defined or not, and we don't want to force a 184 * cast to Boolean. 185 * @param {string=} opt_message A message to use in any error. 186 */ 187 function assert(condition, opt_message) { 188 'use strict'; 189 if (!condition) { 190 var msg = 'Assertion failed'; 191 if (opt_message) 192 msg = msg + ': ' + opt_message; 193 throw new Error(msg); 194 } 195 } 196 197 /** 198 * Get an element that's known to exist by its ID. We use this instead of just 199 * calling getElementById and not checking the result because this lets us 200 * satisfy the JSCompiler type system. 201 * @param {string} id The identifier name. 202 * @return {!Element} the Element. 203 */ 204 function getRequiredElement(id) { 205 var element = document.getElementById(id); 206 assert(element, 'Missing required element: ' + id); 207 return element; 208 } 209 210 /** 211 * Callback invoked by chrome with the apps available. 212 * 213 * Note that calls to this function can occur at any time, not just in 214 * response to a getApps request. For example, when a user installs/uninstalls 215 * an app on another synchronized devices. 216 * @param {Object} data An object with all the data on available 217 * applications. 218 */ 219 function getAppsCallback(data) { 220 // Clean up any existing grabber objects - cancelling any outstanding drag. 221 // Ideally an async app update wouldn't disrupt an active drag but 222 // that would require us to re-use existing elements and detect how the apps 223 // have changed, which would be a lot of work. 224 // Note that we have to explicitly clean up the grabber objects so they stop 225 // listening to events and break the DOM<->JS cycles necessary to enable 226 // collection of all these objects. 227 grabbers.forEach(function(g) { 228 // Note that this may raise DRAG_END/RELEASE events to clean up an 229 // oustanding drag. 230 g.dispose(); 231 }); 232 assert(!draggingAppContainer && !draggingAppOriginalPosition && 233 !draggingAppOriginalPage); 234 grabbers = []; 235 236 // Clear any existing apps pages and dots. 237 // TODO(rbyers): It might be nice to preserve animation of dots after an 238 // uninstall. Could we re-use the existing page and dot elements? It seems 239 // unfortunate to have Chrome send us the entire apps list after an 240 // uninstall. 241 for (var i = 0; i < appsPages.length; i++) { 242 var page = appsPages[i]; 243 var dot = page.navigationDot; 244 245 page.tearDown(); 246 page.parentNode.removeChild(page); 247 dot.parentNode.removeChild(dot); 248 } 249 250 // Get the array of apps and add any special synthesized entries 251 var apps = data.apps; 252 253 // Sort by launch index 254 apps.sort(function(a, b) { 255 return a.app_launch_index - b.app_launch_index; 256 }); 257 258 // Add the apps, creating pages as necessary 259 for (var i = 0; i < apps.length; i++) { 260 var app = apps[i]; 261 var pageIndex = (app.page_index || 0); 262 while (pageIndex >= appsPages.length) { 263 var origPageCount = appsPages.length; 264 appendTilePage(new ntp4.AppsPage('Apps')); 265 // Confirm that appsPages is a live object, updated when a new page is 266 // added (otherwise we'd have an infinite loop) 267 assert(appsPages.length == origPageCount + 1, 'expected new page'); 268 } 269 270 appsPages[pageIndex].appendApp(app); 271 } 272 273 // Add a couple blank apps pages for testing. TODO(estade): remove this. 274 appendTilePage(new ntp4.AppsPage('Foo')); 275 appendTilePage(new ntp4.AppsPage('Bar')); 276 277 // Tell the slider about the pages 278 updateSliderCards(); 279 280 // Mark the current page 281 dots[cardSlider.currentCard].classList.add('selected'); 282 } 283 284 /** 285 * Make a synthesized app object representing the chrome web store. It seems 286 * like this could just as easily come from the back-end, and then would 287 * support being rearranged, etc. 288 * @return {Object} The app object as would be sent from the webui back-end. 289 */ 290 function makeWebstoreApp() { 291 return { 292 id: '', // Empty ID signifies this is a special synthesized app 293 page_index: 0, 294 app_launch_index: -1, // always first 295 name: templateData.web_store_title, 296 launch_url: templateData.web_store_url, 297 icon_big: getThemeUrl('IDR_WEBSTORE_ICON') 298 }; 299 } 300 301 /** 302 * Given a theme resource name, construct a URL for it. 303 * @param {string} resourceName The name of the resource. 304 * @return {string} A url which can be used to load the resource. 305 */ 306 function getThemeUrl(resourceName) { 307 return 'chrome://theme/' + resourceName; 308 } 309 310 /** 311 * Callback invoked by chrome whenever an app preference changes. 312 * The normal NTP uses this to keep track of the current launch-type of an 313 * app, updating the choices in the context menu. We don't have such a menu 314 * so don't use this at all (but it still needs to be here for chrome to 315 * call). 316 * @param {Object} data An object with all the data on available 317 * applications. 318 */ 319 function appsPrefChangeCallback(data) { 320 } 321 322 /** 323 * Invoked whenever the pages in apps-page-list have changed so that 324 * the Slider knows about the new elements. 325 */ 326 function updateSliderCards() { 327 var pageNo = cardSlider.currentCard; 328 if (pageNo >= tilePages.length) 329 pageNo = tilePages.length - 1; 330 var pageArray = []; 331 for (var i = 0; i < tilePages.length; i++) 332 pageArray[i] = tilePages[i]; 333 cardSlider.setCards(pageArray, pageNo); 334 } 335 336 /** 337 * Appends a tile page (for apps or most visited). 338 * 339 * @param {TilePage} page The page element. 340 * @param {boolean=} opt_animate If true, add the class 'new' to the created 341 * dot. 342 */ 343 function appendTilePage(page, opt_animate) { 344 pageList.appendChild(page); 345 346 // Make a deep copy of the dot template to add a new one. 347 var dotCount = dots.length; 348 var newDot = dotTemplate.cloneNode(true); 349 newDot.querySelector('span').textContent = page.pageName; 350 if (opt_animate) 351 newDot.classList.add('new'); 352 dotList.appendChild(newDot); 353 page.navigationDot = newDot; 354 355 // Add click handler to the dot to change the page. 356 // TODO(rbyers): Perhaps this should be TouchHandler.START_EVENT_ (so we 357 // don't rely on synthesized click events, and the change takes effect 358 // before releasing). However, click events seems to be synthesized for a 359 // region outside the border, and a 10px box is too small to require touch 360 // events to fall inside of. We could get around this by adding a box around 361 // the dot for accepting the touch events. 362 function switchPage(e) { 363 cardSlider.selectCard(dotCount, true); 364 e.stopPropagation(); 365 } 366 newDot.addEventListener('click', switchPage); 367 368 // Change pages whenever an app is dragged over a dot. 369 newDot.addEventListener(Grabber.EventType.DRAG_ENTER, switchPage); 370 } 371 /** 372 * Search an elements ancestor chain for the nearest element that is a member 373 * of the specified class. 374 * @param {!Element} element The element to start searching from. 375 * @param {string} className The name of the class to locate. 376 * @return {Element} The first ancestor of the specified class or null. 377 */ 378 function getParentByClassName(element, className) { 379 for (var e = element; e; e = e.parentElement) { 380 if (e.classList.contains(className)) 381 return e; 382 } 383 return null; 384 } 385 386 /** 387 * The container where the app currently being dragged came from. 388 * @type {!Element|undefined} 389 */ 390 var draggingAppContainer; 391 392 /** 393 * The apps-page that the app currently being dragged camed from. 394 * @type {!Element|undefined} 395 */ 396 var draggingAppOriginalPage; 397 398 /** 399 * The element that was originally after the app currently being dragged (or 400 * null if it was the last on the page). 401 * @type {!Element|undefined} 402 */ 403 var draggingAppOriginalPosition; 404 405 /** 406 * Invoked when app dragging begins. 407 * @param {Grabber.Event} e The event from the Grabber indicating the drag. 408 */ 409 function appDragStart(e) { 410 // Pull the element out to the sliderFrame using fixed positioning. This 411 // ensures that the app is not affected (remains under the finger) if the 412 // slider changes cards and is translated. An alternate approach would be 413 // to use fixed positioning for the slider (so that changes to its position 414 // don't affect children that aren't positioned relative to it), but we 415 // don't yet have GPU acceleration for this. 416 var element = e.grabbedElement; 417 418 var pos = element.getBoundingClientRect(); 419 element.style.webkitTransform = ''; 420 421 element.style.position = 'fixed'; 422 // Don't want to zoom around the middle since the left/top co-ordinates 423 // are post-transform values. 424 element.style.webkitTransformOrigin = 'left top'; 425 element.style.left = pos.left + 'px'; 426 element.style.top = pos.top + 'px'; 427 428 // Keep track of what app is being dragged and where it came from 429 assert(!draggingAppContainer, 'got DRAG_START without DRAG_END'); 430 draggingAppContainer = element.parentNode; 431 assert(draggingAppContainer.classList.contains('app-container')); 432 draggingAppOriginalPosition = draggingAppContainer.nextSibling; 433 draggingAppOriginalPage = draggingAppContainer.parentNode; 434 435 // Move the app out of the container 436 // Note that appendChild also removes the element from its current parent. 437 sliderFrame.appendChild(element); 438 } 439 440 /** 441 * Invoked when app dragging terminates (either successfully or not) 442 * @param {Grabber.Event} e The event from the Grabber. 443 */ 444 function appDragEnd(e) { 445 // Stop floating the app 446 var appBeingDragged = e.grabbedElement; 447 assert(appBeingDragged.classList.contains('app')); 448 appBeingDragged.style.position = ''; 449 appBeingDragged.style.webkitTransformOrigin = ''; 450 appBeingDragged.style.left = ''; 451 appBeingDragged.style.top = ''; 452 453 // Ensure the trash can is not active (we won't necessarily get a DRAG_LEAVE 454 // for it - eg. if we drop on it, or the drag is cancelled) 455 trash.classList.remove('hover'); 456 appBeingDragged.classList.remove('trashing'); 457 458 // If we have an active drag (i.e. it wasn't aborted by an app update) 459 if (draggingAppContainer) { 460 // Put the app back into it's container 461 if (appBeingDragged.parentNode != draggingAppContainer) 462 draggingAppContainer.appendChild(appBeingDragged); 463 464 // If we care about the container's original position 465 if (draggingAppOriginalPage) 466 { 467 // Then put the container back where it came from 468 if (draggingAppOriginalPosition) { 469 draggingAppOriginalPage.insertBefore(draggingAppContainer, 470 draggingAppOriginalPosition); 471 } else { 472 draggingAppOriginalPage.appendChild(draggingAppContainer); 473 } 474 } 475 } 476 477 draggingAppContainer = undefined; 478 draggingAppOriginalPage = undefined; 479 draggingAppOriginalPosition = undefined; 480 } 481 482 /** 483 * Invoked when an app is dragged over another app. Updates the DOM to affect 484 * the rearrangement (but doesn't commit the change until the app is dropped). 485 * @param {Grabber.Event} e The event from the Grabber indicating the drag. 486 */ 487 function appDragEnter(e) 488 { 489 assert(draggingAppContainer, 'expected stored container'); 490 var sourceContainer = draggingAppContainer; 491 492 // Ensure enter events delivered to an app-container don't also get 493 // delivered to the document. 494 e.stopPropagation(); 495 496 var curPage = appsPages[cardSlider.currentCard]; 497 var followingContainer = null; 498 499 // If we dragged over a specific app, determine which one to insert before 500 if (e.currentTarget != document) { 501 502 // Start by assuming we'll insert the app before the one dragged over 503 followingContainer = e.currentTarget; 504 assert(followingContainer.classList.contains('app-container'), 505 'expected drag over container'); 506 assert(followingContainer.parentNode == curPage); 507 if (followingContainer == draggingAppContainer) 508 return; 509 510 // But if it's after the current container position then we'll need to 511 // move ahead by one to account for the container being removed. 512 if (curPage == draggingAppContainer.parentNode) { 513 for (var c = draggingAppContainer; c; c = c.nextElementSibling) { 514 if (c == followingContainer) { 515 followingContainer = followingContainer.nextElementSibling; 516 break; 517 } 518 } 519 } 520 } 521 522 // Move the container to the appropriate place on the page 523 curPage.insertBefore(draggingAppContainer, followingContainer); 524 } 525 526 /** 527 * Invoked when an app is dropped on the trash 528 * @param {Grabber.Event} e The event from the Grabber indicating the drop. 529 */ 530 function appTrash(e) { 531 var appElement = e.grabbedElement; 532 assert(appElement.classList.contains('app')); 533 var appId = appElement.getAttribute('app-id'); 534 assert(appId); 535 536 // Mark this drop as handled so that the catch-all drop handler 537 // on the document doesn't see this event. 538 e.stopPropagation(); 539 540 // Tell chrome to uninstall the app (prompting the user) 541 chrome.send('uninstallApp', [appId]); 542 } 543 544 /** 545 * Called when an app is dropped anywhere other than the trash can. Commits 546 * any movement that has occurred. 547 * @param {Grabber.Event} e The event from the Grabber indicating the drop. 548 */ 549 function appDrop(e) { 550 if (!draggingAppContainer) 551 // Drag was aborted (eg. due to an app update) - do nothing 552 return; 553 554 // If the app is dropped back into it's original position then do nothing 555 assert(draggingAppOriginalPage); 556 if (draggingAppContainer.parentNode == draggingAppOriginalPage && 557 draggingAppContainer.nextSibling == draggingAppOriginalPosition) 558 return; 559 560 // Determine which app was being dragged 561 var appElement = e.grabbedElement; 562 assert(appElement.classList.contains('app')); 563 var appId = appElement.getAttribute('app-id'); 564 assert(appId); 565 566 // Update the page index for the app if it's changed. This doesn't trigger 567 // a call to getAppsCallback so we want to do it before reorderApps 568 var pageIndex = cardSlider.currentCard; 569 assert(pageIndex >= 0 && pageIndex < appsPages.length, 570 'page number out of range'); 571 if (appsPages[pageIndex] != draggingAppOriginalPage) 572 chrome.send('setPageIndex', [appId, pageIndex]); 573 574 // Put the app being dragged back into it's container 575 draggingAppContainer.appendChild(appElement); 576 577 // Create a list of all appIds in the order now present in the DOM 578 var appIds = []; 579 for (var page = 0; page < appsPages.length; page++) { 580 var appsOnPage = appsPages[page].getElementsByClassName('app'); 581 for (var i = 0; i < appsOnPage.length; i++) { 582 var id = appsOnPage[i].getAttribute('app-id'); 583 if (id) 584 appIds.push(id); 585 } 586 } 587 588 // We are going to commit this repositioning - clear the original position 589 draggingAppOriginalPage = undefined; 590 draggingAppOriginalPosition = undefined; 591 592 // Tell chrome to update its database to persist this new order of apps This 593 // will cause getAppsCallback to be invoked and the apps to be redrawn. 594 chrome.send('reorderApps', [appId, appIds]); 595 appMoved = true; 596 } 597 598 /** 599 * Set to true if we're currently in rearrange mode and an app has 600 * been successfully dropped to a new location. This indicates that 601 * a getAppsCallback call is pending and we can rely on the DOM being 602 * updated by that. 603 * @type {boolean} 604 */ 605 var appMoved = false; 606 607 /** 608 * Invoked whenever some app is grabbed 609 * @param {Grabber.Event} e The Grabber Grab event. 610 */ 611 function enterRearrangeMode(e) 612 { 613 // Stop the slider from sliding for this touch 614 cardSlider.cancelTouch(); 615 616 // Add an extra blank page in case the user wants to create a new page 617 appendTilePage(new ntp4.AppsPage(''), true); 618 var pageAdded = appsPages.length - 1; 619 window.setTimeout(function() { 620 dots[pageAdded].classList.remove('new'); 621 }, 0); 622 623 updateSliderCards(); 624 625 // Cause the dot-list to grow 626 getRequiredElement('footer').classList.add('rearrange-mode'); 627 628 assert(!appMoved, 'appMoved should not be set yet'); 629 } 630 631 /** 632 * Invoked whenever some app is released 633 * @param {Grabber.Event} e The Grabber RELEASE event. 634 */ 635 function leaveRearrangeMode(e) 636 { 637 // Return the dot-list to normal 638 getRequiredElement('footer').classList.remove('rearrange-mode'); 639 640 // If we didn't successfully re-arrange an app, then we won't be 641 // refreshing the app view in getAppCallback and need to explicitly remove 642 // the extra empty page we added. We don't want to do this in the normal 643 // case because if we did actually drop an app there, we want to retain that 644 // page as our current page number. 645 if (!appMoved) { 646 assert(appsPages[appsPages.length - 1]. 647 getElementsByClassName('app-container').length == 0, 648 'Last app page should be empty'); 649 removePage(appsPages.length - 1); 650 } 651 appMoved = false; 652 } 653 654 /** 655 * Remove the page with the specified index and update the slider. 656 * @param {number} pageNo The index of the page to remove. 657 */ 658 function removePage(pageNo) { 659 pageList.removeChild(tilePages[pageNo]); 660 661 // Remove the corresponding dot 662 // Need to give it a chance to animate though 663 var dot = dots[pageNo]; 664 dot.classList.add('new'); 665 window.setTimeout(function() { 666 // If we've re-created the apps (eg. because an app was uninstalled) then 667 // we will have removed the old dots from the document already, so skip. 668 if (dot.parentNode) 669 dot.parentNode.removeChild(dot); 670 }, DEFAULT_TRANSITION_TIME); 671 672 updateSliderCards(); 673 } 674 675 // TODO(estade): remove |hasAttribution|. 676 // TODO(estade): rename newtab.css to new_tab_theme.css 677 function themeChanged(hasAttribution) { 678 $('themecss').href = 'chrome://theme/css/newtab.css?' + Date.now(); 679 } 680 681 function setRecentlyClosedTabs(dataItems) { 682 $('recently-closed-menu-button').dataItems = dataItems; 683 } 684 685 function setMostVisitedPages(data, firstRun, hasBlacklistedUrls) { 686 mostVisitedPage.data = data; 687 } 688 689 // Return an object with all the exports 690 return { 691 assert: assert, 692 appsPrefChangeCallback: appsPrefChangeCallback, 693 getAppsCallback: getAppsCallback, 694 initialize: initialize, 695 themeChanged: themeChanged, 696 setRecentlyClosedTabs: setRecentlyClosedTabs, 697 setMostVisitedPages: setMostVisitedPages, 698 }; 699}); 700 701// publish ntp globals 702// TODO(estade): update the content handlers to use ntp namespace instead of 703// making these global. 704var assert = ntp4.assert; 705var getAppsCallback = ntp4.getAppsCallback; 706var appsPrefChangeCallback = ntp4.appsPrefChangeCallback; 707var themeChanged = ntp4.themeChanged; 708var recentlyClosedTabs = ntp4.setRecentlyClosedTabs; 709var mostVisitedPages = ntp4.setMostVisitedPages; 710 711document.addEventListener('DOMContentLoaded', ntp4.initialize); 712