diff options
author | newt@chromium.org <newt@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-08-17 00:31:52 +0000 |
---|---|---|
committer | newt@chromium.org <newt@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-08-17 00:31:52 +0000 |
commit | a4ce90b0fba256076f64712cdde8332060cdfe1c (patch) | |
tree | 5d25a8027189600c9422359518b7ea0683424110 /chrome/browser/resources/ntp_android/ntp_android.js | |
parent | 3a36e4b23176a51cc28b5c3126401e98d361aeeb (diff) | |
download | chromium_src-a4ce90b0fba256076f64712cdde8332060cdfe1c.zip chromium_src-a4ce90b0fba256076f64712cdde8332060cdfe1c.tar.gz chromium_src-a4ce90b0fba256076f64712cdde8332060cdfe1c.tar.bz2 |
Upstream Android NTP resources.
This upstreams the Android new tab page html/js/css resources
wholesale. The NTP will be rewritten using Android UI elements in the
near future, so we'd like to upstream this with minimal changes.
BUG=136951
Review URL: https://chromiumcodereview.appspot.com/10831317
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@152008 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/resources/ntp_android/ntp_android.js')
-rw-r--r-- | chrome/browser/resources/ntp_android/ntp_android.js | 2488 |
1 files changed, 2488 insertions, 0 deletions
diff --git a/chrome/browser/resources/ntp_android/ntp_android.js b/chrome/browser/resources/ntp_android/ntp_android.js new file mode 100644 index 0000000..f5411fe --- /dev/null +++ b/chrome/browser/resources/ntp_android/ntp_android.js @@ -0,0 +1,2488 @@ +// 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 send notification types based on whether NTP has + * loaded sent notification. + * @enum {number} + */ + var SendNotificationType = { + LOAD_NOT_DONE: 0, + LOAD_DONE_NOTIFICATION_NOT_SENT: 1, + LOAD_DONE_NOTIFICATION_SENT: 2 + }; + + /** + * Whether to send notification when page is done loading + * @type {boolean} + */ + var finishedLoadingSendNotification = SendNotificationType.LOAD_NOT_DONE; + + /** + * Time the page load finished notification was last sent out + * @type {boolean} + */ + var timeLastSendNotification = 0; + + /** + * Whether the NTP is in incognito mode or not. + * @type {boolean} + */ + var isIncognito = false; + + /** + * 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; + + function setIncognitoMode(incognito) { + isIncognito = incognito; + } + + /** + * The different sections that are displayed. + * @enum {number} + */ + var SectionType = { + BOOKMARKS: 0, + INCOGNITO: 1, + MOST_VISITED: 2, + RECENTLY_CLOSED: 3, + SYNCED_DEVICES: 4, + FOREIGN_SESSION: 5, + FOREIGN_SESSION_HEADER: 6, + SNAPSHOTS: 7, + UNKNOWN: 100, + }; + + /** + * 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, + }; + + /** + * 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: 3, + }; + + /** + * 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 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; + + /** + * 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. + finishedLoadingSendNotification = + SendNotificationType.LOAD_DONE_NOTIFICATION_NOT_SENT; + 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 (finishedLoadingSendNotification != SendNotificationType.LOAD_NOT_DONE) + return; + for (var i = 0; i < imagesBeingLoaded.length; ++i) { + if (imagesBeingLoaded[i].src == url) + return; + } + 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]); + + chrome.send('getMostVisited'); + chrome.send('getRecentlyClosedTabs'); + chrome.send('getForeignSessions'); + chrome.send('getPromotions'); + + 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); + } + + /** + * Notifies the chrome process of the status of the NTP. + */ + function sendNTPNotification() { + var now = new Date(); + if (finishedLoadingSendNotification == + SendNotificationType.LOAD_DONE_NOTIFICATION_NOT_SENT) { + finishedLoadingSendNotification == + SendNotificationType.LOAD_DONE_NOTIFICATION_SENT; + timeLastSendNotification = now.getTime(); + chrome.send('notifyNTPReady'); + } else if (finishedLoadingSendNotification == + SendNotificationType.LOAD_DONE_NOTIFICATION_SENT || + ((now.getTime() - timeLastSendNotification) > 100)) { + // Navigating after the loading complete notification has been sent + // might break tests. + chrome.send('NTPUnexpectedNavigation'); + } + } + + /** + * Triggers the edit bookmark prompt for a given bookmark. + * + * @param {Object} item Object containing information for the selected + * bookmark node. + */ + function editBookmark(item) { + if (item['editable'] !== true) + return; + var editBookmarkUrl = 'chrome://editbookmark/' + + '?id=' + item.id; + if (item['folder']) + editBookmarkUrl += '&isfolder=true'; + window.location = editBookmarkUrl; + } + + /** + * 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('shortcutToBookmark', [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('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 = MOCK ? + touchIconURI : 'chrome://touch-icon/size/64/' + 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 (MOCK) + backgroundUrl = thumbnailURI; + 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'); + title.insertBefore(spacerImg, title.firstChild); + thumbnailCell.appendChild(title); + + wrapClickHandler(thumbnailContainer, 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 = MOCK ? item.icon : 'chrome://touch-icon/' + 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 (w <= 16 || 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); + + chrome.send('getFaviconDominantColor', + [('chrome://favicon/size/16/' + item.url), '' + faviconIndex]); + faviconIndex++; + } else if ((w == 57 && h == 57) || (w == 114 && h == 114)) { + // it's a touch icon + faviconIcon.classList.add('touch-icon'); + } else { + // it's an html5 icon (or at least it's larger) + 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 iconUrl = MOCK ? item.icon : 'chrome://touch-icon/size/64/' + item.url; + listItem.appendChild(createDiv('icon', iconUrl)); + trackImageLoad(iconUrl); + var title = createElement('span', { + textContent: item.title, + className: '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('span', 'title'); + sessionOuterDiv.appendChild(titleContainer); + + // Extra container to allow title & last-sync time to stack vertically. + var sessionInnerDiv = createDiv(null); + titleContainer.appendChild(sessionInnerDiv); + + var title = createDiv('session-name'); + title.textContent = item.title; + sessionInnerDiv.appendChild(title); + + var lastSynced = createDiv('session-last-synced'); + lastSynced.textContent = + templateData.opentabslastsynced + ': ' + item.userVisibleTimestamp; + 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; + promoSetVisibility(); + } + + /** + * 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; + promoSetVisibility(); + } + + /** + * 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); + } + + var clickFunction = function(item) { + chrome.send('metricsHandler:recordAction', ['MobileNTPMostVisited']); + window.location = item.url; + }; + populateData(findList('most_visited'), SectionType.MOST_VISITED, data, + makeMostVisitedItem, clickFunction); + computeDynamicLayout(); + } + + /** + * 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('metricsHandler:recordAction', ['MobileNTPBookmark']); + window.location = item.url; + } + }; + populateData(findList('bookmarks'), SectionType.BOOKMARKS, + filteredBookmarks, makeBookmarkItem, clickFunction); + } + + var bookmarkContainer = $('bookmarks_container'); + + // update the shadows on the breadcrumb bar + computeDynamicLayout(); + } + + /** + * 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 && + (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 && + (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 shouldPromoBeShownOnSyncPromo() { + var snapshotsCount = + currentSnapshots == null ? 0 : currentSnapshots.length; + var sessionsCount = currentSessions == null ? 0 : currentSessions.length; + return promoIsAllowed && + (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' && shouldPromoBeShownOnSyncPromo()) { + chrome.send('recordImpression', ['sync_promo']); + } + } + + /** + * Sets the visibility on all promo-related items as necessary. + */ + function promoSetVisibility() { + var mostVisited = $('promo_message_on_most_visited'); + var openTabs = $('promo_message_on_open_tabs'); + if (shouldPromoBeShownOnMostVisited()) { + mostVisited.style.display = 'block'; + } else { + mostVisited.style.display = 'none'; + } + if (shouldPromoBeShownOnOpenTabs()) { + openTabs.style.display = 'block'; + } else { + openTabs.style.display = 'none'; + } + } + + /** + * 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 mostVisited = $('promo_message_on_most_visited'); + var openTabs = $('promo_message_on_open_tabs'); + var syncPromoLegacy = $('promo_message_on_sync_promo_legacy'); + mostVisited.innerHTML = promotions['promoMessage']; + openTabs.innerHTML = promotions['promoMessage']; + if (promotions['promoMessageLong']) { + syncPromoLegacy.innerHTML = promotions['promoMessageLong']; + } + promoIsAllowed = promotions['promoIsAllowed'] === true; + if (promoIsAllowed) { + var promoTargets = + document.getElementsByClassName('promo-action-target'); + for (var i = 0, len = promoTargets.length; i < len; i++) { + promoTargets[i].href = 'javascript:void(0)'; + promoTargets[i].onclick = promoAction; + } + } + promoSetVisibility(); + } + + /** + * On-click handler for promo email targets. + * Performs the promo action "send email". + * @param {Object} evt User interface event that triggered the action. + */ + function promoAction(evt) { + if (evt.preventDefault) + evt.preventDefault(); + evt.returnValue = false; + 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) { + 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) + editBookmark(contextMenuItem); + 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('shortcutToBookmark', [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; + + 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) { + title.style.left = '0px'; + } 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); + } + + 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'; + } + promoSetVisibility(); + } + + /** + * 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'; + } + } + } + + /** + * 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; + if (windows[0].deviceType == 'win' || + windows[0].deviceType == 'macosx' || + windows[0].deviceType == 'linux' || + windows[0].deviceType == 'chromeos' || + windows[0].deviceType == 'other') { + iconStyle = 'laptop'; + } else if (windows[0].deviceType == 'phone') { + iconStyle = 'phone'; + } else if (windows[0].deviceType == 'tablet') { + iconStyle = 'tablet'; + } else { + console.error( + 'Unknown sync device type found: ', windows[0].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); + openTabsList.push({ + timestamp: tab.timestamp, + title: tab.title, + url: tab.url, + sessionTag: client.tag, + winNum: winNum, + sessionId: tab.sessionId, + icon: tab.icon, + 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('metricsHandler:recordAction', ['MobileNTPForeignSession']); + 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; + + // Get the fold canvas and create a path for the fold shape + var ctx = document.getCSSCanvasContext( + '2d', 'fold_' + index, 12 * scale, 12 * scale); + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, 9 * scale); + ctx.quadraticCurveTo( + 0, 12 * scale, + 3 * scale, 12 * scale); + ctx.lineTo(12 * scale, 12 * scale); + ctx.closePath(); + + // Create a gradient for the fold and fill it + var gradient = ctx.createLinearGradient(12 * scale, 0, 0, 12 * 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'); + } + + /** + * Gets the SectionType String from the enum SectionType. + */ + function getSectionTypeString(section) { + switch (section) { + case SectionType.BOOKMARKS: + return 'bookmarks'; + case SectionType.MOST_VISITED: + return 'most_visited'; + case SectionType.RECENTLY_CLOSED: + return 'recently_closed'; + case SectionType.SYNCED_DEVICES: + return 'synced_devices'; + case SectionType.UNKNOWN: + default: + return 'unknown'; + } + } + + /** + * 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 = getSectionTypeString(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.body.scrollTop = 0; + + // 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 = 20; + 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; + // set it to the unscaled size so centerGrid works correctly + modifyCssRule('body[device="phone"] .thumbnail-cell', + 'width', thumbWidth + 'px'); + + var screenHeight = + document.documentElement.offsetHeight - + getButtonBarPadding(); + + if (isPortrait()) { + mostVisitedList.setAttribute(GRID_COLUMNS, '2'); + listHeight = screenHeight * .85; + listHeight = listHeight >= 420 ? 420 : listHeight; + // 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 *= scale; + 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; + } + + if (section == SectionType.BOOKMARKS && + !contextMenuItem.folder && !isIncognito) { + var menuOptions = [ + [ContextMenuItemIds.BOOKMARK_OPEN_IN_NEW_TAB, + templateData.elementopeninnewtab], + [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) { + menuOptions.push( + [ContextMenuItemIds.BOOKMARK_SHORTCUT, + templateData.bookmarkshortcut]); + } + chrome.send('showContextMenu', menuOptions); + } else if (section == SectionType.BOOKMARKS && + !contextMenuItem.folder && + isIncognito) { + chrome.send('showContextMenu', [ + [ContextMenuItemIds.BOOKMARK_OPEN_IN_INCOGNITO_TAB, + templateData.elementopeninincognitotab] + ]); + } else if (section == SectionType.BOOKMARKS && + contextMenuItem.folder && + contextMenuItem.editable && + !isIncognito) { + chrome.send('showContextMenu', [ + [ContextMenuItemIds.BOOKMARK_EDIT, templateData.editfolder], + [ContextMenuItemIds.BOOKMARK_DELETE, templateData.deletefolder], + ]); + } else if (section == SectionType.MOST_VISITED) { + chrome.send('showContextMenu', [ + [ContextMenuItemIds.MOST_VISITED_OPEN_IN_NEW_TAB, + templateData.elementopeninnewtab], + [ContextMenuItemIds.MOST_VISITED_OPEN_IN_INCOGNITO_TAB, + templateData.elementopeninincognitotab], + [ContextMenuItemIds.MOST_VISITED_REMOVE, templateData.elementremove] + ]); + } else if (section == SectionType.RECENTLY_CLOSED) { + chrome.send('showContextMenu', [ + [ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_NEW_TAB, + templateData.elementopeninnewtab], + [ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_INCOGNITO_TAB, + templateData.elementopeninincognitotab], + [ContextMenuItemIds.RECENTLY_CLOSED_REMOVE, + templateData.elementremove] + ]); + } else if (section == SectionType.FOREIGN_SESSION_HEADER) { + chrome.send('showContextMenu', [ + [ContextMenuItemIds.FOREIGN_SESSIONS_REMOVE, + templateData.elementremove] + ]); + } + return false; + } + + // Return an object with all the exports + return { + bookmarks: bookmarks, + bookmarkChanged: bookmarkChanged, + setForeignSessions: setForeignSessions, + init: init, + onCustomMenuSelected: onCustomMenuSelected, + openSection: openSection, + setFaviconDominantColor: setFaviconDominantColor, + setIncognitoMode: setIncognitoMode, + setMostVisitedPages: setMostVisitedPages, + setPromotions: setPromotions, + setRecentlyClosedTabs: setRecentlyClosedTabs, + setSyncEnabled: setSyncEnabled, + snapshots: snapshots + }; +}); + +///////////////////////////////////////////////////////////////////////////// +//Utility Functions. +///////////////////////////////////////////////////////////////////////////// + +/** + * 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'; +}); |