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