Home | History | Annotate | Download | only in ntp_android
      1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 // File Description:
      6 //     Contains all the necessary functions for rendering the NTP on mobile
      7 //     devices.
      8 
      9 /**
     10  * The event type used to determine when a touch starts.
     11  * @type {string}
     12  */
     13 var PRESS_START_EVT = 'touchstart';
     14 
     15 /**
     16  * The event type used to determine when a touch finishes.
     17  * @type {string}
     18  */
     19 var PRESS_STOP_EVT = 'touchend';
     20 
     21 /**
     22  * The event type used to determine when a touch moves.
     23  * @type {string}
     24  */
     25 var PRESS_MOVE_EVT = 'touchmove';
     26 
     27 cr.define('ntp', function() {
     28   /**
     29    * Constant for the localStorage key used to specify the default bookmark
     30    * folder to be selected when navigating to the bookmark tab for the first
     31    * time of a new NTP instance.
     32    * @type {string}
     33    */
     34   var DEFAULT_BOOKMARK_FOLDER_KEY = 'defaultBookmarkFolder';
     35 
     36   /**
     37    * Constant for the localStorage key used to store whether or not sync was
     38    * enabled on the last call to syncEnabled().
     39    * @type {string}
     40    */
     41   var SYNC_ENABLED_KEY = 'syncEnabled';
     42 
     43   /**
     44    * The time before and item gets marked as active (in milliseconds).  This
     45    * prevents an item from being marked as active when the user is scrolling
     46    * the page.
     47    * @type {number}
     48    */
     49   var ACTIVE_ITEM_DELAY_MS = 100;
     50 
     51   /**
     52    * The CSS class identifier for grid layouts.
     53    * @type {string}
     54    */
     55   var GRID_CSS_CLASS = 'icon-grid';
     56 
     57   /**
     58    * The element to center when centering a GRID_CSS_CLASS.
     59    */
     60   var GRID_CENTER_CSS_CLASS = 'center-icon-grid';
     61 
     62   /**
     63    * Attribute used to specify the number of columns to use in a grid.  If
     64    * left unspecified, the grid will fill the container.
     65    */
     66   var GRID_COLUMNS = 'grid-columns';
     67 
     68   /**
     69    * Attribute used to specify whether the top margin should be set to match
     70    * the left margin of the grid.
     71    */
     72   var GRID_SET_TOP_MARGIN_CLASS = 'grid-set-top-margin';
     73 
     74   /**
     75    * Attribute used to specify whether the margins of individual items within
     76    * the grid should be adjusted to better fill the space.
     77    */
     78   var GRID_SET_ITEM_MARGINS = 'grid-set-item-margins';
     79 
     80   /**
     81    * The CSS class identifier for centered empty section containers.
     82    */
     83   var CENTER_EMPTY_CONTAINER_CSS_CLASS = 'center-empty-container';
     84 
     85   /**
     86    * The CSS class identifier for marking list items as active.
     87    * @type {string}
     88    */
     89   var ACTIVE_LIST_ITEM_CSS_CLASS = 'list-item-active';
     90 
     91   /**
     92    * Attributes set on elements representing data in a section, specifying
     93    * which section that element belongs to. Used for context menus.
     94    * @type {string}
     95    */
     96   var SECTION_KEY = 'sectionType';
     97 
     98   /**
     99    * Attribute set on an element that has a context menu. Specifies the URL for
    100    * which the context menu action should apply.
    101    * @type {string}
    102    */
    103   var CONTEXT_MENU_URL_KEY = 'url';
    104 
    105   /**
    106    * The list of main section panes added.
    107    * @type {Array.<Element>}
    108    */
    109   var panes = [];
    110 
    111   /**
    112    * The list of section prefixes, which are used to append to the hash of the
    113    * page to allow the native toolbar to see url changes when the pane is
    114    * switched.
    115    */
    116   var sectionPrefixes = [];
    117 
    118   /**
    119    * The next available index for new favicons.  Users must increment this
    120    * value once assigning this index to a favicon.
    121    * @type {number}
    122    */
    123   var faviconIndex = 0;
    124 
    125   /**
    126    * The currently selected pane DOM element.
    127    * @type {Element}
    128    */
    129   var currentPane = null;
    130 
    131   /**
    132    * The index of the currently selected top level pane.  The index corresponds
    133    * to the elements defined in {@see #panes}.
    134    * @type {number}
    135    */
    136   var currentPaneIndex;
    137 
    138   /**
    139    * The ID of the bookmark folder currently selected.
    140    * @type {string|number}
    141    */
    142   var bookmarkFolderId = null;
    143 
    144   /**
    145    * The current element active item.
    146    * @type {?Element}
    147    */
    148   var activeItem;
    149 
    150   /**
    151    * The element to be marked as active if no actions cancel it.
    152    * @type {?Element}
    153    */
    154   var pendingActiveItem;
    155 
    156   /**
    157    * The timer ID to mark an element as active.
    158    * @type {number}
    159    */
    160   var activeItemDelayTimerId;
    161 
    162   /**
    163    * Enum for the different load states based on the initialization of the NTP.
    164    * @enum {number}
    165    */
    166   var LoadStatusType = {
    167     LOAD_NOT_DONE: 0,
    168     LOAD_IMAGES_COMPLETE: 1,
    169     LOAD_BOOKMARKS_FINISHED: 2,
    170     LOAD_COMPLETE: 3  // An OR'd combination of all necessary states.
    171   };
    172 
    173   /**
    174    * The current loading status for the NTP.
    175    * @type {LoadStatusType}
    176    */
    177   var loadStatus_ = LoadStatusType.LOAD_NOT_DONE;
    178 
    179   /**
    180    * Whether the loading complete notification has been sent.
    181    * @type {boolean}
    182    */
    183   var finishedLoadingNotificationSent_ = false;
    184 
    185   /**
    186    * Whether the page title has been loaded.
    187    * @type {boolean}
    188    */
    189   var titleLoadedStatus_ = false;
    190 
    191   /**
    192    * Whether the NTP is in incognito mode or not.
    193    * @type {boolean}
    194    */
    195   var isIncognito = false;
    196 
    197   /**
    198    * Whether incognito mode is enabled. (It can be blocked e.g. with a policy.)
    199    * @type {boolean}
    200    */
    201   var isIncognitoEnabled = true;
    202 
    203   /**
    204    * Whether the initial history state has been replaced.  The state will be
    205    * replaced once the bookmark data has loaded to ensure the proper folder
    206    * id is persisted.
    207    * @type {boolean}
    208    */
    209   var replacedInitialState = false;
    210 
    211   /**
    212    * Stores number of most visited pages.
    213    * @type {number}
    214    */
    215   var numberOfMostVisitedPages = 0;
    216 
    217   /**
    218    * Whether there are any recently closed tabs.
    219    * @type {boolean}
    220    */
    221   var hasRecentlyClosedTabs = false;
    222 
    223   /**
    224    * Whether promo is not allowed or not (external to NTP).
    225    * @type {boolean}
    226    */
    227   var promoIsAllowed = false;
    228 
    229   /**
    230    * Whether promo should be shown on Most Visited page (externally set).
    231    * @type {boolean}
    232    */
    233   var promoIsAllowedOnMostVisited = false;
    234 
    235   /**
    236    * Whether promo should be shown on Open Tabs page (externally set).
    237    * @type {boolean}
    238    */
    239   var promoIsAllowedOnOpenTabs = false;
    240 
    241   /**
    242    * Whether promo should show a virtual computer on Open Tabs (externally set).
    243    * @type {boolean}
    244    */
    245   var promoIsAllowedAsVirtualComputer = false;
    246 
    247   /**
    248    * Promo-injected title of a virtual computer on an open tabs pane.
    249    * @type {string}
    250    */
    251   var promoInjectedComputerTitleText = '';
    252 
    253   /**
    254    * Promo-injected last synced text of a virtual computer on an open tabs pane.
    255    * @type {string}
    256    */
    257   var promoInjectedComputerLastSyncedText = '';
    258 
    259   /**
    260    * The different sections that are displayed.
    261    * @enum {number}
    262    */
    263   var SectionType = {
    264     BOOKMARKS: 'bookmarks',
    265     FOREIGN_SESSION: 'foreign_session',
    266     FOREIGN_SESSION_HEADER: 'foreign_session_header',
    267     MOST_VISITED: 'most_visited',
    268     PROMO_VC_SESSION_HEADER: 'promo_vc_session_header',
    269     RECENTLY_CLOSED: 'recently_closed',
    270     SNAPSHOTS: 'snapshots',
    271     UNKNOWN: 'unknown',
    272   };
    273 
    274   /**
    275    * The different ids used of our custom context menu. Sent to the ChromeView
    276    * and sent back when a menu is selected.
    277    * @enum {number}
    278    */
    279   var ContextMenuItemIds = {
    280     BOOKMARK_EDIT: 0,
    281     BOOKMARK_DELETE: 1,
    282     BOOKMARK_OPEN_IN_NEW_TAB: 2,
    283     BOOKMARK_OPEN_IN_INCOGNITO_TAB: 3,
    284     BOOKMARK_SHORTCUT: 4,
    285 
    286     MOST_VISITED_OPEN_IN_NEW_TAB: 10,
    287     MOST_VISITED_OPEN_IN_INCOGNITO_TAB: 11,
    288     MOST_VISITED_REMOVE: 12,
    289 
    290     RECENTLY_CLOSED_OPEN_IN_NEW_TAB: 20,
    291     RECENTLY_CLOSED_OPEN_IN_INCOGNITO_TAB: 21,
    292     RECENTLY_CLOSED_REMOVE: 22,
    293 
    294     FOREIGN_SESSIONS_REMOVE: 30,
    295 
    296     PROMO_VC_SESSION_REMOVE: 40,
    297   };
    298 
    299   /**
    300    * The URL of the element for the context menu.
    301    * @type {string}
    302    */
    303   var contextMenuUrl = null;
    304 
    305   var contextMenuItem = null;
    306 
    307   var currentSnapshots = null;
    308 
    309   var currentSessions = null;
    310 
    311   /**
    312    * The possible states of the sync section
    313    * @enum {number}
    314    */
    315   var SyncState = {
    316     INITIAL: 0,
    317     WAITING_FOR_DATA: 1,
    318     DISPLAYING_LOADING: 2,
    319     DISPLAYED_LOADING: 3,
    320     LOADED: 4,
    321   };
    322 
    323   /**
    324    * The current state of the sync section.
    325    */
    326   var syncState = SyncState.INITIAL;
    327 
    328   /**
    329    * Whether or not sync is enabled. It will be undefined until
    330    * setSyncEnabled() is called.
    331    * @type {?boolean}
    332    */
    333   var syncEnabled = undefined;
    334 
    335   /**
    336    * The current most visited data being displayed.
    337    * @type {Array.<Object>}
    338    */
    339   var mostVisitedData_ = [];
    340 
    341   /**
    342    * The current bookmark data being displayed. Keep a reference to this data
    343    * in case the sync enabled state changes.  In this case, the bookmark data
    344    * will need to be refiltered.
    345    * @type {?Object}
    346    */
    347   var bookmarkData;
    348 
    349   /**
    350    * Keep track of any outstanding timers related to updating the sync section.
    351    */
    352   var syncTimerId = -1;
    353 
    354   /**
    355    * The minimum amount of time that 'Loading...' can be displayed. This is to
    356    * prevent flashing.
    357    */
    358   var SYNC_LOADING_TIMEOUT = 1000;
    359 
    360   /**
    361    * How long to wait for sync data to load before displaying the 'Loading...'
    362    * text to the user.
    363    */
    364   var SYNC_INITIAL_LOAD_TIMEOUT = 1000;
    365 
    366   /**
    367    * An array of images that are currently in loading state. Once an image
    368    * loads it is removed from this array.
    369    */
    370   var imagesBeingLoaded = new Array();
    371 
    372   /**
    373    * Flag indicating if we are on bookmark shortcut mode.
    374    * In this mode, only the bookmark section is available and selecting
    375    * a non-folder bookmark adds it to the home screen.
    376    * Context menu is disabled.
    377    */
    378   var bookmarkShortcutMode = false;
    379 
    380   function setIncognitoMode(incognito) {
    381     isIncognito = incognito;
    382     if (!isIncognito) {
    383       chrome.send('getMostVisited');
    384       chrome.send('getRecentlyClosedTabs');
    385       chrome.send('getForeignSessions');
    386       chrome.send('getPromotions');
    387       chrome.send('getIncognitoDisabled');
    388     }
    389   }
    390 
    391   function setIncognitoEnabled(item) {
    392     isIncognitoEnabled = item.incognitoEnabled;
    393   }
    394 
    395   /**
    396    * Flag set to true when the page is loading its initial set of images. This
    397    * is set to false after all the initial images have loaded.
    398    */
    399   function onInitialImageLoaded(event) {
    400     var url = event.target.src;
    401     for (var i = 0; i < imagesBeingLoaded.length; ++i) {
    402       if (imagesBeingLoaded[i].src == url) {
    403         imagesBeingLoaded.splice(i, 1);
    404         if (imagesBeingLoaded.length == 0) {
    405           // To send out the NTP loading complete notification.
    406           loadStatus_ |= LoadStatusType.LOAD_IMAGES_COMPLETE;
    407           sendNTPNotification();
    408         }
    409       }
    410     }
    411   }
    412 
    413   /**
    414    * Marks the given image as currently being loaded. Once all such images load
    415    * we inform the browser via a hash change.
    416    */
    417   function trackImageLoad(url) {
    418     if (finishedLoadingNotificationSent_)
    419       return;
    420 
    421     for (var i = 0; i < imagesBeingLoaded.length; ++i) {
    422       if (imagesBeingLoaded[i].src == url)
    423         return;
    424     }
    425 
    426     loadStatus_ &= (~LoadStatusType.LOAD_IMAGES_COMPLETE);
    427 
    428     var image = new Image();
    429     image.onload = onInitialImageLoaded;
    430     image.onerror = onInitialImageLoaded;
    431     image.src = url;
    432     imagesBeingLoaded.push(image);
    433   }
    434 
    435   /**
    436    * Initializes all the UI once the page has loaded.
    437    */
    438   function init() {
    439     // Special case to handle NTP caching.
    440     if (window.location.hash == '#cached_ntp')
    441       document.location.hash = '#most_visited';
    442     // Special case to show a specific bookmarks folder.
    443     // Used to show the mobile bookmarks folder after importing.
    444     var bookmarkIdMatch = window.location.hash.match(/#bookmarks:(\d+)/);
    445     if (bookmarkIdMatch && bookmarkIdMatch.length == 2) {
    446       localStorage.setItem(DEFAULT_BOOKMARK_FOLDER_KEY, bookmarkIdMatch[1]);
    447       document.location.hash = '#bookmarks';
    448     }
    449     // Special case to choose a bookmark for adding a shortcut.
    450     // See the doc of bookmarkShortcutMode for details.
    451     if (window.location.hash == '#bookmark_shortcut')
    452       bookmarkShortcutMode = true;
    453     // Make sure a valid section is always displayed.  Both normal and
    454     // incognito NTPs have a bookmarks section.
    455     if (getPaneIndexFromHash() < 0)
    456       document.location.hash = '#bookmarks';
    457 
    458     // Initialize common widgets.
    459     var titleScrollers =
    460         document.getElementsByClassName('section-title-wrapper');
    461     for (var i = 0, len = titleScrollers.length; i < len; i++)
    462       initializeTitleScroller(titleScrollers[i]);
    463 
    464     // Initialize virtual computers for the sync promo.
    465     createPromoVirtualComputers();
    466 
    467     setCurrentBookmarkFolderData(
    468         localStorage.getItem(DEFAULT_BOOKMARK_FOLDER_KEY));
    469 
    470     addMainSection('incognito');
    471     addMainSection('most_visited');
    472     addMainSection('bookmarks');
    473     addMainSection('open_tabs');
    474 
    475     computeDynamicLayout();
    476 
    477     scrollToPane(getPaneIndexFromHash());
    478     updateSyncEmptyState();
    479 
    480     window.onpopstate = onPopStateHandler;
    481     window.addEventListener('hashchange', updatePaneOnHash);
    482     window.addEventListener('resize', windowResizeHandler);
    483 
    484     if (!bookmarkShortcutMode)
    485       window.addEventListener('contextmenu', contextMenuHandler);
    486   }
    487 
    488   function sendNTPTitleLoadedNotification() {
    489     if (!titleLoadedStatus_) {
    490       titleLoadedStatus_ = true;
    491       chrome.send('notifyNTPTitleLoaded');
    492     }
    493   }
    494 
    495   /**
    496    * Notifies the chrome process of the status of the NTP.
    497    */
    498   function sendNTPNotification() {
    499     if (loadStatus_ != LoadStatusType.LOAD_COMPLETE)
    500       return;
    501 
    502     if (!finishedLoadingNotificationSent_) {
    503       finishedLoadingNotificationSent_ = true;
    504       chrome.send('notifyNTPReady');
    505     } else {
    506       // Navigating after the loading complete notification has been sent
    507       // might break tests.
    508       chrome.send('NTPUnexpectedNavigation');
    509     }
    510   }
    511 
    512   /**
    513    * The default click handler for created item shortcuts.
    514    *
    515    * @param {Object} item The item specification.
    516    * @param {function} evt The browser click event triggered.
    517    */
    518   function itemShortcutClickHandler(item, evt) {
    519     // Handle the touch callback
    520     if (item['folder']) {
    521       browseToBookmarkFolder(item.id);
    522     } else {
    523       if (bookmarkShortcutMode) {
    524         chrome.send('createHomeScreenBookmarkShortcut', [item.id]);
    525       } else if (!!item.url) {
    526         window.location = item.url;
    527       }
    528     }
    529   }
    530 
    531   /**
    532    * Opens a recently closed tab.
    533    *
    534    * @param {Object} item An object containing the necessary information to
    535    *     reopen a tab.
    536    */
    537   function openRecentlyClosedTab(item, evt) {
    538     chrome.send('openedRecentlyClosed');
    539     chrome.send('reopenTab', [item.sessionId]);
    540   }
    541 
    542   /**
    543    * Creates a 'div' DOM element.
    544    *
    545    * @param {string} className The CSS class name for the DIV.
    546    * @param {string=} opt_backgroundUrl The background URL to be applied to the
    547    *     DIV if required.
    548    * @return {Element} The newly created DIV element.
    549    */
    550   function createDiv(className, opt_backgroundUrl) {
    551     var div = document.createElement('div');
    552     div.className = className;
    553     if (opt_backgroundUrl)
    554       div.style.backgroundImage = 'url(' + opt_backgroundUrl + ')';
    555     return div;
    556   }
    557 
    558   /**
    559    * Helper for creating new DOM elements.
    560    *
    561    * @param {string} type The type of Element to be created (i.e. 'div',
    562    *     'span').
    563    * @param {Object} params A mapping of element attribute key and values that
    564    *     should be applied to the new element.
    565    * @return {Element} The newly created DOM element.
    566    */
    567   function createElement(type, params) {
    568     var el = document.createElement(type);
    569     if (typeof params === 'string') {
    570       el.className = params;
    571     } else {
    572       for (attr in params) {
    573         el[attr] = params[attr];
    574       }
    575     }
    576     return el;
    577   }
    578 
    579   /**
    580    * Adds a click listener to a specified element with the ability to override
    581    * the default value of itemShortcutClickHandler.
    582    *
    583    * @param {Element} el The element the click listener should be added to.
    584    * @param {Object} item The item data represented by the element.
    585    * @param {function(Object, string, BrowserEvent)=} opt_clickCallback The
    586    *     click callback to be triggered upon selection.
    587    */
    588   function wrapClickHandler(el, item, opt_clickCallback) {
    589     el.addEventListener('click', function(evt) {
    590       var clickCallback =
    591           opt_clickCallback ? opt_clickCallback : itemShortcutClickHandler;
    592       clickCallback(item, evt);
    593     });
    594   }
    595 
    596   /**
    597    * Create a DOM element to contain a recently closed item for a tablet
    598    * device.
    599    *
    600    * @param {Object} item The data of the item used to generate the shortcut.
    601    * @param {function(Object, string, BrowserEvent)=} opt_clickCallback The
    602    *     click callback to be triggered upon selection (if not provided it will
    603    *     use the default -- itemShortcutClickHandler).
    604    * @return {Element} The shortcut element created.
    605    */
    606   function makeRecentlyClosedTabletItem(item, opt_clickCallback) {
    607     var cell = createDiv('cell');
    608 
    609     cell.setAttribute(CONTEXT_MENU_URL_KEY, item.url);
    610 
    611     var iconUrl = item.icon;
    612     if (!iconUrl) {
    613       iconUrl = 'chrome://touch-icon/size/16@' + window.devicePixelRatio +
    614           'x/' + item.url;
    615     }
    616     var icon = createDiv('icon', iconUrl);
    617     trackImageLoad(iconUrl);
    618     cell.appendChild(icon);
    619 
    620     var title = createDiv('title');
    621     title.textContent = item.title;
    622     cell.appendChild(title);
    623 
    624     wrapClickHandler(cell, item, opt_clickCallback);
    625 
    626     return cell;
    627   }
    628 
    629   /**
    630    * Creates a shortcut DOM element based on the item specified item
    631    * configuration using the thumbnail layout used for most visited.  Other
    632    * data types should not use this as they won't have a thumbnail.
    633    *
    634    * @param {Object} item The data of the item used to generate the shortcut.
    635    * @param {function(Object, string, BrowserEvent)=} opt_clickCallback The
    636    *     click callback to be triggered upon selection (if not provided it will
    637    *     use the default -- itemShortcutClickHandler).
    638    * @return {Element} The shortcut element created.
    639    */
    640   function makeMostVisitedItem(item, opt_clickCallback) {
    641     // thumbnail-cell          -- main outer container
    642     //   thumbnail-container   -- container for the thumbnail
    643     //     thumbnail           -- the actual thumbnail image; outer border
    644     //     inner-border        -- inner border
    645     //   title                 -- container for the title
    646     //     img                 -- hack align title text baseline with bottom
    647     //     title text          -- the actual text of the title
    648     var thumbnailCell = createDiv('thumbnail-cell');
    649     var thumbnailContainer = createDiv('thumbnail-container');
    650     var backgroundUrl = item.thumbnailUrl || 'chrome://thumb/' + item.url;
    651     if (backgroundUrl == 'chrome://thumb/chrome://welcome/') {
    652       // Ideally, it would be nice to use the URL as is.  However, as of now
    653       // theme support has been removed from Chrome.  Instead, load the image
    654       // URL from a style and use it.  Don't just use the style because
    655       // trackImageLoad(...) must be called with the background URL.
    656       var welcomeStyle = findCssRule('.welcome-to-chrome').style;
    657       var backgroundImage = welcomeStyle.backgroundImage;
    658       // trim the "url(" prefix and ")" suffix
    659       backgroundUrl = backgroundImage.substring(4, backgroundImage.length - 1);
    660     }
    661     trackImageLoad(backgroundUrl);
    662     var thumbnail = createDiv('thumbnail');
    663     // Use an Image object to ensure the thumbnail image actually exists.  If
    664     // not, this will allow the default to show instead.
    665     var thumbnailImg = new Image();
    666     thumbnailImg.onload = function() {
    667       thumbnail.style.backgroundImage = 'url(' + backgroundUrl + ')';
    668     };
    669     thumbnailImg.src = backgroundUrl;
    670 
    671     thumbnailContainer.appendChild(thumbnail);
    672     var innerBorder = createDiv('inner-border');
    673     thumbnailContainer.appendChild(innerBorder);
    674     thumbnailCell.appendChild(thumbnailContainer);
    675     var title = createDiv('title');
    676     title.textContent = item.title;
    677     var spacerImg = createElement('img', 'title-spacer');
    678     spacerImg.alt = '';
    679     title.insertBefore(spacerImg, title.firstChild);
    680     thumbnailCell.appendChild(title);
    681 
    682     var shade = createDiv('thumbnail-cell-shade');
    683     thumbnailContainer.appendChild(shade);
    684     addActiveTouchListener(shade, 'thumbnail-cell-shade-active');
    685 
    686     wrapClickHandler(thumbnailCell, item, opt_clickCallback);
    687 
    688     thumbnailCell.setAttribute(CONTEXT_MENU_URL_KEY, item.url);
    689     thumbnailCell.contextMenuItem = item;
    690     return thumbnailCell;
    691   }
    692 
    693   /**
    694    * Creates a shortcut DOM element based on the item specified item
    695    * configuration using the favicon layout used for bookmarks.
    696    *
    697    * @param {Object} item The data of the item used to generate the shortcut.
    698    * @param {function(Object, string, BrowserEvent)=} opt_clickCallback The
    699    *     click callback to be triggered upon selection (if not provided it will
    700    *     use the default -- itemShortcutClickHandler).
    701    * @return {Element} The shortcut element created.
    702    */
    703   function makeBookmarkItem(item, opt_clickCallback) {
    704     var holder = createDiv('favicon-cell');
    705     addActiveTouchListener(holder, 'favicon-cell-active');
    706 
    707     holder.setAttribute(CONTEXT_MENU_URL_KEY, item.url);
    708     holder.contextMenuItem = item;
    709     var faviconBox = createDiv('favicon-box');
    710     if (item.folder) {
    711       faviconBox.classList.add('folder');
    712     } else {
    713       var iconUrl = item.icon || 'chrome://touch-icon/largest/' + item.url;
    714       var faviconIcon = createDiv('favicon-icon');
    715       faviconIcon.style.backgroundImage = 'url(' + iconUrl + ')';
    716       trackImageLoad(iconUrl);
    717 
    718       var image = new Image();
    719       image.src = iconUrl;
    720       image.onload = function() {
    721         var w = image.width;
    722         var h = image.height;
    723         if (Math.floor(w) <= 16 || Math.floor(h) <= 16) {
    724           // it's a standard favicon (or at least it's small).
    725           faviconBox.classList.add('document');
    726 
    727           faviconBox.appendChild(
    728               createDiv('color-strip colorstrip-' + faviconIndex));
    729           faviconBox.appendChild(createDiv('bookmark-border'));
    730           var foldDiv = createDiv('fold');
    731           foldDiv.id = 'fold_' + faviconIndex;
    732           foldDiv.style['background'] =
    733               '-webkit-canvas(fold_' + faviconIndex + ')';
    734 
    735           // Use a container so that the fold it self can be zoomed without
    736           // changing the positioning of the fold.
    737           var foldContainer = createDiv('fold-container');
    738           foldContainer.appendChild(foldDiv);
    739           faviconBox.appendChild(foldContainer);
    740 
    741           // FaviconWebUIHandler::HandleGetFaviconDominantColor expects
    742           // an URL that starts with chrome://favicon/size/.
    743           // The handler always loads 16x16 1x favicon and assumes that
    744           // the dominant color for all scale factors is the same.
    745           chrome.send('getFaviconDominantColor',
    746               [('chrome://favicon/size/16@1x/' + item.url), '' + faviconIndex]);
    747           faviconIndex++;
    748         } else if ((w == 57 && h == 57) || (w == 114 && h == 114)) {
    749           // it's a touch icon for 1x or 2x.
    750           faviconIcon.classList.add('touch-icon');
    751         } else {
    752           // It's an html5 icon (or at least it's larger).
    753           // Rescale it to be no bigger than 64x64 dip.
    754           var max = 64;
    755           if (w > max || h > max) {
    756             var scale = (w > h) ? (max / w) : (max / h);
    757             w *= scale;
    758             h *= scale;
    759           }
    760           faviconIcon.style.backgroundSize = w + 'px ' + h + 'px';
    761         }
    762       };
    763       faviconBox.appendChild(faviconIcon);
    764     }
    765     holder.appendChild(faviconBox);
    766 
    767     var title = createDiv('title');
    768     title.textContent = item.title;
    769     holder.appendChild(title);
    770 
    771     wrapClickHandler(holder, item, opt_clickCallback);
    772 
    773     return holder;
    774   }
    775 
    776   /**
    777    * Adds touch listeners to the specified element to apply a class when it is
    778    * selected (removing the class when no longer pressed).
    779    *
    780    * @param {Element} el The element to apply the class to when touched.
    781    * @param {string} activeClass The CSS class name to be applied when active.
    782    */
    783   function addActiveTouchListener(el, activeClass) {
    784     if (!window.touchCancelListener) {
    785       window.touchCancelListener = function(evt) {
    786         if (activeItemDelayTimerId) {
    787           clearTimeout(activeItemDelayTimerId);
    788           activeItemDelayTimerId = undefined;
    789         }
    790         if (!activeItem) {
    791           return;
    792         }
    793         activeItem.classList.remove(activeItem.dataset.activeClass);
    794         activeItem = null;
    795       };
    796       document.addEventListener('touchcancel', window.touchCancelListener);
    797     }
    798     el.dataset.activeClass = activeClass;
    799     el.addEventListener(PRESS_START_EVT, function(evt) {
    800       if (activeItemDelayTimerId) {
    801         clearTimeout(activeItemDelayTimerId);
    802         activeItemDelayTimerId = undefined;
    803       }
    804       activeItemDelayTimerId = setTimeout(function() {
    805         el.classList.add(activeClass);
    806         activeItem = el;
    807       }, ACTIVE_ITEM_DELAY_MS);
    808     });
    809     el.addEventListener(PRESS_STOP_EVT, function(evt) {
    810       if (activeItemDelayTimerId) {
    811         clearTimeout(activeItemDelayTimerId);
    812         activeItemDelayTimerId = undefined;
    813       }
    814       // Add the active class to ensure the pressed state is visible when
    815       // quickly tapping, which can happen if the start and stop events are
    816       // received before the active item delay timer has been executed.
    817       el.classList.add(activeClass);
    818       el.classList.add('no-active-delay');
    819       setTimeout(function() {
    820         el.classList.remove(activeClass);
    821         el.classList.remove('no-active-delay');
    822       }, 0);
    823       activeItem = null;
    824     });
    825   }
    826 
    827   /**
    828    * Creates a shortcut DOM element based on the item specified in the list
    829    * format.
    830    *
    831    * @param {Object} item The data of the item used to generate the shortcut.
    832    * @param {function(Object, string, BrowserEvent)=} opt_clickCallback The
    833    *     click callback to be triggered upon selection (if not provided it will
    834    *     use the default -- itemShortcutClickHandler).
    835    * @return {Element} The shortcut element created.
    836    */
    837   function makeListEntryItem(item, opt_clickCallback) {
    838     var listItem = createDiv('list-item');
    839     addActiveTouchListener(listItem, ACTIVE_LIST_ITEM_CSS_CLASS);
    840     listItem.setAttribute(CONTEXT_MENU_URL_KEY, item.url);
    841     var iconSize = item.iconSize || 64;
    842     var iconUrl = item.icon ||
    843         'chrome://touch-icon/size/' + iconSize + '@1x/' + item.url;
    844     listItem.appendChild(createDiv('icon', iconUrl));
    845     trackImageLoad(iconUrl);
    846     var title = createElement('div', {
    847       textContent: item.title,
    848       className: 'title session_title'
    849     });
    850     listItem.appendChild(title);
    851 
    852     listItem.addEventListener('click', function(evt) {
    853       var clickCallback =
    854           opt_clickCallback ? opt_clickCallback : itemShortcutClickHandler;
    855       clickCallback(item, evt);
    856     });
    857     if (item.divider == 'section') {
    858       // Add a child div because the section divider has a gradient and
    859       // webkit doesn't seem to currently support borders with gradients.
    860       listItem.appendChild(createDiv('section-divider'));
    861     } else {
    862       listItem.classList.add('standard-divider');
    863     }
    864     return listItem;
    865   }
    866 
    867   /**
    868    * Creates a DOM list entry for a remote session or tab.
    869    *
    870    * @param {Object} item The data of the item used to generate the shortcut.
    871    * @param {function(Object, string, BrowserEvent)=} opt_clickCallback The
    872    *     click callback to be triggered upon selection (if not provided it will
    873    *     use the default -- itemShortcutClickHandler).
    874    * @return {Element} The shortcut element created.
    875    */
    876   function makeForeignSessionListEntry(item, opt_clickCallback) {
    877     // Session item
    878     var sessionOuterDiv = createDiv('list-item standard-divider');
    879     addActiveTouchListener(sessionOuterDiv, ACTIVE_LIST_ITEM_CSS_CLASS);
    880     sessionOuterDiv.contextMenuItem = item;
    881 
    882     var icon = createDiv('session-icon ' + item.iconStyle);
    883     sessionOuterDiv.appendChild(icon);
    884 
    885     var titleContainer = createElement('div', 'title');
    886     sessionOuterDiv.appendChild(titleContainer);
    887 
    888     // Extra container to allow title & last-sync time to stack vertically.
    889     var sessionInnerDiv = createDiv('session_container');
    890     titleContainer.appendChild(sessionInnerDiv);
    891 
    892     var title = createDiv('session-name');
    893     title.textContent = item.title;
    894     title.id = item.titleId || '';
    895     sessionInnerDiv.appendChild(title);
    896 
    897     var lastSynced = createDiv('session-last-synced');
    898     lastSynced.textContent =
    899         templateData.opentabslastsynced + ': ' + item.userVisibleTimestamp;
    900     lastSynced.id = item.userVisibleTimestampId || '';
    901     sessionInnerDiv.appendChild(lastSynced);
    902 
    903     sessionOuterDiv.addEventListener('click', function(evt) {
    904       var clickCallback =
    905           opt_clickCallback ? opt_clickCallback : itemShortcutClickHandler;
    906       clickCallback(item, evt);
    907     });
    908     return sessionOuterDiv;
    909   }
    910 
    911   /**
    912    * Saves the number of most visited pages and updates promo visibility.
    913    * @param {number} n Number of most visited pages.
    914    */
    915   function setNumberOfMostVisitedPages(n) {
    916     numberOfMostVisitedPages = n;
    917     updatePromoVisibility();
    918   }
    919 
    920   /**
    921    * Saves the recently closed tabs flag and updates promo visibility.
    922    * @param {boolean} anyTabs Whether there are any recently closed tabs.
    923    */
    924   function setHasRecentlyClosedTabs(anyTabs) {
    925     hasRecentlyClosedTabs = anyTabs;
    926     updatePromoVisibility();
    927   }
    928 
    929   /**
    930    * Updates the most visited pages.
    931    *
    932    * @param {Array.<Object>} List of data for displaying the list of most
    933    *     visited pages (see C++ handler for model description).
    934    * @param {boolean} hasBlacklistedUrls Whether any blacklisted URLs are
    935    *     present.
    936    */
    937   function setMostVisitedPages(data, hasBlacklistedUrls) {
    938     setNumberOfMostVisitedPages(data.length);
    939     // limit the number of most visited items to display
    940     if (isPhone() && data.length > 6) {
    941       data.splice(6, data.length - 6);
    942     } else if (isTablet() && data.length > 8) {
    943       data.splice(8, data.length - 8);
    944     }
    945 
    946     data.forEach(function(item, index) {
    947       item.mostVisitedIndex = index;
    948     });
    949 
    950     if (equals(data, mostVisitedData_))
    951       return;
    952 
    953     var clickFunction = function(item) {
    954       chrome.send('openedMostVisited');
    955       chrome.send('metricsHandler:recordInHistogram',
    956           ['NewTabPage.MostVisited', item.mostVisitedIndex, 8]);
    957       window.location = item.url;
    958     };
    959     populateData(findList('most_visited'), SectionType.MOST_VISITED, data,
    960         makeMostVisitedItem, clickFunction);
    961     computeDynamicLayout();
    962 
    963     mostVisitedData_ = data;
    964   }
    965 
    966   /**
    967    * Updates the recently closed tabs.
    968    *
    969    * @param {Array.<Object>} List of data for displaying the list of recently
    970    *     closed tabs (see C++ handler for model description).
    971    */
    972   function setRecentlyClosedTabs(data) {
    973     var container = $('recently_closed_container');
    974     if (!data || data.length == 0) {
    975       // hide the recently closed section if it is empty.
    976       container.style.display = 'none';
    977       setHasRecentlyClosedTabs(false);
    978     } else {
    979       container.style.display = 'block';
    980       setHasRecentlyClosedTabs(true);
    981       var decoratorFunc = isPhone() ? makeListEntryItem :
    982           makeRecentlyClosedTabletItem;
    983       populateData(findList('recently_closed'), SectionType.RECENTLY_CLOSED,
    984           data, decoratorFunc, openRecentlyClosedTab);
    985     }
    986     computeDynamicLayout();
    987   }
    988 
    989   /**
    990    * Updates the bookmarks.
    991    *
    992    * @param {Array.<Object>} List of data for displaying the bookmarks (see
    993    *     C++ handler for model description).
    994    */
    995   function bookmarks(data) {
    996     bookmarkFolderId = data.id;
    997     if (!replacedInitialState) {
    998       history.replaceState(
    999           {folderId: bookmarkFolderId, selectedPaneIndex: currentPaneIndex},
   1000           null, null);
   1001       replacedInitialState = true;
   1002     }
   1003     if (syncEnabled == undefined) {
   1004       // Wait till we know whether or not sync is enabled before displaying any
   1005       // bookmarks (since they may need to be filtered below)
   1006       bookmarkData = data;
   1007       return;
   1008     }
   1009 
   1010     var titleWrapper = $('bookmarks_title_wrapper');
   1011     setBookmarkTitleHierarchy(
   1012         titleWrapper, data, data['hierarchy']);
   1013 
   1014     var filteredBookmarks = data.bookmarks;
   1015     if (!syncEnabled) {
   1016       filteredBookmarks = filteredBookmarks.filter(function(val) {
   1017         return (val.type != 'BOOKMARK_BAR' && val.type != 'OTHER_NODE');
   1018       });
   1019     }
   1020     if (bookmarkShortcutMode) {
   1021       populateData(findList('bookmarks'), SectionType.BOOKMARKS,
   1022           filteredBookmarks, makeBookmarkItem);
   1023     } else {
   1024       var clickFunction = function(item) {
   1025         if (item['folder']) {
   1026           browseToBookmarkFolder(item.id);
   1027         } else if (!!item.url) {
   1028           chrome.send('openedBookmark');
   1029           window.location = item.url;
   1030         }
   1031       };
   1032       populateData(findList('bookmarks'), SectionType.BOOKMARKS,
   1033           filteredBookmarks, makeBookmarkItem, clickFunction);
   1034     }
   1035 
   1036     var bookmarkContainer = $('bookmarks_container');
   1037 
   1038     // update the shadows on the  breadcrumb bar
   1039     computeDynamicLayout();
   1040 
   1041     if ((loadStatus_ & LoadStatusType.LOAD_BOOKMARKS_FINISHED) !=
   1042         LoadStatusType.LOAD_BOOKMARKS_FINISHED) {
   1043       loadStatus_ |= LoadStatusType.LOAD_BOOKMARKS_FINISHED;
   1044       sendNTPNotification();
   1045     }
   1046   }
   1047 
   1048   /**
   1049    * Checks if promo is allowed and MostVisited requirements are satisfied.
   1050    * @return {boolean} Whether the promo should be shown on most_visited.
   1051    */
   1052   function shouldPromoBeShownOnMostVisited() {
   1053     return promoIsAllowed && promoIsAllowedOnMostVisited &&
   1054         numberOfMostVisitedPages >= 2 && !hasRecentlyClosedTabs;
   1055   }
   1056 
   1057   /**
   1058    * Checks if promo is allowed and OpenTabs requirements are satisfied.
   1059    * @return {boolean} Whether the promo should be shown on open_tabs.
   1060    */
   1061   function shouldPromoBeShownOnOpenTabs() {
   1062     var snapshotsCount =
   1063         currentSnapshots == null ? 0 : currentSnapshots.length;
   1064     var sessionsCount = currentSessions == null ? 0 : currentSessions.length;
   1065     return promoIsAllowed && promoIsAllowedOnOpenTabs &&
   1066         (snapshotsCount + sessionsCount != 0);
   1067   }
   1068 
   1069   /**
   1070    * Checks if promo is allowed and SyncPromo requirements are satisfied.
   1071    * @return {boolean} Whether the promo should be shown on sync_promo.
   1072    */
   1073   function shouldPromoBeShownOnSync() {
   1074     var snapshotsCount =
   1075         currentSnapshots == null ? 0 : currentSnapshots.length;
   1076     var sessionsCount = currentSessions == null ? 0 : currentSessions.length;
   1077     return promoIsAllowed && promoIsAllowedOnOpenTabs &&
   1078         (snapshotsCount + sessionsCount == 0);
   1079   }
   1080 
   1081   /**
   1082    * Records a promo impression on a given section if necessary.
   1083    * @param {string} section Active section name to check.
   1084    */
   1085   function promoUpdateImpressions(section) {
   1086     if (section == 'most_visited' && shouldPromoBeShownOnMostVisited())
   1087       chrome.send('recordImpression', ['most_visited']);
   1088     else if (section == 'open_tabs' && shouldPromoBeShownOnOpenTabs())
   1089       chrome.send('recordImpression', ['open_tabs']);
   1090     else if (section == 'open_tabs' && shouldPromoBeShownOnSync())
   1091       chrome.send('recordImpression', ['sync_promo']);
   1092   }
   1093 
   1094   /**
   1095    * Updates the visibility on all promo-related items as necessary.
   1096    */
   1097   function updatePromoVisibility() {
   1098     var mostVisitedEl = $('promo_message_on_most_visited');
   1099     var openTabsVCEl = $('promo_vc_list');
   1100     var syncPromoLegacyEl = $('promo_message_on_sync_promo_legacy');
   1101     var syncPromoReceivedEl = $('promo_message_on_sync_promo_received');
   1102     mostVisitedEl.style.display =
   1103         shouldPromoBeShownOnMostVisited() ? 'block' : 'none';
   1104     syncPromoReceivedEl.style.display =
   1105         shouldPromoBeShownOnSync() ? 'block' : 'none';
   1106     syncPromoLegacyEl.style.display =
   1107         shouldPromoBeShownOnSync() ? 'none' : 'block';
   1108     openTabsVCEl.style.display =
   1109         (shouldPromoBeShownOnOpenTabs() && promoIsAllowedAsVirtualComputer) ?
   1110             'block' : 'none';
   1111   }
   1112 
   1113   /**
   1114    * Called from native.
   1115    * Clears the promotion.
   1116    */
   1117   function clearPromotions() {
   1118     setPromotions({});
   1119   }
   1120 
   1121   /**
   1122    * Set the element to a parsed and sanitized promotion HTML string.
   1123    * @param {Element} el The element to set the promotion string to.
   1124    * @param {string} html The promotion HTML string.
   1125    * @throws {Error} In case of non supported markup.
   1126    */
   1127   function setPromotionHtml(el, html) {
   1128     if (!el) return;
   1129     el.innerHTML = '';
   1130     if (!html) return;
   1131     var tags = ['BR', 'DIV', 'BUTTON', 'SPAN'];
   1132     var attrs = {
   1133       class: function(node, value) { return true; },
   1134       style: function(node, value) { return true; },
   1135     };
   1136     try {
   1137       var fragment = parseHtmlSubset(html, tags, attrs);
   1138       el.appendChild(fragment);
   1139     } catch (err) {
   1140       console.error(err.toString());
   1141       // Ignore all errors while parsing or setting the element.
   1142     }
   1143   }
   1144 
   1145   /**
   1146    * Called from native.
   1147    * Sets the text for all promo-related items, updates
   1148    * promo-send-email-target items to send email on click and
   1149    * updates the visibility of items.
   1150    * @param {Object} promotions Dictionary used to fill-in the text.
   1151    */
   1152   function setPromotions(promotions) {
   1153     var mostVisitedEl = $('promo_message_on_most_visited');
   1154     var openTabsEl = $('promo_message_on_open_tabs');
   1155     var syncPromoReceivedEl = $('promo_message_on_sync_promo_received');
   1156 
   1157     promoIsAllowed = !!promotions.promoIsAllowed;
   1158     promoIsAllowedOnMostVisited = !!promotions.promoIsAllowedOnMostVisited;
   1159     promoIsAllowedOnOpenTabs = !!promotions.promoIsAllowedOnOpenTabs;
   1160     promoIsAllowedAsVirtualComputer = !!promotions.promoIsAllowedAsVC;
   1161 
   1162     setPromotionHtml(mostVisitedEl, promotions.promoMessage);
   1163     setPromotionHtml(openTabsEl, promotions.promoMessage);
   1164     setPromotionHtml(syncPromoReceivedEl, promotions.promoMessageLong);
   1165 
   1166     promoInjectedComputerTitleText = promotions.promoVCTitle || '';
   1167     promoInjectedComputerLastSyncedText = promotions.promoVCLastSynced || '';
   1168     var openTabsVCTitleEl = $('promo_vc_title');
   1169     if (openTabsVCTitleEl)
   1170       openTabsVCTitleEl.textContent = promoInjectedComputerTitleText;
   1171     var openTabsVCLastSyncEl = $('promo_vc_lastsync');
   1172     if (openTabsVCLastSyncEl)
   1173       openTabsVCLastSyncEl.textContent = promoInjectedComputerLastSyncedText;
   1174 
   1175     if (promoIsAllowed) {
   1176       var promoButtonEls =
   1177           document.getElementsByClassName('promo-button');
   1178       for (var i = 0, len = promoButtonEls.length; i < len; i++) {
   1179         promoButtonEls[i].onclick = executePromoAction;
   1180         addActiveTouchListener(promoButtonEls[i], 'promo-button-active');
   1181       }
   1182     }
   1183     updatePromoVisibility();
   1184   }
   1185 
   1186   /**
   1187    * On-click handler for promo email targets.
   1188    * Performs the promo action "send email".
   1189    * @param {Object} evt User interface event that triggered the action.
   1190    */
   1191   function executePromoAction(evt) {
   1192     evt.preventDefault();
   1193     chrome.send('promoActionTriggered');
   1194   }
   1195 
   1196   /**
   1197    * Called by the browser when a context menu has been selected.
   1198    *
   1199    * @param {number} itemId The id of the item that was selected, as specified
   1200    *     when chrome.send('showContextMenu') was called.
   1201    */
   1202   function onCustomMenuSelected(itemId) {
   1203     if (contextMenuUrl != null) {
   1204       switch (itemId) {
   1205         case ContextMenuItemIds.BOOKMARK_OPEN_IN_NEW_TAB:
   1206         case ContextMenuItemIds.BOOKMARK_OPEN_IN_INCOGNITO_TAB:
   1207           chrome.send('openedBookmark');
   1208           break;
   1209 
   1210         case ContextMenuItemIds.MOST_VISITED_OPEN_IN_NEW_TAB:
   1211         case ContextMenuItemIds.MOST_VISITED_OPEN_IN_INCOGNITO_TAB:
   1212           chrome.send('openedMostVisited');
   1213           if (contextMenuItem) {
   1214             chrome.send('metricsHandler:recordInHistogram',
   1215                 ['NewTabPage.MostVisited',
   1216                  contextMenuItem.mostVisitedIndex,
   1217                  8]);
   1218           }
   1219           break;
   1220 
   1221         case ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_NEW_TAB:
   1222         case ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_INCOGNITO_TAB:
   1223           chrome.send('openedRecentlyClosed');
   1224           break;
   1225       }
   1226     }
   1227 
   1228     switch (itemId) {
   1229       case ContextMenuItemIds.BOOKMARK_OPEN_IN_NEW_TAB:
   1230       case ContextMenuItemIds.MOST_VISITED_OPEN_IN_NEW_TAB:
   1231       case ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_NEW_TAB:
   1232         if (contextMenuUrl != null)
   1233           chrome.send('openInNewTab', [contextMenuUrl]);
   1234         break;
   1235 
   1236       case ContextMenuItemIds.BOOKMARK_OPEN_IN_INCOGNITO_TAB:
   1237       case ContextMenuItemIds.MOST_VISITED_OPEN_IN_INCOGNITO_TAB:
   1238       case ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_INCOGNITO_TAB:
   1239         if (contextMenuUrl != null)
   1240           chrome.send('openInIncognitoTab', [contextMenuUrl]);
   1241         break;
   1242 
   1243       case ContextMenuItemIds.BOOKMARK_EDIT:
   1244         if (contextMenuItem != null)
   1245           chrome.send('editBookmark', [contextMenuItem.id]);
   1246         break;
   1247 
   1248       case ContextMenuItemIds.BOOKMARK_DELETE:
   1249         if (contextMenuUrl != null)
   1250           chrome.send('deleteBookmark', [contextMenuItem.id]);
   1251         break;
   1252 
   1253       case ContextMenuItemIds.MOST_VISITED_REMOVE:
   1254         if (contextMenuUrl != null)
   1255           chrome.send('blacklistURLFromMostVisited', [contextMenuUrl]);
   1256         break;
   1257 
   1258       case ContextMenuItemIds.BOOKMARK_SHORTCUT:
   1259         if (contextMenuUrl != null)
   1260           chrome.send('createHomeScreenBookmarkShortcut', [contextMenuItem.id]);
   1261         break;
   1262 
   1263       case ContextMenuItemIds.RECENTLY_CLOSED_REMOVE:
   1264         chrome.send('clearRecentlyClosed');
   1265         break;
   1266 
   1267       case ContextMenuItemIds.FOREIGN_SESSIONS_REMOVE:
   1268         if (contextMenuItem != null) {
   1269           chrome.send(
   1270               'deleteForeignSession', [contextMenuItem.sessionTag]);
   1271           chrome.send('getForeignSessions');
   1272         }
   1273         break;
   1274 
   1275       case ContextMenuItemIds.PROMO_VC_SESSION_REMOVE:
   1276         chrome.send('promoDisabled');
   1277         break;
   1278 
   1279       default:
   1280         log.error('Unknown context menu selected id=' + itemId);
   1281         break;
   1282     }
   1283   }
   1284 
   1285   /**
   1286    * Generates the full bookmark folder hierarchy and populates the scrollable
   1287    * title element.
   1288    *
   1289    * @param {Element} wrapperEl The wrapper element containing the scrollable
   1290    *     title.
   1291    * @param {string} data The current bookmark folder node.
   1292    * @param {Array.<Object>=} opt_ancestry The folder ancestry of the current
   1293    *     bookmark folder.  The list is ordered in order of closest descendant
   1294    *     (the root will always be the last node).  The definition of each
   1295    *     element is:
   1296    *     - id {number}: Unique ID of the folder (N/A for root node).
   1297    *     - name {string}: Name of the folder (N/A for root node).
   1298    *     - root {boolean}: Whether this is the root node.
   1299    */
   1300   function setBookmarkTitleHierarchy(wrapperEl, data, opt_ancestry) {
   1301     var title = wrapperEl.getElementsByClassName('section-title')[0];
   1302     title.innerHTML = '';
   1303     if (opt_ancestry) {
   1304       for (var i = opt_ancestry.length - 1; i >= 0; i--) {
   1305         var titleCrumb = createBookmarkTitleCrumb_(opt_ancestry[i]);
   1306         title.appendChild(titleCrumb);
   1307         title.appendChild(createDiv('bookmark-separator'));
   1308       }
   1309     }
   1310     var titleCrumb = createBookmarkTitleCrumb_(data);
   1311     titleCrumb.classList.add('title-crumb-active');
   1312     title.appendChild(titleCrumb);
   1313 
   1314     // Ensure the last crumb is as visible as possible.
   1315     var windowWidth =
   1316         wrapperEl.getElementsByClassName('section-title-mask')[0].offsetWidth;
   1317     var crumbWidth = titleCrumb.offsetWidth;
   1318     var leftOffset = titleCrumb.offsetLeft;
   1319 
   1320     var shiftLeft = windowWidth - crumbWidth - leftOffset;
   1321     if (shiftLeft < 0) {
   1322       if (crumbWidth > windowWidth)
   1323         shifLeft = -leftOffset;
   1324 
   1325       // Queue up the scrolling initially to allow for the mask element to
   1326       // be placed into the dom and it's size correctly calculated.
   1327       setTimeout(function() {
   1328         handleTitleScroll(wrapperEl, shiftLeft);
   1329       }, 0);
   1330     } else {
   1331       handleTitleScroll(wrapperEl, 0);
   1332     }
   1333   }
   1334 
   1335   /**
   1336    * Creates a clickable bookmark title crumb.
   1337    * @param {Object} data The crumb data (see setBookmarkTitleHierarchy for
   1338    *     definition of the data object).
   1339    * @return {Element} The clickable title crumb element.
   1340    * @private
   1341    */
   1342   function createBookmarkTitleCrumb_(data) {
   1343     var titleCrumb = createDiv('title-crumb');
   1344     if (data.root) {
   1345       titleCrumb.innerText = templateData.bookmarkstitle;
   1346     } else {
   1347       titleCrumb.innerText = data.title;
   1348     }
   1349     titleCrumb.addEventListener('click', function(evt) {
   1350       browseToBookmarkFolder(data.root ? '0' : data.id);
   1351     });
   1352     return titleCrumb;
   1353   }
   1354 
   1355   /**
   1356    * Handles scrolling a title element.
   1357    * @param {Element} wrapperEl The wrapper element containing the scrollable
   1358    *     title.
   1359    * @param {number} scrollPosition The position to be scrolled to.
   1360    */
   1361   function handleTitleScroll(wrapperEl, scrollPosition) {
   1362     var overflowLeftMask =
   1363         wrapperEl.getElementsByClassName('overflow-left-mask')[0];
   1364     var overflowRightMask =
   1365         wrapperEl.getElementsByClassName('overflow-right-mask')[0];
   1366     var title = wrapperEl.getElementsByClassName('section-title')[0];
   1367     var titleMask = wrapperEl.getElementsByClassName('section-title-mask')[0];
   1368     var titleWidth = title.scrollWidth;
   1369     var containerWidth = titleMask.offsetWidth;
   1370 
   1371     var maxRightScroll = containerWidth - titleWidth;
   1372     var boundedScrollPosition =
   1373         Math.max(maxRightScroll, Math.min(scrollPosition, 0));
   1374 
   1375     overflowLeftMask.style.opacity =
   1376         Math.min(
   1377             1,
   1378             (Math.max(0, -boundedScrollPosition)) + 10 / 30);
   1379 
   1380     overflowRightMask.style.opacity =
   1381         Math.min(
   1382             1,
   1383             (Math.max(0, boundedScrollPosition - maxRightScroll) + 10) / 30);
   1384 
   1385     // Set the position of the title.
   1386     if (titleWidth < containerWidth) {
   1387       // left-align on LTR and right-align on RTL.
   1388       title.style.left = '';
   1389     } else {
   1390       title.style.left = boundedScrollPosition + 'px';
   1391     }
   1392   }
   1393 
   1394   /**
   1395    * Initializes a scrolling title element.
   1396    * @param {Element} wrapperEl The wrapper element of the scrolling title.
   1397    */
   1398   function initializeTitleScroller(wrapperEl) {
   1399     var title = wrapperEl.getElementsByClassName('section-title')[0];
   1400 
   1401     var inTitleScroll = false;
   1402     var startingScrollPosition;
   1403     var startingOffset;
   1404     wrapperEl.addEventListener(PRESS_START_EVT, function(evt) {
   1405       inTitleScroll = true;
   1406       startingScrollPosition = getTouchEventX(evt);
   1407       startingOffset = title.offsetLeft;
   1408     });
   1409     document.body.addEventListener(PRESS_STOP_EVT, function(evt) {
   1410       if (!inTitleScroll)
   1411         return;
   1412       inTitleScroll = false;
   1413     });
   1414     document.body.addEventListener(PRESS_MOVE_EVT, function(evt) {
   1415       if (!inTitleScroll)
   1416         return;
   1417       handleTitleScroll(
   1418           wrapperEl,
   1419           startingOffset - (startingScrollPosition - getTouchEventX(evt)));
   1420       evt.stopPropagation();
   1421     });
   1422   }
   1423 
   1424   /**
   1425    * Handles updates from the underlying bookmark model (calls originate
   1426    * in the WebUI handler for bookmarks).
   1427    *
   1428    * @param {Object} status Describes the type of change that occurred.  Can
   1429    *     contain the following fields:
   1430    *     - parent_id {string}: Unique id of the parent that was affected by
   1431    *                           the change.  If the parent is the bookmark
   1432    *                           bar, then the ID will be 'root'.
   1433    *     - node_id {string}: The unique ID of the node that was affected.
   1434    */
   1435   function bookmarkChanged(status) {
   1436     if (status) {
   1437       var affectedParentNode = status['parent_id'];
   1438       var affectedNodeId = status['node_id'];
   1439       var shouldUpdate = (bookmarkFolderId == affectedParentNode ||
   1440           bookmarkFolderId == affectedNodeId);
   1441       if (shouldUpdate)
   1442         setCurrentBookmarkFolderData(bookmarkFolderId);
   1443     } else {
   1444       // This typically happens when extensive changes could have happened to
   1445       // the model, such as initial load, import and sync.
   1446       setCurrentBookmarkFolderData(bookmarkFolderId);
   1447     }
   1448   }
   1449 
   1450   /**
   1451    * Loads the bookarks data for a given folder.
   1452    *
   1453    * @param {string|number} folderId The ID of the folder to load (or null if
   1454    *     it should load the root folder).
   1455    */
   1456   function setCurrentBookmarkFolderData(folderId) {
   1457     if (folderId != null) {
   1458       chrome.send('getBookmarks', [folderId]);
   1459     } else {
   1460       chrome.send('getBookmarks');
   1461     }
   1462     try {
   1463       if (folderId == null) {
   1464         localStorage.removeItem(DEFAULT_BOOKMARK_FOLDER_KEY);
   1465       } else {
   1466         localStorage.setItem(DEFAULT_BOOKMARK_FOLDER_KEY, folderId);
   1467       }
   1468     } catch (e) {}
   1469   }
   1470 
   1471   /**
   1472    * Navigates to the specified folder and handles loading the required data.
   1473    * Ensures the current folder can be navigated back to using the browser
   1474    * controls.
   1475    *
   1476    * @param {string|number} folderId The ID of the folder to navigate to.
   1477    */
   1478   function browseToBookmarkFolder(folderId) {
   1479     history.pushState(
   1480         {folderId: folderId, selectedPaneIndex: currentPaneIndex},
   1481         null, null);
   1482     setCurrentBookmarkFolderData(folderId);
   1483   }
   1484 
   1485   /**
   1486    * Called to inform the page of the current sync status. If the state has
   1487    * changed from disabled to enabled, it changes the current and default
   1488    * bookmark section to the root directory.  This makes desktop bookmarks are
   1489    * visible.
   1490    */
   1491   function setSyncEnabled(enabled) {
   1492     try {
   1493       if (syncEnabled != undefined && syncEnabled == enabled) {
   1494         // The value didn't change
   1495         return;
   1496       }
   1497       syncEnabled = enabled;
   1498 
   1499       if (enabled) {
   1500         if (!localStorage.getItem(SYNC_ENABLED_KEY)) {
   1501           localStorage.setItem(SYNC_ENABLED_KEY, 'true');
   1502           setCurrentBookmarkFolderData('0');
   1503         }
   1504       } else {
   1505         localStorage.removeItem(SYNC_ENABLED_KEY);
   1506       }
   1507       updatePromoVisibility();
   1508 
   1509       if (bookmarkData) {
   1510         // Bookmark data can now be displayed (or needs to be refiltered)
   1511         bookmarks(bookmarkData);
   1512       }
   1513 
   1514       updateSyncEmptyState();
   1515     } catch (e) {}
   1516   }
   1517 
   1518   /**
   1519    * Handles adding or removing the 'nothing to see here' text from the session
   1520    * list depending on the state of snapshots and sessions.
   1521    *
   1522    * @param {boolean} Whether the call is occuring because of a schedule
   1523    *     timeout.
   1524    */
   1525   function updateSyncEmptyState(timeout) {
   1526     if (syncState == SyncState.DISPLAYING_LOADING && !timeout) {
   1527       // Make sure 'Loading...' is displayed long enough
   1528       return;
   1529     }
   1530 
   1531     var openTabsList = findList('open_tabs');
   1532     var snapshotsList = findList('snapshots');
   1533     var syncPromo = $('sync_promo');
   1534     var syncLoading = $('sync_loading');
   1535     var syncEnableSync = $('sync_enable_sync');
   1536 
   1537     if (syncEnabled == undefined ||
   1538         currentSnapshots == null ||
   1539         currentSessions == null) {
   1540       if (syncState == SyncState.INITIAL) {
   1541         // Wait one second for sync data to come in before displaying loading
   1542         // text.
   1543         syncState = SyncState.WAITING_FOR_DATA;
   1544         syncTimerId = setTimeout(function() { updateSyncEmptyState(true); },
   1545             SYNC_INITIAL_LOAD_TIMEOUT);
   1546       } else if (syncState == SyncState.WAITING_FOR_DATA && timeout) {
   1547         // We've waited for the initial info timeout to pass and still don't
   1548         // have data.  So, display loading text so the user knows something is
   1549         // happening.
   1550         syncState = SyncState.DISPLAYING_LOADING;
   1551         syncLoading.style.display = '-webkit-box';
   1552         centerEmptySections(syncLoading);
   1553         syncTimerId = setTimeout(function() { updateSyncEmptyState(true); },
   1554             SYNC_LOADING_TIMEOUT);
   1555       } else if (syncState == SyncState.DISPLAYING_LOADING) {
   1556         // Allow the Loading... text to go away once data comes in
   1557         syncState = SyncState.DISPLAYED_LOADING;
   1558       }
   1559       return;
   1560     }
   1561 
   1562     if (syncTimerId != -1) {
   1563       clearTimeout(syncTimerId);
   1564       syncTimerId = -1;
   1565     }
   1566     syncState = SyncState.LOADED;
   1567 
   1568     // Hide everything by default, display selectively below
   1569     syncEnableSync.style.display = 'none';
   1570     syncLoading.style.display = 'none';
   1571     syncPromo.style.display = 'none';
   1572 
   1573     var snapshotsCount =
   1574         currentSnapshots == null ? 0 : currentSnapshots.length;
   1575     var sessionsCount = currentSessions == null ? 0 : currentSessions.length;
   1576 
   1577     if (!syncEnabled) {
   1578       syncEnableSync.style.display = '-webkit-box';
   1579       centerEmptySections(syncEnableSync);
   1580     } else if (sessionsCount + snapshotsCount == 0) {
   1581       syncPromo.style.display = '-webkit-box';
   1582       centerEmptySections(syncPromo);
   1583     } else {
   1584       openTabsList.style.display = sessionsCount == 0 ? 'none' : 'block';
   1585       snapshotsList.style.display = snapshotsCount == 0 ? 'none' : 'block';
   1586     }
   1587     updatePromoVisibility();
   1588   }
   1589 
   1590   /**
   1591    * Called externally when updated snapshot data is available.
   1592    *
   1593    * @param {Object} data The snapshot data
   1594    */
   1595   function snapshots(data) {
   1596     var list = findList('snapshots');
   1597     list.innerHTML = '';
   1598 
   1599     currentSnapshots = data;
   1600     updateSyncEmptyState();
   1601 
   1602     if (!data || data.length == 0)
   1603       return;
   1604 
   1605     data.sort(function(a, b) {
   1606       return b.createTime - a.createTime;
   1607     });
   1608 
   1609     // Create the main container
   1610     var snapshotsEl = createElement('div');
   1611     list.appendChild(snapshotsEl);
   1612 
   1613     // Create the header container
   1614     var headerEl = createDiv('session-header');
   1615     snapshotsEl.appendChild(headerEl);
   1616 
   1617     // Create the documents container
   1618     var docsEl = createDiv('session-children-container');
   1619     snapshotsEl.appendChild(docsEl);
   1620 
   1621     // Create the container for the title & icon
   1622     var headerInnerEl = createDiv('list-item standard-divider');
   1623     addActiveTouchListener(headerInnerEl, ACTIVE_LIST_ITEM_CSS_CLASS);
   1624     headerEl.appendChild(headerInnerEl);
   1625 
   1626     // Create the header icon
   1627     headerInnerEl.appendChild(createDiv('session-icon documents'));
   1628 
   1629     // Create the header title
   1630     var titleContainer = createElement('span', 'title');
   1631     headerInnerEl.appendChild(titleContainer);
   1632     var title = createDiv('session-name');
   1633     title.textContent = templateData.receivedDocuments;
   1634     titleContainer.appendChild(title);
   1635 
   1636     // Add support for expanding and collapsing the children
   1637     var expando = createDiv();
   1638     var expandoFunction = createExpandoFunction(expando, docsEl);
   1639     headerInnerEl.addEventListener('click', expandoFunction);
   1640     headerEl.appendChild(expando);
   1641 
   1642     // Support for actually opening the document
   1643     var snapshotClickCallback = function(item) {
   1644       if (!item)
   1645         return;
   1646       if (item.snapshotId) {
   1647         window.location = 'chrome://snapshot/' + item.snapshotId;
   1648       } else if (item.printJobId) {
   1649         window.location = 'chrome://printjob/' + item.printJobId;
   1650       } else {
   1651         window.location = item.url;
   1652       }
   1653     }
   1654 
   1655     // Finally, add the list of documents
   1656     populateData(docsEl, SectionType.SNAPSHOTS, data,
   1657         makeListEntryItem, snapshotClickCallback);
   1658   }
   1659 
   1660   /**
   1661    * Create a function to handle expanding and collapsing a section
   1662    *
   1663    * @param {Element} expando The expando div
   1664    * @param {Element} element The element to expand and collapse
   1665    * @return {function()} A callback function that should be invoked when the
   1666    *     expando is clicked
   1667    */
   1668   function createExpandoFunction(expando, element) {
   1669     expando.className = 'expando open';
   1670     return function() {
   1671       if (element.style.height != '0px') {
   1672         // It seems that '-webkit-transition' only works when explicit pixel
   1673         // values are used.
   1674         setTimeout(function() {
   1675           // If this is the first time to collapse the list, store off the
   1676           // expanded height and also set the height explicitly on the style.
   1677           if (!element.expandedHeight) {
   1678             element.expandedHeight =
   1679                 element.clientHeight + 'px';
   1680             element.style.height = element.expandedHeight;
   1681           }
   1682           // Now set the height to 0.  Note, this is also done in a callback to
   1683           // give the layout engine a chance to run after possibly setting the
   1684           // height above.
   1685           setTimeout(function() {
   1686             element.style.height = '0px';
   1687           }, 0);
   1688         }, 0);
   1689         expando.className = 'expando closed';
   1690       } else {
   1691         element.style.height = element.expandedHeight;
   1692         expando.className = 'expando open';
   1693       }
   1694     }
   1695   }
   1696 
   1697   /**
   1698    * Initializes the promo_vc_list div to look like a foreign session
   1699    * with a desktop.
   1700    */
   1701   function createPromoVirtualComputers() {
   1702     var list = findList('promo_vc');
   1703     list.innerHTML = '';
   1704 
   1705     // Set up the container and the "virtual computer" session header.
   1706     var sessionEl = createDiv();
   1707     list.appendChild(sessionEl);
   1708     var sessionHeader = createDiv('session-header');
   1709     sessionEl.appendChild(sessionHeader);
   1710 
   1711     // Set up the session children container and the promo as a child.
   1712     var sessionChildren = createDiv('session-children-container');
   1713     var promoMessage = createDiv('promo-message');
   1714     promoMessage.id = 'promo_message_on_open_tabs';
   1715     sessionChildren.appendChild(promoMessage);
   1716     sessionEl.appendChild(sessionChildren);
   1717 
   1718     // Add support for expanding and collapsing the children.
   1719     var expando = createDiv();
   1720     var expandoFunction = createExpandoFunction(expando, sessionChildren);
   1721 
   1722     // Fill-in the contents of the "virtual computer" session header.
   1723     var headerList = [{
   1724       'title': promoInjectedComputerTitleText,
   1725       'titleId': 'promo_vc_title',
   1726       'userVisibleTimestamp': promoInjectedComputerLastSyncedText,
   1727       'userVisibleTimestampId': 'promo_vc_lastsync',
   1728       'iconStyle': 'laptop'
   1729     }];
   1730 
   1731     populateData(sessionHeader, SectionType.PROMO_VC_SESSION_HEADER, headerList,
   1732         makeForeignSessionListEntry, expandoFunction);
   1733     sessionHeader.appendChild(expando);
   1734   }
   1735 
   1736   /**
   1737    * Called externally when updated synced sessions data is available.
   1738    *
   1739    * @param {Object} data The snapshot data
   1740    */
   1741   function setForeignSessions(data, tabSyncEnabled) {
   1742     var list = findList('open_tabs');
   1743     list.innerHTML = '';
   1744 
   1745     currentSessions = data;
   1746     updateSyncEmptyState();
   1747 
   1748     // Sort the windows within each client such that more recently
   1749     // modified windows appear first.
   1750     data.forEach(function(client) {
   1751       if (client.windows != null) {
   1752         client.windows.sort(function(a, b) {
   1753           if (b.timestamp == null) {
   1754             return -1;
   1755           } else if (a.timestamp == null) {
   1756             return 1;
   1757           } else {
   1758             return b.timestamp - a.timestamp;
   1759           }
   1760         });
   1761       }
   1762     });
   1763 
   1764     // Sort so more recently modified clients appear first.
   1765     data.sort(function(aClient, bClient) {
   1766       var aWindows = aClient.windows;
   1767       var bWindows = bClient.windows;
   1768       if (bWindows == null || bWindows.length == 0 ||
   1769           bWindows[0].timestamp == null) {
   1770         return -1;
   1771       } else if (aWindows == null || aWindows.length == 0 ||
   1772           aWindows[0].timestamp == null) {
   1773         return 1;
   1774       } else {
   1775         return bWindows[0].timestamp - aWindows[0].timestamp;
   1776       }
   1777     });
   1778 
   1779     data.forEach(function(client, clientNum) {
   1780 
   1781       var windows = client.windows;
   1782       if (windows == null || windows.length == 0)
   1783         return;
   1784 
   1785       // Set up the container for the session header
   1786       var sessionEl = createElement('div');
   1787       list.appendChild(sessionEl);
   1788       var sessionHeader = createDiv('session-header');
   1789       sessionEl.appendChild(sessionHeader);
   1790 
   1791       // Set up the container for the session children
   1792       var sessionChildren = createDiv('session-children-container');
   1793       sessionEl.appendChild(sessionChildren);
   1794 
   1795       var clientName = 'Client ' + clientNum;
   1796       if (client.name)
   1797         clientName = client.name;
   1798 
   1799       var iconStyle;
   1800       var deviceType = client.deviceType;
   1801       if (deviceType == 'win' ||
   1802           deviceType == 'macosx' ||
   1803           deviceType == 'linux' ||
   1804           deviceType == 'chromeos' ||
   1805           deviceType == 'other') {
   1806         iconStyle = 'laptop';
   1807       } else if (deviceType == 'phone') {
   1808         iconStyle = 'phone';
   1809       } else if (deviceType == 'tablet') {
   1810         iconStyle = 'tablet';
   1811       } else {
   1812         console.error('Unknown sync device type found: ', deviceType);
   1813         iconStyle = 'laptop';
   1814       }
   1815       var headerList = [{
   1816         'title': clientName,
   1817         'userVisibleTimestamp': windows[0].userVisibleTimestamp,
   1818         'iconStyle': iconStyle,
   1819         'sessionTag': client.tag,
   1820       }];
   1821 
   1822       var expando = createDiv();
   1823       var expandoFunction = createExpandoFunction(expando, sessionChildren);
   1824       populateData(sessionHeader, SectionType.FOREIGN_SESSION_HEADER,
   1825           headerList, makeForeignSessionListEntry, expandoFunction);
   1826       sessionHeader.appendChild(expando);
   1827 
   1828       // Populate the session children container
   1829       var openTabsList = new Array();
   1830       for (var winNum = 0; winNum < windows.length; winNum++) {
   1831         win = windows[winNum];
   1832         var tabs = win.tabs;
   1833         for (var tabNum = 0; tabNum < tabs.length; tabNum++) {
   1834           var tab = tabs[tabNum];
   1835           // If this is the last tab in the window and there are more windows,
   1836           // use a section divider.
   1837           var needSectionDivider =
   1838               (tabNum + 1 == tabs.length) && (winNum + 1 < windows.length);
   1839           tab.icon = tab.icon || 'chrome://favicon/size/16@1x/' + tab.url;
   1840 
   1841           openTabsList.push({
   1842             timestamp: tab.timestamp,
   1843             title: tab.title,
   1844             url: tab.url,
   1845             sessionTag: client.tag,
   1846             winNum: winNum,
   1847             sessionId: tab.sessionId,
   1848             icon: tab.icon,
   1849             iconSize: 16,
   1850             divider: needSectionDivider ? 'section' : 'standard',
   1851           });
   1852         }
   1853       }
   1854       var tabCallback = function(item, evt) {
   1855         var buttonIndex = 0;
   1856         var altKeyPressed = false;
   1857         var ctrlKeyPressed = false;
   1858         var metaKeyPressed = false;
   1859         var shiftKeyPressed = false;
   1860         if (evt instanceof MouseEvent) {
   1861           buttonIndex = evt.button;
   1862           altKeyPressed = evt.altKey;
   1863           ctrlKeyPressed = evt.ctrlKey;
   1864           metaKeyPressed = evt.metaKey;
   1865           shiftKeyPressed = evt.shiftKey;
   1866         }
   1867         chrome.send('openedForeignSession');
   1868         chrome.send('openForeignSession', [String(item.sessionTag),
   1869             String(item.winNum), String(item.sessionId), buttonIndex,
   1870             altKeyPressed, ctrlKeyPressed, metaKeyPressed, shiftKeyPressed]);
   1871       };
   1872       populateData(sessionChildren, SectionType.FOREIGN_SESSION, openTabsList,
   1873           makeListEntryItem, tabCallback);
   1874     });
   1875   }
   1876 
   1877   /**
   1878    * Updates the dominant favicon color for a given index.
   1879    *
   1880    * @param {number} index The index of the favicon whose dominant color is
   1881    *     being specified.
   1882    * @param {string} color The string encoded color.
   1883    */
   1884   function setFaviconDominantColor(index, color) {
   1885     var colorstrips = document.getElementsByClassName('colorstrip-' + index);
   1886     for (var i = 0; i < colorstrips.length; i++)
   1887       colorstrips[i].style.background = color;
   1888 
   1889     var id = 'fold_' + index;
   1890     var fold = $(id);
   1891     if (!fold)
   1892       return;
   1893     var zoom = window.getComputedStyle(fold).zoom;
   1894     var scale = 1 / window.getComputedStyle(fold).zoom;
   1895 
   1896     // The width/height of the canvas.  Set to 24 so it looks good across all
   1897     // resolutions.
   1898     var cw = 24;
   1899     var ch = 24;
   1900 
   1901     // Get the fold canvas and create a path for the fold shape
   1902     var ctx = document.getCSSCanvasContext(
   1903         '2d', 'fold_' + index, cw * scale, ch * scale);
   1904     ctx.beginPath();
   1905     ctx.moveTo(0, 0);
   1906     ctx.lineTo(0, ch * 0.75 * scale);
   1907     ctx.quadraticCurveTo(
   1908         0, ch * scale,
   1909         cw * .25 * scale, ch * scale);
   1910     ctx.lineTo(cw * scale, ch * scale);
   1911     ctx.closePath();
   1912 
   1913     // Create a gradient for the fold and fill it
   1914     var gradient = ctx.createLinearGradient(cw * scale, 0, 0, ch * scale);
   1915     if (color.indexOf('#') == 0) {
   1916       var r = parseInt(color.substring(1, 3), 16);
   1917       var g = parseInt(color.substring(3, 5), 16);
   1918       var b = parseInt(color.substring(5, 7), 16);
   1919       gradient.addColorStop(0, 'rgba(' + r + ', ' + g + ', ' + b + ', 0.6)');
   1920     } else {
   1921       // assume the color is in the 'rgb(#, #, #)' format
   1922       var rgbBase = color.substring(4, color.length - 1);
   1923       gradient.addColorStop(0, 'rgba(' + rgbBase + ', 0.6)');
   1924     }
   1925     gradient.addColorStop(1, color);
   1926     ctx.fillStyle = gradient;
   1927     ctx.fill();
   1928 
   1929     // Stroke the fold
   1930     ctx.lineWidth = Math.floor(scale);
   1931     ctx.strokeStyle = color;
   1932     ctx.stroke();
   1933     ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
   1934     ctx.stroke();
   1935 
   1936   }
   1937 
   1938   /**
   1939    * Finds the list element corresponding to the given name.
   1940    * @param {string} name The name prefix of the DOM element (<prefix>_list).
   1941    * @return {Element} The list element corresponding with the name.
   1942    */
   1943   function findList(name) {
   1944     return $(name + '_list');
   1945   }
   1946 
   1947   /**
   1948    * Render the given data into the given list, and hide or show the entire
   1949    * container based on whether there are any elements.  The decorator function
   1950    * is used to create the element to be inserted based on the given data
   1951    * object.
   1952    *
   1953    * @param {holder} The dom element that the generated list items will be put
   1954    *     into.
   1955    * @param {SectionType} section The section that data is for.
   1956    * @param {Object} data The data to be populated.
   1957    * @param {function(Object, boolean)} decorator The function that will
   1958    *     handle decorating each item in the data.
   1959    * @param {function(Object, Object)} opt_clickCallback The function that is
   1960    *     called when the item is clicked.
   1961    */
   1962   function populateData(holder, section, data, decorator,
   1963       opt_clickCallback) {
   1964     // Empty other items in the list, if present.
   1965     holder.innerHTML = '';
   1966     var fragment = document.createDocumentFragment();
   1967     if (!data || data.length == 0) {
   1968       fragment.innerHTML = '';
   1969     } else {
   1970       data.forEach(function(item) {
   1971         var el = decorator(item, opt_clickCallback);
   1972         el.setAttribute(SECTION_KEY, section);
   1973         el.id = section + fragment.childNodes.length;
   1974         fragment.appendChild(el);
   1975       });
   1976     }
   1977     holder.appendChild(fragment);
   1978     if (holder.classList.contains(GRID_CSS_CLASS))
   1979       centerGrid(holder);
   1980     centerEmptySections(holder);
   1981   }
   1982 
   1983   /**
   1984    * Given an element containing a list of child nodes arranged in
   1985    * a grid, this will center the grid in the window based on the
   1986    * remaining space.
   1987    * @param {Element} el Container holding the grid cell items.
   1988    */
   1989   function centerGrid(el) {
   1990     var childEl = el.firstChild;
   1991     if (!childEl)
   1992       return;
   1993 
   1994     // Find the element to actually set the margins on.
   1995     var toCenter = el;
   1996     var curEl = toCenter;
   1997     while (curEl && curEl.classList) {
   1998       if (curEl.classList.contains(GRID_CENTER_CSS_CLASS)) {
   1999         toCenter = curEl;
   2000         break;
   2001       }
   2002       curEl = curEl.parentNode;
   2003     }
   2004     var setItemMargins = el.classList.contains(GRID_SET_ITEM_MARGINS);
   2005     var itemWidth = getItemWidth(childEl, setItemMargins);
   2006     var windowWidth = document.documentElement.offsetWidth;
   2007     if (itemWidth >= windowWidth) {
   2008       toCenter.style.paddingLeft = '0';
   2009       toCenter.style.paddingRight = '0';
   2010     } else {
   2011       var numColumns = el.getAttribute(GRID_COLUMNS);
   2012       if (numColumns) {
   2013         numColumns = parseInt(numColumns);
   2014       } else {
   2015         numColumns = Math.floor(windowWidth / itemWidth);
   2016       }
   2017 
   2018       if (setItemMargins) {
   2019         // In this case, try to size each item to fill as much space as
   2020         // possible.
   2021         var gutterSize =
   2022             (windowWidth - itemWidth * numColumns) / (numColumns + 1);
   2023         var childLeftMargin = Math.round(gutterSize / 2);
   2024         var childRightMargin = Math.floor(gutterSize - childLeftMargin);
   2025         var children = el.childNodes;
   2026         for (var i = 0; i < children.length; i++) {
   2027           children[i].style.marginLeft = childLeftMargin + 'px';
   2028           children[i].style.marginRight = childRightMargin + 'px';
   2029         }
   2030         itemWidth += childLeftMargin + childRightMargin;
   2031       }
   2032 
   2033       var remainder = windowWidth - itemWidth * numColumns;
   2034       var leftPadding = Math.round(remainder / 2);
   2035       var rightPadding = Math.floor(remainder - leftPadding);
   2036       toCenter.style.paddingLeft = leftPadding + 'px';
   2037       toCenter.style.paddingRight = rightPadding + 'px';
   2038 
   2039       if (toCenter.classList.contains(GRID_SET_TOP_MARGIN_CLASS)) {
   2040         var childStyle = window.getComputedStyle(childEl);
   2041         var childLeftPadding = parseInt(
   2042             childStyle.getPropertyValue('padding-left'));
   2043         toCenter.style.paddingTop =
   2044             (childLeftMargin + childLeftPadding + leftPadding) + 'px';
   2045       }
   2046     }
   2047   }
   2048 
   2049   /**
   2050    * Finds and centers all child grid elements for a given node (the grids
   2051    * do not need to be direct descendants and can reside anywhere in the node
   2052    * hierarchy).
   2053    * @param {Element} el The node containing the grid child nodes.
   2054    */
   2055   function centerChildGrids(el) {
   2056     var grids = el.getElementsByClassName(GRID_CSS_CLASS);
   2057     for (var i = 0; i < grids.length; i++)
   2058       centerGrid(grids[i]);
   2059   }
   2060 
   2061   /**
   2062    * Finds and vertically centers all 'empty' elements for a given node (the
   2063    * 'empty' elements do not need to be direct descendants and can reside
   2064    * anywhere in the node hierarchy).
   2065    * @param {Element} el The node containing the 'empty' child nodes.
   2066    */
   2067   function centerEmptySections(el) {
   2068     if (el.classList &&
   2069         el.classList.contains(CENTER_EMPTY_CONTAINER_CSS_CLASS)) {
   2070       centerEmptySection(el);
   2071     }
   2072     var empties = el.getElementsByClassName(CENTER_EMPTY_CONTAINER_CSS_CLASS);
   2073     for (var i = 0; i < empties.length; i++) {
   2074       centerEmptySection(empties[i]);
   2075     }
   2076   }
   2077 
   2078   /**
   2079    * Set the top of the given element to the top of the parent and set the
   2080    * height to (bottom of document - top).
   2081    *
   2082    * @param {Element} el Container holding the centered content.
   2083    */
   2084   function centerEmptySection(el) {
   2085     var parent = el.parentNode;
   2086     var top = parent.offsetTop;
   2087     var bottom = (
   2088         document.documentElement.offsetHeight - getButtonBarPadding());
   2089     el.style.height = (bottom - top) + 'px';
   2090     el.style.top = top + 'px';
   2091   }
   2092 
   2093   /**
   2094    * Finds the index of the panel specified by its prefix.
   2095    * @param {string} The string prefix for the panel.
   2096    * @return {number} The index of the panel.
   2097    */
   2098   function getPaneIndex(panePrefix) {
   2099     var pane = $(panePrefix + '_container');
   2100 
   2101     if (pane != null) {
   2102       var index = panes.indexOf(pane);
   2103 
   2104       if (index >= 0)
   2105         return index;
   2106     }
   2107     return 0;
   2108   }
   2109 
   2110   /**
   2111    * Finds the index of the panel specified by location hash.
   2112    * @return {number} The index of the panel.
   2113    */
   2114   function getPaneIndexFromHash() {
   2115     var paneIndex;
   2116     if (window.location.hash == '#bookmarks') {
   2117       paneIndex = getPaneIndex('bookmarks');
   2118     } else if (window.location.hash == '#bookmark_shortcut') {
   2119       paneIndex = getPaneIndex('bookmarks');
   2120     } else if (window.location.hash == '#most_visited') {
   2121       paneIndex = getPaneIndex('most_visited');
   2122     } else if (window.location.hash == '#open_tabs') {
   2123       paneIndex = getPaneIndex('open_tabs');
   2124     } else if (window.location.hash == '#incognito') {
   2125       paneIndex = getPaneIndex('incognito');
   2126     } else {
   2127       // Couldn't find a good section
   2128       paneIndex = -1;
   2129     }
   2130     return paneIndex;
   2131   }
   2132 
   2133   /**
   2134    * Selects a pane from the top level list (Most Visited, Bookmarks, etc...).
   2135    * @param {number} paneIndex The index of the pane to be selected.
   2136    * @return {boolean} Whether the selected pane has changed.
   2137    */
   2138   function scrollToPane(paneIndex) {
   2139     var pane = panes[paneIndex];
   2140 
   2141     if (pane == currentPane)
   2142       return false;
   2143 
   2144     var newHash = '#' + sectionPrefixes[paneIndex];
   2145     // If updated hash matches the current one in the URL, we need to call
   2146     // updatePaneOnHash directly as updating the hash to the same value will
   2147     // not trigger the 'hashchange' event.
   2148     if (bookmarkShortcutMode || newHash == document.location.hash)
   2149       updatePaneOnHash();
   2150     computeDynamicLayout();
   2151     promoUpdateImpressions(sectionPrefixes[paneIndex]);
   2152     return true;
   2153   }
   2154 
   2155   /**
   2156    * Updates the pane based on the current hash.
   2157    */
   2158   function updatePaneOnHash() {
   2159     var paneIndex = getPaneIndexFromHash();
   2160     var pane = panes[paneIndex];
   2161 
   2162     if (currentPane)
   2163       currentPane.classList.remove('selected');
   2164     pane.classList.add('selected');
   2165     currentPane = pane;
   2166     currentPaneIndex = paneIndex;
   2167 
   2168     setScrollTopForDocument(document, 0);
   2169 
   2170     var panelPrefix = sectionPrefixes[paneIndex];
   2171     var title = templateData[panelPrefix + '_document_title'];
   2172     if (!title)
   2173       title = templateData['title'];
   2174     document.title = title;
   2175 
   2176     sendNTPTitleLoadedNotification();
   2177 
   2178     // TODO (dtrainor): Could potentially add logic to reset the bookmark state
   2179     // if they are moving to that pane.  This logic was in there before, but
   2180     // was removed due to the fact that we have to go to this pane as part of
   2181     // the history navigation.
   2182   }
   2183 
   2184   /**
   2185    * Adds a top level section to the NTP.
   2186    * @param {string} panelPrefix The prefix of the element IDs corresponding
   2187    *     to the container of the content.
   2188    * @param {boolean=} opt_canBeDefault Whether this section can be marked as
   2189    *     the default starting point for subsequent instances of the NTP.  The
   2190    *     default value for this is true.
   2191    */
   2192   function addMainSection(panelPrefix) {
   2193     var paneEl = $(panelPrefix + '_container');
   2194     var paneIndex = panes.push(paneEl) - 1;
   2195     sectionPrefixes.push(panelPrefix);
   2196   }
   2197 
   2198   /**
   2199    * Handles the dynamic layout of the components on the new tab page.  Only
   2200    * layouts that require calculation based on the screen size should go in
   2201    * this function as it will be called during all resize changes
   2202    * (orientation, keyword being displayed).
   2203    */
   2204   function computeDynamicLayout() {
   2205     // Update the scrolling titles to ensure they are not in a now invalid
   2206     // scroll position.
   2207     var titleScrollers =
   2208         document.getElementsByClassName('section-title-wrapper');
   2209     for (var i = 0, len = titleScrollers.length; i < len; i++) {
   2210       var titleEl =
   2211           titleScrollers[i].getElementsByClassName('section-title')[0];
   2212       handleTitleScroll(
   2213           titleScrollers[i],
   2214           titleEl.offsetLeft);
   2215     }
   2216 
   2217     updateMostVisitedStyle();
   2218     updateMostVisitedHeight();
   2219   }
   2220 
   2221   /**
   2222    * The centering of the 'recently closed' section is different depending on
   2223    * the orientation of the device.  In landscape, it should be left-aligned
   2224    * with the 'most used' section.  In portrait, it should be centered in the
   2225    * screen.
   2226    */
   2227   function updateMostVisitedStyle() {
   2228     if (isTablet()) {
   2229       updateMostVisitedStyleTablet();
   2230     } else {
   2231       updateMostVisitedStylePhone();
   2232     }
   2233   }
   2234 
   2235   /**
   2236    * Updates the style of the most visited pane for the phone.
   2237    */
   2238   function updateMostVisitedStylePhone() {
   2239     var mostVisitedList = $('most_visited_list');
   2240     var childEl = mostVisitedList.firstChild;
   2241     if (!childEl)
   2242       return;
   2243 
   2244     // 'natural' height and width of the thumbnail
   2245     var thumbHeight = 72;
   2246     var thumbWidth = 108;
   2247     var labelHeight = 25;
   2248     var labelWidth = thumbWidth + 20;
   2249     var labelLeft = (thumbWidth - labelWidth) / 2;
   2250     var itemHeight = thumbHeight + labelHeight;
   2251 
   2252     // default vertical margin between items
   2253     var itemMarginTop = 0;
   2254     var itemMarginBottom = 0;
   2255     var itemMarginLeft = 20;
   2256     var itemMarginRight = 20;
   2257 
   2258     var listHeight = 0;
   2259 
   2260     var screenHeight =
   2261         document.documentElement.offsetHeight -
   2262         getButtonBarPadding();
   2263 
   2264     if (isPortrait()) {
   2265       mostVisitedList.setAttribute(GRID_COLUMNS, '2');
   2266       listHeight = screenHeight * .85;
   2267       // Ensure that listHeight is not too small and not too big.
   2268       listHeight = Math.max(listHeight, (itemHeight * 3) + 20);
   2269       listHeight = Math.min(listHeight, 420);
   2270       // Size for 3 rows (4 gutters)
   2271       itemMarginTop = (listHeight - (itemHeight * 3)) / 4;
   2272     } else {
   2273       mostVisitedList.setAttribute(GRID_COLUMNS, '3');
   2274       listHeight = screenHeight;
   2275 
   2276       // If the screen height is less than targetHeight, scale the size of the
   2277       // thumbnails such that the margin between the thumbnails remains
   2278       // constant.
   2279       var targetHeight = 220;
   2280       if (screenHeight < targetHeight) {
   2281         var targetRemainder = targetHeight - 2 * (thumbHeight + labelHeight);
   2282         var scale = (screenHeight - 2 * labelHeight -
   2283             targetRemainder) / (2 * thumbHeight);
   2284         // update values based on scale
   2285         thumbWidth = Math.round(thumbWidth * scale);
   2286         thumbHeight = Math.round(thumbHeight * scale);
   2287         labelWidth = thumbWidth + 20;
   2288         itemHeight = thumbHeight + labelHeight;
   2289       }
   2290 
   2291       // scale the vertical margin such that the items fit perfectly on the
   2292       // screen
   2293       var remainder = screenHeight - (2 * itemHeight);
   2294       var margin = (remainder / 2);
   2295       margin = margin > 24 ? 24 : margin;
   2296       itemMarginTop = Math.round(margin / 2);
   2297       itemMarginBottom = Math.round(margin - itemMarginTop);
   2298     }
   2299 
   2300     mostVisitedList.style.minHeight = listHeight + 'px';
   2301 
   2302     modifyCssRule('body[device="phone"] .thumbnail-cell',
   2303         'height', itemHeight + 'px');
   2304     modifyCssRule('body[device="phone"] #most_visited_list .thumbnail',
   2305         'height', thumbHeight + 'px');
   2306     modifyCssRule('body[device="phone"] #most_visited_list .thumbnail',
   2307         'width', thumbWidth + 'px');
   2308     modifyCssRule(
   2309         'body[device="phone"] #most_visited_list .thumbnail-container',
   2310         'height', thumbHeight + 'px');
   2311     modifyCssRule(
   2312         'body[device="phone"] #most_visited_list .thumbnail-container',
   2313         'width', thumbWidth + 'px');
   2314     modifyCssRule('body[device="phone"] #most_visited_list .title',
   2315         'width', labelWidth + 'px');
   2316     modifyCssRule('body[device="phone"] #most_visited_list .title',
   2317         'left', labelLeft + 'px');
   2318     modifyCssRule('body[device="phone"] #most_visited_list .inner-border',
   2319         'height', thumbHeight - 2 + 'px');
   2320     modifyCssRule('body[device="phone"] #most_visited_list .inner-border',
   2321         'width', thumbWidth - 2 + 'px');
   2322 
   2323     modifyCssRule('body[device="phone"] .thumbnail-cell',
   2324         'margin-left', itemMarginLeft + 'px');
   2325     modifyCssRule('body[device="phone"] .thumbnail-cell',
   2326         'margin-right', itemMarginRight + 'px');
   2327     modifyCssRule('body[device="phone"] .thumbnail-cell',
   2328         'margin-top', itemMarginTop + 'px');
   2329     modifyCssRule('body[device="phone"] .thumbnail-cell',
   2330         'margin-bottom', itemMarginBottom + 'px');
   2331 
   2332     centerChildGrids($('most_visited_container'));
   2333   }
   2334 
   2335   /**
   2336    * Updates the style of the most visited pane for the tablet.
   2337    */
   2338   function updateMostVisitedStyleTablet() {
   2339     function setCenterIconGrid(el, set) {
   2340       if (set) {
   2341         el.classList.add(GRID_CENTER_CSS_CLASS);
   2342       } else {
   2343         el.classList.remove(GRID_CENTER_CSS_CLASS);
   2344         el.style.paddingLeft = '0px';
   2345         el.style.paddingRight = '0px';
   2346       }
   2347     }
   2348     var isPortrait = document.documentElement.offsetWidth <
   2349         document.documentElement.offsetHeight;
   2350     var mostVisitedContainer = $('most_visited_container');
   2351     var mostVisitedList = $('most_visited_list');
   2352     var recentlyClosedContainer = $('recently_closed_container');
   2353     var recentlyClosedList = $('recently_closed_list');
   2354 
   2355     setCenterIconGrid(mostVisitedContainer, !isPortrait);
   2356     setCenterIconGrid(mostVisitedList, isPortrait);
   2357     setCenterIconGrid(recentlyClosedContainer, isPortrait);
   2358     if (isPortrait) {
   2359       recentlyClosedList.classList.add(GRID_CSS_CLASS);
   2360     } else {
   2361       recentlyClosedList.classList.remove(GRID_CSS_CLASS);
   2362     }
   2363 
   2364     // Make the recently closed list visually left align with the most recently
   2365     // closed items in landscape mode.  It will be reset by the grid centering
   2366     // in portrait mode.
   2367     if (!isPortrait)
   2368       recentlyClosedContainer.style.paddingLeft = '14px';
   2369   }
   2370 
   2371   /**
   2372    * This handles updating some of the spacing to make the 'recently closed'
   2373    * section appear at the bottom of the page.
   2374    */
   2375   function updateMostVisitedHeight() {
   2376     if (!isTablet())
   2377       return;
   2378     // subtract away height of button bar
   2379     var windowHeight = document.documentElement.offsetHeight;
   2380     var padding = parseInt(window.getComputedStyle(document.body)
   2381         .getPropertyValue('padding-bottom'));
   2382     $('most_visited_container').style.minHeight =
   2383         (windowHeight - padding) + 'px';
   2384   }
   2385 
   2386   /**
   2387    * Called by the native toolbar to open a different section. This handles
   2388    * updating the hash url which in turns makes a history entry.
   2389    *
   2390    * @param {string} section The section to switch to.
   2391    */
   2392   var openSection = function(section) {
   2393     if (!scrollToPane(getPaneIndex(section)))
   2394       return;
   2395     // Update the url so the native toolbar knows the pane has changed and
   2396     // to create a history entry.
   2397     document.location.hash = '#' + section;
   2398   }
   2399 
   2400   /////////////////////////////////////////////////////////////////////////////
   2401   // NTP Scoped Window Event Listeners.
   2402   /////////////////////////////////////////////////////////////////////////////
   2403 
   2404   /**
   2405    * Handles history on pop state changes.
   2406    */
   2407   function onPopStateHandler(event) {
   2408     if (event.state != null) {
   2409       var evtState = event.state;
   2410       // Navigate back to the previously selected panel and ensure the same
   2411       // bookmarks are loaded.
   2412       var selectedPaneIndex = evtState.selectedPaneIndex == undefined ?
   2413           0 : evtState.selectedPaneIndex;
   2414 
   2415       scrollToPane(selectedPaneIndex);
   2416       setCurrentBookmarkFolderData(evtState.folderId);
   2417     } else {
   2418       // When loading the page, replace the default state with one that
   2419       // specifies the default panel loaded via localStorage as well as the
   2420       // default bookmark folder.
   2421       history.replaceState(
   2422           {folderId: bookmarkFolderId, selectedPaneIndex: currentPaneIndex},
   2423           null, null);
   2424     }
   2425   }
   2426 
   2427   /**
   2428    * Handles window resize events.
   2429    */
   2430   function windowResizeHandler() {
   2431     // Scroll to the current pane to refactor all the margins and offset.
   2432     scrollToPane(currentPaneIndex);
   2433     computeDynamicLayout();
   2434     // Center the padding for each of the grid views.
   2435     centerChildGrids(document);
   2436     centerEmptySections(document);
   2437   }
   2438 
   2439   /*
   2440    * We implement the context menu ourselves.
   2441    */
   2442   function contextMenuHandler(evt) {
   2443     var section = SectionType.UNKNOWN;
   2444     contextMenuUrl = null;
   2445     contextMenuItem = null;
   2446     // The node with a menu have been tagged with their section and url.
   2447     // Let's find these tags.
   2448     var node = evt.target;
   2449     while (node) {
   2450       if (section == SectionType.UNKNOWN &&
   2451           node.getAttribute &&
   2452           node.getAttribute(SECTION_KEY) != null) {
   2453         section = node.getAttribute(SECTION_KEY);
   2454         if (contextMenuUrl != null)
   2455           break;
   2456       }
   2457       if (contextMenuUrl == null) {
   2458         contextMenuUrl = node.getAttribute(CONTEXT_MENU_URL_KEY);
   2459         contextMenuItem = node.contextMenuItem;
   2460         if (section != SectionType.UNKNOWN)
   2461           break;
   2462       }
   2463       node = node.parentNode;
   2464     }
   2465 
   2466     var menuOptions;
   2467 
   2468     if (section == SectionType.BOOKMARKS &&
   2469         !contextMenuItem.folder && !isIncognito) {
   2470       menuOptions = [
   2471         [
   2472           ContextMenuItemIds.BOOKMARK_OPEN_IN_NEW_TAB,
   2473           templateData.elementopeninnewtab
   2474         ]
   2475       ];
   2476       if (isIncognitoEnabled) {
   2477         menuOptions.push([
   2478           ContextMenuItemIds.BOOKMARK_OPEN_IN_INCOGNITO_TAB,
   2479           templateData.elementopeninincognitotab
   2480         ]);
   2481       }
   2482       if (contextMenuItem.editable) {
   2483         menuOptions.push(
   2484             [ContextMenuItemIds.BOOKMARK_EDIT, templateData.bookmarkedit],
   2485             [ContextMenuItemIds.BOOKMARK_DELETE, templateData.bookmarkdelete]);
   2486       }
   2487       if (contextMenuUrl.search('chrome://') == -1 &&
   2488           contextMenuUrl.search('about://') == -1 &&
   2489           document.body.getAttribute('shortcut_item_enabled') == 'true') {
   2490         menuOptions.push([
   2491           ContextMenuItemIds.BOOKMARK_SHORTCUT,
   2492           templateData.bookmarkshortcut
   2493         ]);
   2494       }
   2495     } else if (section == SectionType.BOOKMARKS &&
   2496                !contextMenuItem.folder &&
   2497                isIncognito) {
   2498       menuOptions = [
   2499         [
   2500           ContextMenuItemIds.BOOKMARK_OPEN_IN_INCOGNITO_TAB,
   2501           templateData.elementopeninincognitotab
   2502         ]
   2503       ];
   2504     } else if (section == SectionType.BOOKMARKS &&
   2505                contextMenuItem.folder &&
   2506                contextMenuItem.editable &&
   2507                !isIncognito) {
   2508       menuOptions = [
   2509         [ContextMenuItemIds.BOOKMARK_EDIT, templateData.editfolder],
   2510         [ContextMenuItemIds.BOOKMARK_DELETE, templateData.deletefolder]
   2511       ];
   2512     } else if (section == SectionType.MOST_VISITED) {
   2513       menuOptions = [
   2514         [
   2515           ContextMenuItemIds.MOST_VISITED_OPEN_IN_NEW_TAB,
   2516           templateData.elementopeninnewtab
   2517         ],
   2518       ];
   2519       if (isIncognitoEnabled) {
   2520         menuOptions.push([
   2521           ContextMenuItemIds.MOST_VISITED_OPEN_IN_INCOGNITO_TAB,
   2522           templateData.elementopeninincognitotab
   2523         ]);
   2524       }
   2525       menuOptions.push(
   2526         [ContextMenuItemIds.MOST_VISITED_REMOVE, templateData.elementremove]);
   2527     } else if (section == SectionType.RECENTLY_CLOSED) {
   2528       menuOptions = [
   2529         [
   2530           ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_NEW_TAB,
   2531           templateData.elementopeninnewtab
   2532         ],
   2533       ];
   2534       if (isIncognitoEnabled) {
   2535         menuOptions.push([
   2536           ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_INCOGNITO_TAB,
   2537           templateData.elementopeninincognitotab
   2538         ]);
   2539       }
   2540       menuOptions.push(
   2541         [ContextMenuItemIds.RECENTLY_CLOSED_REMOVE, templateData.removeall]);
   2542     } else if (section == SectionType.FOREIGN_SESSION_HEADER) {
   2543       menuOptions = [
   2544         [
   2545           ContextMenuItemIds.FOREIGN_SESSIONS_REMOVE,
   2546           templateData.elementremove
   2547         ]
   2548       ];
   2549     } else if (section == SectionType.PROMO_VC_SESSION_HEADER) {
   2550       menuOptions = [
   2551         [
   2552           ContextMenuItemIds.PROMO_VC_SESSION_REMOVE,
   2553           templateData.elementremove
   2554         ]
   2555       ];
   2556     }
   2557 
   2558     if (menuOptions)
   2559       chrome.send('showContextMenu', menuOptions);
   2560 
   2561     return false;
   2562   }
   2563 
   2564   // Return an object with all the exports
   2565   return {
   2566     bookmarks: bookmarks,
   2567     bookmarkChanged: bookmarkChanged,
   2568     clearPromotions: clearPromotions,
   2569     init: init,
   2570     setIncognitoEnabled: setIncognitoEnabled,
   2571     onCustomMenuSelected: onCustomMenuSelected,
   2572     openSection: openSection,
   2573     setFaviconDominantColor: setFaviconDominantColor,
   2574     setForeignSessions: setForeignSessions,
   2575     setIncognitoMode: setIncognitoMode,
   2576     setMostVisitedPages: setMostVisitedPages,
   2577     setPromotions: setPromotions,
   2578     setRecentlyClosedTabs: setRecentlyClosedTabs,
   2579     setSyncEnabled: setSyncEnabled,
   2580     snapshots: snapshots
   2581   };
   2582 });
   2583 
   2584 /////////////////////////////////////////////////////////////////////////////
   2585 //Utility Functions.
   2586 /////////////////////////////////////////////////////////////////////////////
   2587 
   2588 /**
   2589  * A best effort approach for checking simple data object equality.
   2590  * @param {?} val1 The first value to check equality for.
   2591  * @param {?} val2 The second value to check equality for.
   2592  * @return {boolean} Whether the two objects are equal(ish).
   2593  */
   2594 function equals(val1, val2) {
   2595   if (typeof val1 != 'object' || typeof val2 != 'object')
   2596     return val1 === val2;
   2597 
   2598   // Object and array equality checks.
   2599   var keyCountVal1 = 0;
   2600   for (var key in val1) {
   2601     if (!(key in val2) || !equals(val1[key], val2[key]))
   2602       return false;
   2603     keyCountVal1++;
   2604   }
   2605   var keyCountVal2 = 0;
   2606   for (var key in val2)
   2607     keyCountVal2++;
   2608   if (keyCountVal1 != keyCountVal2)
   2609     return false;
   2610   return true;
   2611 }
   2612 
   2613 /**
   2614  * Alias for document.getElementById.
   2615  * @param {string} id The ID of the element to find.
   2616  * @return {HTMLElement} The found element or null if not found.
   2617  */
   2618 function $(id) {
   2619   return document.getElementById(id);
   2620 }
   2621 
   2622 /**
   2623  * @return {boolean} Whether the device is currently in portrait mode.
   2624  */
   2625 function isPortrait() {
   2626   return document.documentElement.offsetWidth <
   2627       document.documentElement.offsetHeight;
   2628 }
   2629 
   2630 /**
   2631  * Determine if the page should be formatted for tablets.
   2632  * @return {boolean} true if the device is a tablet, false otherwise.
   2633  */
   2634 function isTablet() {
   2635   return document.body.getAttribute('device') == 'tablet';
   2636 }
   2637 
   2638 /**
   2639  * Determine if the page should be formatted for phones.
   2640  * @return {boolean} true if the device is a phone, false otherwise.
   2641  */
   2642 function isPhone() {
   2643   return document.body.getAttribute('device') == 'phone';
   2644 }
   2645 
   2646 /**
   2647  * Get the page X coordinate of a touch event.
   2648  * @param {TouchEvent} evt The touch event triggered by the browser.
   2649  * @return {number} The page X coordinate of the touch event.
   2650  */
   2651 function getTouchEventX(evt) {
   2652   return (evt.touches[0] || e.changedTouches[0]).pageX;
   2653 }
   2654 
   2655 /**
   2656  * Get the page Y coordinate of a touch event.
   2657  * @param {TouchEvent} evt The touch event triggered by the browser.
   2658  * @return {number} The page Y coordinate of the touch event.
   2659  */
   2660 function getTouchEventY(evt) {
   2661   return (evt.touches[0] || e.changedTouches[0]).pageY;
   2662 }
   2663 
   2664 /**
   2665  * @param {Element} el The item to get the width of.
   2666  * @param {boolean} excludeMargin If true, exclude the width of the margin.
   2667  * @return {number} The total width of a given item.
   2668  */
   2669 function getItemWidth(el, excludeMargin) {
   2670   var elStyle = window.getComputedStyle(el);
   2671   var width = el.offsetWidth;
   2672   if (!width || width == 0) {
   2673     width = parseInt(elStyle.getPropertyValue('width'));
   2674     width +=
   2675         parseInt(elStyle.getPropertyValue('border-left-width')) +
   2676         parseInt(elStyle.getPropertyValue('border-right-width'));
   2677     width +=
   2678         parseInt(elStyle.getPropertyValue('padding-left')) +
   2679         parseInt(elStyle.getPropertyValue('padding-right'));
   2680   }
   2681   if (!excludeMargin) {
   2682     width += parseInt(elStyle.getPropertyValue('margin-left')) +
   2683         parseInt(elStyle.getPropertyValue('margin-right'));
   2684   }
   2685   return width;
   2686 }
   2687 
   2688 /**
   2689  * @return {number} The padding height of the body due to the button bar
   2690  */
   2691 function getButtonBarPadding() {
   2692   var body = document.getElementsByTagName('body')[0];
   2693   var style = window.getComputedStyle(body);
   2694   return parseInt(style.getPropertyValue('padding-bottom'));
   2695 }
   2696 
   2697 /**
   2698  * Modify a css rule
   2699  * @param {string} selector The selector for the rule (passed to findCssRule())
   2700  * @param {string} property The property to update
   2701  * @param {string} value The value to update the property to
   2702  * @return {boolean} true if the rule was updated, false otherwise.
   2703  */
   2704 function modifyCssRule(selector, property, value) {
   2705   var rule = findCssRule(selector);
   2706   if (!rule)
   2707     return false;
   2708   rule.style[property] = value;
   2709   return true;
   2710 }
   2711 
   2712 /**
   2713  * Find a particular CSS rule.  The stylesheets attached to the document
   2714  * are traversed in reverse order.  The rules in each stylesheet are also
   2715  * traversed in reverse order.  The first rule found to match the selector
   2716  * is returned.
   2717  * @param {string} selector The selector for the rule.
   2718  * @return {Object} The rule if one was found, null otherwise
   2719  */
   2720 function findCssRule(selector) {
   2721   var styleSheets = document.styleSheets;
   2722   for (i = styleSheets.length - 1; i >= 0; i--) {
   2723     var styleSheet = styleSheets[i];
   2724     var rules = styleSheet.cssRules;
   2725     if (rules == null)
   2726       continue;
   2727     for (j = rules.length - 1; j >= 0; j--) {
   2728       if (rules[j].selectorText == selector)
   2729         return rules[j];
   2730     }
   2731   }
   2732 }
   2733 
   2734 /////////////////////////////////////////////////////////////////////////////
   2735 // NTP Entry point.
   2736 /////////////////////////////////////////////////////////////////////////////
   2737 
   2738 /*
   2739  * Handles initializing the UI when the page has finished loading.
   2740  */
   2741 window.addEventListener('DOMContentLoaded', function(evt) {
   2742   ntp.init();
   2743   $('content-area').style.display = 'block';
   2744 });
   2745