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 // To avoid creating tons of unnecessary nodes. We assume we cannot fit more 6 // than this many items in the miniview. 7 var MAX_MINIVIEW_ITEMS = 15; 8 9 // Extra spacing at the top of the layout. 10 var LAYOUT_SPACING_TOP = 25; 11 12 // The visible height of the expanded maxiview. 13 var maxiviewVisibleHeight = 0; 14 15 var APP_LAUNCH = { 16 // The histogram buckets (keep in sync with extension_constants.h). 17 NTP_APPS_MAXIMIZED: 0, 18 NTP_APPS_COLLAPSED: 1, 19 NTP_APPS_MENU: 2, 20 NTP_MOST_VISITED: 3, 21 NTP_RECENTLY_CLOSED: 4, 22 NTP_APP_RE_ENABLE: 16 23 }; 24 25 var APP_LAUNCH_URL = { 26 // The URL prefix for pings that record app launches by URL. 27 PING_BY_URL: 'record-app-launch-by-url', 28 29 // The URL prefix for pings that record app launches by ID. 30 PING_BY_ID: 'record-app-launch-by-id', 31 32 // The URL prefix used by the webstore link 'ping' attributes. 33 PING_WEBSTORE: 'record-webstore-launch' 34 }; 35 36 function getAppPingUrl(prefix, data, bucket) { 37 return [APP_LAUNCH_URL[prefix], 38 encodeURIComponent(data), 39 APP_LAUNCH[bucket]].join('+'); 40 } 41 42 function getSectionCloseButton(sectionId) { 43 return document.querySelector('#' + sectionId + ' .section-close-button'); 44 } 45 46 function getSectionMenuButton(sectionId) { 47 return $(sectionId + '-button'); 48 } 49 50 function getSectionMenuButtonTextId(sectionId) { 51 return sectionId.replace(/-/g, ''); 52 } 53 54 function setSectionMenuMode(sectionId, section, menuModeEnabled, menuModeMask) { 55 var el = $(sectionId); 56 if (!menuModeEnabled) { 57 // Because sections are collapsed when they are in menu mode, it is not 58 // necessary to restore the maxiview here. It will happen if the section 59 // header is clicked. 60 // TODO(aa): Sections should maintain their collapse state when minimized. 61 el.classList.remove('menu'); 62 shownSections &= ~menuModeMask; 63 } else { 64 if (section) { 65 hideSection(section); // To hide the maxiview. 66 } 67 el.classList.add('menu'); 68 shownSections |= menuModeMask; 69 } 70 layoutSections(); 71 } 72 73 function clearClosedMenu(menu) { 74 menu.innerHTML = ''; 75 } 76 77 function addClosedMenuEntryWithLink(menu, a) { 78 var span = document.createElement('span'); 79 a.className += ' item menuitem'; 80 span.appendChild(a); 81 menu.appendChild(span); 82 } 83 84 function addClosedMenuEntry(menu, url, title, imageUrl, opt_pingUrl) { 85 var a = document.createElement('a'); 86 a.href = url; 87 a.textContent = title; 88 a.style.backgroundImage = 'url(' + imageUrl + ')'; 89 if (opt_pingUrl) 90 a.ping = opt_pingUrl; 91 addClosedMenuEntryWithLink(menu, a); 92 } 93 94 function addClosedMenuFooter(menu, sectionId, mask, opt_section) { 95 menu.appendChild(document.createElement('hr')); 96 97 var span = document.createElement('span'); 98 var a = span.appendChild(document.createElement('a')); 99 a.href = ''; 100 if (cr.isChromeOS) { 101 a.textContent = localStrings.getString('expandMenu'); 102 } else { 103 a.textContent = 104 localStrings.getString(getSectionMenuButtonTextId(sectionId)); 105 } 106 a.className = 'item'; 107 a.addEventListener( 108 'click', 109 function(e) { 110 getSectionMenuButton(sectionId).hideMenu(); 111 e.preventDefault(); 112 setSectionMenuMode(sectionId, opt_section, false, mask); 113 shownSections &= ~mask; 114 saveShownSections(); 115 }); 116 menu.appendChild(span); 117 } 118 119 function initializeSection(sectionId, mask, opt_section) { 120 var button = getSectionCloseButton(sectionId); 121 button.addEventListener( 122 'click', 123 function() { 124 setSectionMenuMode(sectionId, opt_section, true, mask); 125 saveShownSections(); 126 }); 127 } 128 129 function updateSimpleSection(id, section) { 130 var elm = $(id); 131 var maxiview = getSectionMaxiview(elm); 132 var miniview = getSectionMiniview(elm); 133 if (shownSections & section) { 134 // The section is expanded, so the maxiview should be opaque (visible) and 135 // the miniview should be hidden. 136 elm.classList.remove('collapsed'); 137 if (maxiview) { 138 maxiview.classList.remove('collapsed'); 139 maxiview.classList.add('opaque'); 140 } 141 if (miniview) 142 miniview.classList.remove('opaque'); 143 } else { 144 // The section is collapsed, so the maxiview should be hidden and the 145 // miniview should be opaque. 146 elm.classList.add('collapsed'); 147 if (maxiview) { 148 maxiview.classList.add('collapsed'); 149 maxiview.classList.remove('opaque'); 150 } 151 if (miniview) 152 miniview.classList.add('opaque'); 153 } 154 } 155 156 var sessionItems = []; 157 158 function foreignSessions(data) { 159 logEvent('received foreign sessions'); 160 // We need to store the foreign sessions so we can update the layout on a 161 // resize. 162 sessionItems = data; 163 renderForeignSessions(); 164 layoutSections(); 165 } 166 167 function renderForeignSessions() { 168 // Remove all existing items and create new items. 169 var sessionElement = $('foreign-sessions'); 170 var parentSessionElement = sessionElement.lastElementChild; 171 parentSessionElement.textContent = ''; 172 173 // For each client, create entries and append the lists together. 174 sessionItems.forEach(function(item, i) { 175 // TODO(zea): Get real client names. See crbug/59672. 176 var name = 'Client ' + i; 177 parentSessionElement.appendChild(createForeignSession(item, name)); 178 }); 179 180 layoutForeignSessions(); 181 } 182 183 function layoutForeignSessions() { 184 var sessionElement = $('foreign-sessions'); 185 // We cannot use clientWidth here since the width has a transition. 186 var availWidth = useSmallGrid() ? 692 : 920; 187 var parentSessEl = sessionElement.lastElementChild; 188 189 if (parentSessEl.hasChildNodes()) { 190 sessionElement.classList.remove('disabled'); 191 sessionElement.classList.remove('opaque'); 192 } else { 193 sessionElement.classList.add('disabled'); 194 sessionElement.classList.add('opaque'); 195 } 196 } 197 198 function createForeignSession(client, name) { 199 // Vertically stack the windows in a client. 200 var stack = document.createElement('div'); 201 stack.className = 'foreign-session-client item link'; 202 stack.textContent = name; 203 stack.sessionTag = client[0].sessionTag; 204 205 client.forEach(function(win, i) { 206 // Create a window entry. 207 var winSpan = document.createElement('span'); 208 var winEl = document.createElement('p'); 209 winEl.className = 'item link window'; 210 winEl.tabItems = win.tabs; 211 winEl.tabIndex = 0; 212 winEl.textContent = formatTabsText(win.tabs.length); 213 winEl.xtitle = win.title; 214 winEl.sessionTag = win.sessionTag; 215 winEl.winNum = i; 216 winEl.addEventListener('click', maybeOpenForeignWindow); 217 winEl.addEventListener('keydown', 218 handleIfEnterKey(maybeOpenForeignWindow)); 219 winSpan.appendChild(winEl); 220 221 // Sort tabs by MRU order 222 win.tabs.sort(function(a, b) { 223 return a.timestamp < b.timestamp; 224 }); 225 226 // Create individual tab information. 227 win.tabs.forEach(function(data) { 228 var tabEl = document.createElement('a'); 229 tabEl.className = 'item link tab'; 230 tabEl.href = data.timestamp; 231 tabEl.style.backgroundImage = url('chrome://favicon/' + data.url); 232 tabEl.dir = data.direction; 233 tabEl.textContent = data.title; 234 tabEl.sessionTag = win.sessionTag; 235 tabEl.winNum = i; 236 tabEl.sessionId = data.sessionId; 237 tabEl.addEventListener('click', maybeOpenForeignTab); 238 tabEl.addEventListener('keydown', 239 handleIfEnterKey(maybeOpenForeignTab)); 240 241 winSpan.appendChild(tabEl); 242 }); 243 244 // Append the window. 245 stack.appendChild(winSpan); 246 }); 247 return stack; 248 } 249 250 var recentItems = []; 251 252 function recentlyClosedTabs(data) { 253 logEvent('received recently closed tabs'); 254 // We need to store the recent items so we can update the layout on a resize. 255 recentItems = data; 256 renderRecentlyClosed(); 257 layoutSections(); 258 } 259 260 function renderRecentlyClosed() { 261 // Remove all existing items and create new items. 262 var recentElement = $('recently-closed'); 263 var parentEl = recentElement.lastElementChild; 264 parentEl.textContent = ''; 265 var recentMenu = $('recently-closed-menu'); 266 clearClosedMenu(recentMenu); 267 268 recentItems.forEach(function(item) { 269 parentEl.appendChild(createRecentItem(item)); 270 addRecentMenuItem(recentMenu, item); 271 }); 272 addClosedMenuFooter(recentMenu, 'recently-closed', MENU_RECENT); 273 274 layoutRecentlyClosed(); 275 } 276 277 function createRecentItem(data) { 278 var isWindow = data.type == 'window'; 279 var el; 280 if (isWindow) { 281 el = document.createElement('span'); 282 el.className = 'item link window'; 283 el.tabItems = data.tabs; 284 el.tabIndex = 0; 285 el.textContent = formatTabsText(data.tabs.length); 286 } else { 287 el = document.createElement('a'); 288 el.className = 'item'; 289 el.href = data.url; 290 el.ping = getAppPingUrl( 291 'PING_BY_URL', data.url, 'NTP_RECENTLY_CLOSED'); 292 el.style.backgroundImage = url('chrome://favicon/' + data.url); 293 el.dir = data.direction; 294 el.textContent = data.title; 295 } 296 el.sessionId = data.sessionId; 297 el.xtitle = data.title; 298 el.sessionTag = data.sessionTag; 299 var wrapperEl = document.createElement('span'); 300 wrapperEl.appendChild(el); 301 return wrapperEl; 302 } 303 304 function addRecentMenuItem(menu, data) { 305 var isWindow = data.type == 'window'; 306 var a = document.createElement('a'); 307 if (isWindow) { 308 a.textContent = formatTabsText(data.tabs.length); 309 a.className = 'window'; // To get the icon from the CSS .window rule. 310 a.href = ''; // To make underline show up. 311 } else { 312 a.href = data.url; 313 a.ping = getAppPingUrl( 314 'PING_BY_URL', data.url, 'NTP_RECENTLY_CLOSED'); 315 a.style.backgroundImage = 'url(chrome://favicon/' + data.url + ')'; 316 a.textContent = data.title; 317 } 318 function clickHandler(e) { 319 chrome.send('reopenTab', [String(data.sessionId)]); 320 e.preventDefault(); 321 } 322 a.addEventListener('click', clickHandler); 323 addClosedMenuEntryWithLink(menu, a); 324 } 325 326 function saveShownSections() { 327 chrome.send('setShownSections', [shownSections]); 328 } 329 330 var LayoutMode = { 331 SMALL: 1, 332 NORMAL: 2 333 }; 334 335 var layoutMode = useSmallGrid() ? LayoutMode.SMALL : LayoutMode.NORMAL; 336 337 function handleWindowResize() { 338 if (window.innerWidth < 10) { 339 // We're probably a background tab, so don't do anything. 340 return; 341 } 342 343 // TODO(jstritar): Remove the small-layout class and revert back to the 344 // @media (max-width) directive once http://crbug.com/70930 is fixed. 345 var oldLayoutMode = layoutMode; 346 var b = useSmallGrid(); 347 if (b) { 348 layoutMode = LayoutMode.SMALL; 349 document.body.classList.add('small-layout'); 350 } else { 351 layoutMode = LayoutMode.NORMAL; 352 document.body.classList.remove('small-layout'); 353 } 354 355 if (layoutMode != oldLayoutMode){ 356 mostVisited.useSmallGrid = b; 357 mostVisited.layout(); 358 apps.layout({force:true}); 359 renderRecentlyClosed(); 360 renderForeignSessions(); 361 updateAllMiniviewClippings(); 362 } 363 364 layoutSections(); 365 } 366 367 // Stores some information about each section necessary to layout. A new 368 // instance is constructed for each section on each layout. 369 function SectionLayoutInfo(section) { 370 this.section = section; 371 this.header = section.querySelector('h2'); 372 this.miniview = section.querySelector('.miniview'); 373 this.maxiview = getSectionMaxiview(section); 374 this.expanded = this.maxiview && !section.classList.contains('collapsed'); 375 this.fixedHeight = this.section.offsetHeight; 376 this.scrollingHeight = 0; 377 378 if (this.expanded) 379 this.scrollingHeight = this.maxiview.offsetHeight; 380 } 381 382 // Get all sections to be layed out. 383 SectionLayoutInfo.getAll = function() { 384 var sections = document.querySelectorAll( 385 '.section:not(.disabled):not(.menu)'); 386 var result = []; 387 for (var i = 0, section; section = sections[i]; i++) { 388 result.push(new SectionLayoutInfo(section)); 389 } 390 return result; 391 }; 392 393 // Ensure the miniview sections don't have any clipped items. 394 function updateMiniviewClipping(miniview) { 395 var clipped = false; 396 for (var j = 0, item; item = miniview.children[j]; j++) { 397 item.style.display = ''; 398 if (clipped || 399 (item.offsetLeft + item.offsetWidth) > miniview.offsetWidth) { 400 item.style.display = 'none'; 401 clipped = true; 402 } else { 403 item.style.display = ''; 404 } 405 } 406 } 407 408 // Ensure none of the miniviews have any clipped items. 409 function updateAllMiniviewClippings() { 410 var miniviews = document.querySelectorAll('.section.collapsed .miniview'); 411 for (var i = 0, miniview; miniview = miniviews[i]; i++) { 412 updateMiniviewClipping(miniview); 413 } 414 } 415 416 // Returns whether or not vertical scrollbars are present. 417 function hasScrollBars() { 418 return window.innerHeight != document.body.clientHeight; 419 } 420 421 // Enables scrollbars (they will only show up if needed). 422 function showScrollBars() { 423 document.body.classList.remove('noscroll'); 424 } 425 426 // Hides all scrollbars. 427 function hideScrollBars() { 428 document.body.classList.add('noscroll'); 429 } 430 431 // Returns whether or not the sections are currently animating due to a 432 // section transition. 433 function isAnimating() { 434 var de = document.documentElement; 435 return de.getAttribute('enable-section-animations') == 'true'; 436 } 437 438 // Layout the sections in a modified accordian. The header and miniview, if 439 // visible are fixed within the viewport. If there is an expanded section, its 440 // it scrolls. 441 // 442 // ============================= 443 // | collapsed section | <- Any collapsed sections are fixed position. 444 // | and miniview | 445 // |---------------------------| 446 // | expanded section | 447 // | | <- There can be one expanded section and it 448 // | and maxiview | is absolutely positioned so that it can 449 // | | scroll "underneath" the fixed elements. 450 // | | 451 // |---------------------------| 452 // | another collapsed section | 453 // |---------------------------| 454 // 455 // We want the main frame scrollbar to be the one that scrolls the expanded 456 // region. To get this effect, we make the fixed elements position:fixed and the 457 // scrollable element position:absolute. We also artificially increase the 458 // height of the document so that it is possible to scroll down enough to 459 // display the end of the document, even with any fixed elements at the bottom 460 // of the viewport. 461 // 462 // There is a final twist: If the intrinsic height of the expanded section is 463 // less than the available height (because the window is tall), any collapsed 464 // sections sinch up and sit below the expanded section. This is so that we 465 // don't have a bunch of dead whitespace in the case of expanded sections that 466 // aren't very tall. 467 function layoutSections() { 468 // While transitioning sections, we only want scrollbars to appear if they're 469 // already present or the window is being resized (so there's no animation). 470 if (!hasScrollBars() && isAnimating()) 471 hideScrollBars(); 472 473 var sections = SectionLayoutInfo.getAll(); 474 var expandedSection = null; 475 var headerHeight = LAYOUT_SPACING_TOP; 476 var footerHeight = 0; 477 478 // Calculate the height of the fixed elements above the expanded section. Also 479 // take note of the expanded section, if there is one. 480 var i; 481 var section; 482 for (i = 0; section = sections[i]; i++) { 483 headerHeight += section.fixedHeight; 484 if (section.expanded) { 485 expandedSection = section; 486 i++; 487 break; 488 } 489 } 490 491 // Calculate the height of the fixed elements below the expanded section, if 492 // any. 493 for (; section = sections[i]; i++) { 494 footerHeight += section.fixedHeight; 495 } 496 // Leave room for bottom bar if it's visible. 497 footerHeight += $('closed-sections-bar').offsetHeight; 498 499 500 // Determine the height to use for the expanded section. If there isn't enough 501 // space to show the expanded section completely, this will be the available 502 // height. Otherwise, we use the intrinsic height of the expanded section. 503 var expandedSectionHeight; 504 if (expandedSection) { 505 var flexHeight = window.innerHeight - headerHeight - footerHeight; 506 if (flexHeight < expandedSection.scrollingHeight) { 507 expandedSectionHeight = flexHeight; 508 509 // Also, artificially expand the height of the document so that we can see 510 // the entire expanded section. 511 // 512 // TODO(aa): Where does this come from? It is the difference between what 513 // we set document.body.style.height to and what 514 // document.body.scrollHeight measures afterward. I expect them to be the 515 // same if document.body has no margins. 516 var fudge = 44; 517 document.body.style.height = 518 headerHeight + 519 expandedSection.scrollingHeight + 520 footerHeight + 521 fudge + 522 'px'; 523 } else { 524 expandedSectionHeight = expandedSection.scrollingHeight; 525 document.body.style.height = ''; 526 } 527 } else { 528 // We only set the document height when a section is expanded. If 529 // all sections are collapsed, then get rid of the previous height. 530 document.body.style.height = ''; 531 } 532 533 maxiviewVisibleHeight = expandedSectionHeight; 534 535 // Now position all the elements. 536 var y = LAYOUT_SPACING_TOP; 537 for (i = 0, section; section = sections[i]; i++) { 538 section.section.style.top = y + 'px'; 539 y += section.fixedHeight; 540 541 if (section.maxiview) { 542 if (section == expandedSection) { 543 section.maxiview.style.top = y + 'px'; 544 } else { 545 // The miniviews fade out gradually, so it may have height at this 546 // point. We position the maxiview as if the miniview was not displayed 547 // by subtracting off the miniview's total height (height + margin). 548 var miniviewFudge = 40; // miniview margin-bottom + margin-top 549 var miniviewHeight = section.miniview.offsetHeight + miniviewFudge; 550 section.maxiview.style.top = y - miniviewHeight + 'px'; 551 } 552 } 553 554 if (section.maxiview && section == expandedSection) 555 updateMask(section.maxiview, expandedSectionHeight); 556 557 if (section == expandedSection) 558 y += expandedSectionHeight; 559 } 560 if (cr.isChromeOS) 561 $('closed-sections-bar').style.top = y + 'px'; 562 563 updateMenuSections(); 564 updateAttributionDisplay(y); 565 } 566 567 function updateMask(maxiview, visibleHeightPx) { 568 // We want to end up with 10px gradients at the top and bottom of 569 // visibleHeight, but webkit-mask only supports expression in terms of 570 // percentages. 571 572 // We might not have enough room to do 10px gradients on each side. To get the 573 // right effect, we don't want to make the gradients smaller, but make them 574 // appear to mush into each other. 575 var gradientHeightPx = Math.min(10, Math.floor(visibleHeightPx / 2)); 576 var gradientDestination = 'rgba(0,0,0,' + (gradientHeightPx / 10) + ')'; 577 578 var bottomSpacing = 15; 579 var first = parseFloat(maxiview.style.top) / window.innerHeight; 580 var second = first + gradientHeightPx / window.innerHeight; 581 var fourth = first + (visibleHeightPx - bottomSpacing) / window.innerHeight; 582 var third = fourth - gradientHeightPx / window.innerHeight; 583 584 var gradientArguments = [ 585 'transparent', 586 getColorStopString(first, 'transparent'), 587 getColorStopString(second, gradientDestination), 588 getColorStopString(third, gradientDestination), 589 getColorStopString(fourth, 'transparent'), 590 'transparent' 591 ]; 592 593 var gradient = '-webkit-linear-gradient(' + gradientArguments.join(',') + ')'; 594 maxiview.style.WebkitMaskImage = gradient; 595 } 596 597 function getColorStopString(height, color) { 598 // TODO(arv): The CSS3 gradient syntax allows px units so we should simplify 599 // this to use pixels instead. 600 return color + ' ' + height * 100 + '%'; 601 } 602 603 // Updates the visibility of the menu buttons for each section, based on 604 // whether they are currently enabled and in menu mode. 605 function updateMenuSections() { 606 var elms = document.getElementsByClassName('section'); 607 for (var i = 0, elm; elm = elms[i]; i++) { 608 var button = getSectionMenuButton(elm.id); 609 if (!button) 610 continue; 611 612 if (!elm.classList.contains('disabled') && 613 elm.classList.contains('menu')) { 614 button.style.display = 'inline-block'; 615 } else { 616 button.style.display = 'none'; 617 } 618 } 619 } 620 621 window.addEventListener('resize', handleWindowResize); 622 623 var sectionToElementMap; 624 function getSectionElement(section) { 625 if (!sectionToElementMap) { 626 sectionToElementMap = {}; 627 for (var key in Section) { 628 sectionToElementMap[Section[key]] = 629 document.querySelector('.section[section=' + key + ']'); 630 } 631 } 632 return sectionToElementMap[section]; 633 } 634 635 function getSectionMaxiview(section) { 636 return $(section.id + '-maxiview'); 637 } 638 639 function getSectionMiniview(section) { 640 return section.querySelector('.miniview'); 641 } 642 643 // You usually want to call |showOnlySection()| instead of this. 644 function showSection(section) { 645 if (!(section & shownSections)) { 646 shownSections |= section; 647 var el = getSectionElement(section); 648 if (el) { 649 el.classList.remove('collapsed'); 650 651 var maxiview = getSectionMaxiview(el); 652 if (maxiview) { 653 maxiview.classList.remove('collapsing'); 654 maxiview.classList.remove('collapsed'); 655 // The opacity won't transition if you toggle the display property 656 // at the same time. To get a fade effect, we set the opacity 657 // asynchronously from another function, after the display is toggled. 658 // 1) 'collapsed' (display: none, opacity: 0) 659 // 2) none (display: block, opacity: 0) 660 // 3) 'opaque' (display: block, opacity: 1) 661 setTimeout(function () { 662 maxiview.classList.add('opaque'); 663 }, 0); 664 } 665 666 var miniview = getSectionMiniview(el); 667 if (miniview) { 668 // The miniview is hidden immediately (no need to set this async). 669 miniview.classList.remove('opaque'); 670 } 671 } 672 673 switch (section) { 674 case Section.THUMB: 675 mostVisited.visible = true; 676 mostVisited.layout(); 677 break; 678 case Section.APPS: 679 apps.visible = true; 680 apps.layout({disableAnimations:true}); 681 break; 682 } 683 } 684 } 685 686 // Show this section and hide all other sections - at most one section can 687 // be open at one time. 688 function showOnlySection(section) { 689 for (var p in Section) { 690 if (p == section) 691 showSection(Section[p]); 692 else 693 hideSection(Section[p]); 694 } 695 } 696 697 function hideSection(section) { 698 if (section & shownSections) { 699 shownSections &= ~section; 700 701 switch (section) { 702 case Section.THUMB: 703 mostVisited.visible = false; 704 mostVisited.layout(); 705 break; 706 case Section.APPS: 707 apps.visible = false; 708 apps.layout(); 709 break; 710 } 711 712 var el = getSectionElement(section); 713 if (el) { 714 el.classList.add('collapsed'); 715 716 var maxiview = getSectionMaxiview(el); 717 if (maxiview) { 718 maxiview.classList.add(isDoneLoading() ? 'collapsing' : 'collapsed'); 719 maxiview.classList.remove('opaque'); 720 } 721 722 var miniview = getSectionMiniview(el); 723 if (miniview) { 724 // We need to set this asynchronously to properly get the fade effect. 725 setTimeout(function() { 726 miniview.classList.add('opaque'); 727 }, 0); 728 updateMiniviewClipping(miniview); 729 } 730 } 731 } 732 } 733 734 window.addEventListener('webkitTransitionEnd', function(e) { 735 if (e.target.classList.contains('collapsing')) { 736 e.target.classList.add('collapsed'); 737 e.target.classList.remove('collapsing'); 738 } 739 740 if (e.target.classList.contains('maxiview') || 741 e.target.classList.contains('miniview')) { 742 document.documentElement.removeAttribute('enable-section-animations'); 743 showScrollBars(); 744 } 745 }); 746 747 /** 748 * Callback when the shown sections changes in another NTP. 749 * @param {number} newShownSections Bitmask of the shown sections. 750 */ 751 function setShownSections(newShownSections) { 752 for (var key in Section) { 753 if (newShownSections & Section[key]) 754 showSection(Section[key]); 755 else 756 hideSection(Section[key]); 757 } 758 setSectionMenuMode('apps', Section.APPS, newShownSections & MENU_APPS, 759 MENU_APPS); 760 setSectionMenuMode('most-visited', Section.THUMB, 761 newShownSections & MENU_THUMB, MENU_THUMB); 762 setSectionMenuMode('recently-closed', undefined, 763 newShownSections & MENU_RECENT, MENU_RECENT); 764 layoutSections(); 765 } 766 767 // Recently closed 768 769 function layoutRecentlyClosed() { 770 var recentElement = $('recently-closed'); 771 var miniview = getSectionMiniview(recentElement); 772 773 updateMiniviewClipping(miniview); 774 775 if (miniview.hasChildNodes()) { 776 recentElement.classList.remove('disabled'); 777 miniview.classList.add('opaque'); 778 } else { 779 recentElement.classList.add('disabled'); 780 miniview.classList.remove('opaque'); 781 } 782 783 layoutSections(); 784 } 785 786 /** 787 * This function is called by the backend whenever the sync status section 788 * needs to be updated to reflect recent sync state changes. The backend passes 789 * the new status information in the newMessage parameter. The state includes 790 * the following: 791 * 792 * syncsectionisvisible: true if the sync section needs to show up on the new 793 * tab page and false otherwise. 794 * title: the header for the sync status section. 795 * msg: the actual message (e.g. "Synced to foo (a] gmail.com"). 796 * linkisvisible: true if the link element should be visible within the sync 797 * section and false otherwise. 798 * linktext: the text to display as the link in the sync status (only used if 799 * linkisvisible is true). 800 * linkurlisset: true if an URL should be set as the href for the link and false 801 * otherwise. If this field is false, then clicking on the link 802 * will result in sending a message to the backend (see 803 * 'SyncLinkClicked'). 804 * linkurl: the URL to use as the element's href (only used if linkurlisset is 805 * true). 806 */ 807 function syncMessageChanged(newMessage) { 808 var syncStatusElement = $('sync-status'); 809 810 // Hide the section if the message is emtpy. 811 if (!newMessage['syncsectionisvisible']) { 812 syncStatusElement.classList.add('disabled'); 813 return; 814 } 815 816 syncStatusElement.classList.remove('disabled'); 817 818 var content = syncStatusElement.children[0]; 819 820 // Set the sync section background color based on the state. 821 if (newMessage.msgtype == 'error') { 822 content.style.backgroundColor = 'tomato'; 823 } else { 824 content.style.backgroundColor = ''; 825 } 826 827 // Set the text for the header and sync message. 828 var titleElement = content.firstElementChild; 829 titleElement.textContent = newMessage.title; 830 var messageElement = titleElement.nextElementSibling; 831 messageElement.textContent = newMessage.msg; 832 833 // Remove what comes after the message 834 while (messageElement.nextSibling) { 835 content.removeChild(messageElement.nextSibling); 836 } 837 838 if (newMessage.linkisvisible) { 839 var el; 840 if (newMessage.linkurlisset) { 841 // Use a link 842 el = document.createElement('a'); 843 el.href = newMessage.linkurl; 844 } else { 845 el = document.createElement('button'); 846 el.className = 'link'; 847 el.addEventListener('click', syncSectionLinkClicked); 848 } 849 el.textContent = newMessage.linktext; 850 content.appendChild(el); 851 fixLinkUnderline(el); 852 } 853 854 layoutSections(); 855 } 856 857 /** 858 * Invoked when the link in the sync promo or sync status section is clicked. 859 */ 860 function syncSectionLinkClicked(e) { 861 chrome.send('SyncLinkClicked'); 862 e.preventDefault(); 863 } 864 865 /** 866 * Invoked when link to start sync in the promo message is clicked, and Chrome 867 * has already been synced to an account. 868 */ 869 function syncAlreadyEnabled(message) { 870 showNotification(message.syncEnabledMessage); 871 } 872 873 /** 874 * Returns the text used for a recently closed window. 875 * @param {number} numTabs Number of tabs in the window. 876 * @return {string} The text to use. 877 */ 878 function formatTabsText(numTabs) { 879 if (numTabs == 1) 880 return localStrings.getString('closedwindowsingle'); 881 return localStrings.getStringF('closedwindowmultiple', numTabs); 882 } 883 884 // Theme related 885 886 function themeChanged(hasAttribution) { 887 document.documentElement.setAttribute('hasattribution', hasAttribution); 888 $('themecss').href = 'chrome://theme/css/newtab.css?' + Date.now(); 889 updateAttribution(); 890 } 891 892 function updateAttribution() { 893 // Default value for standard NTP with no theme attribution or custom logo. 894 logEvent('updateAttribution called'); 895 var imageId = 'IDR_PRODUCT_LOGO'; 896 // Theme attribution always overrides custom logos. 897 if (document.documentElement.getAttribute('hasattribution') == 'true') { 898 logEvent('updateAttribution called with THEME ATTR'); 899 imageId = 'IDR_THEME_NTP_ATTRIBUTION'; 900 } else if (document.documentElement.getAttribute('customlogo') == 'true') { 901 logEvent('updateAttribution with CUSTOMLOGO'); 902 imageId = 'IDR_CUSTOM_PRODUCT_LOGO'; 903 } 904 905 $('attribution-img').src = 'chrome://theme/' + imageId + '?' + Date.now(); 906 } 907 908 // If the content overlaps with the attribution, we bump its opacity down. 909 function updateAttributionDisplay(contentBottom) { 910 var attribution = $('attribution'); 911 var main = $('main'); 912 var rtl = document.documentElement.dir == 'rtl'; 913 var contentRect = main.getBoundingClientRect(); 914 var attributionRect = attribution.getBoundingClientRect(); 915 916 // Hack. See comments for '.haslayout' in new_new_tab.css. 917 if (attributionRect.width == 0) 918 return; 919 else 920 attribution.classList.remove('nolayout'); 921 922 if (contentBottom > attribution.offsetTop) { 923 if ((!rtl && contentRect.right > attributionRect.left) || 924 (rtl && attributionRect.right > contentRect.left)) { 925 attribution.classList.add('obscured'); 926 return; 927 } 928 } 929 930 attribution.classList.remove('obscured'); 931 } 932 933 function bookmarkBarAttached() { 934 document.documentElement.setAttribute('bookmarkbarattached', 'true'); 935 } 936 937 function bookmarkBarDetached() { 938 document.documentElement.setAttribute('bookmarkbarattached', 'false'); 939 } 940 941 function viewLog() { 942 var lines = []; 943 var start = log[0][1]; 944 945 for (var i = 0; i < log.length; i++) { 946 lines.push((log[i][1] - start) + ': ' + log[i][0]); 947 } 948 949 console.log(lines.join('\n')); 950 } 951 952 // We apply the size class here so that we don't trigger layout animations 953 // onload. 954 955 handleWindowResize(); 956 957 var localStrings = new LocalStrings(); 958 959 /////////////////////////////////////////////////////////////////////////////// 960 // Things we know are not needed at startup go below here 961 962 function afterTransition(f) { 963 if (!isDoneLoading()) { 964 // Make sure we do not use a timer during load since it slows down the UI. 965 f(); 966 } else { 967 // The duration of all transitions are .15s 968 window.setTimeout(f, 150); 969 } 970 } 971 972 // Notification 973 974 975 var notificationTimeout; 976 977 /* 978 * Displays a message (either a string or a document fragment) in the 979 * notification slot at the top of the NTP. A close button ("x") will be 980 * inserted at the end of the message. 981 * @param {string|Node} message String or node to use as message. 982 * @param {string} actionText The text to show as a link next to the message. 983 * @param {function=} opt_f Function to call when the user clicks the action 984 * link. 985 * @param {number=} opt_delay The time in milliseconds before hiding the 986 * notification. 987 */ 988 function showNotification(message, actionText, opt_f, opt_delay) { 989 // TODO(arv): Create a notification component. 990 var notificationElement = $('notification'); 991 var f = opt_f || function() {}; 992 var delay = opt_delay || 10000; 993 994 function show() { 995 window.clearTimeout(notificationTimeout); 996 notificationElement.classList.add('show'); 997 document.body.classList.add('notification-shown'); 998 } 999 1000 function delayedHide() { 1001 notificationTimeout = window.setTimeout(hideNotification, delay); 1002 } 1003 1004 function doAction() { 1005 f(); 1006 closeNotification(); 1007 } 1008 1009 function closeNotification() { 1010 if (notification.classList.contains('promo')) 1011 chrome.send('closePromo'); 1012 hideNotification(); 1013 } 1014 1015 // Remove classList entries from previous notifications. 1016 notification.classList.remove('first-run'); 1017 notification.classList.remove('promo'); 1018 1019 var messageContainer = notificationElement.firstElementChild; 1020 var actionLink = notificationElement.querySelector('#action-link'); 1021 var closeButton = notificationElement.querySelector('#notification-close'); 1022 1023 // Remove any previous actionLink entry. 1024 actionLink.textContent = ''; 1025 1026 $('notification-close').onclick = closeNotification; 1027 1028 if (typeof message == 'string') { 1029 messageContainer.textContent = message; 1030 } else { 1031 messageContainer.textContent = ''; // Remove all children. 1032 messageContainer.appendChild(message); 1033 } 1034 1035 if (actionText) { 1036 actionLink.style.display = ''; 1037 actionLink.textContent = actionText; 1038 } else { 1039 actionLink.style.display = 'none'; 1040 } 1041 1042 actionLink.onclick = doAction; 1043 actionLink.onkeydown = handleIfEnterKey(doAction); 1044 notificationElement.onmouseover = show; 1045 notificationElement.onmouseout = delayedHide; 1046 actionLink.onfocus = show; 1047 actionLink.onblur = delayedHide; 1048 // Enable tabbing to the link now that it is shown. 1049 actionLink.tabIndex = 0; 1050 1051 show(); 1052 delayedHide(); 1053 } 1054 1055 /** 1056 * Hides the notifier. 1057 */ 1058 function hideNotification() { 1059 var notificationElement = $('notification'); 1060 notificationElement.classList.remove('show'); 1061 document.body.classList.remove('notification-shown'); 1062 var actionLink = notificationElement.querySelector('#actionlink'); 1063 var closeButton = notificationElement.querySelector('#notification-close'); 1064 // Prevent tabbing to the hidden link. 1065 // Setting tabIndex to -1 only prevents future tabbing to it. If, however, the 1066 // user switches window or a tab and then moves back to this tab the element 1067 // may gain focus. We therefore make sure that we blur the element so that the 1068 // element focus is not restored when coming back to this window. 1069 if (actionLink) { 1070 actionLink.tabIndex = -1; 1071 actionLink.blur(); 1072 } 1073 if (closeButton) { 1074 closeButton.tabIndex = -1; 1075 closeButton.blur(); 1076 } 1077 } 1078 1079 function showPromoNotification() { 1080 showNotification(parseHtmlSubset(localStrings.getString('serverpromo')), 1081 localStrings.getString('syncpromotext'), 1082 function () { chrome.send('SyncLinkClicked'); }, 1083 60000); 1084 var notificationElement = $('notification'); 1085 notification.classList.add('promo'); 1086 } 1087 1088 $('main').addEventListener('click', function(e) { 1089 var p = e.target; 1090 while (p && p.tagName != 'H2') { 1091 // In case the user clicks on a button we do not want to expand/collapse a 1092 // section. 1093 if (p.tagName == 'BUTTON') 1094 return; 1095 p = p.parentNode; 1096 } 1097 1098 if (!p) 1099 return; 1100 1101 p = p.parentNode; 1102 if (!getSectionMaxiview(p)) 1103 return; 1104 1105 toggleSectionVisibilityAndAnimate(p.getAttribute('section')); 1106 }); 1107 1108 $('most-visited-settings').addEventListener('click', function() { 1109 $('clear-all-blacklisted').execute(); 1110 }); 1111 1112 function toggleSectionVisibilityAndAnimate(section) { 1113 if (!section) 1114 return; 1115 1116 // It looks better to return the scroll to the top when toggling sections. 1117 document.body.scrollTop = 0; 1118 1119 // We set it back in webkitTransitionEnd. 1120 document.documentElement.setAttribute('enable-section-animations', 'true'); 1121 if (shownSections & Section[section]) { 1122 hideSection(Section[section]); 1123 } else { 1124 showOnlySection(section); 1125 } 1126 layoutSections(); 1127 saveShownSections(); 1128 } 1129 1130 function handleIfEnterKey(f) { 1131 return function(e) { 1132 if (e.keyIdentifier == 'Enter') 1133 f(e); 1134 }; 1135 } 1136 1137 function maybeReopenTab(e) { 1138 var el = findAncestor(e.target, function(el) { 1139 return el.sessionId !== undefined; 1140 }); 1141 if (el) { 1142 chrome.send('reopenTab', [String(el.sessionId)]); 1143 e.preventDefault(); 1144 1145 setWindowTooltipTimeout(); 1146 } 1147 } 1148 1149 // Note that the openForeignSession calls can fail, resulting this method to 1150 // not have any action (hence the maybe). 1151 function maybeOpenForeignSession(e) { 1152 var el = findAncestor(e.target, function(el) { 1153 return el.sessionTag !== undefined; 1154 }); 1155 if (el) { 1156 chrome.send('openForeignSession', [String(el.sessionTag)]); 1157 e.stopPropagation(); 1158 e.preventDefault(); 1159 setWindowTooltipTimeout(); 1160 } 1161 } 1162 1163 function maybeOpenForeignWindow(e) { 1164 var el = findAncestor(e.target, function(el) { 1165 return el.winNum !== undefined; 1166 }); 1167 if (el) { 1168 chrome.send('openForeignSession', [String(el.sessionTag), 1169 String(el.winNum)]); 1170 e.stopPropagation(); 1171 e.preventDefault(); 1172 setWindowTooltipTimeout(); 1173 } 1174 } 1175 1176 function maybeOpenForeignTab(e) { 1177 var el = findAncestor(e.target, function(el) { 1178 return el.sessionId !== undefined; 1179 }); 1180 if (el) { 1181 chrome.send('openForeignSession', [String(el.sessionTag), String(el.winNum), 1182 String(el.sessionId)]); 1183 e.stopPropagation(); 1184 e.preventDefault(); 1185 setWindowTooltipTimeout(); 1186 } 1187 } 1188 1189 // HACK(arv): After the window onblur event happens we get a mouseover event 1190 // on the next item and we want to make sure that we do not show a tooltip 1191 // for that. 1192 function setWindowTooltipTimeout(e) { 1193 window.setTimeout(function() { 1194 windowTooltip.hide(); 1195 }, 2 * WindowTooltip.DELAY); 1196 } 1197 1198 function maybeShowWindowTooltip(e) { 1199 var f = function(el) { 1200 return el.tabItems !== undefined; 1201 }; 1202 var el = findAncestor(e.target, f); 1203 var relatedEl = findAncestor(e.relatedTarget, f); 1204 if (el && el != relatedEl) { 1205 windowTooltip.handleMouseOver(e, el, el.tabItems); 1206 } 1207 } 1208 1209 1210 var recentlyClosedElement = $('recently-closed'); 1211 1212 recentlyClosedElement.addEventListener('click', maybeReopenTab); 1213 recentlyClosedElement.addEventListener('keydown', 1214 handleIfEnterKey(maybeReopenTab)); 1215 1216 recentlyClosedElement.addEventListener('mouseover', maybeShowWindowTooltip); 1217 recentlyClosedElement.addEventListener('focus', maybeShowWindowTooltip, true); 1218 1219 var foreignSessionElement = $('foreign-sessions'); 1220 1221 foreignSessionElement.addEventListener('click', maybeOpenForeignSession); 1222 foreignSessionElement.addEventListener('keydown', 1223 handleIfEnterKey( 1224 maybeOpenForeignSession)); 1225 1226 foreignSessionElement.addEventListener('mouseover', maybeShowWindowTooltip); 1227 foreignSessionElement.addEventListener('focus', maybeShowWindowTooltip, true); 1228 1229 /** 1230 * This object represents a tooltip representing a closed window. It is 1231 * shown when hovering over a closed window item or when the item is focused. It 1232 * gets hidden when blurred or when mousing out of the menu or the item. 1233 * @param {Element} tooltipEl The element to use as the tooltip. 1234 * @constructor 1235 */ 1236 function WindowTooltip(tooltipEl) { 1237 this.tooltipEl = tooltipEl; 1238 this.boundHide_ = this.hide.bind(this); 1239 this.boundHandleMouseOut_ = this.handleMouseOut.bind(this); 1240 } 1241 1242 WindowTooltip.trackMouseMove_ = function(e) { 1243 WindowTooltip.clientX = e.clientX; 1244 WindowTooltip.clientY = e.clientY; 1245 }; 1246 1247 /** 1248 * Time in ms to delay before the tooltip is shown. 1249 * @type {number} 1250 */ 1251 WindowTooltip.DELAY = 300; 1252 1253 WindowTooltip.prototype = { 1254 timer: 0, 1255 handleMouseOver: function(e, linkEl, tabs) { 1256 this.linkEl_ = linkEl; 1257 if (e.type == 'mouseover') { 1258 this.linkEl_.addEventListener('mousemove', WindowTooltip.trackMouseMove_); 1259 this.linkEl_.addEventListener('mouseout', this.boundHandleMouseOut_); 1260 } else { // focus 1261 this.linkEl_.addEventListener('blur', this.boundHide_); 1262 } 1263 this.timer = window.setTimeout(this.show.bind(this, e.type, linkEl, tabs), 1264 WindowTooltip.DELAY); 1265 }, 1266 show: function(type, linkEl, tabs) { 1267 window.addEventListener('blur', this.boundHide_); 1268 this.linkEl_.removeEventListener('mousemove', 1269 WindowTooltip.trackMouseMove_); 1270 window.clearTimeout(this.timer); 1271 1272 this.renderItems(tabs); 1273 var rect = linkEl.getBoundingClientRect(); 1274 var bodyRect = document.body.getBoundingClientRect(); 1275 var rtl = document.documentElement.dir == 'rtl'; 1276 1277 this.tooltipEl.style.display = 'block'; 1278 var tooltipRect = this.tooltipEl.getBoundingClientRect(); 1279 var x, y; 1280 1281 // When focused show below, like a drop down menu. 1282 if (type == 'focus') { 1283 x = rtl ? 1284 rect.left + bodyRect.left + rect.width - this.tooltipEl.offsetWidth : 1285 rect.left + bodyRect.left; 1286 y = rect.top + bodyRect.top + rect.height; 1287 } else { 1288 x = bodyRect.left + (rtl ? 1289 WindowTooltip.clientX - this.tooltipEl.offsetWidth : 1290 WindowTooltip.clientX); 1291 // Offset like a tooltip 1292 y = 20 + WindowTooltip.clientY + bodyRect.top; 1293 } 1294 1295 // We need to ensure that the tooltip is inside the window viewport. 1296 x = Math.min(x, bodyRect.width - tooltipRect.width); 1297 x = Math.max(x, 0); 1298 y = Math.min(y, bodyRect.height - tooltipRect.height); 1299 y = Math.max(y, 0); 1300 1301 this.tooltipEl.style.left = x + 'px'; 1302 this.tooltipEl.style.top = y + 'px'; 1303 }, 1304 handleMouseOut: function(e) { 1305 // Don't hide when move to another item in the link. 1306 var f = function(el) { 1307 return el.tabItems !== undefined; 1308 }; 1309 var el = findAncestor(e.target, f); 1310 var relatedEl = findAncestor(e.relatedTarget, f); 1311 if (el && el != relatedEl) { 1312 this.hide(); 1313 } 1314 }, 1315 hide: function() { 1316 window.clearTimeout(this.timer); 1317 window.removeEventListener('blur', this.boundHide_); 1318 this.linkEl_.removeEventListener('mousemove', 1319 WindowTooltip.trackMouseMove_); 1320 this.linkEl_.removeEventListener('mouseout', this.boundHandleMouseOut_); 1321 this.linkEl_.removeEventListener('blur', this.boundHide_); 1322 this.linkEl_ = null; 1323 1324 this.tooltipEl.style.display = 'none'; 1325 }, 1326 renderItems: function(tabs) { 1327 var tooltip = this.tooltipEl; 1328 tooltip.textContent = ''; 1329 1330 tabs.forEach(function(tab) { 1331 var span = document.createElement('span'); 1332 span.className = 'item'; 1333 span.style.backgroundImage = url('chrome://favicon/' + tab.url); 1334 span.dir = tab.direction; 1335 span.textContent = tab.title; 1336 tooltip.appendChild(span); 1337 }); 1338 } 1339 }; 1340 1341 var windowTooltip = new WindowTooltip($('window-tooltip')); 1342 1343 window.addEventListener('load', 1344 logEvent.bind(global, 'Tab.NewTabOnload', true)); 1345 1346 window.addEventListener('resize', handleWindowResize); 1347 document.addEventListener('DOMContentLoaded', 1348 logEvent.bind(global, 'Tab.NewTabDOMContentLoaded', true)); 1349 1350 // Whether or not we should send the initial 'GetSyncMessage' to the backend 1351 // depends on the value of the attribue 'syncispresent' which the backend sets 1352 // to indicate if there is code in the backend which is capable of processing 1353 // this message. This attribute is loaded by the JSTemplate and therefore we 1354 // must make sure we check the attribute after the DOM is loaded. 1355 document.addEventListener('DOMContentLoaded', 1356 callGetSyncMessageIfSyncIsPresent); 1357 1358 /** 1359 * The sync code is not yet built by default on all platforms so we have to 1360 * make sure we don't send the initial sync message to the backend unless the 1361 * backend told us that the sync code is present. 1362 */ 1363 function callGetSyncMessageIfSyncIsPresent() { 1364 if (document.documentElement.getAttribute('syncispresent') == 'true') { 1365 chrome.send('GetSyncMessage'); 1366 } 1367 } 1368 1369 // Tooltip for elements that have text that overflows. 1370 document.addEventListener('mouseover', function(e) { 1371 // We don't want to do this while we are dragging because it makes things very 1372 // janky 1373 if (mostVisited.isDragging()) { 1374 return; 1375 } 1376 1377 var el = findAncestor(e.target, function(el) { 1378 return el.xtitle; 1379 }); 1380 if (el && el.xtitle != el.title) { 1381 if (el.scrollWidth > el.clientWidth) { 1382 el.title = el.xtitle; 1383 } else { 1384 el.title = ''; 1385 } 1386 } 1387 }); 1388 1389 /** 1390 * Makes links and buttons support a different underline color. 1391 * @param {Node} node The node to search for links and buttons in. 1392 */ 1393 function fixLinkUnderlines(node) { 1394 var elements = node.querySelectorAll('a,button'); 1395 Array.prototype.forEach.call(elements, fixLinkUnderline); 1396 } 1397 1398 /** 1399 * Wraps the content of an element in a a link-color span. 1400 * @param {Element} el The element to wrap. 1401 */ 1402 function fixLinkUnderline(el) { 1403 var span = document.createElement('span'); 1404 span.className = 'link-color'; 1405 while (el.hasChildNodes()) { 1406 span.appendChild(el.firstChild); 1407 } 1408 el.appendChild(span); 1409 } 1410 1411 updateAttribution(); 1412 1413 function initializeLogin() { 1414 chrome.send('initializeLogin', []); 1415 } 1416 1417 function updateLogin(login) { 1418 $('login-container').style.display = login ? 'block' : ''; 1419 if (login) 1420 $('login-username').textContent = login; 1421 1422 } 1423 1424 var mostVisited = new MostVisited( 1425 $('most-visited-maxiview'), 1426 document.querySelector('#most-visited .miniview'), 1427 $('most-visited-menu'), 1428 useSmallGrid(), 1429 shownSections & Section.THUMB); 1430 1431 function mostVisitedPages(data, firstRun, hasBlacklistedUrls) { 1432 logEvent('received most visited pages'); 1433 1434 mostVisited.updateSettingsLink(hasBlacklistedUrls); 1435 mostVisited.data = data; 1436 mostVisited.layout(); 1437 layoutSections(); 1438 1439 // Remove class name in a timeout so that changes done in this JS thread are 1440 // not animated. 1441 window.setTimeout(function() { 1442 mostVisited.ensureSmallGridCorrect(); 1443 maybeDoneLoading(); 1444 }, 1); 1445 1446 if (localStrings.getString('serverpromo')) { 1447 showPromoNotification(); 1448 } 1449 } 1450 1451 function maybeDoneLoading() { 1452 if (mostVisited.data && apps.loaded) 1453 document.body.classList.remove('loading'); 1454 } 1455 1456 function isDoneLoading() { 1457 return !document.body.classList.contains('loading'); 1458 } 1459 1460 // Initialize the listener for the "hide this" link on the apps promo. We do 1461 // this outside of getAppsCallback because it only needs to be done once per 1462 // NTP load. 1463 document.addEventListener('DOMContentLoaded', function() { 1464 $('apps-promo-hide').addEventListener('click', function() { 1465 chrome.send('hideAppsPromo', []); 1466 document.documentElement.classList.remove('apps-promo-visible'); 1467 layoutSections(); 1468 }); 1469 }); 1470