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