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