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