• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
13var PRESS_START_EVT = 'touchstart';
14
15/**
16 * The event type used to determine when a touch finishes.
17 * @type {string}
18 */
19var PRESS_STOP_EVT = 'touchend';
20
21/**
22 * The event type used to determine when a touch moves.
23 * @type {string}
24 */
25var PRESS_MOVE_EVT = 'touchmove';
26
27cr.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 */
2594function 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 */
2618function $(id) {
2619  return document.getElementById(id);
2620}
2621
2622/**
2623 * @return {boolean} Whether the device is currently in portrait mode.
2624 */
2625function 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 */
2634function 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 */
2642function 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 */
2651function 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 */
2660function 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 */
2669function 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 */
2691function 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 */
2704function 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 */
2720function 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 */
2741window.addEventListener('DOMContentLoaded', function(evt) {
2742  ntp.init();
2743  $('content-area').style.display = 'block';
2744});
2745