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