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