blob: ad62da041f40a9ee09e7da6ae4773ab1205ab1a2 [file] [log] [blame]
// 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.
// File Description:
// Contains all the necessary functions for rendering the NTP on mobile
// devices.
/**
* The event type used to determine when a touch starts.
* @type {string}
*/
var PRESS_START_EVT = 'touchstart';
/**
* The event type used to determine when a touch finishes.
* @type {string}
*/
var PRESS_STOP_EVT = 'touchend';
/**
* The event type used to determine when a touch moves.
* @type {string}
*/
var PRESS_MOVE_EVT = 'touchmove';
cr.define('ntp', function() {
/**
* Constant for the localStorage key used to specify the default bookmark
* folder to be selected when navigating to the bookmark tab for the first
* time of a new NTP instance.
* @type {string}
*/
var DEFAULT_BOOKMARK_FOLDER_KEY = 'defaultBookmarkFolder';
/**
* Constant for the localStorage key used to store whether or not sync was
* enabled on the last call to syncEnabled().
* @type {string}
*/
var SYNC_ENABLED_KEY = 'syncEnabled';
/**
* The time before and item gets marked as active (in milliseconds). This
* prevents an item from being marked as active when the user is scrolling
* the page.
* @type {number}
*/
var ACTIVE_ITEM_DELAY_MS = 100;
/**
* The CSS class identifier for grid layouts.
* @type {string}
*/
var GRID_CSS_CLASS = 'icon-grid';
/**
* The element to center when centering a GRID_CSS_CLASS.
*/
var GRID_CENTER_CSS_CLASS = 'center-icon-grid';
/**
* Attribute used to specify the number of columns to use in a grid. If
* left unspecified, the grid will fill the container.
*/
var GRID_COLUMNS = 'grid-columns';
/**
* Attribute used to specify whether the top margin should be set to match
* the left margin of the grid.
*/
var GRID_SET_TOP_MARGIN_CLASS = 'grid-set-top-margin';
/**
* Attribute used to specify whether the margins of individual items within
* the grid should be adjusted to better fill the space.
*/
var GRID_SET_ITEM_MARGINS = 'grid-set-item-margins';
/**
* The CSS class identifier for centered empty section containers.
*/
var CENTER_EMPTY_CONTAINER_CSS_CLASS = 'center-empty-container';
/**
* The CSS class identifier for marking list items as active.
* @type {string}
*/
var ACTIVE_LIST_ITEM_CSS_CLASS = 'list-item-active';
/**
* Attributes set on elements representing data in a section, specifying
* which section that element belongs to. Used for context menus.
* @type {string}
*/
var SECTION_KEY = 'sectionType';
/**
* Attribute set on an element that has a context menu. Specifies the URL for
* which the context menu action should apply.
* @type {string}
*/
var CONTEXT_MENU_URL_KEY = 'url';
/**
* The list of main section panes added.
* @type {Array.<Element>}
*/
var panes = [];
/**
* The list of section prefixes, which are used to append to the hash of the
* page to allow the native toolbar to see url changes when the pane is
* switched.
*/
var sectionPrefixes = [];
/**
* The next available index for new favicons. Users must increment this
* value once assigning this index to a favicon.
* @type {number}
*/
var faviconIndex = 0;
/**
* The currently selected pane DOM element.
* @type {Element}
*/
var currentPane = null;
/**
* The index of the currently selected top level pane. The index corresponds
* to the elements defined in {@see #panes}.
* @type {number}
*/
var currentPaneIndex;
/**
* The ID of the bookmark folder currently selected.
* @type {string|number}
*/
var bookmarkFolderId = null;
/**
* The current element active item.
* @type {?Element}
*/
var activeItem;
/**
* The element to be marked as active if no actions cancel it.
* @type {?Element}
*/
var pendingActiveItem;
/**
* The timer ID to mark an element as active.
* @type {number}
*/
var activeItemDelayTimerId;
/**
* Enum for the different load states based on the initialization of the NTP.
* @enum {number}
*/
var LoadStatusType = {
LOAD_NOT_DONE: 0,
LOAD_IMAGES_COMPLETE: 1,
LOAD_BOOKMARKS_FINISHED: 2,
LOAD_COMPLETE: 3 // An OR'd combination of all necessary states.
};
/**
* The current loading status for the NTP.
* @type {LoadStatusType}
*/
var loadStatus_ = LoadStatusType.LOAD_NOT_DONE;
/**
* Whether the loading complete notification has been sent.
* @type {boolean}
*/
var finishedLoadingNotificationSent_ = false;
/**
* Whether the page title has been loaded.
* @type {boolean}
*/
var titleLoadedStatus_ = false;
/**
* Whether the NTP is in incognito mode or not.
* @type {boolean}
*/
var isIncognito = false;
/**
* Whether incognito mode is enabled. (It can be blocked e.g. with a policy.)
* @type {boolean}
*/
var isIncognitoEnabled = true;
/**
* Whether the initial history state has been replaced. The state will be
* replaced once the bookmark data has loaded to ensure the proper folder
* id is persisted.
* @type {boolean}
*/
var replacedInitialState = false;
/**
* Stores number of most visited pages.
* @type {number}
*/
var numberOfMostVisitedPages = 0;
/**
* Whether there are any recently closed tabs.
* @type {boolean}
*/
var hasRecentlyClosedTabs = false;
/**
* Whether promo is not allowed or not (external to NTP).
* @type {boolean}
*/
var promoIsAllowed = false;
/**
* Whether promo should be shown on Most Visited page (externally set).
* @type {boolean}
*/
var promoIsAllowedOnMostVisited = false;
/**
* Whether promo should be shown on Open Tabs page (externally set).
* @type {boolean}
*/
var promoIsAllowedOnOpenTabs = false;
/**
* Whether promo should show a virtual computer on Open Tabs (externally set).
* @type {boolean}
*/
var promoIsAllowedAsVirtualComputer = false;
/**
* Promo-injected title of a virtual computer on an open tabs pane.
* @type {string}
*/
var promoInjectedComputerTitleText = '';
/**
* Promo-injected last synced text of a virtual computer on an open tabs pane.
* @type {string}
*/
var promoInjectedComputerLastSyncedText = '';
/**
* The different sections that are displayed.
* @enum {number}
*/
var SectionType = {
BOOKMARKS: 'bookmarks',
FOREIGN_SESSION: 'foreign_session',
FOREIGN_SESSION_HEADER: 'foreign_session_header',
MOST_VISITED: 'most_visited',
PROMO_VC_SESSION_HEADER: 'promo_vc_session_header',
RECENTLY_CLOSED: 'recently_closed',
SNAPSHOTS: 'snapshots',
UNKNOWN: 'unknown',
};
/**
* The different ids used of our custom context menu. Sent to the ChromeView
* and sent back when a menu is selected.
* @enum {number}
*/
var ContextMenuItemIds = {
BOOKMARK_EDIT: 0,
BOOKMARK_DELETE: 1,
BOOKMARK_OPEN_IN_NEW_TAB: 2,
BOOKMARK_OPEN_IN_INCOGNITO_TAB: 3,
BOOKMARK_SHORTCUT: 4,
MOST_VISITED_OPEN_IN_NEW_TAB: 10,
MOST_VISITED_OPEN_IN_INCOGNITO_TAB: 11,
MOST_VISITED_REMOVE: 12,
RECENTLY_CLOSED_OPEN_IN_NEW_TAB: 20,
RECENTLY_CLOSED_OPEN_IN_INCOGNITO_TAB: 21,
RECENTLY_CLOSED_REMOVE: 22,
FOREIGN_SESSIONS_REMOVE: 30,
PROMO_VC_SESSION_REMOVE: 40,
};
/**
* The URL of the element for the context menu.
* @type {string}
*/
var contextMenuUrl = null;
var contextMenuItem = null;
var currentSnapshots = null;
var currentSessions = null;
/**
* The possible states of the sync section
* @enum {number}
*/
var SyncState = {
INITIAL: 0,
WAITING_FOR_DATA: 1,
DISPLAYING_LOADING: 2,
DISPLAYED_LOADING: 3,
LOADED: 4,
};
/**
* The current state of the sync section.
*/
var syncState = SyncState.INITIAL;
/**
* Whether or not sync is enabled. It will be undefined until
* setSyncEnabled() is called.
* @type {?boolean}
*/
var syncEnabled = undefined;
/**
* The current most visited data being displayed.
* @type {Array.<Object>}
*/
var mostVisitedData_ = [];
/**
* The current bookmark data being displayed. Keep a reference to this data
* in case the sync enabled state changes. In this case, the bookmark data
* will need to be refiltered.
* @type {?Object}
*/
var bookmarkData;
/**
* Keep track of any outstanding timers related to updating the sync section.
*/
var syncTimerId = -1;
/**
* The minimum amount of time that 'Loading...' can be displayed. This is to
* prevent flashing.
*/
var SYNC_LOADING_TIMEOUT = 1000;
/**
* How long to wait for sync data to load before displaying the 'Loading...'
* text to the user.
*/
var SYNC_INITIAL_LOAD_TIMEOUT = 1000;
/**
* An array of images that are currently in loading state. Once an image
* loads it is removed from this array.
*/
var imagesBeingLoaded = new Array();
/**
* Flag indicating if we are on bookmark shortcut mode.
* In this mode, only the bookmark section is available and selecting
* a non-folder bookmark adds it to the home screen.
* Context menu is disabled.
*/
var bookmarkShortcutMode = false;
function setIncognitoMode(incognito) {
isIncognito = incognito;
if (!isIncognito) {
chrome.send('getMostVisited');
chrome.send('getRecentlyClosedTabs');
chrome.send('getForeignSessions');
chrome.send('getPromotions');
chrome.send('getIncognitoDisabled');
}
}
function setIncognitoEnabled(item) {
isIncognitoEnabled = item.incognitoEnabled;
}
/**
* Flag set to true when the page is loading its initial set of images. This
* is set to false after all the initial images have loaded.
*/
function onInitialImageLoaded(event) {
var url = event.target.src;
for (var i = 0; i < imagesBeingLoaded.length; ++i) {
if (imagesBeingLoaded[i].src == url) {
imagesBeingLoaded.splice(i, 1);
if (imagesBeingLoaded.length == 0) {
// To send out the NTP loading complete notification.
loadStatus_ |= LoadStatusType.LOAD_IMAGES_COMPLETE;
sendNTPNotification();
}
}
}
}
/**
* Marks the given image as currently being loaded. Once all such images load
* we inform the browser via a hash change.
*/
function trackImageLoad(url) {
if (finishedLoadingNotificationSent_)
return;
for (var i = 0; i < imagesBeingLoaded.length; ++i) {
if (imagesBeingLoaded[i].src == url)
return;
}
loadStatus_ &= (~LoadStatusType.LOAD_IMAGES_COMPLETE);
var image = new Image();
image.onload = onInitialImageLoaded;
image.onerror = onInitialImageLoaded;
image.src = url;
imagesBeingLoaded.push(image);
}
/**
* Initializes all the UI once the page has loaded.
*/
function init() {
// Special case to handle NTP caching.
if (window.location.hash == '#cached_ntp')
document.location.hash = '#most_visited';
// Special case to show a specific bookmarks folder.
// Used to show the mobile bookmarks folder after importing.
var bookmarkIdMatch = window.location.hash.match(/#bookmarks:(\d+)/);
if (bookmarkIdMatch && bookmarkIdMatch.length == 2) {
localStorage.setItem(DEFAULT_BOOKMARK_FOLDER_KEY, bookmarkIdMatch[1]);
document.location.hash = '#bookmarks';
}
// Special case to choose a bookmark for adding a shortcut.
// See the doc of bookmarkShortcutMode for details.
if (window.location.hash == '#bookmark_shortcut')
bookmarkShortcutMode = true;
// Make sure a valid section is always displayed. Both normal and
// incognito NTPs have a bookmarks section.
if (getPaneIndexFromHash() < 0)
document.location.hash = '#bookmarks';
// Initialize common widgets.
var titleScrollers =
document.getElementsByClassName('section-title-wrapper');
for (var i = 0, len = titleScrollers.length; i < len; i++)
initializeTitleScroller(titleScrollers[i]);
// Initialize virtual computers for the sync promo.
createPromoVirtualComputers();
setCurrentBookmarkFolderData(
localStorage.getItem(DEFAULT_BOOKMARK_FOLDER_KEY));
addMainSection('incognito');
addMainSection('most_visited');
addMainSection('bookmarks');
addMainSection('open_tabs');
computeDynamicLayout();
scrollToPane(getPaneIndexFromHash());
updateSyncEmptyState();
window.onpopstate = onPopStateHandler;
window.addEventListener('hashchange', updatePaneOnHash);
window.addEventListener('resize', windowResizeHandler);
if (!bookmarkShortcutMode)
window.addEventListener('contextmenu', contextMenuHandler);
}
function sendNTPTitleLoadedNotification() {
if (!titleLoadedStatus_) {
titleLoadedStatus_ = true;
chrome.send('notifyNTPTitleLoaded');
}
}
/**
* Notifies the chrome process of the status of the NTP.
*/
function sendNTPNotification() {
if (loadStatus_ != LoadStatusType.LOAD_COMPLETE)
return;
if (!finishedLoadingNotificationSent_) {
finishedLoadingNotificationSent_ = true;
chrome.send('notifyNTPReady');
} else {
// Navigating after the loading complete notification has been sent
// might break tests.
chrome.send('NTPUnexpectedNavigation');
}
}
/**
* The default click handler for created item shortcuts.
*
* @param {Object} item The item specification.
* @param {function} evt The browser click event triggered.
*/
function itemShortcutClickHandler(item, evt) {
// Handle the touch callback
if (item['folder']) {
browseToBookmarkFolder(item.id);
} else {
if (bookmarkShortcutMode) {
chrome.send('createHomeScreenBookmarkShortcut', [item.id]);
} else if (!!item.url) {
window.location = item.url;
}
}
}
/**
* Opens a recently closed tab.
*
* @param {Object} item An object containing the necessary information to
* reopen a tab.
*/
function openRecentlyClosedTab(item, evt) {
chrome.send('openedRecentlyClosed');
chrome.send('reopenTab', [item.sessionId]);
}
/**
* Creates a 'div' DOM element.
*
* @param {string} className The CSS class name for the DIV.
* @param {string=} opt_backgroundUrl The background URL to be applied to the
* DIV if required.
* @return {Element} The newly created DIV element.
*/
function createDiv(className, opt_backgroundUrl) {
var div = document.createElement('div');
div.className = className;
if (opt_backgroundUrl)
div.style.backgroundImage = 'url(' + opt_backgroundUrl + ')';
return div;
}
/**
* Helper for creating new DOM elements.
*
* @param {string} type The type of Element to be created (i.e. 'div',
* 'span').
* @param {Object} params A mapping of element attribute key and values that
* should be applied to the new element.
* @return {Element} The newly created DOM element.
*/
function createElement(type, params) {
var el = document.createElement(type);
if (typeof params === 'string') {
el.className = params;
} else {
for (attr in params) {
el[attr] = params[attr];
}
}
return el;
}
/**
* Adds a click listener to a specified element with the ability to override
* the default value of itemShortcutClickHandler.
*
* @param {Element} el The element the click listener should be added to.
* @param {Object} item The item data represented by the element.
* @param {function(Object, string, BrowserEvent)=} opt_clickCallback The
* click callback to be triggered upon selection.
*/
function wrapClickHandler(el, item, opt_clickCallback) {
el.addEventListener('click', function(evt) {
var clickCallback =
opt_clickCallback ? opt_clickCallback : itemShortcutClickHandler;
clickCallback(item, evt);
});
}
/**
* Create a DOM element to contain a recently closed item for a tablet
* device.
*
* @param {Object} item The data of the item used to generate the shortcut.
* @param {function(Object, string, BrowserEvent)=} opt_clickCallback The
* click callback to be triggered upon selection (if not provided it will
* use the default -- itemShortcutClickHandler).
* @return {Element} The shortcut element created.
*/
function makeRecentlyClosedTabletItem(item, opt_clickCallback) {
var cell = createDiv('cell');
cell.setAttribute(CONTEXT_MENU_URL_KEY, item.url);
var iconUrl = item.icon;
if (!iconUrl) {
iconUrl = 'chrome://touch-icon/size/16@' + window.devicePixelRatio +
'x/' + item.url;
}
var icon = createDiv('icon', iconUrl);
trackImageLoad(iconUrl);
cell.appendChild(icon);
var title = createDiv('title');
title.textContent = item.title;
cell.appendChild(title);
wrapClickHandler(cell, item, opt_clickCallback);
return cell;
}
/**
* Creates a shortcut DOM element based on the item specified item
* configuration using the thumbnail layout used for most visited. Other
* data types should not use this as they won't have a thumbnail.
*
* @param {Object} item The data of the item used to generate the shortcut.
* @param {function(Object, string, BrowserEvent)=} opt_clickCallback The
* click callback to be triggered upon selection (if not provided it will
* use the default -- itemShortcutClickHandler).
* @return {Element} The shortcut element created.
*/
function makeMostVisitedItem(item, opt_clickCallback) {
// thumbnail-cell -- main outer container
// thumbnail-container -- container for the thumbnail
// thumbnail -- the actual thumbnail image; outer border
// inner-border -- inner border
// title -- container for the title
// img -- hack align title text baseline with bottom
// title text -- the actual text of the title
var thumbnailCell = createDiv('thumbnail-cell');
var thumbnailContainer = createDiv('thumbnail-container');
var backgroundUrl = item.thumbnailUrl || 'chrome://thumb/' + item.url;
if (backgroundUrl == 'chrome://thumb/chrome://welcome/') {
// Ideally, it would be nice to use the URL as is. However, as of now
// theme support has been removed from Chrome. Instead, load the image
// URL from a style and use it. Don't just use the style because
// trackImageLoad(...) must be called with the background URL.
var welcomeStyle = findCssRule('.welcome-to-chrome').style;
var backgroundImage = welcomeStyle.backgroundImage;
// trim the "url(" prefix and ")" suffix
backgroundUrl = backgroundImage.substring(4, backgroundImage.length - 1);
}
trackImageLoad(backgroundUrl);
var thumbnail = createDiv('thumbnail');
// Use an Image object to ensure the thumbnail image actually exists. If
// not, this will allow the default to show instead.
var thumbnailImg = new Image();
thumbnailImg.onload = function() {
thumbnail.style.backgroundImage = 'url(' + backgroundUrl + ')';
};
thumbnailImg.src = backgroundUrl;
thumbnailContainer.appendChild(thumbnail);
var innerBorder = createDiv('inner-border');
thumbnailContainer.appendChild(innerBorder);
thumbnailCell.appendChild(thumbnailContainer);
var title = createDiv('title');
title.textContent = item.title;
var spacerImg = createElement('img', 'title-spacer');
spacerImg.alt = '';
title.insertBefore(spacerImg, title.firstChild);
thumbnailCell.appendChild(title);
var shade = createDiv('thumbnail-cell-shade');
thumbnailContainer.appendChild(shade);
addActiveTouchListener(shade, 'thumbnail-cell-shade-active');
wrapClickHandler(thumbnailCell, item, opt_clickCallback);
thumbnailCell.setAttribute(CONTEXT_MENU_URL_KEY, item.url);
thumbnailCell.contextMenuItem = item;
return thumbnailCell;
}
/**
* Creates a shortcut DOM element based on the item specified item
* configuration using the favicon layout used for bookmarks.
*
* @param {Object} item The data of the item used to generate the shortcut.
* @param {function(Object, string, BrowserEvent)=} opt_clickCallback The
* click callback to be triggered upon selection (if not provided it will
* use the default -- itemShortcutClickHandler).
* @return {Element} The shortcut element created.
*/
function makeBookmarkItem(item, opt_clickCallback) {
var holder = createDiv('favicon-cell');
addActiveTouchListener(holder, 'favicon-cell-active');
holder.setAttribute(CONTEXT_MENU_URL_KEY, item.url);
holder.contextMenuItem = item;
var faviconBox = createDiv('favicon-box');
if (item.folder) {
faviconBox.classList.add('folder');
} else {
var iconUrl = item.icon || 'chrome://touch-icon/largest/' + item.url;
var faviconIcon = createDiv('favicon-icon');
faviconIcon.style.backgroundImage = 'url(' + iconUrl + ')';
trackImageLoad(iconUrl);
var image = new Image();
image.src = iconUrl;
image.onload = function() {
var w = image.width;
var h = image.height;
if (Math.floor(w) <= 16 || Math.floor(h) <= 16) {
// it's a standard favicon (or at least it's small).
faviconBox.classList.add('document');
faviconBox.appendChild(
createDiv('color-strip colorstrip-' + faviconIndex));
faviconBox.appendChild(createDiv('bookmark-border'));
var foldDiv = createDiv('fold');
foldDiv.id = 'fold_' + faviconIndex;
foldDiv.style['background'] =
'-webkit-canvas(fold_' + faviconIndex + ')';
// Use a container so that the fold it self can be zoomed without
// changing the positioning of the fold.
var foldContainer = createDiv('fold-container');
foldContainer.appendChild(foldDiv);
faviconBox.appendChild(foldContainer);
// FaviconWebUIHandler::HandleGetFaviconDominantColor expects
// an URL that starts with chrome://favicon/size/.
// The handler always loads 16x16 1x favicon and assumes that
// the dominant color for all scale factors is the same.
chrome.send('getFaviconDominantColor',
[('chrome://favicon/size/16@1x/' + item.url), '' + faviconIndex]);
faviconIndex++;
} else if ((w == 57 && h == 57) || (w == 114 && h == 114)) {
// it's a touch icon for 1x or 2x.
faviconIcon.classList.add('touch-icon');
} else {
// It's an html5 icon (or at least it's larger).
// Rescale it to be no bigger than 64x64 dip.
var max = 64;
if (w > max || h > max) {
var scale = (w > h) ? (max / w) : (max / h);
w *= scale;
h *= scale;
}
faviconIcon.style.backgroundSize = w + 'px ' + h + 'px';
}
};
faviconBox.appendChild(faviconIcon);
}
holder.appendChild(faviconBox);
var title = createDiv('title');
title.textContent = item.title;
holder.appendChild(title);
wrapClickHandler(holder, item, opt_clickCallback);
return holder;
}
/**
* Adds touch listeners to the specified element to apply a class when it is
* selected (removing the class when no longer pressed).
*
* @param {Element} el The element to apply the class to when touched.
* @param {string} activeClass The CSS class name to be applied when active.
*/
function addActiveTouchListener(el, activeClass) {
if (!window.touchCancelListener) {
window.touchCancelListener = function(evt) {
if (activeItemDelayTimerId) {
clearTimeout(activeItemDelayTimerId);
activeItemDelayTimerId = undefined;
}
if (!activeItem) {
return;
}
activeItem.classList.remove(activeItem.dataset.activeClass);
activeItem = null;
};
document.addEventListener('touchcancel', window.touchCancelListener);
}
el.dataset.activeClass = activeClass;
el.addEventListener(PRESS_START_EVT, function(evt) {
if (activeItemDelayTimerId) {
clearTimeout(activeItemDelayTimerId);
activeItemDelayTimerId = undefined;
}
activeItemDelayTimerId = setTimeout(function() {
el.classList.add(activeClass);
activeItem = el;
}, ACTIVE_ITEM_DELAY_MS);
});
el.addEventListener(PRESS_STOP_EVT, function(evt) {
if (activeItemDelayTimerId) {
clearTimeout(activeItemDelayTimerId);
activeItemDelayTimerId = undefined;
}
// Add the active class to ensure the pressed state is visible when
// quickly tapping, which can happen if the start and stop events are
// received before the active item delay timer has been executed.
el.classList.add(activeClass);
el.classList.add('no-active-delay');
setTimeout(function() {
el.classList.remove(activeClass);
el.classList.remove('no-active-delay');
}, 0);
activeItem = null;
});
}
/**
* Creates a shortcut DOM element based on the item specified in the list
* format.
*
* @param {Object} item The data of the item used to generate the shortcut.
* @param {function(Object, string, BrowserEvent)=} opt_clickCallback The
* click callback to be triggered upon selection (if not provided it will
* use the default -- itemShortcutClickHandler).
* @return {Element} The shortcut element created.
*/
function makeListEntryItem(item, opt_clickCallback) {
var listItem = createDiv('list-item');
addActiveTouchListener(listItem, ACTIVE_LIST_ITEM_CSS_CLASS);
listItem.setAttribute(CONTEXT_MENU_URL_KEY, item.url);
var iconSize = item.iconSize || 64;
var iconUrl = item.icon ||
'chrome://touch-icon/size/' + iconSize + '@1x/' + item.url;
listItem.appendChild(createDiv('icon', iconUrl));
trackImageLoad(iconUrl);
var title = createElement('div', {
textContent: item.title,
className: 'title session_title'
});
listItem.appendChild(title);
listItem.addEventListener('click', function(evt) {
var clickCallback =
opt_clickCallback ? opt_clickCallback : itemShortcutClickHandler;
clickCallback(item, evt);
});
if (item.divider == 'section') {
// Add a child div because the section divider has a gradient and
// webkit doesn't seem to currently support borders with gradients.
listItem.appendChild(createDiv('section-divider'));
} else {
listItem.classList.add('standard-divider');
}
return listItem;
}
/**
* Creates a DOM list entry for a remote session or tab.
*
* @param {Object} item The data of the item used to generate the shortcut.
* @param {function(Object, string, BrowserEvent)=} opt_clickCallback The
* click callback to be triggered upon selection (if not provided it will
* use the default -- itemShortcutClickHandler).
* @return {Element} The shortcut element created.
*/
function makeForeignSessionListEntry(item, opt_clickCallback) {
// Session item
var sessionOuterDiv = createDiv('list-item standard-divider');
addActiveTouchListener(sessionOuterDiv, ACTIVE_LIST_ITEM_CSS_CLASS);
sessionOuterDiv.contextMenuItem = item;
var icon = createDiv('session-icon ' + item.iconStyle);
sessionOuterDiv.appendChild(icon);
var titleContainer = createElement('div', 'title');
sessionOuterDiv.appendChild(titleContainer);
// Extra container to allow title & last-sync time to stack vertically.
var sessionInnerDiv = createDiv('session_container');
titleContainer.appendChild(sessionInnerDiv);
var title = createDiv('session-name');
title.textContent = item.title;
title.id = item.titleId || '';
sessionInnerDiv.appendChild(title);
var lastSynced = createDiv('session-last-synced');
lastSynced.textContent =
templateData.opentabslastsynced + ': ' + item.userVisibleTimestamp;
lastSynced.id = item.userVisibleTimestampId || '';
sessionInnerDiv.appendChild(lastSynced);
sessionOuterDiv.addEventListener('click', function(evt) {
var clickCallback =
opt_clickCallback ? opt_clickCallback : itemShortcutClickHandler;
clickCallback(item, evt);
});
return sessionOuterDiv;
}
/**
* Saves the number of most visited pages and updates promo visibility.
* @param {number} n Number of most visited pages.
*/
function setNumberOfMostVisitedPages(n) {
numberOfMostVisitedPages = n;
updatePromoVisibility();
}
/**
* Saves the recently closed tabs flag and updates promo visibility.
* @param {boolean} anyTabs Whether there are any recently closed tabs.
*/
function setHasRecentlyClosedTabs(anyTabs) {
hasRecentlyClosedTabs = anyTabs;
updatePromoVisibility();
}
/**
* Updates the most visited pages.
*
* @param {Array.<Object>} List of data for displaying the list of most
* visited pages (see C++ handler for model description).
* @param {boolean} hasBlacklistedUrls Whether any blacklisted URLs are
* present.
*/
function setMostVisitedPages(data, hasBlacklistedUrls) {
setNumberOfMostVisitedPages(data.length);
// limit the number of most visited items to display
if (isPhone() && data.length > 6) {
data.splice(6, data.length - 6);
} else if (isTablet() && data.length > 8) {
data.splice(8, data.length - 8);
}
if (equals(data, mostVisitedData_))
return;
var clickFunction = function(item) {
chrome.send('openedMostVisited');
window.location = item.url;
};
populateData(findList('most_visited'), SectionType.MOST_VISITED, data,
makeMostVisitedItem, clickFunction);
computeDynamicLayout();
mostVisitedData_ = data;
}
/**
* Updates the recently closed tabs.
*
* @param {Array.<Object>} List of data for displaying the list of recently
* closed tabs (see C++ handler for model description).
*/
function setRecentlyClosedTabs(data) {
var container = $('recently_closed_container');
if (!data || data.length == 0) {
// hide the recently closed section if it is empty.
container.style.display = 'none';
setHasRecentlyClosedTabs(false);
} else {
container.style.display = 'block';
setHasRecentlyClosedTabs(true);
var decoratorFunc = isPhone() ? makeListEntryItem :
makeRecentlyClosedTabletItem;
populateData(findList('recently_closed'), SectionType.RECENTLY_CLOSED,
data, decoratorFunc, openRecentlyClosedTab);
}
computeDynamicLayout();
}
/**
* Updates the bookmarks.
*
* @param {Array.<Object>} List of data for displaying the bookmarks (see
* C++ handler for model description).
*/
function bookmarks(data) {
bookmarkFolderId = data.id;
if (!replacedInitialState) {
history.replaceState(
{folderId: bookmarkFolderId, selectedPaneIndex: currentPaneIndex},
null, null);
replacedInitialState = true;
}
if (syncEnabled == undefined) {
// Wait till we know whether or not sync is enabled before displaying any
// bookmarks (since they may need to be filtered below)
bookmarkData = data;
return;
}
var titleWrapper = $('bookmarks_title_wrapper');
setBookmarkTitleHierarchy(
titleWrapper, data, data['hierarchy']);
var filteredBookmarks = data.bookmarks;
if (!syncEnabled) {
filteredBookmarks = filteredBookmarks.filter(function(val) {
return (val.type != 'BOOKMARK_BAR' && val.type != 'OTHER_NODE');
});
}
if (bookmarkShortcutMode) {
populateData(findList('bookmarks'), SectionType.BOOKMARKS,
filteredBookmarks, makeBookmarkItem);
} else {
var clickFunction = function(item) {
if (item['folder']) {
browseToBookmarkFolder(item.id);
} else if (!!item.url) {
chrome.send('openedBookmark');
window.location = item.url;
}
};
populateData(findList('bookmarks'), SectionType.BOOKMARKS,
filteredBookmarks, makeBookmarkItem, clickFunction);
}
var bookmarkContainer = $('bookmarks_container');
// update the shadows on the breadcrumb bar
computeDynamicLayout();
if ((loadStatus_ & LoadStatusType.LOAD_BOOKMARKS_FINISHED) !=
LoadStatusType.LOAD_BOOKMARKS_FINISHED) {
loadStatus_ |= LoadStatusType.LOAD_BOOKMARKS_FINISHED;
sendNTPNotification();
}
}
/**
* Checks if promo is allowed and MostVisited requirements are satisfied.
* @return {boolean} Whether the promo should be shown on most_visited.
*/
function shouldPromoBeShownOnMostVisited() {
return promoIsAllowed && promoIsAllowedOnMostVisited &&
numberOfMostVisitedPages >= 2 && !hasRecentlyClosedTabs;
}
/**
* Checks if promo is allowed and OpenTabs requirements are satisfied.
* @return {boolean} Whether the promo should be shown on open_tabs.
*/
function shouldPromoBeShownOnOpenTabs() {
var snapshotsCount =
currentSnapshots == null ? 0 : currentSnapshots.length;
var sessionsCount = currentSessions == null ? 0 : currentSessions.length;
return promoIsAllowed && promoIsAllowedOnOpenTabs &&
(snapshotsCount + sessionsCount != 0);
}
/**
* Checks if promo is allowed and SyncPromo requirements are satisfied.
* @return {boolean} Whether the promo should be shown on sync_promo.
*/
function shouldPromoBeShownOnSync() {
var snapshotsCount =
currentSnapshots == null ? 0 : currentSnapshots.length;
var sessionsCount = currentSessions == null ? 0 : currentSessions.length;
return promoIsAllowed && promoIsAllowedOnOpenTabs &&
(snapshotsCount + sessionsCount == 0);
}
/**
* Records a promo impression on a given section if necessary.
* @param {string} section Active section name to check.
*/
function promoUpdateImpressions(section) {
if (section == 'most_visited' && shouldPromoBeShownOnMostVisited())
chrome.send('recordImpression', ['most_visited']);
else if (section == 'open_tabs' && shouldPromoBeShownOnOpenTabs())
chrome.send('recordImpression', ['open_tabs']);
else if (section == 'open_tabs' && shouldPromoBeShownOnSync())
chrome.send('recordImpression', ['sync_promo']);
}
/**
* Updates the visibility on all promo-related items as necessary.
*/
function updatePromoVisibility() {
var mostVisitedEl = $('promo_message_on_most_visited');
var openTabsVCEl = $('promo_vc_list');
var syncPromoLegacyEl = $('promo_message_on_sync_promo_legacy');
var syncPromoReceivedEl = $('promo_message_on_sync_promo_received');
mostVisitedEl.style.display =
shouldPromoBeShownOnMostVisited() ? 'block' : 'none';
syncPromoReceivedEl.style.display =
shouldPromoBeShownOnSync() ? 'block' : 'none';
syncPromoLegacyEl.style.display =
shouldPromoBeShownOnSync() ? 'none' : 'block';
openTabsVCEl.style.display =
(shouldPromoBeShownOnOpenTabs() && promoIsAllowedAsVirtualComputer) ?
'block' : 'none';
}
/**
* Called from native.
* Clears the promotion.
*/
function clearPromotions() {
setPromotions({});
}
/**
* Set the element to a parsed and sanitized promotion HTML string.
* @param {Element} el The element to set the promotion string to.
* @param {string} html The promotion HTML string.
* @throws {Error} In case of non supported markup.
*/
function setPromotionHtml(el, html) {
if (!el) return;
el.innerHTML = '';
if (!html) return;
var tags = ['BR', 'DIV', 'BUTTON', 'SPAN'];
var attrs = {
class: function(node, value) { return true; },
style: function(node, value) { return true; },
};
try {
var fragment = parseHtmlSubset(html, tags, attrs);
el.appendChild(fragment);
} catch (err) {
console.error(err.toString());
// Ignore all errors while parsing or setting the element.
}
}
/**
* Called from native.
* Sets the text for all promo-related items, updates
* promo-send-email-target items to send email on click and
* updates the visibility of items.
* @param {Object} promotions Dictionary used to fill-in the text.
*/
function setPromotions(promotions) {
var mostVisitedEl = $('promo_message_on_most_visited');
var openTabsEl = $('promo_message_on_open_tabs');
var syncPromoReceivedEl = $('promo_message_on_sync_promo_received');
promoIsAllowed = !!promotions.promoIsAllowed;
promoIsAllowedOnMostVisited = !!promotions.promoIsAllowedOnMostVisited;
promoIsAllowedOnOpenTabs = !!promotions.promoIsAllowedOnOpenTabs;
promoIsAllowedAsVirtualComputer = !!promotions.promoIsAllowedAsVC;
setPromotionHtml(mostVisitedEl, promotions.promoMessage);
setPromotionHtml(openTabsEl, promotions.promoMessage);
setPromotionHtml(syncPromoReceivedEl, promotions.promoMessageLong);
promoInjectedComputerTitleText = promotions.promoVCTitle || '';
promoInjectedComputerLastSyncedText = promotions.promoVCLastSynced || '';
var openTabsVCTitleEl = $('promo_vc_title');
if (openTabsVCTitleEl)
openTabsVCTitleEl.textContent = promoInjectedComputerTitleText;
var openTabsVCLastSyncEl = $('promo_vc_lastsync');
if (openTabsVCLastSyncEl)
openTabsVCLastSyncEl.textContent = promoInjectedComputerLastSyncedText;
if (promoIsAllowed) {
var promoButtonEls =
document.getElementsByClassName('promo-button');
for (var i = 0, len = promoButtonEls.length; i < len; i++) {
promoButtonEls[i].onclick = executePromoAction;
addActiveTouchListener(promoButtonEls[i], 'promo-button-active');
}
}
updatePromoVisibility();
}
/**
* On-click handler for promo email targets.
* Performs the promo action "send email".
* @param {Object} evt User interface event that triggered the action.
*/
function executePromoAction(evt) {
evt.preventDefault();
chrome.send('promoActionTriggered');
}
/**
* Called by the browser when a context menu has been selected.
*
* @param {number} itemId The id of the item that was selected, as specified
* when chrome.send('showContextMenu') was called.
*/
function onCustomMenuSelected(itemId) {
if (contextMenuUrl != null) {
switch (itemId) {
case ContextMenuItemIds.BOOKMARK_OPEN_IN_NEW_TAB:
case ContextMenuItemIds.BOOKMARK_OPEN_IN_INCOGNITO_TAB:
chrome.send('openedBookmark');
break;
case ContextMenuItemIds.MOST_VISITED_OPEN_IN_NEW_TAB:
case ContextMenuItemIds.MOST_VISITED_OPEN_IN_INCOGNITO_TAB:
chrome.send('openedMostVisited');
break;
case ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_NEW_TAB:
case ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_INCOGNITO_TAB:
chrome.send('openedRecentlyClosed');
break;
}
}
switch (itemId) {
case ContextMenuItemIds.BOOKMARK_OPEN_IN_NEW_TAB:
case ContextMenuItemIds.MOST_VISITED_OPEN_IN_NEW_TAB:
case ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_NEW_TAB:
if (contextMenuUrl != null)
chrome.send('openInNewTab', [contextMenuUrl]);
break;
case ContextMenuItemIds.BOOKMARK_OPEN_IN_INCOGNITO_TAB:
case ContextMenuItemIds.MOST_VISITED_OPEN_IN_INCOGNITO_TAB:
case ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_INCOGNITO_TAB:
if (contextMenuUrl != null)
chrome.send('openInIncognitoTab', [contextMenuUrl]);
break;
case ContextMenuItemIds.BOOKMARK_EDIT:
if (contextMenuItem != null)
chrome.send('editBookmark', [contextMenuItem.id]);
break;
case ContextMenuItemIds.BOOKMARK_DELETE:
if (contextMenuUrl != null)
chrome.send('deleteBookmark', [contextMenuItem.id]);
break;
case ContextMenuItemIds.MOST_VISITED_REMOVE:
if (contextMenuUrl != null)
chrome.send('blacklistURLFromMostVisited', [contextMenuUrl]);
break;
case ContextMenuItemIds.BOOKMARK_SHORTCUT:
if (contextMenuUrl != null)
chrome.send('createHomeScreenBookmarkShortcut', [contextMenuItem.id]);
break;
case ContextMenuItemIds.RECENTLY_CLOSED_REMOVE:
chrome.send('clearRecentlyClosed');
break;
case ContextMenuItemIds.FOREIGN_SESSIONS_REMOVE:
if (contextMenuItem != null) {
chrome.send(
'deleteForeignSession', [contextMenuItem.sessionTag]);
chrome.send('getForeignSessions');
}
break;
case ContextMenuItemIds.PROMO_VC_SESSION_REMOVE:
chrome.send('promoDisabled');
break;
default:
log.error('Unknown context menu selected id=' + itemId);
break;
}
}
/**
* Generates the full bookmark folder hierarchy and populates the scrollable
* title element.
*
* @param {Element} wrapperEl The wrapper element containing the scrollable
* title.
* @param {string} data The current bookmark folder node.
* @param {Array.<Object>=} opt_ancestry The folder ancestry of the current
* bookmark folder. The list is ordered in order of closest descendant
* (the root will always be the last node). The definition of each
* element is:
* - id {number}: Unique ID of the folder (N/A for root node).
* - name {string}: Name of the folder (N/A for root node).
* - root {boolean}: Whether this is the root node.
*/
function setBookmarkTitleHierarchy(wrapperEl, data, opt_ancestry) {
var title = wrapperEl.getElementsByClassName('section-title')[0];
title.innerHTML = '';
if (opt_ancestry) {
for (var i = opt_ancestry.length - 1; i >= 0; i--) {
var titleCrumb = createBookmarkTitleCrumb_(opt_ancestry[i]);
title.appendChild(titleCrumb);
title.appendChild(createDiv('bookmark-separator'));
}
}
var titleCrumb = createBookmarkTitleCrumb_(data);
titleCrumb.classList.add('title-crumb-active');
title.appendChild(titleCrumb);
// Ensure the last crumb is as visible as possible.
var windowWidth =
wrapperEl.getElementsByClassName('section-title-mask')[0].offsetWidth;
var crumbWidth = titleCrumb.offsetWidth;
var leftOffset = titleCrumb.offsetLeft;
var shiftLeft = windowWidth - crumbWidth - leftOffset;
if (shiftLeft < 0) {
if (crumbWidth > windowWidth)
shifLeft = -leftOffset;
// Queue up the scrolling initially to allow for the mask element to
// be placed into the dom and it's size correctly calculated.
setTimeout(function() {
handleTitleScroll(wrapperEl, shiftLeft);
}, 0);
} else {
handleTitleScroll(wrapperEl, 0);
}
}
/**
* Creates a clickable bookmark title crumb.
* @param {Object} data The crumb data (see setBookmarkTitleHierarchy for
* definition of the data object).
* @return {Element} The clickable title crumb element.
* @private
*/
function createBookmarkTitleCrumb_(data) {
var titleCrumb = createDiv('title-crumb');
if (data.root) {
titleCrumb.innerText = templateData.bookmarkstitle;
} else {
titleCrumb.innerText = data.title;
}
titleCrumb.addEventListener('click', function(evt) {
browseToBookmarkFolder(data.root ? '0' : data.id);
});
return titleCrumb;
}
/**
* Handles scrolling a title element.
* @param {Element} wrapperEl The wrapper element containing the scrollable
* title.
* @param {number} scrollPosition The position to be scrolled to.
*/
function handleTitleScroll(wrapperEl, scrollPosition) {
var overflowLeftMask =
wrapperEl.getElementsByClassName('overflow-left-mask')[0];
var overflowRightMask =
wrapperEl.getElementsByClassName('overflow-right-mask')[0];
var title = wrapperEl.getElementsByClassName('section-title')[0];
var titleMask = wrapperEl.getElementsByClassName('section-title-mask')[0];
var titleWidth = title.scrollWidth;
var containerWidth = titleMask.offsetWidth;
var maxRightScroll = containerWidth - titleWidth;
var boundedScrollPosition =
Math.max(maxRightScroll, Math.min(scrollPosition, 0));
overflowLeftMask.style.opacity =
Math.min(
1,
(Math.max(0, -boundedScrollPosition)) + 10 / 30);
overflowRightMask.style.opacity =
Math.min(
1,
(Math.max(0, boundedScrollPosition - maxRightScroll) + 10) / 30);
// Set the position of the title.
if (titleWidth < containerWidth) {
// left-align on LTR and right-align on RTL.
title.style.left = '';
} else {
title.style.left = boundedScrollPosition + 'px';
}
}
/**
* Initializes a scrolling title element.
* @param {Element} wrapperEl The wrapper element of the scrolling title.
*/
function initializeTitleScroller(wrapperEl) {
var title = wrapperEl.getElementsByClassName('section-title')[0];
var inTitleScroll = false;
var startingScrollPosition;
var startingOffset;
wrapperEl.addEventListener(PRESS_START_EVT, function(evt) {
inTitleScroll = true;
startingScrollPosition = getTouchEventX(evt);
startingOffset = title.offsetLeft;
});
document.body.addEventListener(PRESS_STOP_EVT, function(evt) {
if (!inTitleScroll)
return;
inTitleScroll = false;
});
document.body.addEventListener(PRESS_MOVE_EVT, function(evt) {
if (!inTitleScroll)
return;
handleTitleScroll(
wrapperEl,
startingOffset - (startingScrollPosition - getTouchEventX(evt)));
evt.stopPropagation();
});
}
/**
* Handles updates from the underlying bookmark model (calls originate
* in the WebUI handler for bookmarks).
*
* @param {Object} status Describes the type of change that occurred. Can
* contain the following fields:
* - parent_id {string}: Unique id of the parent that was affected by
* the change. If the parent is the bookmark
* bar, then the ID will be 'root'.
* - node_id {string}: The unique ID of the node that was affected.
*/
function bookmarkChanged(status) {
if (status) {
var affectedParentNode = status['parent_id'];
var affectedNodeId = status['node_id'];
var shouldUpdate = (bookmarkFolderId == affectedParentNode ||
bookmarkFolderId == affectedNodeId);
if (shouldUpdate)
setCurrentBookmarkFolderData(bookmarkFolderId);
} else {
// This typically happens when extensive changes could have happened to
// the model, such as initial load, import and sync.
setCurrentBookmarkFolderData(bookmarkFolderId);
}
}
/**
* Loads the bookarks data for a given folder.
*
* @param {string|number} folderId The ID of the folder to load (or null if
* it should load the root folder).
*/
function setCurrentBookmarkFolderData(folderId) {
if (folderId != null) {
chrome.send('getBookmarks', [folderId]);
} else {
chrome.send('getBookmarks');
}
try {
if (folderId == null) {
localStorage.removeItem(DEFAULT_BOOKMARK_FOLDER_KEY);
} else {
localStorage.setItem(DEFAULT_BOOKMARK_FOLDER_KEY, folderId);
}
} catch (e) {}
}
/**
* Navigates to the specified folder and handles loading the required data.
* Ensures the current folder can be navigated back to using the browser
* controls.
*
* @param {string|number} folderId The ID of the folder to navigate to.
*/
function browseToBookmarkFolder(folderId) {
history.pushState(
{folderId: folderId, selectedPaneIndex: currentPaneIndex},
null, null);
setCurrentBookmarkFolderData(folderId);
}
/**
* Called to inform the page of the current sync status. If the state has
* changed from disabled to enabled, it changes the current and default
* bookmark section to the root directory. This makes desktop bookmarks are
* visible.
*/
function setSyncEnabled(enabled) {
try {
if (syncEnabled != undefined && syncEnabled == enabled) {
// The value didn't change
return;
}
syncEnabled = enabled;
if (enabled) {
if (!localStorage.getItem(SYNC_ENABLED_KEY)) {
localStorage.setItem(SYNC_ENABLED_KEY, 'true');
setCurrentBookmarkFolderData('0');
}
} else {
localStorage.removeItem(SYNC_ENABLED_KEY);
}
updatePromoVisibility();
if (bookmarkData) {
// Bookmark data can now be displayed (or needs to be refiltered)
bookmarks(bookmarkData);
}
updateSyncEmptyState();
} catch (e) {}
}
/**
* Handles adding or removing the 'nothing to see here' text from the session
* list depending on the state of snapshots and sessions.
*
* @param {boolean} Whether the call is occuring because of a schedule
* timeout.
*/
function updateSyncEmptyState(timeout) {
if (syncState == SyncState.DISPLAYING_LOADING && !timeout) {
// Make sure 'Loading...' is displayed long enough
return;
}
var openTabsList = findList('open_tabs');
var snapshotsList = findList('snapshots');
var syncPromo = $('sync_promo');
var syncLoading = $('sync_loading');
var syncEnableSync = $('sync_enable_sync');
if (syncEnabled == undefined ||
currentSnapshots == null ||
currentSessions == null) {
if (syncState == SyncState.INITIAL) {
// Wait one second for sync data to come in before displaying loading
// text.
syncState = SyncState.WAITING_FOR_DATA;
syncTimerId = setTimeout(function() { updateSyncEmptyState(true); },
SYNC_INITIAL_LOAD_TIMEOUT);
} else if (syncState == SyncState.WAITING_FOR_DATA && timeout) {
// We've waited for the initial info timeout to pass and still don't
// have data. So, display loading text so the user knows something is
// happening.
syncState = SyncState.DISPLAYING_LOADING;
syncLoading.style.display = '-webkit-box';
centerEmptySections(syncLoading);
syncTimerId = setTimeout(function() { updateSyncEmptyState(true); },
SYNC_LOADING_TIMEOUT);
} else if (syncState == SyncState.DISPLAYING_LOADING) {
// Allow the Loading... text to go away once data comes in
syncState = SyncState.DISPLAYED_LOADING;
}
return;
}
if (syncTimerId != -1) {
clearTimeout(syncTimerId);
syncTimerId = -1;
}
syncState = SyncState.LOADED;
// Hide everything by default, display selectively below
syncEnableSync.style.display = 'none';
syncLoading.style.display = 'none';
syncPromo.style.display = 'none';
var snapshotsCount =
currentSnapshots == null ? 0 : currentSnapshots.length;
var sessionsCount = currentSessions == null ? 0 : currentSessions.length;
if (!syncEnabled) {
syncEnableSync.style.display = '-webkit-box';
centerEmptySections(syncEnableSync);
} else if (sessionsCount + snapshotsCount == 0) {
syncPromo.style.display = '-webkit-box';
centerEmptySections(syncPromo);
} else {
openTabsList.style.display = sessionsCount == 0 ? 'none' : 'block';
snapshotsList.style.display = snapshotsCount == 0 ? 'none' : 'block';
}
updatePromoVisibility();
}
/**
* Called externally when updated snapshot data is available.
*
* @param {Object} data The snapshot data
*/
function snapshots(data) {
var list = findList('snapshots');
list.innerHTML = '';
currentSnapshots = data;
updateSyncEmptyState();
if (!data || data.length == 0)
return;
data.sort(function(a, b) {
return b.createTime - a.createTime;
});
// Create the main container
var snapshotsEl = createElement('div');
list.appendChild(snapshotsEl);
// Create the header container
var headerEl = createDiv('session-header');
snapshotsEl.appendChild(headerEl);
// Create the documents container
var docsEl = createDiv('session-children-container');
snapshotsEl.appendChild(docsEl);
// Create the container for the title & icon
var headerInnerEl = createDiv('list-item standard-divider');
addActiveTouchListener(headerInnerEl, ACTIVE_LIST_ITEM_CSS_CLASS);
headerEl.appendChild(headerInnerEl);
// Create the header icon
headerInnerEl.appendChild(createDiv('session-icon documents'));
// Create the header title
var titleContainer = createElement('span', 'title');
headerInnerEl.appendChild(titleContainer);
var title = createDiv('session-name');
title.textContent = templateData.receivedDocuments;
titleContainer.appendChild(title);
// Add support for expanding and collapsing the children
var expando = createDiv();
var expandoFunction = createExpandoFunction(expando, docsEl);
headerInnerEl.addEventListener('click', expandoFunction);
headerEl.appendChild(expando);
// Support for actually opening the document
var snapshotClickCallback = function(item) {
if (!item)
return;
if (item.snapshotId) {
window.location = 'chrome://snapshot/' + item.snapshotId;
} else if (item.printJobId) {
window.location = 'chrome://printjob/' + item.printJobId;
} else {
window.location = item.url;
}
}
// Finally, add the list of documents
populateData(docsEl, SectionType.SNAPSHOTS, data,
makeListEntryItem, snapshotClickCallback);
}
/**
* Create a function to handle expanding and collapsing a section
*
* @param {Element} expando The expando div
* @param {Element} element The element to expand and collapse
* @return {function()} A callback function that should be invoked when the
* expando is clicked
*/
function createExpandoFunction(expando, element) {
expando.className = 'expando open';
return function() {
if (element.style.height != '0px') {
// It seems that '-webkit-transition' only works when explicit pixel
// values are used.
setTimeout(function() {
// If this is the first time to collapse the list, store off the
// expanded height and also set the height explicitly on the style.
if (!element.expandedHeight) {
element.expandedHeight =
element.clientHeight + 'px';
element.style.height = element.expandedHeight;
}
// Now set the height to 0. Note, this is also done in a callback to
// give the layout engine a chance to run after possibly setting the
// height above.
setTimeout(function() {
element.style.height = '0px';
}, 0);
}, 0);
expando.className = 'expando closed';
} else {
element.style.height = element.expandedHeight;
expando.className = 'expando open';
}
}
}
/**
* Initializes the promo_vc_list div to look like a foreign session
* with a desktop.
*/
function createPromoVirtualComputers() {
var list = findList('promo_vc');
list.innerHTML = '';
// Set up the container and the "virtual computer" session header.
var sessionEl = createDiv();
list.appendChild(sessionEl);
var sessionHeader = createDiv('session-header');
sessionEl.appendChild(sessionHeader);
// Set up the session children container and the promo as a child.
var sessionChildren = createDiv('session-children-container');
var promoMessage = createDiv('promo-message');
promoMessage.id = 'promo_message_on_open_tabs';
sessionChildren.appendChild(promoMessage);
sessionEl.appendChild(sessionChildren);
// Add support for expanding and collapsing the children.
var expando = createDiv();
var expandoFunction = createExpandoFunction(expando, sessionChildren);
// Fill-in the contents of the "virtual computer" session header.
var headerList = [{
'title': promoInjectedComputerTitleText,
'titleId': 'promo_vc_title',
'userVisibleTimestamp': promoInjectedComputerLastSyncedText,
'userVisibleTimestampId': 'promo_vc_lastsync',
'iconStyle': 'laptop'
}];
populateData(sessionHeader, SectionType.PROMO_VC_SESSION_HEADER, headerList,
makeForeignSessionListEntry, expandoFunction);
sessionHeader.appendChild(expando);
}
/**
* Called externally when updated synced sessions data is available.
*
* @param {Object} data The snapshot data
*/
function setForeignSessions(data, tabSyncEnabled) {
var list = findList('open_tabs');
list.innerHTML = '';
currentSessions = data;
updateSyncEmptyState();
// Sort the windows within each client such that more recently
// modified windows appear first.
data.forEach(function(client) {
if (client.windows != null) {
client.windows.sort(function(a, b) {
if (b.timestamp == null) {
return -1;
} else if (a.timestamp == null) {
return 1;
} else {
return b.timestamp - a.timestamp;
}
});
}
});
// Sort so more recently modified clients appear first.
data.sort(function(aClient, bClient) {
var aWindows = aClient.windows;
var bWindows = bClient.windows;
if (bWindows == null || bWindows.length == 0 ||
bWindows[0].timestamp == null) {
return -1;
} else if (aWindows == null || aWindows.length == 0 ||
aWindows[0].timestamp == null) {
return 1;
} else {
return bWindows[0].timestamp - aWindows[0].timestamp;
}
});
data.forEach(function(client, clientNum) {
var windows = client.windows;
if (windows == null || windows.length == 0)
return;
// Set up the container for the session header
var sessionEl = createElement('div');
list.appendChild(sessionEl);
var sessionHeader = createDiv('session-header');
sessionEl.appendChild(sessionHeader);
// Set up the container for the session children
var sessionChildren = createDiv('session-children-container');
sessionEl.appendChild(sessionChildren);
var clientName = 'Client ' + clientNum;
if (client.name)
clientName = client.name;
var iconStyle;
var deviceType = client.deviceType;
if (deviceType == 'win' ||
deviceType == 'macosx' ||
deviceType == 'linux' ||
deviceType == 'chromeos' ||
deviceType == 'other') {
iconStyle = 'laptop';
} else if (deviceType == 'phone') {
iconStyle = 'phone';
} else if (deviceType == 'tablet') {
iconStyle = 'tablet';
} else {
console.error('Unknown sync device type found: ', deviceType);
iconStyle = 'laptop';
}
var headerList = [{
'title': clientName,
'userVisibleTimestamp': windows[0].userVisibleTimestamp,
'iconStyle': iconStyle,
'sessionTag': client.tag,
}];
var expando = createDiv();
var expandoFunction = createExpandoFunction(expando, sessionChildren);
populateData(sessionHeader, SectionType.FOREIGN_SESSION_HEADER,
headerList, makeForeignSessionListEntry, expandoFunction);
sessionHeader.appendChild(expando);
// Populate the session children container
var openTabsList = new Array();
for (var winNum = 0; winNum < windows.length; winNum++) {
win = windows[winNum];
var tabs = win.tabs;
for (var tabNum = 0; tabNum < tabs.length; tabNum++) {
var tab = tabs[tabNum];
// If this is the last tab in the window and there are more windows,
// use a section divider.
var needSectionDivider =
(tabNum + 1 == tabs.length) && (winNum + 1 < windows.length);
tab.icon = tab.icon || 'chrome://favicon/size/16@1x/' + tab.url;
openTabsList.push({
timestamp: tab.timestamp,
title: tab.title,
url: tab.url,
sessionTag: client.tag,
winNum: winNum,
sessionId: tab.sessionId,
icon: tab.icon,
iconSize: 16,
divider: needSectionDivider ? 'section' : 'standard',
});
}
}
var tabCallback = function(item, evt) {
var buttonIndex = 0;
var altKeyPressed = false;
var ctrlKeyPressed = false;
var metaKeyPressed = false;
var shiftKeyPressed = false;
if (evt instanceof MouseEvent) {
buttonIndex = evt.button;
altKeyPressed = evt.altKey;
ctrlKeyPressed = evt.ctrlKey;
metaKeyPressed = evt.metaKey;
shiftKeyPressed = evt.shiftKey;
}
chrome.send('openedForeignSession');
chrome.send('openForeignSession', [String(item.sessionTag),
String(item.winNum), String(item.sessionId), buttonIndex,
altKeyPressed, ctrlKeyPressed, metaKeyPressed, shiftKeyPressed]);
};
populateData(sessionChildren, SectionType.FOREIGN_SESSION, openTabsList,
makeListEntryItem, tabCallback);
});
}
/**
* Updates the dominant favicon color for a given index.
*
* @param {number} index The index of the favicon whose dominant color is
* being specified.
* @param {string} color The string encoded color.
*/
function setFaviconDominantColor(index, color) {
var colorstrips = document.getElementsByClassName('colorstrip-' + index);
for (var i = 0; i < colorstrips.length; i++)
colorstrips[i].style.background = color;
var id = 'fold_' + index;
var fold = $(id);
if (!fold)
return;
var zoom = window.getComputedStyle(fold).zoom;
var scale = 1 / window.getComputedStyle(fold).zoom;
// The width/height of the canvas. Set to 24 so it looks good across all
// resolutions.
var cw = 24;
var ch = 24;
// Get the fold canvas and create a path for the fold shape
var ctx = document.getCSSCanvasContext(
'2d', 'fold_' + index, cw * scale, ch * scale);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, ch * 0.75 * scale);
ctx.quadraticCurveTo(
0, ch * scale,
cw * .25 * scale, ch * scale);
ctx.lineTo(cw * scale, ch * scale);
ctx.closePath();
// Create a gradient for the fold and fill it
var gradient = ctx.createLinearGradient(cw * scale, 0, 0, ch * scale);
if (color.indexOf('#') == 0) {
var r = parseInt(color.substring(1, 3), 16);
var g = parseInt(color.substring(3, 5), 16);
var b = parseInt(color.substring(5, 7), 16);
gradient.addColorStop(0, 'rgba(' + r + ', ' + g + ', ' + b + ', 0.6)');
} else {
// assume the color is in the 'rgb(#, #, #)' format
var rgbBase = color.substring(4, color.length - 1);
gradient.addColorStop(0, 'rgba(' + rgbBase + ', 0.6)');
}
gradient.addColorStop(1, color);
ctx.fillStyle = gradient;
ctx.fill();
// Stroke the fold
ctx.lineWidth = Math.floor(scale);
ctx.strokeStyle = color;
ctx.stroke();
ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
ctx.stroke();
}
/**
* Finds the list element corresponding to the given name.
* @param {string} name The name prefix of the DOM element (<prefix>_list).
* @return {Element} The list element corresponding with the name.
*/
function findList(name) {
return $(name + '_list');
}
/**
* Render the given data into the given list, and hide or show the entire
* container based on whether there are any elements. The decorator function
* is used to create the element to be inserted based on the given data
* object.
*
* @param {holder} The dom element that the generated list items will be put
* into.
* @param {SectionType} section The section that data is for.
* @param {Object} data The data to be populated.
* @param {function(Object, boolean)} decorator The function that will
* handle decorating each item in the data.
* @param {function(Object, Object)} opt_clickCallback The function that is
* called when the item is clicked.
*/
function populateData(holder, section, data, decorator,
opt_clickCallback) {
// Empty other items in the list, if present.
holder.innerHTML = '';
var fragment = document.createDocumentFragment();
if (!data || data.length == 0) {
fragment.innerHTML = '';
} else {
data.forEach(function(item) {
var el = decorator(item, opt_clickCallback);
el.setAttribute(SECTION_KEY, section);
el.id = section + fragment.childNodes.length;
fragment.appendChild(el);
});
}
holder.appendChild(fragment);
if (holder.classList.contains(GRID_CSS_CLASS))
centerGrid(holder);
centerEmptySections(holder);
}
/**
* Given an element containing a list of child nodes arranged in
* a grid, this will center the grid in the window based on the
* remaining space.
* @param {Element} el Container holding the grid cell items.
*/
function centerGrid(el) {
var childEl = el.firstChild;
if (!childEl)
return;
// Find the element to actually set the margins on.
var toCenter = el;
var curEl = toCenter;
while (curEl && curEl.classList) {
if (curEl.classList.contains(GRID_CENTER_CSS_CLASS)) {
toCenter = curEl;
break;
}
curEl = curEl.parentNode;
}
var setItemMargins = el.classList.contains(GRID_SET_ITEM_MARGINS);
var itemWidth = getItemWidth(childEl, setItemMargins);
var windowWidth = document.documentElement.offsetWidth;
if (itemWidth >= windowWidth) {
toCenter.style.paddingLeft = '0';
toCenter.style.paddingRight = '0';
} else {
var numColumns = el.getAttribute(GRID_COLUMNS);
if (numColumns) {
numColumns = parseInt(numColumns);
} else {
numColumns = Math.floor(windowWidth / itemWidth);
}
if (setItemMargins) {
// In this case, try to size each item to fill as much space as
// possible.
var gutterSize =
(windowWidth - itemWidth * numColumns) / (numColumns + 1);
var childLeftMargin = Math.round(gutterSize / 2);
var childRightMargin = Math.floor(gutterSize - childLeftMargin);
var children = el.childNodes;
for (var i = 0; i < children.length; i++) {
children[i].style.marginLeft = childLeftMargin + 'px';
children[i].style.marginRight = childRightMargin + 'px';
}
itemWidth += childLeftMargin + childRightMargin;
}
var remainder = windowWidth - itemWidth * numColumns;
var leftPadding = Math.round(remainder / 2);
var rightPadding = Math.floor(remainder - leftPadding);
toCenter.style.paddingLeft = leftPadding + 'px';
toCenter.style.paddingRight = rightPadding + 'px';
if (toCenter.classList.contains(GRID_SET_TOP_MARGIN_CLASS)) {
var childStyle = window.getComputedStyle(childEl);
var childLeftPadding = parseInt(
childStyle.getPropertyValue('padding-left'));
toCenter.style.paddingTop =
(childLeftMargin + childLeftPadding + leftPadding) + 'px';
}
}
}
/**
* Finds and centers all child grid elements for a given node (the grids
* do not need to be direct descendants and can reside anywhere in the node
* hierarchy).
* @param {Element} el The node containing the grid child nodes.
*/
function centerChildGrids(el) {
var grids = el.getElementsByClassName(GRID_CSS_CLASS);
for (var i = 0; i < grids.length; i++)
centerGrid(grids[i]);
}
/**
* Finds and vertically centers all 'empty' elements for a given node (the
* 'empty' elements do not need to be direct descendants and can reside
* anywhere in the node hierarchy).
* @param {Element} el The node containing the 'empty' child nodes.
*/
function centerEmptySections(el) {
if (el.classList &&
el.classList.contains(CENTER_EMPTY_CONTAINER_CSS_CLASS)) {
centerEmptySection(el);
}
var empties = el.getElementsByClassName(CENTER_EMPTY_CONTAINER_CSS_CLASS);
for (var i = 0; i < empties.length; i++) {
centerEmptySection(empties[i]);
}
}
/**
* Set the top of the given element to the top of the parent and set the
* height to (bottom of document - top).
*
* @param {Element} el Container holding the centered content.
*/
function centerEmptySection(el) {
var parent = el.parentNode;
var top = parent.offsetTop;
var bottom = (
document.documentElement.offsetHeight - getButtonBarPadding());
el.style.height = (bottom - top) + 'px';
el.style.top = top + 'px';
}
/**
* Finds the index of the panel specified by its prefix.
* @param {string} The string prefix for the panel.
* @return {number} The index of the panel.
*/
function getPaneIndex(panePrefix) {
var pane = $(panePrefix + '_container');
if (pane != null) {
var index = panes.indexOf(pane);
if (index >= 0)
return index;
}
return 0;
}
/**
* Finds the index of the panel specified by location hash.
* @return {number} The index of the panel.
*/
function getPaneIndexFromHash() {
var paneIndex;
if (window.location.hash == '#bookmarks') {
paneIndex = getPaneIndex('bookmarks');
} else if (window.location.hash == '#bookmark_shortcut') {
paneIndex = getPaneIndex('bookmarks');
} else if (window.location.hash == '#most_visited') {
paneIndex = getPaneIndex('most_visited');
} else if (window.location.hash == '#open_tabs') {
paneIndex = getPaneIndex('open_tabs');
} else if (window.location.hash == '#incognito') {
paneIndex = getPaneIndex('incognito');
} else {
// Couldn't find a good section
paneIndex = -1;
}
return paneIndex;
}
/**
* Selects a pane from the top level list (Most Visited, Bookmarks, etc...).
* @param {number} paneIndex The index of the pane to be selected.
* @return {boolean} Whether the selected pane has changed.
*/
function scrollToPane(paneIndex) {
var pane = panes[paneIndex];
if (pane == currentPane)
return false;
var newHash = '#' + sectionPrefixes[paneIndex];
// If updated hash matches the current one in the URL, we need to call
// updatePaneOnHash directly as updating the hash to the same value will
// not trigger the 'hashchange' event.
if (bookmarkShortcutMode || newHash == document.location.hash)
updatePaneOnHash();
computeDynamicLayout();
promoUpdateImpressions(sectionPrefixes[paneIndex]);
return true;
}
/**
* Updates the pane based on the current hash.
*/
function updatePaneOnHash() {
var paneIndex = getPaneIndexFromHash();
var pane = panes[paneIndex];
if (currentPane)
currentPane.classList.remove('selected');
pane.classList.add('selected');
currentPane = pane;
currentPaneIndex = paneIndex;
document.documentElement.scrollTop = 0;
var panelPrefix = sectionPrefixes[paneIndex];
var title = templateData[panelPrefix + '_document_title'];
if (!title)
title = templateData['title'];
document.title = title;
sendNTPTitleLoadedNotification();
// TODO (dtrainor): Could potentially add logic to reset the bookmark state
// if they are moving to that pane. This logic was in there before, but
// was removed due to the fact that we have to go to this pane as part of
// the history navigation.
}
/**
* Adds a top level section to the NTP.
* @param {string} panelPrefix The prefix of the element IDs corresponding
* to the container of the content.
* @param {boolean=} opt_canBeDefault Whether this section can be marked as
* the default starting point for subsequent instances of the NTP. The
* default value for this is true.
*/
function addMainSection(panelPrefix) {
var paneEl = $(panelPrefix + '_container');
var paneIndex = panes.push(paneEl) - 1;
sectionPrefixes.push(panelPrefix);
}
/**
* Handles the dynamic layout of the components on the new tab page. Only
* layouts that require calculation based on the screen size should go in
* this function as it will be called during all resize changes
* (orientation, keyword being displayed).
*/
function computeDynamicLayout() {
// Update the scrolling titles to ensure they are not in a now invalid
// scroll position.
var titleScrollers =
document.getElementsByClassName('section-title-wrapper');
for (var i = 0, len = titleScrollers.length; i < len; i++) {
var titleEl =
titleScrollers[i].getElementsByClassName('section-title')[0];
handleTitleScroll(
titleScrollers[i],
titleEl.offsetLeft);
}
updateMostVisitedStyle();
updateMostVisitedHeight();
}
/**
* The centering of the 'recently closed' section is different depending on
* the orientation of the device. In landscape, it should be left-aligned
* with the 'most used' section. In portrait, it should be centered in the
* screen.
*/
function updateMostVisitedStyle() {
if (isTablet()) {
updateMostVisitedStyleTablet();
} else {
updateMostVisitedStylePhone();
}
}
/**
* Updates the style of the most visited pane for the phone.
*/
function updateMostVisitedStylePhone() {
var mostVisitedList = $('most_visited_list');
var childEl = mostVisitedList.firstChild;
if (!childEl)
return;
// 'natural' height and width of the thumbnail
var thumbHeight = 72;
var thumbWidth = 108;
var labelHeight = 25;
var labelWidth = thumbWidth + 20;
var labelLeft = (thumbWidth - labelWidth) / 2;
var itemHeight = thumbHeight + labelHeight;
// default vertical margin between items
var itemMarginTop = 0;
var itemMarginBottom = 0;
var itemMarginLeft = 20;
var itemMarginRight = 20;
var listHeight = 0;
var screenHeight =
document.documentElement.offsetHeight -
getButtonBarPadding();
if (isPortrait()) {
mostVisitedList.setAttribute(GRID_COLUMNS, '2');
listHeight = screenHeight * .85;
// Ensure that listHeight is not too small and not too big.
listHeight = Math.max(listHeight, (itemHeight * 3) + 20);
listHeight = Math.min(listHeight, 420);
// Size for 3 rows (4 gutters)
itemMarginTop = (listHeight - (itemHeight * 3)) / 4;
} else {
mostVisitedList.setAttribute(GRID_COLUMNS, '3');
listHeight = screenHeight;
// If the screen height is less than targetHeight, scale the size of the
// thumbnails such that the margin between the thumbnails remains
// constant.
var targetHeight = 220;
if (screenHeight < targetHeight) {
var targetRemainder = targetHeight - 2 * (thumbHeight + labelHeight);
var scale = (screenHeight - 2 * labelHeight -
targetRemainder) / (2 * thumbHeight);
// update values based on scale
thumbWidth = Math.round(thumbWidth * scale);
thumbHeight = Math.round(thumbHeight * scale);
labelWidth = thumbWidth + 20;
itemHeight = thumbHeight + labelHeight;
}
// scale the vertical margin such that the items fit perfectly on the
// screen
var remainder = screenHeight - (2 * itemHeight);
var margin = (remainder / 2);
margin = margin > 24 ? 24 : margin;
itemMarginTop = Math.round(margin / 2);
itemMarginBottom = Math.round(margin - itemMarginTop);
}
mostVisitedList.style.minHeight = listHeight + 'px';
modifyCssRule('body[device="phone"] .thumbnail-cell',
'height', itemHeight + 'px');
modifyCssRule('body[device="phone"] #most_visited_list .thumbnail',
'height', thumbHeight + 'px');
modifyCssRule('body[device="phone"] #most_visited_list .thumbnail',
'width', thumbWidth + 'px');
modifyCssRule(
'body[device="phone"] #most_visited_list .thumbnail-container',
'height', thumbHeight + 'px');
modifyCssRule(
'body[device="phone"] #most_visited_list .thumbnail-container',
'width', thumbWidth + 'px');
modifyCssRule('body[device="phone"] #most_visited_list .title',
'width', labelWidth + 'px');
modifyCssRule('body[device="phone"] #most_visited_list .title',
'left', labelLeft + 'px');
modifyCssRule('body[device="phone"] #most_visited_list .inner-border',
'height', thumbHeight - 2 + 'px');
modifyCssRule('body[device="phone"] #most_visited_list .inner-border',
'width', thumbWidth - 2 + 'px');
modifyCssRule('body[device="phone"] .thumbnail-cell',
'margin-left', itemMarginLeft + 'px');
modifyCssRule('body[device="phone"] .thumbnail-cell',
'margin-right', itemMarginRight + 'px');
modifyCssRule('body[device="phone"] .thumbnail-cell',
'margin-top', itemMarginTop + 'px');
modifyCssRule('body[device="phone"] .thumbnail-cell',
'margin-bottom', itemMarginBottom + 'px');
centerChildGrids($('most_visited_container'));
}
/**
* Updates the style of the most visited pane for the tablet.
*/
function updateMostVisitedStyleTablet() {
function setCenterIconGrid(el, set) {
if (set) {
el.classList.add(GRID_CENTER_CSS_CLASS);
} else {
el.classList.remove(GRID_CENTER_CSS_CLASS);
el.style.paddingLeft = '0px';
el.style.paddingRight = '0px';
}
}
var isPortrait = document.documentElement.offsetWidth <
document.documentElement.offsetHeight;
var mostVisitedContainer = $('most_visited_container');
var mostVisitedList = $('most_visited_list');
var recentlyClosedContainer = $('recently_closed_container');
var recentlyClosedList = $('recently_closed_list');
setCenterIconGrid(mostVisitedContainer, !isPortrait);
setCenterIconGrid(mostVisitedList, isPortrait);
setCenterIconGrid(recentlyClosedContainer, isPortrait);
if (isPortrait) {
recentlyClosedList.classList.add(GRID_CSS_CLASS);
} else {
recentlyClosedList.classList.remove(GRID_CSS_CLASS);
}
// Make the recently closed list visually left align with the most recently
// closed items in landscape mode. It will be reset by the grid centering
// in portrait mode.
if (!isPortrait)
recentlyClosedContainer.style.paddingLeft = '14px';
}
/**
* This handles updating some of the spacing to make the 'recently closed'
* section appear at the bottom of the page.
*/
function updateMostVisitedHeight() {
if (!isTablet())
return;
// subtract away height of button bar
var windowHeight = document.documentElement.offsetHeight;
var padding = parseInt(window.getComputedStyle(document.body)
.getPropertyValue('padding-bottom'));
$('most_visited_container').style.minHeight =
(windowHeight - padding) + 'px';
}
/**
* Called by the native toolbar to open a different section. This handles
* updating the hash url which in turns makes a history entry.
*
* @param {string} section The section to switch to.
*/
var openSection = function(section) {
if (!scrollToPane(getPaneIndex(section)))
return;
// Update the url so the native toolbar knows the pane has changed and
// to create a history entry.
document.location.hash = '#' + section;
}
/////////////////////////////////////////////////////////////////////////////
// NTP Scoped Window Event Listeners.
/////////////////////////////////////////////////////////////////////////////
/**
* Handles history on pop state changes.
*/
function onPopStateHandler(event) {
if (event.state != null) {
var evtState = event.state;
// Navigate back to the previously selected panel and ensure the same
// bookmarks are loaded.
var selectedPaneIndex = evtState.selectedPaneIndex == undefined ?
0 : evtState.selectedPaneIndex;
scrollToPane(selectedPaneIndex);
setCurrentBookmarkFolderData(evtState.folderId);
} else {
// When loading the page, replace the default state with one that
// specifies the default panel loaded via localStorage as well as the
// default bookmark folder.
history.replaceState(
{folderId: bookmarkFolderId, selectedPaneIndex: currentPaneIndex},
null, null);
}
}
/**
* Handles window resize events.
*/
function windowResizeHandler() {
// Scroll to the current pane to refactor all the margins and offset.
scrollToPane(currentPaneIndex);
computeDynamicLayout();
// Center the padding for each of the grid views.
centerChildGrids(document);
centerEmptySections(document);
}
/*
* We implement the context menu ourselves.
*/
function contextMenuHandler(evt) {
var section = SectionType.UNKNOWN;
contextMenuUrl = null;
contextMenuItem = null;
// The node with a menu have been tagged with their section and url.
// Let's find these tags.
var node = evt.target;
while (node) {
if (section == SectionType.UNKNOWN &&
node.getAttribute &&
node.getAttribute(SECTION_KEY) != null) {
section = node.getAttribute(SECTION_KEY);
if (contextMenuUrl != null)
break;
}
if (contextMenuUrl == null) {
contextMenuUrl = node.getAttribute(CONTEXT_MENU_URL_KEY);
contextMenuItem = node.contextMenuItem;
if (section != SectionType.UNKNOWN)
break;
}
node = node.parentNode;
}
var menuOptions;
if (section == SectionType.BOOKMARKS &&
!contextMenuItem.folder && !isIncognito) {
menuOptions = [
[
ContextMenuItemIds.BOOKMARK_OPEN_IN_NEW_TAB,
templateData.elementopeninnewtab
]
];
if (isIncognitoEnabled) {
menuOptions.push([
ContextMenuItemIds.BOOKMARK_OPEN_IN_INCOGNITO_TAB,
templateData.elementopeninincognitotab
]);
}
if (contextMenuItem.editable) {
menuOptions.push(
[ContextMenuItemIds.BOOKMARK_EDIT, templateData.bookmarkedit],
[ContextMenuItemIds.BOOKMARK_DELETE, templateData.bookmarkdelete]);
}
if (contextMenuUrl.search('chrome://') == -1 &&
contextMenuUrl.search('about://') == -1 &&
document.body.getAttribute('shortcut_item_enabled') == 'true') {
menuOptions.push([
ContextMenuItemIds.BOOKMARK_SHORTCUT,
templateData.bookmarkshortcut
]);
}
} else if (section == SectionType.BOOKMARKS &&
!contextMenuItem.folder &&
isIncognito) {
menuOptions = [
[
ContextMenuItemIds.BOOKMARK_OPEN_IN_INCOGNITO_TAB,
templateData.elementopeninincognitotab
]
];
} else if (section == SectionType.BOOKMARKS &&
contextMenuItem.folder &&
contextMenuItem.editable &&
!isIncognito) {
menuOptions = [
[ContextMenuItemIds.BOOKMARK_EDIT, templateData.editfolder],
[ContextMenuItemIds.BOOKMARK_DELETE, templateData.deletefolder]
];
} else if (section == SectionType.MOST_VISITED) {
menuOptions = [
[
ContextMenuItemIds.MOST_VISITED_OPEN_IN_NEW_TAB,
templateData.elementopeninnewtab
],
];
if (isIncognitoEnabled) {
menuOptions.push([
ContextMenuItemIds.MOST_VISITED_OPEN_IN_INCOGNITO_TAB,
templateData.elementopeninincognitotab
]);
}
menuOptions.push(
[ContextMenuItemIds.MOST_VISITED_REMOVE, templateData.elementremove]);
} else if (section == SectionType.RECENTLY_CLOSED) {
menuOptions = [
[
ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_NEW_TAB,
templateData.elementopeninnewtab
],
];
if (isIncognitoEnabled) {
menuOptions.push([
ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_INCOGNITO_TAB,
templateData.elementopeninincognitotab
]);
}
menuOptions.push(
[ContextMenuItemIds.RECENTLY_CLOSED_REMOVE, templateData.removeall]);
} else if (section == SectionType.FOREIGN_SESSION_HEADER) {
menuOptions = [
[
ContextMenuItemIds.FOREIGN_SESSIONS_REMOVE,
templateData.elementremove
]
];
} else if (section == SectionType.PROMO_VC_SESSION_HEADER) {
menuOptions = [
[
ContextMenuItemIds.PROMO_VC_SESSION_REMOVE,
templateData.elementremove
]
];
}
if (menuOptions)
chrome.send('showContextMenu', menuOptions);
return false;
}
// Return an object with all the exports
return {
bookmarks: bookmarks,
bookmarkChanged: bookmarkChanged,
clearPromotions: clearPromotions,
init: init,
setIncognitoEnabled: setIncognitoEnabled,
onCustomMenuSelected: onCustomMenuSelected,
openSection: openSection,
setFaviconDominantColor: setFaviconDominantColor,
setForeignSessions: setForeignSessions,
setIncognitoMode: setIncognitoMode,
setMostVisitedPages: setMostVisitedPages,
setPromotions: setPromotions,
setRecentlyClosedTabs: setRecentlyClosedTabs,
setSyncEnabled: setSyncEnabled,
snapshots: snapshots
};
});
/////////////////////////////////////////////////////////////////////////////
//Utility Functions.
/////////////////////////////////////////////////////////////////////////////
/**
* A best effort approach for checking simple data object equality.
* @param {?} val1 The first value to check equality for.
* @param {?} val2 The second value to check equality for.
* @return {boolean} Whether the two objects are equal(ish).
*/
function equals(val1, val2) {
if (typeof val1 != 'object' || typeof val2 != 'object')
return val1 === val2;
// Object and array equality checks.
var keyCountVal1 = 0;
for (var key in val1) {
if (!(key in val2) || !equals(val1[key], val2[key]))
return false;
keyCountVal1++;
}
var keyCountVal2 = 0;
for (var key in val2)
keyCountVal2++;
if (keyCountVal1 != keyCountVal2)
return false;
return true;
}
/**
* Alias for document.getElementById.
* @param {string} id The ID of the element to find.
* @return {HTMLElement} The found element or null if not found.
*/
function $(id) {
return document.getElementById(id);
}
/**
* @return {boolean} Whether the device is currently in portrait mode.
*/
function isPortrait() {
return document.documentElement.offsetWidth <
document.documentElement.offsetHeight;
}
/**
* Determine if the page should be formatted for tablets.
* @return {boolean} true if the device is a tablet, false otherwise.
*/
function isTablet() {
return document.body.getAttribute('device') == 'tablet';
}
/**
* Determine if the page should be formatted for phones.
* @return {boolean} true if the device is a phone, false otherwise.
*/
function isPhone() {
return document.body.getAttribute('device') == 'phone';
}
/**
* Get the page X coordinate of a touch event.
* @param {TouchEvent} evt The touch event triggered by the browser.
* @return {number} The page X coordinate of the touch event.
*/
function getTouchEventX(evt) {
return (evt.touches[0] || e.changedTouches[0]).pageX;
}
/**
* Get the page Y coordinate of a touch event.
* @param {TouchEvent} evt The touch event triggered by the browser.
* @return {number} The page Y coordinate of the touch event.
*/
function getTouchEventY(evt) {
return (evt.touches[0] || e.changedTouches[0]).pageY;
}
/**
* @param {Element} el The item to get the width of.
* @param {boolean} excludeMargin If true, exclude the width of the margin.
* @return {number} The total width of a given item.
*/
function getItemWidth(el, excludeMargin) {
var elStyle = window.getComputedStyle(el);
var width = el.offsetWidth;
if (!width || width == 0) {
width = parseInt(elStyle.getPropertyValue('width'));
width +=
parseInt(elStyle.getPropertyValue('border-left-width')) +
parseInt(elStyle.getPropertyValue('border-right-width'));
width +=
parseInt(elStyle.getPropertyValue('padding-left')) +
parseInt(elStyle.getPropertyValue('padding-right'));
}
if (!excludeMargin) {
width += parseInt(elStyle.getPropertyValue('margin-left')) +
parseInt(elStyle.getPropertyValue('margin-right'));
}
return width;
}
/**
* @return {number} The padding height of the body due to the button bar
*/
function getButtonBarPadding() {
var body = document.getElementsByTagName('body')[0];
var style = window.getComputedStyle(body);
return parseInt(style.getPropertyValue('padding-bottom'));
}
/**
* Modify a css rule
* @param {string} selector The selector for the rule (passed to findCssRule())
* @param {string} property The property to update
* @param {string} value The value to update the property to
* @return {boolean} true if the rule was updated, false otherwise.
*/
function modifyCssRule(selector, property, value) {
var rule = findCssRule(selector);
if (!rule)
return false;
rule.style[property] = value;
return true;
}
/**
* Find a particular CSS rule. The stylesheets attached to the document
* are traversed in reverse order. The rules in each stylesheet are also
* traversed in reverse order. The first rule found to match the selector
* is returned.
* @param {string} selector The selector for the rule.
* @return {Object} The rule if one was found, null otherwise
*/
function findCssRule(selector) {
var styleSheets = document.styleSheets;
for (i = styleSheets.length - 1; i >= 0; i--) {
var styleSheet = styleSheets[i];
var rules = styleSheet.cssRules;
if (rules == null)
continue;
for (j = rules.length - 1; j >= 0; j--) {
if (rules[j].selectorText == selector)
return rules[j];
}
}
}
/////////////////////////////////////////////////////////////////////////////
// NTP Entry point.
/////////////////////////////////////////////////////////////////////////////
/*
* Handles initializing the UI when the page has finished loading.
*/
window.addEventListener('DOMContentLoaded', function(evt) {
ntp.init();
$('content-area').style.display = 'block';
});