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