Index: chrome/browser/resources/ntp_android/ntp_android.js |
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 0000000000000000000000000000000000000000..f5411fe9cd36836a0741512828b46e4d7dfa188f |
--- /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'; |
+}); |