// 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(); }); });