new_new_tab.js revision dc0f95d653279beabeb9817299e2902918ba123e
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. 7var MAX_MINIVIEW_ITEMS = 15; 8 9// Extra spacing at the top of the layout. 10var LAYOUT_SPACING_TOP = 25; 11 12// The visible height of the expanded maxiview. 13var maxiviewVisibleHeight = 0; 14 15var 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 25var 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 36function getAppPingUrl(prefix, data, bucket) { 37 return [APP_LAUNCH_URL[prefix], 38 encodeURIComponent(data), 39 APP_LAUNCH[bucket]].join('+'); 40} 41 42function getSectionCloseButton(sectionId) { 43 return document.querySelector('#' + sectionId + ' .section-close-button'); 44} 45 46function getSectionMenuButton(sectionId) { 47 return $(sectionId + '-button'); 48} 49 50function getSectionMenuButtonTextId(sectionId) { 51 return sectionId.replace(/-/g, ''); 52} 53 54function 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 73function clearClosedMenu(menu) { 74 menu.innerHTML = ''; 75} 76 77function addClosedMenuEntryWithLink(menu, a) { 78 var span = document.createElement('span'); 79 a.className += ' item menuitem'; 80 span.appendChild(a); 81 menu.appendChild(span); 82} 83 84function 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.setAttribute('ping', opt_pingUrl); 91 addClosedMenuEntryWithLink(menu, a); 92} 93 94function 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 119function 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 129function 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 156var sessionItems = []; 157 158function 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 167function 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 183function 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 198function 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 250var recentItems = []; 251 252function 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 260function 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 277function 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.setAttribute('ping', 291 getAppPingUrl('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 304function 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.setAttribute('ping', 314 getAppPingUrl('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 326function saveShownSections() { 327 chrome.send('setShownSections', [shownSections]); 328} 329 330var LayoutMode = { 331 SMALL: 1, 332 NORMAL: 2 333}; 334 335var layoutMode = useSmallGrid() ? LayoutMode.SMALL : LayoutMode.NORMAL; 336 337function 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. 369function 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. 383SectionLayoutInfo.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. 394function 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. 409function 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. 417function hasScrollBars() { 418 return window.innerHeight != document.body.clientHeight; 419} 420 421// Enables scrollbars (they will only show up if needed). 422function showScrollBars() { 423 document.body.classList.remove('noscroll'); 424} 425 426// Hides all scrollbars. 427function 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. 433function 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. 467function 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 567function 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 597function 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. 605function 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 621window.addEventListener('resize', handleWindowResize); 622 623var sectionToElementMap; 624function 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 635function getSectionMaxiview(section) { 636 return $(section.id + '-maxiview'); 637} 638 639function getSectionMiniview(section) { 640 return section.querySelector('.miniview'); 641} 642 643// You usually want to call |showOnlySection()| instead of this. 644function 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. 688function 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 697function 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 734window.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 */ 751function 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 769function 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@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 */ 807function 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 */ 860function 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 */ 869function 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 */ 878function formatTabsText(numTabs) { 879 if (numTabs == 1) 880 return localStrings.getString('closedwindowsingle'); 881 return localStrings.getStringF('closedwindowmultiple', numTabs); 882} 883 884// Theme related 885 886function themeChanged(hasAttribution) { 887 document.documentElement.setAttribute('hasattribution', hasAttribution); 888 $('themecss').href = 'chrome://theme/css/newtab.css?' + Date.now(); 889 updateAttribution(); 890} 891 892function 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. 909function 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 933function bookmarkBarAttached() { 934 document.documentElement.setAttribute('bookmarkbarattached', 'true'); 935} 936 937function bookmarkBarDetached() { 938 document.documentElement.setAttribute('bookmarkbarattached', 'false'); 939} 940 941function 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 955handleWindowResize(); 956 957var localStrings = new LocalStrings(); 958 959/////////////////////////////////////////////////////////////////////////////// 960// Things we know are not needed at startup go below here 961 962function 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 975var 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 */ 988function 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 */ 1058function 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 1079function showFirstRunNotification() { 1080 showNotification(localStrings.getString('firstrunnotification'), 1081 null, null, 30000); 1082 var notificationElement = $('notification'); 1083 notification.classList.add('first-run'); 1084} 1085 1086function showPromoNotification() { 1087 showNotification(parseHtmlSubset(localStrings.getString('serverpromo')), 1088 localStrings.getString('syncpromotext'), 1089 function () { chrome.send('SyncLinkClicked'); }, 1090 60000); 1091 var notificationElement = $('notification'); 1092 notification.classList.add('promo'); 1093} 1094 1095$('main').addEventListener('click', function(e) { 1096 var p = e.target; 1097 while (p && p.tagName != 'H2') { 1098 // In case the user clicks on a button we do not want to expand/collapse a 1099 // section. 1100 if (p.tagName == 'BUTTON') 1101 return; 1102 p = p.parentNode; 1103 } 1104 1105 if (!p) 1106 return; 1107 1108 p = p.parentNode; 1109 if (!getSectionMaxiview(p)) 1110 return; 1111 1112 toggleSectionVisibilityAndAnimate(p.getAttribute('section')); 1113}); 1114 1115$('most-visited-settings').addEventListener('click', function() { 1116 $('clear-all-blacklisted').execute(); 1117}); 1118 1119function toggleSectionVisibilityAndAnimate(section) { 1120 if (!section) 1121 return; 1122 1123 // It looks better to return the scroll to the top when toggling sections. 1124 document.body.scrollTop = 0; 1125 1126 // We set it back in webkitTransitionEnd. 1127 document.documentElement.setAttribute('enable-section-animations', 'true'); 1128 if (shownSections & Section[section]) { 1129 hideSection(Section[section]); 1130 } else { 1131 showOnlySection(section); 1132 } 1133 layoutSections(); 1134 saveShownSections(); 1135} 1136 1137function handleIfEnterKey(f) { 1138 return function(e) { 1139 if (e.keyIdentifier == 'Enter') 1140 f(e); 1141 }; 1142} 1143 1144function maybeReopenTab(e) { 1145 var el = findAncestor(e.target, function(el) { 1146 return el.sessionId !== undefined; 1147 }); 1148 if (el) { 1149 chrome.send('reopenTab', [String(el.sessionId)]); 1150 e.preventDefault(); 1151 1152 setWindowTooltipTimeout(); 1153 } 1154} 1155 1156// Note that the openForeignSession calls can fail, resulting this method to 1157// not have any action (hence the maybe). 1158function maybeOpenForeignSession(e) { 1159 var el = findAncestor(e.target, function(el) { 1160 return el.sessionTag !== undefined; 1161 }); 1162 if (el) { 1163 chrome.send('openForeignSession', [String(el.sessionTag)]); 1164 e.stopPropagation(); 1165 e.preventDefault(); 1166 setWindowTooltipTimeout(); 1167 } 1168} 1169 1170function maybeOpenForeignWindow(e) { 1171 var el = findAncestor(e.target, function(el) { 1172 return el.winNum !== undefined; 1173 }); 1174 if (el) { 1175 chrome.send('openForeignSession', [String(el.sessionTag), 1176 String(el.winNum)]); 1177 e.stopPropagation(); 1178 e.preventDefault(); 1179 setWindowTooltipTimeout(); 1180 } 1181} 1182 1183function maybeOpenForeignTab(e) { 1184 var el = findAncestor(e.target, function(el) { 1185 return el.sessionId !== undefined; 1186 }); 1187 if (el) { 1188 chrome.send('openForeignSession', [String(el.sessionTag), String(el.winNum), 1189 String(el.sessionId)]); 1190 e.stopPropagation(); 1191 e.preventDefault(); 1192 setWindowTooltipTimeout(); 1193 } 1194} 1195 1196// HACK(arv): After the window onblur event happens we get a mouseover event 1197// on the next item and we want to make sure that we do not show a tooltip 1198// for that. 1199function setWindowTooltipTimeout(e) { 1200 window.setTimeout(function() { 1201 windowTooltip.hide(); 1202 }, 2 * WindowTooltip.DELAY); 1203} 1204 1205function maybeShowWindowTooltip(e) { 1206 var f = function(el) { 1207 return el.tabItems !== undefined; 1208 }; 1209 var el = findAncestor(e.target, f); 1210 var relatedEl = findAncestor(e.relatedTarget, f); 1211 if (el && el != relatedEl) { 1212 windowTooltip.handleMouseOver(e, el, el.tabItems); 1213 } 1214} 1215 1216 1217var recentlyClosedElement = $('recently-closed'); 1218 1219recentlyClosedElement.addEventListener('click', maybeReopenTab); 1220recentlyClosedElement.addEventListener('keydown', 1221 handleIfEnterKey(maybeReopenTab)); 1222 1223recentlyClosedElement.addEventListener('mouseover', maybeShowWindowTooltip); 1224recentlyClosedElement.addEventListener('focus', maybeShowWindowTooltip, true); 1225 1226var foreignSessionElement = $('foreign-sessions'); 1227 1228foreignSessionElement.addEventListener('click', maybeOpenForeignSession); 1229foreignSessionElement.addEventListener('keydown', 1230 handleIfEnterKey( 1231 maybeOpenForeignSession)); 1232 1233foreignSessionElement.addEventListener('mouseover', maybeShowWindowTooltip); 1234foreignSessionElement.addEventListener('focus', maybeShowWindowTooltip, true); 1235 1236/** 1237 * This object represents a tooltip representing a closed window. It is 1238 * shown when hovering over a closed window item or when the item is focused. It 1239 * gets hidden when blurred or when mousing out of the menu or the item. 1240 * @param {Element} tooltipEl The element to use as the tooltip. 1241 * @constructor 1242 */ 1243function WindowTooltip(tooltipEl) { 1244 this.tooltipEl = tooltipEl; 1245 this.boundHide_ = this.hide.bind(this); 1246 this.boundHandleMouseOut_ = this.handleMouseOut.bind(this); 1247} 1248 1249WindowTooltip.trackMouseMove_ = function(e) { 1250 WindowTooltip.clientX = e.clientX; 1251 WindowTooltip.clientY = e.clientY; 1252}; 1253 1254/** 1255 * Time in ms to delay before the tooltip is shown. 1256 * @type {number} 1257 */ 1258WindowTooltip.DELAY = 300; 1259 1260WindowTooltip.prototype = { 1261 timer: 0, 1262 handleMouseOver: function(e, linkEl, tabs) { 1263 this.linkEl_ = linkEl; 1264 if (e.type == 'mouseover') { 1265 this.linkEl_.addEventListener('mousemove', WindowTooltip.trackMouseMove_); 1266 this.linkEl_.addEventListener('mouseout', this.boundHandleMouseOut_); 1267 } else { // focus 1268 this.linkEl_.addEventListener('blur', this.boundHide_); 1269 } 1270 this.timer = window.setTimeout(this.show.bind(this, e.type, linkEl, tabs), 1271 WindowTooltip.DELAY); 1272 }, 1273 show: function(type, linkEl, tabs) { 1274 window.addEventListener('blur', this.boundHide_); 1275 this.linkEl_.removeEventListener('mousemove', 1276 WindowTooltip.trackMouseMove_); 1277 window.clearTimeout(this.timer); 1278 1279 this.renderItems(tabs); 1280 var rect = linkEl.getBoundingClientRect(); 1281 var bodyRect = document.body.getBoundingClientRect(); 1282 var rtl = document.documentElement.dir == 'rtl'; 1283 1284 this.tooltipEl.style.display = 'block'; 1285 var tooltipRect = this.tooltipEl.getBoundingClientRect(); 1286 var x, y; 1287 1288 // When focused show below, like a drop down menu. 1289 if (type == 'focus') { 1290 x = rtl ? 1291 rect.left + bodyRect.left + rect.width - this.tooltipEl.offsetWidth : 1292 rect.left + bodyRect.left; 1293 y = rect.top + bodyRect.top + rect.height; 1294 } else { 1295 x = bodyRect.left + (rtl ? 1296 WindowTooltip.clientX - this.tooltipEl.offsetWidth : 1297 WindowTooltip.clientX); 1298 // Offset like a tooltip 1299 y = 20 + WindowTooltip.clientY + bodyRect.top; 1300 } 1301 1302 // We need to ensure that the tooltip is inside the window viewport. 1303 x = Math.min(x, bodyRect.width - tooltipRect.width); 1304 x = Math.max(x, 0); 1305 y = Math.min(y, bodyRect.height - tooltipRect.height); 1306 y = Math.max(y, 0); 1307 1308 this.tooltipEl.style.left = x + 'px'; 1309 this.tooltipEl.style.top = y + 'px'; 1310 }, 1311 handleMouseOut: function(e) { 1312 // Don't hide when move to another item in the link. 1313 var f = function(el) { 1314 return el.tabItems !== undefined; 1315 }; 1316 var el = findAncestor(e.target, f); 1317 var relatedEl = findAncestor(e.relatedTarget, f); 1318 if (el && el != relatedEl) { 1319 this.hide(); 1320 } 1321 }, 1322 hide: function() { 1323 window.clearTimeout(this.timer); 1324 window.removeEventListener('blur', this.boundHide_); 1325 this.linkEl_.removeEventListener('mousemove', 1326 WindowTooltip.trackMouseMove_); 1327 this.linkEl_.removeEventListener('mouseout', this.boundHandleMouseOut_); 1328 this.linkEl_.removeEventListener('blur', this.boundHide_); 1329 this.linkEl_ = null; 1330 1331 this.tooltipEl.style.display = 'none'; 1332 }, 1333 renderItems: function(tabs) { 1334 var tooltip = this.tooltipEl; 1335 tooltip.textContent = ''; 1336 1337 tabs.forEach(function(tab) { 1338 var span = document.createElement('span'); 1339 span.className = 'item'; 1340 span.style.backgroundImage = url('chrome://favicon/' + tab.url); 1341 span.dir = tab.direction; 1342 span.textContent = tab.title; 1343 tooltip.appendChild(span); 1344 }); 1345 } 1346}; 1347 1348var windowTooltip = new WindowTooltip($('window-tooltip')); 1349 1350window.addEventListener('load', 1351 logEvent.bind(global, 'Tab.NewTabOnload', true)); 1352 1353window.addEventListener('resize', handleWindowResize); 1354document.addEventListener('DOMContentLoaded', 1355 logEvent.bind(global, 'Tab.NewTabDOMContentLoaded', true)); 1356 1357// Whether or not we should send the initial 'GetSyncMessage' to the backend 1358// depends on the value of the attribue 'syncispresent' which the backend sets 1359// to indicate if there is code in the backend which is capable of processing 1360// this message. This attribute is loaded by the JSTemplate and therefore we 1361// must make sure we check the attribute after the DOM is loaded. 1362document.addEventListener('DOMContentLoaded', 1363 callGetSyncMessageIfSyncIsPresent); 1364 1365/** 1366 * The sync code is not yet built by default on all platforms so we have to 1367 * make sure we don't send the initial sync message to the backend unless the 1368 * backend told us that the sync code is present. 1369 */ 1370function callGetSyncMessageIfSyncIsPresent() { 1371 if (document.documentElement.getAttribute('syncispresent') == 'true') { 1372 chrome.send('GetSyncMessage'); 1373 } 1374} 1375 1376// Tooltip for elements that have text that overflows. 1377document.addEventListener('mouseover', function(e) { 1378 // We don't want to do this while we are dragging because it makes things very 1379 // janky 1380 if (mostVisited.isDragging()) { 1381 return; 1382 } 1383 1384 var el = findAncestor(e.target, function(el) { 1385 return el.xtitle; 1386 }); 1387 if (el && el.xtitle != el.title) { 1388 if (el.scrollWidth > el.clientWidth) { 1389 el.title = el.xtitle; 1390 } else { 1391 el.title = ''; 1392 } 1393 } 1394}); 1395 1396/** 1397 * Makes links and buttons support a different underline color. 1398 * @param {Node} node The node to search for links and buttons in. 1399 */ 1400function fixLinkUnderlines(node) { 1401 var elements = node.querySelectorAll('a,button'); 1402 Array.prototype.forEach.call(elements, fixLinkUnderline); 1403} 1404 1405/** 1406 * Wraps the content of an element in a a link-color span. 1407 * @param {Element} el The element to wrap. 1408 */ 1409function fixLinkUnderline(el) { 1410 var span = document.createElement('span'); 1411 span.className = 'link-color'; 1412 while (el.hasChildNodes()) { 1413 span.appendChild(el.firstChild); 1414 } 1415 el.appendChild(span); 1416} 1417 1418updateAttribution(); 1419 1420function initializeLogin() { 1421 chrome.send('initializeLogin', []); 1422} 1423 1424function updateLogin(login) { 1425 $('login-container').style.display = login ? 'block' : ''; 1426 if (login) 1427 $('login-username').textContent = login; 1428 1429} 1430 1431var mostVisited = new MostVisited( 1432 $('most-visited-maxiview'), 1433 document.querySelector('#most-visited .miniview'), 1434 $('most-visited-menu'), 1435 useSmallGrid(), 1436 shownSections & Section.THUMB); 1437 1438function mostVisitedPages(data, firstRun, hasBlacklistedUrls) { 1439 logEvent('received most visited pages'); 1440 1441 mostVisited.updateSettingsLink(hasBlacklistedUrls); 1442 mostVisited.data = data; 1443 mostVisited.layout(); 1444 layoutSections(); 1445 1446 // Remove class name in a timeout so that changes done in this JS thread are 1447 // not animated. 1448 window.setTimeout(function() { 1449 mostVisited.ensureSmallGridCorrect(); 1450 maybeDoneLoading(); 1451 }, 1); 1452 1453 // Only show the first run notification if first run. 1454 if (firstRun) { 1455 showFirstRunNotification(); 1456 } else if (localStrings.getString('serverpromo')) { 1457 showPromoNotification(); 1458 } 1459} 1460 1461function maybeDoneLoading() { 1462 if (mostVisited.data && apps.loaded) 1463 document.body.classList.remove('loading'); 1464} 1465 1466function isDoneLoading() { 1467 return !document.body.classList.contains('loading'); 1468} 1469 1470// Initialize the apps promo. 1471document.addEventListener('DOMContentLoaded', function() { 1472 var promoLink = document.querySelector('#apps-promo-text1 a'); 1473 promoLink.id = 'apps-promo-link'; 1474 promoLink.href = localStrings.getString('web_store_url'); 1475 1476 $('apps-promo-hide').addEventListener('click', function() { 1477 chrome.send('hideAppsPromo', []); 1478 document.documentElement.classList.remove('apps-promo-visible'); 1479 layoutSections(); 1480 }); 1481}); 1482