| // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // To avoid creating tons of unnecessary nodes. We assume we cannot fit more |
| // than this many items in the miniview. |
| var MAX_MINIVIEW_ITEMS = 15; |
| |
| // Extra spacing at the top of the layout. |
| var LAYOUT_SPACING_TOP = 25; |
| |
| // The visible height of the expanded maxiview. |
| var maxiviewVisibleHeight = 0; |
| |
| var APP_LAUNCH = { |
| // The histogram buckets (keep in sync with extension_constants.h). |
| NTP_APPS_MAXIMIZED: 0, |
| NTP_APPS_COLLAPSED: 1, |
| NTP_APPS_MENU: 2, |
| NTP_MOST_VISITED: 3, |
| NTP_RECENTLY_CLOSED: 4, |
| NTP_APP_RE_ENABLE: 16 |
| }; |
| |
| var APP_LAUNCH_URL = { |
| // The URL prefix for pings that record app launches by URL. |
| PING_BY_URL: 'record-app-launch-by-url', |
| |
| // The URL prefix for pings that record app launches by ID. |
| PING_BY_ID: 'record-app-launch-by-id', |
| |
| // The URL prefix used by the webstore link 'ping' attributes. |
| PING_WEBSTORE: 'record-webstore-launch' |
| }; |
| |
| function getAppPingUrl(prefix, data, bucket) { |
| return [APP_LAUNCH_URL[prefix], |
| encodeURIComponent(data), |
| APP_LAUNCH[bucket]].join('+'); |
| } |
| |
| function getSectionCloseButton(sectionId) { |
| return document.querySelector('#' + sectionId + ' .section-close-button'); |
| } |
| |
| function getSectionMenuButton(sectionId) { |
| return $(sectionId + '-button'); |
| } |
| |
| function getSectionMenuButtonTextId(sectionId) { |
| return sectionId.replace(/-/g, ''); |
| } |
| |
| function setSectionMenuMode(sectionId, section, menuModeEnabled, menuModeMask) { |
| var el = $(sectionId); |
| if (!menuModeEnabled) { |
| // Because sections are collapsed when they are in menu mode, it is not |
| // necessary to restore the maxiview here. It will happen if the section |
| // header is clicked. |
| // TODO(aa): Sections should maintain their collapse state when minimized. |
| el.classList.remove('menu'); |
| shownSections &= ~menuModeMask; |
| } else { |
| if (section) { |
| hideSection(section); // To hide the maxiview. |
| } |
| el.classList.add('menu'); |
| shownSections |= menuModeMask; |
| } |
| layoutSections(); |
| } |
| |
| function clearClosedMenu(menu) { |
| menu.innerHTML = ''; |
| } |
| |
| function addClosedMenuEntryWithLink(menu, a) { |
| var span = document.createElement('span'); |
| a.className += ' item menuitem'; |
| span.appendChild(a); |
| menu.appendChild(span); |
| } |
| |
| function addClosedMenuEntry(menu, url, title, imageUrl, opt_pingUrl) { |
| var a = document.createElement('a'); |
| a.href = url; |
| a.textContent = title; |
| a.style.backgroundImage = 'url(' + imageUrl + ')'; |
| if (opt_pingUrl) |
| a.ping = opt_pingUrl; |
| addClosedMenuEntryWithLink(menu, a); |
| } |
| |
| function addClosedMenuFooter(menu, sectionId, mask, opt_section) { |
| menu.appendChild(document.createElement('hr')); |
| |
| var span = document.createElement('span'); |
| var a = span.appendChild(document.createElement('a')); |
| a.href = ''; |
| if (cr.isChromeOS) { |
| a.textContent = localStrings.getString('expandMenu'); |
| } else { |
| a.textContent = |
| localStrings.getString(getSectionMenuButtonTextId(sectionId)); |
| } |
| a.className = 'item'; |
| a.addEventListener( |
| 'click', |
| function(e) { |
| getSectionMenuButton(sectionId).hideMenu(); |
| e.preventDefault(); |
| setSectionMenuMode(sectionId, opt_section, false, mask); |
| shownSections &= ~mask; |
| saveShownSections(); |
| }); |
| menu.appendChild(span); |
| } |
| |
| function initializeSection(sectionId, mask, opt_section) { |
| var button = getSectionCloseButton(sectionId); |
| button.addEventListener( |
| 'click', |
| function() { |
| setSectionMenuMode(sectionId, opt_section, true, mask); |
| saveShownSections(); |
| }); |
| } |
| |
| function updateSimpleSection(id, section) { |
| var elm = $(id); |
| var maxiview = getSectionMaxiview(elm); |
| var miniview = getSectionMiniview(elm); |
| if (shownSections & section) { |
| // The section is expanded, so the maxiview should be opaque (visible) and |
| // the miniview should be hidden. |
| elm.classList.remove('collapsed'); |
| if (maxiview) { |
| maxiview.classList.remove('collapsed'); |
| maxiview.classList.add('opaque'); |
| } |
| if (miniview) |
| miniview.classList.remove('opaque'); |
| } else { |
| // The section is collapsed, so the maxiview should be hidden and the |
| // miniview should be opaque. |
| elm.classList.add('collapsed'); |
| if (maxiview) { |
| maxiview.classList.add('collapsed'); |
| maxiview.classList.remove('opaque'); |
| } |
| if (miniview) |
| miniview.classList.add('opaque'); |
| } |
| } |
| |
| var sessionItems = []; |
| |
| function foreignSessions(data) { |
| logEvent('received foreign sessions'); |
| // We need to store the foreign sessions so we can update the layout on a |
| // resize. |
| sessionItems = data; |
| renderForeignSessions(); |
| layoutSections(); |
| } |
| |
| function renderForeignSessions() { |
| // Remove all existing items and create new items. |
| var sessionElement = $('foreign-sessions'); |
| var parentSessionElement = sessionElement.lastElementChild; |
| parentSessionElement.textContent = ''; |
| |
| // For each client, create entries and append the lists together. |
| sessionItems.forEach(function(item, i) { |
| // TODO(zea): Get real client names. See crbug/59672. |
| var name = 'Client ' + i; |
| parentSessionElement.appendChild(createForeignSession(item, name)); |
| }); |
| |
| layoutForeignSessions(); |
| } |
| |
| function layoutForeignSessions() { |
| var sessionElement = $('foreign-sessions'); |
| // We cannot use clientWidth here since the width has a transition. |
| var availWidth = useSmallGrid() ? 692 : 920; |
| var parentSessEl = sessionElement.lastElementChild; |
| |
| if (parentSessEl.hasChildNodes()) { |
| sessionElement.classList.remove('disabled'); |
| sessionElement.classList.remove('opaque'); |
| } else { |
| sessionElement.classList.add('disabled'); |
| sessionElement.classList.add('opaque'); |
| } |
| } |
| |
| function createForeignSession(client, name) { |
| // Vertically stack the windows in a client. |
| var stack = document.createElement('div'); |
| stack.className = 'foreign-session-client item link'; |
| stack.textContent = name; |
| stack.sessionTag = client[0].sessionTag; |
| |
| client.forEach(function(win, i) { |
| // Create a window entry. |
| var winSpan = document.createElement('span'); |
| var winEl = document.createElement('p'); |
| winEl.className = 'item link window'; |
| winEl.tabItems = win.tabs; |
| winEl.tabIndex = 0; |
| winEl.textContent = formatTabsText(win.tabs.length); |
| winEl.xtitle = win.title; |
| winEl.sessionTag = win.sessionTag; |
| winEl.winNum = i; |
| winEl.addEventListener('click', maybeOpenForeignWindow); |
| winEl.addEventListener('keydown', |
| handleIfEnterKey(maybeOpenForeignWindow)); |
| winSpan.appendChild(winEl); |
| |
| // Sort tabs by MRU order |
| win.tabs.sort(function(a, b) { |
| return a.timestamp < b.timestamp; |
| }); |
| |
| // Create individual tab information. |
| win.tabs.forEach(function(data) { |
| var tabEl = document.createElement('a'); |
| tabEl.className = 'item link tab'; |
| tabEl.href = data.timestamp; |
| tabEl.style.backgroundImage = url('chrome://favicon/' + data.url); |
| tabEl.dir = data.direction; |
| tabEl.textContent = data.title; |
| tabEl.sessionTag = win.sessionTag; |
| tabEl.winNum = i; |
| tabEl.sessionId = data.sessionId; |
| tabEl.addEventListener('click', maybeOpenForeignTab); |
| tabEl.addEventListener('keydown', |
| handleIfEnterKey(maybeOpenForeignTab)); |
| |
| winSpan.appendChild(tabEl); |
| }); |
| |
| // Append the window. |
| stack.appendChild(winSpan); |
| }); |
| return stack; |
| } |
| |
| var recentItems = []; |
| |
| function recentlyClosedTabs(data) { |
| logEvent('received recently closed tabs'); |
| // We need to store the recent items so we can update the layout on a resize. |
| recentItems = data; |
| renderRecentlyClosed(); |
| layoutSections(); |
| } |
| |
| function renderRecentlyClosed() { |
| // Remove all existing items and create new items. |
| var recentElement = $('recently-closed'); |
| var parentEl = recentElement.lastElementChild; |
| parentEl.textContent = ''; |
| var recentMenu = $('recently-closed-menu'); |
| clearClosedMenu(recentMenu); |
| |
| recentItems.forEach(function(item) { |
| parentEl.appendChild(createRecentItem(item)); |
| addRecentMenuItem(recentMenu, item); |
| }); |
| addClosedMenuFooter(recentMenu, 'recently-closed', MENU_RECENT); |
| |
| layoutRecentlyClosed(); |
| } |
| |
| function createRecentItem(data) { |
| var isWindow = data.type == 'window'; |
| var el; |
| if (isWindow) { |
| el = document.createElement('span'); |
| el.className = 'item link window'; |
| el.tabItems = data.tabs; |
| el.tabIndex = 0; |
| el.textContent = formatTabsText(data.tabs.length); |
| } else { |
| el = document.createElement('a'); |
| el.className = 'item'; |
| el.href = data.url; |
| el.ping = getAppPingUrl( |
| 'PING_BY_URL', data.url, 'NTP_RECENTLY_CLOSED'); |
| el.style.backgroundImage = url('chrome://favicon/' + data.url); |
| el.dir = data.direction; |
| el.textContent = data.title; |
| } |
| el.sessionId = data.sessionId; |
| el.xtitle = data.title; |
| el.sessionTag = data.sessionTag; |
| var wrapperEl = document.createElement('span'); |
| wrapperEl.appendChild(el); |
| return wrapperEl; |
| } |
| |
| function addRecentMenuItem(menu, data) { |
| var isWindow = data.type == 'window'; |
| var a = document.createElement('a'); |
| if (isWindow) { |
| a.textContent = formatTabsText(data.tabs.length); |
| a.className = 'window'; // To get the icon from the CSS .window rule. |
| a.href = ''; // To make underline show up. |
| } else { |
| a.href = data.url; |
| a.ping = getAppPingUrl( |
| 'PING_BY_URL', data.url, 'NTP_RECENTLY_CLOSED'); |
| a.style.backgroundImage = 'url(chrome://favicon/' + data.url + ')'; |
| a.textContent = data.title; |
| } |
| function clickHandler(e) { |
| chrome.send('reopenTab', [String(data.sessionId)]); |
| e.preventDefault(); |
| } |
| a.addEventListener('click', clickHandler); |
| addClosedMenuEntryWithLink(menu, a); |
| } |
| |
| function saveShownSections() { |
| chrome.send('setShownSections', [shownSections]); |
| } |
| |
| var LayoutMode = { |
| SMALL: 1, |
| NORMAL: 2 |
| }; |
| |
| var layoutMode = useSmallGrid() ? LayoutMode.SMALL : LayoutMode.NORMAL; |
| |
| function handleWindowResize() { |
| if (window.innerWidth < 10) { |
| // We're probably a background tab, so don't do anything. |
| return; |
| } |
| |
| // TODO(jstritar): Remove the small-layout class and revert back to the |
| // @media (max-width) directive once http://crbug.com/70930 is fixed. |
| var oldLayoutMode = layoutMode; |
| var b = useSmallGrid(); |
| if (b) { |
| layoutMode = LayoutMode.SMALL; |
| document.body.classList.add('small-layout'); |
| } else { |
| layoutMode = LayoutMode.NORMAL; |
| document.body.classList.remove('small-layout'); |
| } |
| |
| if (layoutMode != oldLayoutMode){ |
| mostVisited.useSmallGrid = b; |
| mostVisited.layout(); |
| apps.layout({force:true}); |
| renderRecentlyClosed(); |
| renderForeignSessions(); |
| updateAllMiniviewClippings(); |
| } |
| |
| layoutSections(); |
| } |
| |
| // Stores some information about each section necessary to layout. A new |
| // instance is constructed for each section on each layout. |
| function SectionLayoutInfo(section) { |
| this.section = section; |
| this.header = section.querySelector('h2'); |
| this.miniview = section.querySelector('.miniview'); |
| this.maxiview = getSectionMaxiview(section); |
| this.expanded = this.maxiview && !section.classList.contains('collapsed'); |
| this.fixedHeight = this.section.offsetHeight; |
| this.scrollingHeight = 0; |
| |
| if (this.expanded) |
| this.scrollingHeight = this.maxiview.offsetHeight; |
| } |
| |
| // Get all sections to be layed out. |
| SectionLayoutInfo.getAll = function() { |
| var sections = document.querySelectorAll( |
| '.section:not(.disabled):not(.menu)'); |
| var result = []; |
| for (var i = 0, section; section = sections[i]; i++) { |
| result.push(new SectionLayoutInfo(section)); |
| } |
| return result; |
| }; |
| |
| // Ensure the miniview sections don't have any clipped items. |
| function updateMiniviewClipping(miniview) { |
| var clipped = false; |
| for (var j = 0, item; item = miniview.children[j]; j++) { |
| item.style.display = ''; |
| if (clipped || |
| (item.offsetLeft + item.offsetWidth) > miniview.offsetWidth) { |
| item.style.display = 'none'; |
| clipped = true; |
| } else { |
| item.style.display = ''; |
| } |
| } |
| } |
| |
| // Ensure none of the miniviews have any clipped items. |
| function updateAllMiniviewClippings() { |
| var miniviews = document.querySelectorAll('.section.collapsed .miniview'); |
| for (var i = 0, miniview; miniview = miniviews[i]; i++) { |
| updateMiniviewClipping(miniview); |
| } |
| } |
| |
| // Returns whether or not vertical scrollbars are present. |
| function hasScrollBars() { |
| return window.innerHeight != document.body.clientHeight; |
| } |
| |
| // Enables scrollbars (they will only show up if needed). |
| function showScrollBars() { |
| document.body.classList.remove('noscroll'); |
| } |
| |
| // Hides all scrollbars. |
| function hideScrollBars() { |
| document.body.classList.add('noscroll'); |
| } |
| |
| // Returns whether or not the sections are currently animating due to a |
| // section transition. |
| function isAnimating() { |
| var de = document.documentElement; |
| return de.getAttribute('enable-section-animations') == 'true'; |
| } |
| |
| // Layout the sections in a modified accordian. The header and miniview, if |
| // visible are fixed within the viewport. If there is an expanded section, its |
| // it scrolls. |
| // |
| // ============================= |
| // | collapsed section | <- Any collapsed sections are fixed position. |
| // | and miniview | |
| // |---------------------------| |
| // | expanded section | |
| // | | <- There can be one expanded section and it |
| // | and maxiview | is absolutely positioned so that it can |
| // | | scroll "underneath" the fixed elements. |
| // | | |
| // |---------------------------| |
| // | another collapsed section | |
| // |---------------------------| |
| // |
| // We want the main frame scrollbar to be the one that scrolls the expanded |
| // region. To get this effect, we make the fixed elements position:fixed and the |
| // scrollable element position:absolute. We also artificially increase the |
| // height of the document so that it is possible to scroll down enough to |
| // display the end of the document, even with any fixed elements at the bottom |
| // of the viewport. |
| // |
| // There is a final twist: If the intrinsic height of the expanded section is |
| // less than the available height (because the window is tall), any collapsed |
| // sections sinch up and sit below the expanded section. This is so that we |
| // don't have a bunch of dead whitespace in the case of expanded sections that |
| // aren't very tall. |
| function layoutSections() { |
| // While transitioning sections, we only want scrollbars to appear if they're |
| // already present or the window is being resized (so there's no animation). |
| if (!hasScrollBars() && isAnimating()) |
| hideScrollBars(); |
| |
| var sections = SectionLayoutInfo.getAll(); |
| var expandedSection = null; |
| var headerHeight = LAYOUT_SPACING_TOP; |
| var footerHeight = 0; |
| |
| // Calculate the height of the fixed elements above the expanded section. Also |
| // take note of the expanded section, if there is one. |
| var i; |
| var section; |
| for (i = 0; section = sections[i]; i++) { |
| headerHeight += section.fixedHeight; |
| if (section.expanded) { |
| expandedSection = section; |
| i++; |
| break; |
| } |
| } |
| |
| // Calculate the height of the fixed elements below the expanded section, if |
| // any. |
| for (; section = sections[i]; i++) { |
| footerHeight += section.fixedHeight; |
| } |
| // Leave room for bottom bar if it's visible. |
| footerHeight += $('closed-sections-bar').offsetHeight; |
| |
| |
| // Determine the height to use for the expanded section. If there isn't enough |
| // space to show the expanded section completely, this will be the available |
| // height. Otherwise, we use the intrinsic height of the expanded section. |
| var expandedSectionHeight; |
| if (expandedSection) { |
| var flexHeight = window.innerHeight - headerHeight - footerHeight; |
| if (flexHeight < expandedSection.scrollingHeight) { |
| expandedSectionHeight = flexHeight; |
| |
| // Also, artificially expand the height of the document so that we can see |
| // the entire expanded section. |
| // |
| // TODO(aa): Where does this come from? It is the difference between what |
| // we set document.body.style.height to and what |
| // document.body.scrollHeight measures afterward. I expect them to be the |
| // same if document.body has no margins. |
| var fudge = 44; |
| document.body.style.height = |
| headerHeight + |
| expandedSection.scrollingHeight + |
| footerHeight + |
| fudge + |
| 'px'; |
| } else { |
| expandedSectionHeight = expandedSection.scrollingHeight; |
| document.body.style.height = ''; |
| } |
| } else { |
| // We only set the document height when a section is expanded. If |
| // all sections are collapsed, then get rid of the previous height. |
| document.body.style.height = ''; |
| } |
| |
| maxiviewVisibleHeight = expandedSectionHeight; |
| |
| // Now position all the elements. |
| var y = LAYOUT_SPACING_TOP; |
| for (i = 0, section; section = sections[i]; i++) { |
| section.section.style.top = y + 'px'; |
| y += section.fixedHeight; |
| |
| if (section.maxiview) { |
| if (section == expandedSection) { |
| section.maxiview.style.top = y + 'px'; |
| } else { |
| // The miniviews fade out gradually, so it may have height at this |
| // point. We position the maxiview as if the miniview was not displayed |
| // by subtracting off the miniview's total height (height + margin). |
| var miniviewFudge = 40; // miniview margin-bottom + margin-top |
| var miniviewHeight = section.miniview.offsetHeight + miniviewFudge; |
| section.maxiview.style.top = y - miniviewHeight + 'px'; |
| } |
| } |
| |
| if (section.maxiview && section == expandedSection) |
| updateMask(section.maxiview, expandedSectionHeight); |
| |
| if (section == expandedSection) |
| y += expandedSectionHeight; |
| } |
| if (cr.isChromeOS) |
| $('closed-sections-bar').style.top = y + 'px'; |
| |
| updateMenuSections(); |
| updateAttributionDisplay(y); |
| } |
| |
| function updateMask(maxiview, visibleHeightPx) { |
| // We want to end up with 10px gradients at the top and bottom of |
| // visibleHeight, but webkit-mask only supports expression in terms of |
| // percentages. |
| |
| // We might not have enough room to do 10px gradients on each side. To get the |
| // right effect, we don't want to make the gradients smaller, but make them |
| // appear to mush into each other. |
| var gradientHeightPx = Math.min(10, Math.floor(visibleHeightPx / 2)); |
| var gradientDestination = 'rgba(0,0,0,' + (gradientHeightPx / 10) + ')'; |
| |
| var bottomSpacing = 15; |
| var first = parseFloat(maxiview.style.top) / window.innerHeight; |
| var second = first + gradientHeightPx / window.innerHeight; |
| var fourth = first + (visibleHeightPx - bottomSpacing) / window.innerHeight; |
| var third = fourth - gradientHeightPx / window.innerHeight; |
| |
| var gradientArguments = [ |
| 'transparent', |
| getColorStopString(first, 'transparent'), |
| getColorStopString(second, gradientDestination), |
| getColorStopString(third, gradientDestination), |
| getColorStopString(fourth, 'transparent'), |
| 'transparent' |
| ]; |
| |
| var gradient = '-webkit-linear-gradient(' + gradientArguments.join(',') + ')'; |
| maxiview.style.WebkitMaskImage = gradient; |
| } |
| |
| function getColorStopString(height, color) { |
| // TODO(arv): The CSS3 gradient syntax allows px units so we should simplify |
| // this to use pixels instead. |
| return color + ' ' + height * 100 + '%'; |
| } |
| |
| // Updates the visibility of the menu buttons for each section, based on |
| // whether they are currently enabled and in menu mode. |
| function updateMenuSections() { |
| var elms = document.getElementsByClassName('section'); |
| for (var i = 0, elm; elm = elms[i]; i++) { |
| var button = getSectionMenuButton(elm.id); |
| if (!button) |
| continue; |
| |
| if (!elm.classList.contains('disabled') && |
| elm.classList.contains('menu')) { |
| button.style.display = 'inline-block'; |
| } else { |
| button.style.display = 'none'; |
| } |
| } |
| } |
| |
| window.addEventListener('resize', handleWindowResize); |
| |
| var sectionToElementMap; |
| function getSectionElement(section) { |
| if (!sectionToElementMap) { |
| sectionToElementMap = {}; |
| for (var key in Section) { |
| sectionToElementMap[Section[key]] = |
| document.querySelector('.section[section=' + key + ']'); |
| } |
| } |
| return sectionToElementMap[section]; |
| } |
| |
| function getSectionMaxiview(section) { |
| return $(section.id + '-maxiview'); |
| } |
| |
| function getSectionMiniview(section) { |
| return section.querySelector('.miniview'); |
| } |
| |
| // You usually want to call |showOnlySection()| instead of this. |
| function showSection(section) { |
| if (!(section & shownSections)) { |
| shownSections |= section; |
| var el = getSectionElement(section); |
| if (el) { |
| el.classList.remove('collapsed'); |
| |
| var maxiview = getSectionMaxiview(el); |
| if (maxiview) { |
| maxiview.classList.remove('collapsing'); |
| maxiview.classList.remove('collapsed'); |
| // The opacity won't transition if you toggle the display property |
| // at the same time. To get a fade effect, we set the opacity |
| // asynchronously from another function, after the display is toggled. |
| // 1) 'collapsed' (display: none, opacity: 0) |
| // 2) none (display: block, opacity: 0) |
| // 3) 'opaque' (display: block, opacity: 1) |
| setTimeout(function () { |
| maxiview.classList.add('opaque'); |
| }, 0); |
| } |
| |
| var miniview = getSectionMiniview(el); |
| if (miniview) { |
| // The miniview is hidden immediately (no need to set this async). |
| miniview.classList.remove('opaque'); |
| } |
| } |
| |
| switch (section) { |
| case Section.THUMB: |
| mostVisited.visible = true; |
| mostVisited.layout(); |
| break; |
| case Section.APPS: |
| apps.visible = true; |
| apps.layout({disableAnimations:true}); |
| break; |
| } |
| } |
| } |
| |
| // Show this section and hide all other sections - at most one section can |
| // be open at one time. |
| function showOnlySection(section) { |
| for (var p in Section) { |
| if (p == section) |
| showSection(Section[p]); |
| else |
| hideSection(Section[p]); |
| } |
| } |
| |
| function hideSection(section) { |
| if (section & shownSections) { |
| shownSections &= ~section; |
| |
| switch (section) { |
| case Section.THUMB: |
| mostVisited.visible = false; |
| mostVisited.layout(); |
| break; |
| case Section.APPS: |
| apps.visible = false; |
| apps.layout(); |
| break; |
| } |
| |
| var el = getSectionElement(section); |
| if (el) { |
| el.classList.add('collapsed'); |
| |
| var maxiview = getSectionMaxiview(el); |
| if (maxiview) { |
| maxiview.classList.add(isDoneLoading() ? 'collapsing' : 'collapsed'); |
| maxiview.classList.remove('opaque'); |
| } |
| |
| var miniview = getSectionMiniview(el); |
| if (miniview) { |
| // We need to set this asynchronously to properly get the fade effect. |
| setTimeout(function() { |
| miniview.classList.add('opaque'); |
| }, 0); |
| updateMiniviewClipping(miniview); |
| } |
| } |
| } |
| } |
| |
| window.addEventListener('webkitTransitionEnd', function(e) { |
| if (e.target.classList.contains('collapsing')) { |
| e.target.classList.add('collapsed'); |
| e.target.classList.remove('collapsing'); |
| } |
| |
| if (e.target.classList.contains('maxiview') || |
| e.target.classList.contains('miniview')) { |
| document.documentElement.removeAttribute('enable-section-animations'); |
| showScrollBars(); |
| } |
| }); |
| |
| /** |
| * Callback when the shown sections changes in another NTP. |
| * @param {number} newShownSections Bitmask of the shown sections. |
| */ |
| function setShownSections(newShownSections) { |
| for (var key in Section) { |
| if (newShownSections & Section[key]) |
| showSection(Section[key]); |
| else |
| hideSection(Section[key]); |
| } |
| setSectionMenuMode('apps', Section.APPS, newShownSections & MENU_APPS, |
| MENU_APPS); |
| setSectionMenuMode('most-visited', Section.THUMB, |
| newShownSections & MENU_THUMB, MENU_THUMB); |
| setSectionMenuMode('recently-closed', undefined, |
| newShownSections & MENU_RECENT, MENU_RECENT); |
| layoutSections(); |
| } |
| |
| // Recently closed |
| |
| function layoutRecentlyClosed() { |
| var recentElement = $('recently-closed'); |
| var miniview = getSectionMiniview(recentElement); |
| |
| updateMiniviewClipping(miniview); |
| |
| if (miniview.hasChildNodes()) { |
| recentElement.classList.remove('disabled'); |
| miniview.classList.add('opaque'); |
| } else { |
| recentElement.classList.add('disabled'); |
| miniview.classList.remove('opaque'); |
| } |
| |
| layoutSections(); |
| } |
| |
| /** |
| * This function is called by the backend whenever the sync status section |
| * needs to be updated to reflect recent sync state changes. The backend passes |
| * the new status information in the newMessage parameter. The state includes |
| * the following: |
| * |
| * syncsectionisvisible: true if the sync section needs to show up on the new |
| * tab page and false otherwise. |
| * title: the header for the sync status section. |
| * msg: the actual message (e.g. "Synced to foo@gmail.com"). |
| * linkisvisible: true if the link element should be visible within the sync |
| * section and false otherwise. |
| * linktext: the text to display as the link in the sync status (only used if |
| * linkisvisible is true). |
| * linkurlisset: true if an URL should be set as the href for the link and false |
| * otherwise. If this field is false, then clicking on the link |
| * will result in sending a message to the backend (see |
| * 'SyncLinkClicked'). |
| * linkurl: the URL to use as the element's href (only used if linkurlisset is |
| * true). |
| */ |
| function syncMessageChanged(newMessage) { |
| var syncStatusElement = $('sync-status'); |
| |
| // Hide the section if the message is emtpy. |
| if (!newMessage['syncsectionisvisible']) { |
| syncStatusElement.classList.add('disabled'); |
| return; |
| } |
| |
| syncStatusElement.classList.remove('disabled'); |
| |
| var content = syncStatusElement.children[0]; |
| |
| // Set the sync section background color based on the state. |
| if (newMessage.msgtype == 'error') { |
| content.style.backgroundColor = 'tomato'; |
| } else { |
| content.style.backgroundColor = ''; |
| } |
| |
| // Set the text for the header and sync message. |
| var titleElement = content.firstElementChild; |
| titleElement.textContent = newMessage.title; |
| var messageElement = titleElement.nextElementSibling; |
| messageElement.textContent = newMessage.msg; |
| |
| // Remove what comes after the message |
| while (messageElement.nextSibling) { |
| content.removeChild(messageElement.nextSibling); |
| } |
| |
| if (newMessage.linkisvisible) { |
| var el; |
| if (newMessage.linkurlisset) { |
| // Use a link |
| el = document.createElement('a'); |
| el.href = newMessage.linkurl; |
| } else { |
| el = document.createElement('button'); |
| el.className = 'link'; |
| el.addEventListener('click', syncSectionLinkClicked); |
| } |
| el.textContent = newMessage.linktext; |
| content.appendChild(el); |
| fixLinkUnderline(el); |
| } |
| |
| layoutSections(); |
| } |
| |
| /** |
| * Invoked when the link in the sync promo or sync status section is clicked. |
| */ |
| function syncSectionLinkClicked(e) { |
| chrome.send('SyncLinkClicked'); |
| e.preventDefault(); |
| } |
| |
| /** |
| * Invoked when link to start sync in the promo message is clicked, and Chrome |
| * has already been synced to an account. |
| */ |
| function syncAlreadyEnabled(message) { |
| showNotification(message.syncEnabledMessage); |
| } |
| |
| /** |
| * Returns the text used for a recently closed window. |
| * @param {number} numTabs Number of tabs in the window. |
| * @return {string} The text to use. |
| */ |
| function formatTabsText(numTabs) { |
| if (numTabs == 1) |
| return localStrings.getString('closedwindowsingle'); |
| return localStrings.getStringF('closedwindowmultiple', numTabs); |
| } |
| |
| // Theme related |
| |
| function themeChanged(hasAttribution) { |
| document.documentElement.setAttribute('hasattribution', hasAttribution); |
| $('themecss').href = 'chrome://theme/css/newtab.css?' + Date.now(); |
| updateAttribution(); |
| } |
| |
| function updateAttribution() { |
| // Default value for standard NTP with no theme attribution or custom logo. |
| logEvent('updateAttribution called'); |
| var imageId = 'IDR_PRODUCT_LOGO'; |
| // Theme attribution always overrides custom logos. |
| if (document.documentElement.getAttribute('hasattribution') == 'true') { |
| logEvent('updateAttribution called with THEME ATTR'); |
| imageId = 'IDR_THEME_NTP_ATTRIBUTION'; |
| } else if (document.documentElement.getAttribute('customlogo') == 'true') { |
| logEvent('updateAttribution with CUSTOMLOGO'); |
| imageId = 'IDR_CUSTOM_PRODUCT_LOGO'; |
| } |
| |
| $('attribution-img').src = 'chrome://theme/' + imageId + '?' + Date.now(); |
| } |
| |
| // If the content overlaps with the attribution, we bump its opacity down. |
| function updateAttributionDisplay(contentBottom) { |
| var attribution = $('attribution'); |
| var main = $('main'); |
| var rtl = document.documentElement.dir == 'rtl'; |
| var contentRect = main.getBoundingClientRect(); |
| var attributionRect = attribution.getBoundingClientRect(); |
| |
| // Hack. See comments for '.haslayout' in new_new_tab.css. |
| if (attributionRect.width == 0) |
| return; |
| else |
| attribution.classList.remove('nolayout'); |
| |
| if (contentBottom > attribution.offsetTop) { |
| if ((!rtl && contentRect.right > attributionRect.left) || |
| (rtl && attributionRect.right > contentRect.left)) { |
| attribution.classList.add('obscured'); |
| return; |
| } |
| } |
| |
| attribution.classList.remove('obscured'); |
| } |
| |
| function bookmarkBarAttached() { |
| document.documentElement.setAttribute('bookmarkbarattached', 'true'); |
| } |
| |
| function bookmarkBarDetached() { |
| document.documentElement.setAttribute('bookmarkbarattached', 'false'); |
| } |
| |
| function viewLog() { |
| var lines = []; |
| var start = log[0][1]; |
| |
| for (var i = 0; i < log.length; i++) { |
| lines.push((log[i][1] - start) + ': ' + log[i][0]); |
| } |
| |
| console.log(lines.join('\n')); |
| } |
| |
| // We apply the size class here so that we don't trigger layout animations |
| // onload. |
| |
| handleWindowResize(); |
| |
| var localStrings = new LocalStrings(); |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Things we know are not needed at startup go below here |
| |
| function afterTransition(f) { |
| if (!isDoneLoading()) { |
| // Make sure we do not use a timer during load since it slows down the UI. |
| f(); |
| } else { |
| // The duration of all transitions are .15s |
| window.setTimeout(f, 150); |
| } |
| } |
| |
| // Notification |
| |
| |
| var notificationTimeout; |
| |
| /* |
| * Displays a message (either a string or a document fragment) in the |
| * notification slot at the top of the NTP. A close button ("x") will be |
| * inserted at the end of the message. |
| * @param {string|Node} message String or node to use as message. |
| * @param {string} actionText The text to show as a link next to the message. |
| * @param {function=} opt_f Function to call when the user clicks the action |
| * link. |
| * @param {number=} opt_delay The time in milliseconds before hiding the |
| * notification. |
| */ |
| function showNotification(message, actionText, opt_f, opt_delay) { |
| // TODO(arv): Create a notification component. |
| var notificationElement = $('notification'); |
| var f = opt_f || function() {}; |
| var delay = opt_delay || 10000; |
| |
| function show() { |
| window.clearTimeout(notificationTimeout); |
| notificationElement.classList.add('show'); |
| document.body.classList.add('notification-shown'); |
| } |
| |
| function delayedHide() { |
| notificationTimeout = window.setTimeout(hideNotification, delay); |
| } |
| |
| function doAction() { |
| f(); |
| closeNotification(); |
| } |
| |
| function closeNotification() { |
| if (notification.classList.contains('promo')) |
| chrome.send('closePromo'); |
| hideNotification(); |
| } |
| |
| // Remove classList entries from previous notifications. |
| notification.classList.remove('first-run'); |
| notification.classList.remove('promo'); |
| |
| var messageContainer = notificationElement.firstElementChild; |
| var actionLink = notificationElement.querySelector('#action-link'); |
| var closeButton = notificationElement.querySelector('#notification-close'); |
| |
| // Remove any previous actionLink entry. |
| actionLink.textContent = ''; |
| |
| $('notification-close').onclick = closeNotification; |
| |
| if (typeof message == 'string') { |
| messageContainer.textContent = message; |
| } else { |
| messageContainer.textContent = ''; // Remove all children. |
| messageContainer.appendChild(message); |
| } |
| |
| if (actionText) { |
| actionLink.style.display = ''; |
| actionLink.textContent = actionText; |
| } else { |
| actionLink.style.display = 'none'; |
| } |
| |
| actionLink.onclick = doAction; |
| actionLink.onkeydown = handleIfEnterKey(doAction); |
| notificationElement.onmouseover = show; |
| notificationElement.onmouseout = delayedHide; |
| actionLink.onfocus = show; |
| actionLink.onblur = delayedHide; |
| // Enable tabbing to the link now that it is shown. |
| actionLink.tabIndex = 0; |
| |
| show(); |
| delayedHide(); |
| } |
| |
| /** |
| * Hides the notifier. |
| */ |
| function hideNotification() { |
| var notificationElement = $('notification'); |
| notificationElement.classList.remove('show'); |
| document.body.classList.remove('notification-shown'); |
| var actionLink = notificationElement.querySelector('#actionlink'); |
| var closeButton = notificationElement.querySelector('#notification-close'); |
| // Prevent tabbing to the hidden link. |
| // Setting tabIndex to -1 only prevents future tabbing to it. If, however, the |
| // user switches window or a tab and then moves back to this tab the element |
| // may gain focus. We therefore make sure that we blur the element so that the |
| // element focus is not restored when coming back to this window. |
| if (actionLink) { |
| actionLink.tabIndex = -1; |
| actionLink.blur(); |
| } |
| if (closeButton) { |
| closeButton.tabIndex = -1; |
| closeButton.blur(); |
| } |
| } |
| |
| function showPromoNotification() { |
| showNotification(parseHtmlSubset(localStrings.getString('serverpromo')), |
| localStrings.getString('syncpromotext'), |
| function () { chrome.send('SyncLinkClicked'); }, |
| 60000); |
| var notificationElement = $('notification'); |
| notification.classList.add('promo'); |
| } |
| |
| $('main').addEventListener('click', function(e) { |
| var p = e.target; |
| while (p && p.tagName != 'H2') { |
| // In case the user clicks on a button we do not want to expand/collapse a |
| // section. |
| if (p.tagName == 'BUTTON') |
| return; |
| p = p.parentNode; |
| } |
| |
| if (!p) |
| return; |
| |
| p = p.parentNode; |
| if (!getSectionMaxiview(p)) |
| return; |
| |
| toggleSectionVisibilityAndAnimate(p.getAttribute('section')); |
| }); |
| |
| $('most-visited-settings').addEventListener('click', function() { |
| $('clear-all-blacklisted').execute(); |
| }); |
| |
| function toggleSectionVisibilityAndAnimate(section) { |
| if (!section) |
| return; |
| |
| // It looks better to return the scroll to the top when toggling sections. |
| document.body.scrollTop = 0; |
| |
| // We set it back in webkitTransitionEnd. |
| document.documentElement.setAttribute('enable-section-animations', 'true'); |
| if (shownSections & Section[section]) { |
| hideSection(Section[section]); |
| } else { |
| showOnlySection(section); |
| } |
| layoutSections(); |
| saveShownSections(); |
| } |
| |
| function handleIfEnterKey(f) { |
| return function(e) { |
| if (e.keyIdentifier == 'Enter') |
| f(e); |
| }; |
| } |
| |
| function maybeReopenTab(e) { |
| var el = findAncestor(e.target, function(el) { |
| return el.sessionId !== undefined; |
| }); |
| if (el) { |
| chrome.send('reopenTab', [String(el.sessionId)]); |
| e.preventDefault(); |
| |
| setWindowTooltipTimeout(); |
| } |
| } |
| |
| // Note that the openForeignSession calls can fail, resulting this method to |
| // not have any action (hence the maybe). |
| function maybeOpenForeignSession(e) { |
| var el = findAncestor(e.target, function(el) { |
| return el.sessionTag !== undefined; |
| }); |
| if (el) { |
| chrome.send('openForeignSession', [String(el.sessionTag)]); |
| e.stopPropagation(); |
| e.preventDefault(); |
| setWindowTooltipTimeout(); |
| } |
| } |
| |
| function maybeOpenForeignWindow(e) { |
| var el = findAncestor(e.target, function(el) { |
| return el.winNum !== undefined; |
| }); |
| if (el) { |
| chrome.send('openForeignSession', [String(el.sessionTag), |
| String(el.winNum)]); |
| e.stopPropagation(); |
| e.preventDefault(); |
| setWindowTooltipTimeout(); |
| } |
| } |
| |
| function maybeOpenForeignTab(e) { |
| var el = findAncestor(e.target, function(el) { |
| return el.sessionId !== undefined; |
| }); |
| if (el) { |
| chrome.send('openForeignSession', [String(el.sessionTag), String(el.winNum), |
| String(el.sessionId)]); |
| e.stopPropagation(); |
| e.preventDefault(); |
| setWindowTooltipTimeout(); |
| } |
| } |
| |
| // HACK(arv): After the window onblur event happens we get a mouseover event |
| // on the next item and we want to make sure that we do not show a tooltip |
| // for that. |
| function setWindowTooltipTimeout(e) { |
| window.setTimeout(function() { |
| windowTooltip.hide(); |
| }, 2 * WindowTooltip.DELAY); |
| } |
| |
| function maybeShowWindowTooltip(e) { |
| var f = function(el) { |
| return el.tabItems !== undefined; |
| }; |
| var el = findAncestor(e.target, f); |
| var relatedEl = findAncestor(e.relatedTarget, f); |
| if (el && el != relatedEl) { |
| windowTooltip.handleMouseOver(e, el, el.tabItems); |
| } |
| } |
| |
| |
| var recentlyClosedElement = $('recently-closed'); |
| |
| recentlyClosedElement.addEventListener('click', maybeReopenTab); |
| recentlyClosedElement.addEventListener('keydown', |
| handleIfEnterKey(maybeReopenTab)); |
| |
| recentlyClosedElement.addEventListener('mouseover', maybeShowWindowTooltip); |
| recentlyClosedElement.addEventListener('focus', maybeShowWindowTooltip, true); |
| |
| var foreignSessionElement = $('foreign-sessions'); |
| |
| foreignSessionElement.addEventListener('click', maybeOpenForeignSession); |
| foreignSessionElement.addEventListener('keydown', |
| handleIfEnterKey( |
| maybeOpenForeignSession)); |
| |
| foreignSessionElement.addEventListener('mouseover', maybeShowWindowTooltip); |
| foreignSessionElement.addEventListener('focus', maybeShowWindowTooltip, true); |
| |
| /** |
| * This object represents a tooltip representing a closed window. It is |
| * shown when hovering over a closed window item or when the item is focused. It |
| * gets hidden when blurred or when mousing out of the menu or the item. |
| * @param {Element} tooltipEl The element to use as the tooltip. |
| * @constructor |
| */ |
| function WindowTooltip(tooltipEl) { |
| this.tooltipEl = tooltipEl; |
| this.boundHide_ = this.hide.bind(this); |
| this.boundHandleMouseOut_ = this.handleMouseOut.bind(this); |
| } |
| |
| WindowTooltip.trackMouseMove_ = function(e) { |
| WindowTooltip.clientX = e.clientX; |
| WindowTooltip.clientY = e.clientY; |
| }; |
| |
| /** |
| * Time in ms to delay before the tooltip is shown. |
| * @type {number} |
| */ |
| WindowTooltip.DELAY = 300; |
| |
| WindowTooltip.prototype = { |
| timer: 0, |
| handleMouseOver: function(e, linkEl, tabs) { |
| this.linkEl_ = linkEl; |
| if (e.type == 'mouseover') { |
| this.linkEl_.addEventListener('mousemove', WindowTooltip.trackMouseMove_); |
| this.linkEl_.addEventListener('mouseout', this.boundHandleMouseOut_); |
| } else { // focus |
| this.linkEl_.addEventListener('blur', this.boundHide_); |
| } |
| this.timer = window.setTimeout(this.show.bind(this, e.type, linkEl, tabs), |
| WindowTooltip.DELAY); |
| }, |
| show: function(type, linkEl, tabs) { |
| window.addEventListener('blur', this.boundHide_); |
| this.linkEl_.removeEventListener('mousemove', |
| WindowTooltip.trackMouseMove_); |
| window.clearTimeout(this.timer); |
| |
| this.renderItems(tabs); |
| var rect = linkEl.getBoundingClientRect(); |
| var bodyRect = document.body.getBoundingClientRect(); |
| var rtl = document.documentElement.dir == 'rtl'; |
| |
| this.tooltipEl.style.display = 'block'; |
| var tooltipRect = this.tooltipEl.getBoundingClientRect(); |
| var x, y; |
| |
| // When focused show below, like a drop down menu. |
| if (type == 'focus') { |
| x = rtl ? |
| rect.left + bodyRect.left + rect.width - this.tooltipEl.offsetWidth : |
| rect.left + bodyRect.left; |
| y = rect.top + bodyRect.top + rect.height; |
| } else { |
| x = bodyRect.left + (rtl ? |
| WindowTooltip.clientX - this.tooltipEl.offsetWidth : |
| WindowTooltip.clientX); |
| // Offset like a tooltip |
| y = 20 + WindowTooltip.clientY + bodyRect.top; |
| } |
| |
| // We need to ensure that the tooltip is inside the window viewport. |
| x = Math.min(x, bodyRect.width - tooltipRect.width); |
| x = Math.max(x, 0); |
| y = Math.min(y, bodyRect.height - tooltipRect.height); |
| y = Math.max(y, 0); |
| |
| this.tooltipEl.style.left = x + 'px'; |
| this.tooltipEl.style.top = y + 'px'; |
| }, |
| handleMouseOut: function(e) { |
| // Don't hide when move to another item in the link. |
| var f = function(el) { |
| return el.tabItems !== undefined; |
| }; |
| var el = findAncestor(e.target, f); |
| var relatedEl = findAncestor(e.relatedTarget, f); |
| if (el && el != relatedEl) { |
| this.hide(); |
| } |
| }, |
| hide: function() { |
| window.clearTimeout(this.timer); |
| window.removeEventListener('blur', this.boundHide_); |
| this.linkEl_.removeEventListener('mousemove', |
| WindowTooltip.trackMouseMove_); |
| this.linkEl_.removeEventListener('mouseout', this.boundHandleMouseOut_); |
| this.linkEl_.removeEventListener('blur', this.boundHide_); |
| this.linkEl_ = null; |
| |
| this.tooltipEl.style.display = 'none'; |
| }, |
| renderItems: function(tabs) { |
| var tooltip = this.tooltipEl; |
| tooltip.textContent = ''; |
| |
| tabs.forEach(function(tab) { |
| var span = document.createElement('span'); |
| span.className = 'item'; |
| span.style.backgroundImage = url('chrome://favicon/' + tab.url); |
| span.dir = tab.direction; |
| span.textContent = tab.title; |
| tooltip.appendChild(span); |
| }); |
| } |
| }; |
| |
| var windowTooltip = new WindowTooltip($('window-tooltip')); |
| |
| window.addEventListener('load', |
| logEvent.bind(global, 'Tab.NewTabOnload', true)); |
| |
| window.addEventListener('resize', handleWindowResize); |
| document.addEventListener('DOMContentLoaded', |
| logEvent.bind(global, 'Tab.NewTabDOMContentLoaded', true)); |
| |
| // Whether or not we should send the initial 'GetSyncMessage' to the backend |
| // depends on the value of the attribue 'syncispresent' which the backend sets |
| // to indicate if there is code in the backend which is capable of processing |
| // this message. This attribute is loaded by the JSTemplate and therefore we |
| // must make sure we check the attribute after the DOM is loaded. |
| document.addEventListener('DOMContentLoaded', |
| callGetSyncMessageIfSyncIsPresent); |
| |
| /** |
| * The sync code is not yet built by default on all platforms so we have to |
| * make sure we don't send the initial sync message to the backend unless the |
| * backend told us that the sync code is present. |
| */ |
| function callGetSyncMessageIfSyncIsPresent() { |
| if (document.documentElement.getAttribute('syncispresent') == 'true') { |
| chrome.send('GetSyncMessage'); |
| } |
| } |
| |
| // Tooltip for elements that have text that overflows. |
| document.addEventListener('mouseover', function(e) { |
| // We don't want to do this while we are dragging because it makes things very |
| // janky |
| if (mostVisited.isDragging()) { |
| return; |
| } |
| |
| var el = findAncestor(e.target, function(el) { |
| return el.xtitle; |
| }); |
| if (el && el.xtitle != el.title) { |
| if (el.scrollWidth > el.clientWidth) { |
| el.title = el.xtitle; |
| } else { |
| el.title = ''; |
| } |
| } |
| }); |
| |
| /** |
| * Makes links and buttons support a different underline color. |
| * @param {Node} node The node to search for links and buttons in. |
| */ |
| function fixLinkUnderlines(node) { |
| var elements = node.querySelectorAll('a,button'); |
| Array.prototype.forEach.call(elements, fixLinkUnderline); |
| } |
| |
| /** |
| * Wraps the content of an element in a a link-color span. |
| * @param {Element} el The element to wrap. |
| */ |
| function fixLinkUnderline(el) { |
| var span = document.createElement('span'); |
| span.className = 'link-color'; |
| while (el.hasChildNodes()) { |
| span.appendChild(el.firstChild); |
| } |
| el.appendChild(span); |
| } |
| |
| updateAttribution(); |
| |
| function initializeLogin() { |
| chrome.send('initializeLogin', []); |
| } |
| |
| function updateLogin(login) { |
| $('login-container').style.display = login ? 'block' : ''; |
| if (login) |
| $('login-username').textContent = login; |
| |
| } |
| |
| var mostVisited = new MostVisited( |
| $('most-visited-maxiview'), |
| document.querySelector('#most-visited .miniview'), |
| $('most-visited-menu'), |
| useSmallGrid(), |
| shownSections & Section.THUMB); |
| |
| function mostVisitedPages(data, firstRun, hasBlacklistedUrls) { |
| logEvent('received most visited pages'); |
| |
| mostVisited.updateSettingsLink(hasBlacklistedUrls); |
| mostVisited.data = data; |
| mostVisited.layout(); |
| layoutSections(); |
| |
| // Remove class name in a timeout so that changes done in this JS thread are |
| // not animated. |
| window.setTimeout(function() { |
| mostVisited.ensureSmallGridCorrect(); |
| maybeDoneLoading(); |
| }, 1); |
| |
| if (localStrings.getString('serverpromo')) { |
| showPromoNotification(); |
| } |
| } |
| |
| function maybeDoneLoading() { |
| if (mostVisited.data && apps.loaded) |
| document.body.classList.remove('loading'); |
| } |
| |
| function isDoneLoading() { |
| return !document.body.classList.contains('loading'); |
| } |
| |
| // Initialize the listener for the "hide this" link on the apps promo. We do |
| // this outside of getAppsCallback because it only needs to be done once per |
| // NTP load. |
| document.addEventListener('DOMContentLoaded', function() { |
| $('apps-promo-hide').addEventListener('click', function() { |
| chrome.send('hideAppsPromo', []); |
| document.documentElement.classList.remove('apps-promo-visible'); |
| layoutSections(); |
| }); |
| }); |