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.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.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 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.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 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 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 1112function 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 1130function handleIfEnterKey(f) { 1131 return function(e) { 1132 if (e.keyIdentifier == 'Enter') 1133 f(e); 1134 }; 1135} 1136 1137function 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). 1151function 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 1163function 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 1176function 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. 1192function setWindowTooltipTimeout(e) { 1193 window.setTimeout(function() { 1194 windowTooltip.hide(); 1195 }, 2 * WindowTooltip.DELAY); 1196} 1197 1198function 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 1210var recentlyClosedElement = $('recently-closed'); 1211 1212recentlyClosedElement.addEventListener('click', maybeReopenTab); 1213recentlyClosedElement.addEventListener('keydown', 1214 handleIfEnterKey(maybeReopenTab)); 1215 1216recentlyClosedElement.addEventListener('mouseover', maybeShowWindowTooltip); 1217recentlyClosedElement.addEventListener('focus', maybeShowWindowTooltip, true); 1218 1219var foreignSessionElement = $('foreign-sessions'); 1220 1221foreignSessionElement.addEventListener('click', maybeOpenForeignSession); 1222foreignSessionElement.addEventListener('keydown', 1223 handleIfEnterKey( 1224 maybeOpenForeignSession)); 1225 1226foreignSessionElement.addEventListener('mouseover', maybeShowWindowTooltip); 1227foreignSessionElement.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 */ 1236function WindowTooltip(tooltipEl) { 1237 this.tooltipEl = tooltipEl; 1238 this.boundHide_ = this.hide.bind(this); 1239 this.boundHandleMouseOut_ = this.handleMouseOut.bind(this); 1240} 1241 1242WindowTooltip.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 */ 1251WindowTooltip.DELAY = 300; 1252 1253WindowTooltip.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 1341var windowTooltip = new WindowTooltip($('window-tooltip')); 1342 1343window.addEventListener('load', 1344 logEvent.bind(global, 'Tab.NewTabOnload', true)); 1345 1346window.addEventListener('resize', handleWindowResize); 1347document.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. 1355document.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 */ 1363function callGetSyncMessageIfSyncIsPresent() { 1364 if (document.documentElement.getAttribute('syncispresent') == 'true') { 1365 chrome.send('GetSyncMessage'); 1366 } 1367} 1368 1369// Tooltip for elements that have text that overflows. 1370document.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 */ 1393function 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 */ 1402function 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 1411updateAttribution(); 1412 1413function initializeLogin() { 1414 chrome.send('initializeLogin', []); 1415} 1416 1417function updateLogin(login) { 1418 $('login-container').style.display = login ? 'block' : ''; 1419 if (login) 1420 $('login-username').textContent = login; 1421 1422} 1423 1424var mostVisited = new MostVisited( 1425 $('most-visited-maxiview'), 1426 document.querySelector('#most-visited .miniview'), 1427 $('most-visited-menu'), 1428 useSmallGrid(), 1429 shownSections & Section.THUMB); 1430 1431function 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 1451function maybeDoneLoading() { 1452 if (mostVisited.data && apps.loaded) 1453 document.body.classList.remove('loading'); 1454} 1455 1456function 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. 1463document.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