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