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.
6 // Contains all the necessary functions for rendering the NTP on mobile
10 * The event type used to determine when a touch starts.
13 var PRESS_START_EVT = 'touchstart';
16 * The event type used to determine when a touch finishes.
19 var PRESS_STOP_EVT = 'touchend';
22 * The event type used to determine when a touch moves.
25 var PRESS_MOVE_EVT = 'touchmove';
27 cr.define('ntp', function() {
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.
34 var DEFAULT_BOOKMARK_FOLDER_KEY = 'defaultBookmarkFolder';
37 * Constant for the localStorage key used to store whether or not sync was
38 * enabled on the last call to syncEnabled().
41 var SYNC_ENABLED_KEY = 'syncEnabled';
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
49 var ACTIVE_ITEM_DELAY_MS = 100;
52 * The CSS class identifier for grid layouts.
55 var GRID_CSS_CLASS = 'icon-grid';
58 * The element to center when centering a GRID_CSS_CLASS.
60 var GRID_CENTER_CSS_CLASS = 'center-icon-grid';
63 * Attribute used to specify the number of columns to use in a grid. If
64 * left unspecified, the grid will fill the container.
66 var GRID_COLUMNS = 'grid-columns';
69 * Attribute used to specify whether the top margin should be set to match
70 * the left margin of the grid.
72 var GRID_SET_TOP_MARGIN_CLASS = 'grid-set-top-margin';
75 * Attribute used to specify whether the margins of individual items within
76 * the grid should be adjusted to better fill the space.
78 var GRID_SET_ITEM_MARGINS = 'grid-set-item-margins';
81 * The CSS class identifier for centered empty section containers.
83 var CENTER_EMPTY_CONTAINER_CSS_CLASS = 'center-empty-container';
86 * The CSS class identifier for marking list items as active.
89 var ACTIVE_LIST_ITEM_CSS_CLASS = 'list-item-active';
92 * Attributes set on elements representing data in a section, specifying
93 * which section that element belongs to. Used for context menus.
96 var SECTION_KEY = 'sectionType';
99 * Attribute set on an element that has a context menu. Specifies the URL for
100 * which the context menu action should apply.
103 var CONTEXT_MENU_URL_KEY = 'url';
106 * The list of main section panes added.
107 * @type {Array.<Element>}
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
116 var sectionPrefixes = [];
119 * The next available index for new favicons. Users must increment this
120 * value once assigning this index to a favicon.
123 var faviconIndex = 0;
126 * The currently selected pane DOM element.
129 var currentPane = null;
132 * The index of the currently selected top level pane. The index corresponds
133 * to the elements defined in {@see #panes}.
136 var currentPaneIndex;
139 * The ID of the bookmark folder currently selected.
140 * @type {string|number}
142 var bookmarkFolderId = null;
145 * The current element active item.
151 * The element to be marked as active if no actions cancel it.
154 var pendingActiveItem;
157 * The timer ID to mark an element as active.
160 var activeItemDelayTimerId;
163 * Enum for the different load states based on the initialization of the NTP.
166 var LoadStatusType = {
168 LOAD_IMAGES_COMPLETE: 1,
169 LOAD_BOOKMARKS_FINISHED: 2,
170 LOAD_COMPLETE: 3 // An OR'd combination of all necessary states.
174 * The current loading status for the NTP.
175 * @type {LoadStatusType}
177 var loadStatus_ = LoadStatusType.LOAD_NOT_DONE;
180 * Whether the loading complete notification has been sent.
183 var finishedLoadingNotificationSent_ = false;
186 * Whether the page title has been loaded.
189 var titleLoadedStatus_ = false;
192 * Whether the NTP is in incognito mode or not.
195 var isIncognito = false;
198 * Whether incognito mode is enabled. (It can be blocked e.g. with a policy.)
201 var isIncognitoEnabled = true;
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
209 var replacedInitialState = false;
212 * Stores number of most visited pages.
215 var numberOfMostVisitedPages = 0;
218 * Whether there are any recently closed tabs.
221 var hasRecentlyClosedTabs = false;
224 * Whether promo is not allowed or not (external to NTP).
227 var promoIsAllowed = false;
230 * Whether promo should be shown on Most Visited page (externally set).
233 var promoIsAllowedOnMostVisited = false;
236 * Whether promo should be shown on Open Tabs page (externally set).
239 var promoIsAllowedOnOpenTabs = false;
242 * Whether promo should show a virtual computer on Open Tabs (externally set).
245 var promoIsAllowedAsVirtualComputer = false;
248 * Promo-injected title of a virtual computer on an open tabs pane.
251 var promoInjectedComputerTitleText = '';
254 * Promo-injected last synced text of a virtual computer on an open tabs pane.
257 var promoInjectedComputerLastSyncedText = '';
260 * The different sections that are displayed.
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',
275 * The different ids used of our custom context menu. Sent to the ChromeView
276 * and sent back when a menu is selected.
279 var ContextMenuItemIds = {
282 BOOKMARK_OPEN_IN_NEW_TAB: 2,
283 BOOKMARK_OPEN_IN_INCOGNITO_TAB: 3,
284 BOOKMARK_SHORTCUT: 4,
286 MOST_VISITED_OPEN_IN_NEW_TAB: 10,
287 MOST_VISITED_OPEN_IN_INCOGNITO_TAB: 11,
288 MOST_VISITED_REMOVE: 12,
290 RECENTLY_CLOSED_OPEN_IN_NEW_TAB: 20,
291 RECENTLY_CLOSED_OPEN_IN_INCOGNITO_TAB: 21,
292 RECENTLY_CLOSED_REMOVE: 22,
294 FOREIGN_SESSIONS_REMOVE: 30,
296 PROMO_VC_SESSION_REMOVE: 40,
300 * The URL of the element for the context menu.
303 var contextMenuUrl = null;
305 var contextMenuItem = null;
307 var currentSnapshots = null;
309 var currentSessions = null;
312 * The possible states of the sync section
318 DISPLAYING_LOADING: 2,
319 DISPLAYED_LOADING: 3,
324 * The current state of the sync section.
326 var syncState = SyncState.INITIAL;
329 * Whether or not sync is enabled. It will be undefined until
330 * setSyncEnabled() is called.
333 var syncEnabled = undefined;
336 * The current most visited data being displayed.
337 * @type {Array.<Object>}
339 var mostVisitedData_ = [];
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.
350 * Keep track of any outstanding timers related to updating the sync section.
352 var syncTimerId = -1;
355 * The minimum amount of time that 'Loading...' can be displayed. This is to
358 var SYNC_LOADING_TIMEOUT = 1000;
361 * How long to wait for sync data to load before displaying the 'Loading...'
364 var SYNC_INITIAL_LOAD_TIMEOUT = 1000;
367 * An array of images that are currently in loading state. Once an image
368 * loads it is removed from this array.
370 var imagesBeingLoaded = new Array();
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.
378 var bookmarkShortcutMode = false;
380 function setIncognitoMode(incognito) {
381 isIncognito = incognito;
383 chrome.send('getMostVisited');
384 chrome.send('getRecentlyClosedTabs');
385 chrome.send('getForeignSessions');
386 chrome.send('getPromotions');
387 chrome.send('getIncognitoDisabled');
391 function setIncognitoEnabled(item) {
392 isIncognitoEnabled = item.incognitoEnabled;
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.
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();
414 * Marks the given image as currently being loaded. Once all such images load
415 * we inform the browser via a hash change.
417 function trackImageLoad(url) {
418 if (finishedLoadingNotificationSent_)
421 for (var i = 0; i < imagesBeingLoaded.length; ++i) {
422 if (imagesBeingLoaded[i].src == url)
426 loadStatus_ &= (~LoadStatusType.LOAD_IMAGES_COMPLETE);
428 var image = new Image();
429 image.onload = onInitialImageLoaded;
430 image.onerror = onInitialImageLoaded;
432 imagesBeingLoaded.push(image);
436 * Initializes all the UI once the page has loaded.
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';
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';
458 // Initialize common widgets.
460 document.getElementsByClassName('section-title-wrapper');
461 for (var i = 0, len = titleScrollers.length; i < len; i++)
462 initializeTitleScroller(titleScrollers[i]);
464 // Initialize virtual computers for the sync promo.
465 createPromoVirtualComputers();
467 setCurrentBookmarkFolderData(
468 localStorage.getItem(DEFAULT_BOOKMARK_FOLDER_KEY));
470 addMainSection('incognito');
471 addMainSection('most_visited');
472 addMainSection('bookmarks');
473 addMainSection('open_tabs');
475 computeDynamicLayout();
477 scrollToPane(getPaneIndexFromHash());
478 updateSyncEmptyState();
480 window.onpopstate = onPopStateHandler;
481 window.addEventListener('hashchange', updatePaneOnHash);
482 window.addEventListener('resize', windowResizeHandler);
484 if (!bookmarkShortcutMode)
485 window.addEventListener('contextmenu', contextMenuHandler);
488 function sendNTPTitleLoadedNotification() {
489 if (!titleLoadedStatus_) {
490 titleLoadedStatus_ = true;
491 chrome.send('notifyNTPTitleLoaded');
496 * Notifies the chrome process of the status of the NTP.
498 function sendNTPNotification() {
499 if (loadStatus_ != LoadStatusType.LOAD_COMPLETE)
502 if (!finishedLoadingNotificationSent_) {
503 finishedLoadingNotificationSent_ = true;
504 chrome.send('notifyNTPReady');
506 // Navigating after the loading complete notification has been sent
507 // might break tests.
508 chrome.send('NTPUnexpectedNavigation');
513 * The default click handler for created item shortcuts.
515 * @param {Object} item The item specification.
516 * @param {function} evt The browser click event triggered.
518 function itemShortcutClickHandler(item, evt) {
519 // Handle the touch callback
520 if (item['folder']) {
521 browseToBookmarkFolder(item.id);
523 if (bookmarkShortcutMode) {
524 chrome.send('createHomeScreenBookmarkShortcut', [item.id]);
525 } else if (!!item.url) {
526 window.location = item.url;
532 * Opens a recently closed tab.
534 * @param {Object} item An object containing the necessary information to
537 function openRecentlyClosedTab(item, evt) {
538 chrome.send('openedRecentlyClosed');
539 chrome.send('reopenTab', [item.sessionId]);
543 * Creates a 'div' DOM element.
545 * @param {string} className The CSS class name for the DIV.
546 * @param {string=} opt_backgroundUrl The background URL to be applied to the
548 * @return {Element} The newly created DIV element.
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 + ')';
559 * Helper for creating new DOM elements.
561 * @param {string} type The type of Element to be created (i.e. 'div',
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.
567 function createElement(type, params) {
568 var el = document.createElement(type);
569 if (typeof params === 'string') {
570 el.className = params;
572 for (attr in params) {
573 el[attr] = params[attr];
580 * Adds a click listener to a specified element with the ability to override
581 * the default value of itemShortcutClickHandler.
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.
588 function wrapClickHandler(el, item, opt_clickCallback) {
589 el.addEventListener('click', function(evt) {
591 opt_clickCallback ? opt_clickCallback : itemShortcutClickHandler;
592 clickCallback(item, evt);
597 * Create a DOM element to contain a recently closed item for a tablet
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.
606 function makeRecentlyClosedTabletItem(item, opt_clickCallback) {
607 var cell = createDiv('cell');
609 cell.setAttribute(CONTEXT_MENU_URL_KEY, item.url);
611 var iconUrl = item.icon;
613 iconUrl = 'chrome://touch-icon/size/16@' + window.devicePixelRatio +
616 var icon = createDiv('icon', iconUrl);
617 trackImageLoad(iconUrl);
618 cell.appendChild(icon);
620 var title = createDiv('title');
621 title.textContent = item.title;
622 cell.appendChild(title);
624 wrapClickHandler(cell, item, opt_clickCallback);
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.
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.
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);
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 + ')';
669 thumbnailImg.src = backgroundUrl;
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');
679 title.insertBefore(spacerImg, title.firstChild);
680 thumbnailCell.appendChild(title);
682 var shade = createDiv('thumbnail-cell-shade');
683 thumbnailContainer.appendChild(shade);
684 addActiveTouchListener(shade, 'thumbnail-cell-shade-active');
686 wrapClickHandler(thumbnailCell, item, opt_clickCallback);
688 thumbnailCell.setAttribute(CONTEXT_MENU_URL_KEY, item.url);
689 thumbnailCell.contextMenuItem = item;
690 return thumbnailCell;
694 * Creates a shortcut DOM element based on the item specified item
695 * configuration using the favicon layout used for bookmarks.
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.
703 function makeBookmarkItem(item, opt_clickCallback) {
704 var holder = createDiv('favicon-cell');
705 addActiveTouchListener(holder, 'favicon-cell-active');
707 holder.setAttribute(CONTEXT_MENU_URL_KEY, item.url);
708 holder.contextMenuItem = item;
709 var faviconBox = createDiv('favicon-box');
711 faviconBox.classList.add('folder');
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);
718 var image = new Image();
720 image.onload = function() {
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');
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 + ')';
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);
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]);
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');
752 // It's an html5 icon (or at least it's larger).
753 // Rescale it to be no bigger than 64x64 dip.
755 if (w > max || h > max) {
756 var scale = (w > h) ? (max / w) : (max / h);
760 faviconIcon.style.backgroundSize = w + 'px ' + h + 'px';
763 faviconBox.appendChild(faviconIcon);
765 holder.appendChild(faviconBox);
767 var title = createDiv('title');
768 title.textContent = item.title;
769 holder.appendChild(title);
771 wrapClickHandler(holder, item, opt_clickCallback);
777 * Adds touch listeners to the specified element to apply a class when it is
778 * selected (removing the class when no longer pressed).
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.
783 function addActiveTouchListener(el, activeClass) {
784 if (!window.touchCancelListener) {
785 window.touchCancelListener = function(evt) {
786 if (activeItemDelayTimerId) {
787 clearTimeout(activeItemDelayTimerId);
788 activeItemDelayTimerId = undefined;
793 activeItem.classList.remove(activeItem.dataset.activeClass);
796 document.addEventListener('touchcancel', window.touchCancelListener);
798 el.dataset.activeClass = activeClass;
799 el.addEventListener(PRESS_START_EVT, function(evt) {
800 if (activeItemDelayTimerId) {
801 clearTimeout(activeItemDelayTimerId);
802 activeItemDelayTimerId = undefined;
804 activeItemDelayTimerId = setTimeout(function() {
805 el.classList.add(activeClass);
807 }, ACTIVE_ITEM_DELAY_MS);
809 el.addEventListener(PRESS_STOP_EVT, function(evt) {
810 if (activeItemDelayTimerId) {
811 clearTimeout(activeItemDelayTimerId);
812 activeItemDelayTimerId = undefined;
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');
828 * Creates a shortcut DOM element based on the item specified in the list
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.
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'
850 listItem.appendChild(title);
852 listItem.addEventListener('click', function(evt) {
854 opt_clickCallback ? opt_clickCallback : itemShortcutClickHandler;
855 clickCallback(item, evt);
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'));
862 listItem.classList.add('standard-divider');
868 * Creates a DOM list entry for a remote session or tab.
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.
876 function makeForeignSessionListEntry(item, opt_clickCallback) {
878 var sessionOuterDiv = createDiv('list-item standard-divider');
879 addActiveTouchListener(sessionOuterDiv, ACTIVE_LIST_ITEM_CSS_CLASS);
880 sessionOuterDiv.contextMenuItem = item;
882 var icon = createDiv('session-icon ' + item.iconStyle);
883 sessionOuterDiv.appendChild(icon);
885 var titleContainer = createElement('div', 'title');
886 sessionOuterDiv.appendChild(titleContainer);
888 // Extra container to allow title & last-sync time to stack vertically.
889 var sessionInnerDiv = createDiv('session_container');
890 titleContainer.appendChild(sessionInnerDiv);
892 var title = createDiv('session-name');
893 title.textContent = item.title;
894 title.id = item.titleId || '';
895 sessionInnerDiv.appendChild(title);
897 var lastSynced = createDiv('session-last-synced');
898 lastSynced.textContent =
899 templateData.opentabslastsynced + ': ' + item.userVisibleTimestamp;
900 lastSynced.id = item.userVisibleTimestampId || '';
901 sessionInnerDiv.appendChild(lastSynced);
903 sessionOuterDiv.addEventListener('click', function(evt) {
905 opt_clickCallback ? opt_clickCallback : itemShortcutClickHandler;
906 clickCallback(item, evt);
908 return sessionOuterDiv;
912 * Saves the number of most visited pages and updates promo visibility.
913 * @param {number} n Number of most visited pages.
915 function setNumberOfMostVisitedPages(n) {
916 numberOfMostVisitedPages = n;
917 updatePromoVisibility();
921 * Saves the recently closed tabs flag and updates promo visibility.
922 * @param {boolean} anyTabs Whether there are any recently closed tabs.
924 function setHasRecentlyClosedTabs(anyTabs) {
925 hasRecentlyClosedTabs = anyTabs;
926 updatePromoVisibility();
930 * Updates the most visited pages.
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
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);
946 data.forEach(function(item, index) {
947 item.mostVisitedIndex = index;
950 if (equals(data, mostVisitedData_))
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;
959 populateData(findList('most_visited'), SectionType.MOST_VISITED, data,
960 makeMostVisitedItem, clickFunction);
961 computeDynamicLayout();
963 mostVisitedData_ = data;
967 * Updates the recently closed tabs.
969 * @param {Array.<Object>} List of data for displaying the list of recently
970 * closed tabs (see C++ handler for model description).
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);
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);
986 computeDynamicLayout();
990 * Updates the bookmarks.
992 * @param {Array.<Object>} List of data for displaying the bookmarks (see
993 * C++ handler for model description).
995 function bookmarks(data) {
996 bookmarkFolderId = data.id;
997 if (!replacedInitialState) {
998 history.replaceState(
999 {folderId: bookmarkFolderId, selectedPaneIndex: currentPaneIndex},
1001 replacedInitialState = true;
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;
1010 var titleWrapper = $('bookmarks_title_wrapper');
1011 setBookmarkTitleHierarchy(
1012 titleWrapper, data, data['hierarchy']);
1014 var filteredBookmarks = data.bookmarks;
1016 filteredBookmarks = filteredBookmarks.filter(function(val) {
1017 return (val.type != 'BOOKMARK_BAR' && val.type != 'OTHER_NODE');
1020 if (bookmarkShortcutMode) {
1021 populateData(findList('bookmarks'), SectionType.BOOKMARKS,
1022 filteredBookmarks, makeBookmarkItem);
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;
1032 populateData(findList('bookmarks'), SectionType.BOOKMARKS,
1033 filteredBookmarks, makeBookmarkItem, clickFunction);
1036 var bookmarkContainer = $('bookmarks_container');
1038 // update the shadows on the breadcrumb bar
1039 computeDynamicLayout();
1041 if ((loadStatus_ & LoadStatusType.LOAD_BOOKMARKS_FINISHED) !=
1042 LoadStatusType.LOAD_BOOKMARKS_FINISHED) {
1043 loadStatus_ |= LoadStatusType.LOAD_BOOKMARKS_FINISHED;
1044 sendNTPNotification();
1049 * Checks if promo is allowed and MostVisited requirements are satisfied.
1050 * @return {boolean} Whether the promo should be shown on most_visited.
1052 function shouldPromoBeShownOnMostVisited() {
1053 return promoIsAllowed && promoIsAllowedOnMostVisited &&
1054 numberOfMostVisitedPages >= 2 && !hasRecentlyClosedTabs;
1058 * Checks if promo is allowed and OpenTabs requirements are satisfied.
1059 * @return {boolean} Whether the promo should be shown on open_tabs.
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);
1070 * Checks if promo is allowed and SyncPromo requirements are satisfied.
1071 * @return {boolean} Whether the promo should be shown on sync_promo.
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);
1082 * Records a promo impression on a given section if necessary.
1083 * @param {string} section Active section name to check.
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']);
1095 * Updates the visibility on all promo-related items as necessary.
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) ?
1114 * Called from native.
1115 * Clears the promotion.
1117 function clearPromotions() {
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.
1127 function setPromotionHtml(el, html) {
1131 var tags = ['BR', 'DIV', 'BUTTON', 'SPAN'];
1133 class: function(node, value) { return true; },
1134 style: function(node, value) { return true; },
1137 var fragment = parseHtmlSubset(html, tags, attrs);
1138 el.appendChild(fragment);
1140 console.error(err.toString());
1141 // Ignore all errors while parsing or setting the element.
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.
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');
1157 promoIsAllowed = !!promotions.promoIsAllowed;
1158 promoIsAllowedOnMostVisited = !!promotions.promoIsAllowedOnMostVisited;
1159 promoIsAllowedOnOpenTabs = !!promotions.promoIsAllowedOnOpenTabs;
1160 promoIsAllowedAsVirtualComputer = !!promotions.promoIsAllowedAsVC;
1162 setPromotionHtml(mostVisitedEl, promotions.promoMessage);
1163 setPromotionHtml(openTabsEl, promotions.promoMessage);
1164 setPromotionHtml(syncPromoReceivedEl, promotions.promoMessageLong);
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;
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');
1183 updatePromoVisibility();
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.
1191 function executePromoAction(evt) {
1192 evt.preventDefault();
1193 chrome.send('promoActionTriggered');
1197 * Called by the browser when a context menu has been selected.
1199 * @param {number} itemId The id of the item that was selected, as specified
1200 * when chrome.send('showContextMenu') was called.
1202 function onCustomMenuSelected(itemId) {
1203 if (contextMenuUrl != null) {
1205 case ContextMenuItemIds.BOOKMARK_OPEN_IN_NEW_TAB:
1206 case ContextMenuItemIds.BOOKMARK_OPEN_IN_INCOGNITO_TAB:
1207 chrome.send('openedBookmark');
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,
1221 case ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_NEW_TAB:
1222 case ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_INCOGNITO_TAB:
1223 chrome.send('openedRecentlyClosed');
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]);
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]);
1243 case ContextMenuItemIds.BOOKMARK_EDIT:
1244 if (contextMenuItem != null)
1245 chrome.send('editBookmark', [contextMenuItem.id]);
1248 case ContextMenuItemIds.BOOKMARK_DELETE:
1249 if (contextMenuUrl != null)
1250 chrome.send('deleteBookmark', [contextMenuItem.id]);
1253 case ContextMenuItemIds.MOST_VISITED_REMOVE:
1254 if (contextMenuUrl != null)
1255 chrome.send('blacklistURLFromMostVisited', [contextMenuUrl]);
1258 case ContextMenuItemIds.BOOKMARK_SHORTCUT:
1259 if (contextMenuUrl != null)
1260 chrome.send('createHomeScreenBookmarkShortcut', [contextMenuItem.id]);
1263 case ContextMenuItemIds.RECENTLY_CLOSED_REMOVE:
1264 chrome.send('clearRecentlyClosed');
1267 case ContextMenuItemIds.FOREIGN_SESSIONS_REMOVE:
1268 if (contextMenuItem != null) {
1270 'deleteForeignSession', [contextMenuItem.sessionTag]);
1271 chrome.send('getForeignSessions');
1275 case ContextMenuItemIds.PROMO_VC_SESSION_REMOVE:
1276 chrome.send('promoDisabled');
1280 log.error('Unknown context menu selected id=' + itemId);
1286 * Generates the full bookmark folder hierarchy and populates the scrollable
1289 * @param {Element} wrapperEl The wrapper element containing the scrollable
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
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.
1300 function setBookmarkTitleHierarchy(wrapperEl, data, opt_ancestry) {
1301 var title = wrapperEl.getElementsByClassName('section-title')[0];
1302 title.innerHTML = '';
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'));
1310 var titleCrumb = createBookmarkTitleCrumb_(data);
1311 titleCrumb.classList.add('title-crumb-active');
1312 title.appendChild(titleCrumb);
1314 // Ensure the last crumb is as visible as possible.
1316 wrapperEl.getElementsByClassName('section-title-mask')[0].offsetWidth;
1317 var crumbWidth = titleCrumb.offsetWidth;
1318 var leftOffset = titleCrumb.offsetLeft;
1320 var shiftLeft = windowWidth - crumbWidth - leftOffset;
1321 if (shiftLeft < 0) {
1322 if (crumbWidth > windowWidth)
1323 shifLeft = -leftOffset;
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);
1331 handleTitleScroll(wrapperEl, 0);
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.
1342 function createBookmarkTitleCrumb_(data) {
1343 var titleCrumb = createDiv('title-crumb');
1345 titleCrumb.innerText = templateData.bookmarkstitle;
1347 titleCrumb.innerText = data.title;
1349 titleCrumb.addEventListener('click', function(evt) {
1350 browseToBookmarkFolder(data.root ? '0' : data.id);
1356 * Handles scrolling a title element.
1357 * @param {Element} wrapperEl The wrapper element containing the scrollable
1359 * @param {number} scrollPosition The position to be scrolled to.
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;
1371 var maxRightScroll = containerWidth - titleWidth;
1372 var boundedScrollPosition =
1373 Math.max(maxRightScroll, Math.min(scrollPosition, 0));
1375 overflowLeftMask.style.opacity =
1378 (Math.max(0, -boundedScrollPosition)) + 10 / 30);
1380 overflowRightMask.style.opacity =
1383 (Math.max(0, boundedScrollPosition - maxRightScroll) + 10) / 30);
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 = '';
1390 title.style.left = boundedScrollPosition + 'px';
1395 * Initializes a scrolling title element.
1396 * @param {Element} wrapperEl The wrapper element of the scrolling title.
1398 function initializeTitleScroller(wrapperEl) {
1399 var title = wrapperEl.getElementsByClassName('section-title')[0];
1401 var inTitleScroll = false;
1402 var startingScrollPosition;
1404 wrapperEl.addEventListener(PRESS_START_EVT, function(evt) {
1405 inTitleScroll = true;
1406 startingScrollPosition = getTouchEventX(evt);
1407 startingOffset = title.offsetLeft;
1409 document.body.addEventListener(PRESS_STOP_EVT, function(evt) {
1412 inTitleScroll = false;
1414 document.body.addEventListener(PRESS_MOVE_EVT, function(evt) {
1419 startingOffset - (startingScrollPosition - getTouchEventX(evt)));
1420 evt.stopPropagation();
1425 * Handles updates from the underlying bookmark model (calls originate
1426 * in the WebUI handler for bookmarks).
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.
1435 function bookmarkChanged(status) {
1437 var affectedParentNode = status['parent_id'];
1438 var affectedNodeId = status['node_id'];
1439 var shouldUpdate = (bookmarkFolderId == affectedParentNode ||
1440 bookmarkFolderId == affectedNodeId);
1442 setCurrentBookmarkFolderData(bookmarkFolderId);
1444 // This typically happens when extensive changes could have happened to
1445 // the model, such as initial load, import and sync.
1446 setCurrentBookmarkFolderData(bookmarkFolderId);
1451 * Loads the bookarks data for a given folder.
1453 * @param {string|number} folderId The ID of the folder to load (or null if
1454 * it should load the root folder).
1456 function setCurrentBookmarkFolderData(folderId) {
1457 if (folderId != null) {
1458 chrome.send('getBookmarks', [folderId]);
1460 chrome.send('getBookmarks');
1463 if (folderId == null) {
1464 localStorage.removeItem(DEFAULT_BOOKMARK_FOLDER_KEY);
1466 localStorage.setItem(DEFAULT_BOOKMARK_FOLDER_KEY, folderId);
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
1476 * @param {string|number} folderId The ID of the folder to navigate to.
1478 function browseToBookmarkFolder(folderId) {
1480 {folderId: folderId, selectedPaneIndex: currentPaneIndex},
1482 setCurrentBookmarkFolderData(folderId);
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
1491 function setSyncEnabled(enabled) {
1493 if (syncEnabled != undefined && syncEnabled == enabled) {
1494 // The value didn't change
1497 syncEnabled = enabled;
1500 if (!localStorage.getItem(SYNC_ENABLED_KEY)) {
1501 localStorage.setItem(SYNC_ENABLED_KEY, 'true');
1502 setCurrentBookmarkFolderData('0');
1505 localStorage.removeItem(SYNC_ENABLED_KEY);
1507 updatePromoVisibility();
1510 // Bookmark data can now be displayed (or needs to be refiltered)
1511 bookmarks(bookmarkData);
1514 updateSyncEmptyState();
1519 * Handles adding or removing the 'nothing to see here' text from the session
1520 * list depending on the state of snapshots and sessions.
1522 * @param {boolean} Whether the call is occuring because of a schedule
1525 function updateSyncEmptyState(timeout) {
1526 if (syncState == SyncState.DISPLAYING_LOADING && !timeout) {
1527 // Make sure 'Loading...' is displayed long enough
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');
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
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
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;
1562 if (syncTimerId != -1) {
1563 clearTimeout(syncTimerId);
1566 syncState = SyncState.LOADED;
1568 // Hide everything by default, display selectively below
1569 syncEnableSync.style.display = 'none';
1570 syncLoading.style.display = 'none';
1571 syncPromo.style.display = 'none';
1573 var snapshotsCount =
1574 currentSnapshots == null ? 0 : currentSnapshots.length;
1575 var sessionsCount = currentSessions == null ? 0 : currentSessions.length;
1578 syncEnableSync.style.display = '-webkit-box';
1579 centerEmptySections(syncEnableSync);
1580 } else if (sessionsCount + snapshotsCount == 0) {
1581 syncPromo.style.display = '-webkit-box';
1582 centerEmptySections(syncPromo);
1584 openTabsList.style.display = sessionsCount == 0 ? 'none' : 'block';
1585 snapshotsList.style.display = snapshotsCount == 0 ? 'none' : 'block';
1587 updatePromoVisibility();
1591 * Called externally when updated snapshot data is available.
1593 * @param {Object} data The snapshot data
1595 function snapshots(data) {
1596 var list = findList('snapshots');
1597 list.innerHTML = '';
1599 currentSnapshots = data;
1600 updateSyncEmptyState();
1602 if (!data || data.length == 0)
1605 data.sort(function(a, b) {
1606 return b.createTime - a.createTime;
1609 // Create the main container
1610 var snapshotsEl = createElement('div');
1611 list.appendChild(snapshotsEl);
1613 // Create the header container
1614 var headerEl = createDiv('session-header');
1615 snapshotsEl.appendChild(headerEl);
1617 // Create the documents container
1618 var docsEl = createDiv('session-children-container');
1619 snapshotsEl.appendChild(docsEl);
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);
1626 // Create the header icon
1627 headerInnerEl.appendChild(createDiv('session-icon documents'));
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);
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);
1642 // Support for actually opening the document
1643 var snapshotClickCallback = function(item) {
1646 if (item.snapshotId) {
1647 window.location = 'chrome://snapshot/' + item.snapshotId;
1648 } else if (item.printJobId) {
1649 window.location = 'chrome://printjob/' + item.printJobId;
1651 window.location = item.url;
1655 // Finally, add the list of documents
1656 populateData(docsEl, SectionType.SNAPSHOTS, data,
1657 makeListEntryItem, snapshotClickCallback);
1661 * Create a function to handle expanding and collapsing a section
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
1668 function createExpandoFunction(expando, element) {
1669 expando.className = 'expando open';
1671 if (element.style.height != '0px') {
1672 // It seems that '-webkit-transition' only works when explicit pixel
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;
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
1685 setTimeout(function() {
1686 element.style.height = '0px';
1689 expando.className = 'expando closed';
1691 element.style.height = element.expandedHeight;
1692 expando.className = 'expando open';
1698 * Initializes the promo_vc_list div to look like a foreign session
1701 function createPromoVirtualComputers() {
1702 var list = findList('promo_vc');
1703 list.innerHTML = '';
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);
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);
1718 // Add support for expanding and collapsing the children.
1719 var expando = createDiv();
1720 var expandoFunction = createExpandoFunction(expando, sessionChildren);
1722 // Fill-in the contents of the "virtual computer" session header.
1724 'title': promoInjectedComputerTitleText,
1725 'titleId': 'promo_vc_title',
1726 'userVisibleTimestamp': promoInjectedComputerLastSyncedText,
1727 'userVisibleTimestampId': 'promo_vc_lastsync',
1728 'iconStyle': 'laptop'
1731 populateData(sessionHeader, SectionType.PROMO_VC_SESSION_HEADER, headerList,
1732 makeForeignSessionListEntry, expandoFunction);
1733 sessionHeader.appendChild(expando);
1737 * Called externally when updated synced sessions data is available.
1739 * @param {Object} data The snapshot data
1741 function setForeignSessions(data, tabSyncEnabled) {
1742 var list = findList('open_tabs');
1743 list.innerHTML = '';
1745 currentSessions = data;
1746 updateSyncEmptyState();
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) {
1755 } else if (a.timestamp == null) {
1758 return b.timestamp - a.timestamp;
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) {
1771 } else if (aWindows == null || aWindows.length == 0 ||
1772 aWindows[0].timestamp == null) {
1775 return bWindows[0].timestamp - aWindows[0].timestamp;
1779 data.forEach(function(client, clientNum) {
1781 var windows = client.windows;
1782 if (windows == null || windows.length == 0)
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);
1791 // Set up the container for the session children
1792 var sessionChildren = createDiv('session-children-container');
1793 sessionEl.appendChild(sessionChildren);
1795 var clientName = 'Client ' + clientNum;
1797 clientName = client.name;
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';
1812 console.error('Unknown sync device type found: ', deviceType);
1813 iconStyle = 'laptop';
1816 'title': clientName,
1817 'userVisibleTimestamp': windows[0].userVisibleTimestamp,
1818 'iconStyle': iconStyle,
1819 'sessionTag': client.tag,
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);
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;
1842 timestamp: tab.timestamp,
1845 sessionTag: client.tag,
1847 sessionId: tab.sessionId,
1850 divider: needSectionDivider ? 'section' : 'standard',
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;
1867 chrome.send('openedForeignSession');
1868 chrome.send('openForeignSession', [String(item.sessionTag),
1869 String(item.winNum), String(item.sessionId), buttonIndex,
1870 altKeyPressed, ctrlKeyPressed, metaKeyPressed, shiftKeyPressed]);
1872 populateData(sessionChildren, SectionType.FOREIGN_SESSION, openTabsList,
1873 makeListEntryItem, tabCallback);
1878 * Updates the dominant favicon color for a given index.
1880 * @param {number} index The index of the favicon whose dominant color is
1882 * @param {string} color The string encoded color.
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;
1889 var id = 'fold_' + index;
1893 var zoom = window.getComputedStyle(fold).zoom;
1894 var scale = 1 / window.getComputedStyle(fold).zoom;
1896 // The width/height of the canvas. Set to 24 so it looks good across all
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);
1906 ctx.lineTo(0, ch * 0.75 * scale);
1907 ctx.quadraticCurveTo(
1909 cw * .25 * scale, ch * scale);
1910 ctx.lineTo(cw * scale, ch * scale);
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)');
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)');
1925 gradient.addColorStop(1, color);
1926 ctx.fillStyle = gradient;
1930 ctx.lineWidth = Math.floor(scale);
1931 ctx.strokeStyle = color;
1933 ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
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.
1943 function findList(name) {
1944 return $(name + '_list');
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
1953 * @param {holder} The dom element that the generated list items will be put
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.
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 = '';
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);
1977 holder.appendChild(fragment);
1978 if (holder.classList.contains(GRID_CSS_CLASS))
1980 centerEmptySections(holder);
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
1987 * @param {Element} el Container holding the grid cell items.
1989 function centerGrid(el) {
1990 var childEl = el.firstChild;
1994 // Find the element to actually set the margins on.
1996 var curEl = toCenter;
1997 while (curEl && curEl.classList) {
1998 if (curEl.classList.contains(GRID_CENTER_CSS_CLASS)) {
2002 curEl = curEl.parentNode;
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';
2011 var numColumns = el.getAttribute(GRID_COLUMNS);
2013 numColumns = parseInt(numColumns);
2015 numColumns = Math.floor(windowWidth / itemWidth);
2018 if (setItemMargins) {
2019 // In this case, try to size each item to fill as much space as
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';
2030 itemWidth += childLeftMargin + childRightMargin;
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';
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';
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
2053 * @param {Element} el The node containing the grid child nodes.
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]);
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.
2067 function centerEmptySections(el) {
2069 el.classList.contains(CENTER_EMPTY_CONTAINER_CSS_CLASS)) {
2070 centerEmptySection(el);
2072 var empties = el.getElementsByClassName(CENTER_EMPTY_CONTAINER_CSS_CLASS);
2073 for (var i = 0; i < empties.length; i++) {
2074 centerEmptySection(empties[i]);
2079 * Set the top of the given element to the top of the parent and set the
2080 * height to (bottom of document - top).
2082 * @param {Element} el Container holding the centered content.
2084 function centerEmptySection(el) {
2085 var parent = el.parentNode;
2086 var top = parent.offsetTop;
2088 document.documentElement.offsetHeight - getButtonBarPadding());
2089 el.style.height = (bottom - top) + 'px';
2090 el.style.top = top + 'px';
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.
2098 function getPaneIndex(panePrefix) {
2099 var pane = $(panePrefix + '_container');
2102 var index = panes.indexOf(pane);
2111 * Finds the index of the panel specified by location hash.
2112 * @return {number} The index of the panel.
2114 function getPaneIndexFromHash() {
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');
2127 // Couldn't find a good section
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.
2138 function scrollToPane(paneIndex) {
2139 var pane = panes[paneIndex];
2141 if (pane == currentPane)
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)
2150 computeDynamicLayout();
2151 promoUpdateImpressions(sectionPrefixes[paneIndex]);
2156 * Updates the pane based on the current hash.
2158 function updatePaneOnHash() {
2159 var paneIndex = getPaneIndexFromHash();
2160 var pane = panes[paneIndex];
2163 currentPane.classList.remove('selected');
2164 pane.classList.add('selected');
2166 currentPaneIndex = paneIndex;
2168 setScrollTopForDocument(document, 0);
2170 var panelPrefix = sectionPrefixes[paneIndex];
2171 var title = templateData[panelPrefix + '_document_title'];
2173 title = templateData['title'];
2174 document.title = title;
2176 sendNTPTitleLoadedNotification();
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.
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.
2192 function addMainSection(panelPrefix) {
2193 var paneEl = $(panelPrefix + '_container');
2194 var paneIndex = panes.push(paneEl) - 1;
2195 sectionPrefixes.push(panelPrefix);
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).
2204 function computeDynamicLayout() {
2205 // Update the scrolling titles to ensure they are not in a now invalid
2207 var titleScrollers =
2208 document.getElementsByClassName('section-title-wrapper');
2209 for (var i = 0, len = titleScrollers.length; i < len; i++) {
2211 titleScrollers[i].getElementsByClassName('section-title')[0];
2214 titleEl.offsetLeft);
2217 updateMostVisitedStyle();
2218 updateMostVisitedHeight();
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
2227 function updateMostVisitedStyle() {
2229 updateMostVisitedStyleTablet();
2231 updateMostVisitedStylePhone();
2236 * Updates the style of the most visited pane for the phone.
2238 function updateMostVisitedStylePhone() {
2239 var mostVisitedList = $('most_visited_list');
2240 var childEl = mostVisitedList.firstChild;
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;
2252 // default vertical margin between items
2253 var itemMarginTop = 0;
2254 var itemMarginBottom = 0;
2255 var itemMarginLeft = 20;
2256 var itemMarginRight = 20;
2261 document.documentElement.offsetHeight -
2262 getButtonBarPadding();
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;
2273 mostVisitedList.setAttribute(GRID_COLUMNS, '3');
2274 listHeight = screenHeight;
2276 // If the screen height is less than targetHeight, scale the size of the
2277 // thumbnails such that the margin between the thumbnails remains
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;
2291 // scale the vertical margin such that the items fit perfectly on the
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);
2300 mostVisitedList.style.minHeight = listHeight + 'px';
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');
2309 'body[device="phone"] #most_visited_list .thumbnail-container',
2310 'height', thumbHeight + 'px');
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');
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');
2332 centerChildGrids($('most_visited_container'));
2336 * Updates the style of the most visited pane for the tablet.
2338 function updateMostVisitedStyleTablet() {
2339 function setCenterIconGrid(el, set) {
2341 el.classList.add(GRID_CENTER_CSS_CLASS);
2343 el.classList.remove(GRID_CENTER_CSS_CLASS);
2344 el.style.paddingLeft = '0px';
2345 el.style.paddingRight = '0px';
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');
2355 setCenterIconGrid(mostVisitedContainer, !isPortrait);
2356 setCenterIconGrid(mostVisitedList, isPortrait);
2357 setCenterIconGrid(recentlyClosedContainer, isPortrait);
2359 recentlyClosedList.classList.add(GRID_CSS_CLASS);
2361 recentlyClosedList.classList.remove(GRID_CSS_CLASS);
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.
2368 recentlyClosedContainer.style.paddingLeft = '14px';
2372 * This handles updating some of the spacing to make the 'recently closed'
2373 * section appear at the bottom of the page.
2375 function updateMostVisitedHeight() {
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';
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.
2390 * @param {string} section The section to switch to.
2392 var openSection = function(section) {
2393 if (!scrollToPane(getPaneIndex(section)))
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;
2400 /////////////////////////////////////////////////////////////////////////////
2401 // NTP Scoped Window Event Listeners.
2402 /////////////////////////////////////////////////////////////////////////////
2405 * Handles history on pop state changes.
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;
2415 scrollToPane(selectedPaneIndex);
2416 setCurrentBookmarkFolderData(evtState.folderId);
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},
2428 * Handles window resize events.
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);
2440 * We implement the context menu ourselves.
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;
2450 if (section == SectionType.UNKNOWN &&
2451 node.getAttribute &&
2452 node.getAttribute(SECTION_KEY) != null) {
2453 section = node.getAttribute(SECTION_KEY);
2454 if (contextMenuUrl != null)
2457 if (contextMenuUrl == null) {
2458 contextMenuUrl = node.getAttribute(CONTEXT_MENU_URL_KEY);
2459 contextMenuItem = node.contextMenuItem;
2460 if (section != SectionType.UNKNOWN)
2463 node = node.parentNode;
2468 if (section == SectionType.BOOKMARKS &&
2469 !contextMenuItem.folder && !isIncognito) {
2472 ContextMenuItemIds.BOOKMARK_OPEN_IN_NEW_TAB,
2473 templateData.elementopeninnewtab
2476 if (isIncognitoEnabled) {
2478 ContextMenuItemIds.BOOKMARK_OPEN_IN_INCOGNITO_TAB,
2479 templateData.elementopeninincognitotab
2482 if (contextMenuItem.editable) {
2484 [ContextMenuItemIds.BOOKMARK_EDIT, templateData.bookmarkedit],
2485 [ContextMenuItemIds.BOOKMARK_DELETE, templateData.bookmarkdelete]);
2487 if (contextMenuUrl.search('chrome://') == -1 &&
2488 contextMenuUrl.search('about://') == -1 &&
2489 document.body.getAttribute('shortcut_item_enabled') == 'true') {
2491 ContextMenuItemIds.BOOKMARK_SHORTCUT,
2492 templateData.bookmarkshortcut
2495 } else if (section == SectionType.BOOKMARKS &&
2496 !contextMenuItem.folder &&
2500 ContextMenuItemIds.BOOKMARK_OPEN_IN_INCOGNITO_TAB,
2501 templateData.elementopeninincognitotab
2504 } else if (section == SectionType.BOOKMARKS &&
2505 contextMenuItem.folder &&
2506 contextMenuItem.editable &&
2509 [ContextMenuItemIds.BOOKMARK_EDIT, templateData.editfolder],
2510 [ContextMenuItemIds.BOOKMARK_DELETE, templateData.deletefolder]
2512 } else if (section == SectionType.MOST_VISITED) {
2515 ContextMenuItemIds.MOST_VISITED_OPEN_IN_NEW_TAB,
2516 templateData.elementopeninnewtab
2519 if (isIncognitoEnabled) {
2521 ContextMenuItemIds.MOST_VISITED_OPEN_IN_INCOGNITO_TAB,
2522 templateData.elementopeninincognitotab
2526 [ContextMenuItemIds.MOST_VISITED_REMOVE, templateData.elementremove]);
2527 } else if (section == SectionType.RECENTLY_CLOSED) {
2530 ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_NEW_TAB,
2531 templateData.elementopeninnewtab
2534 if (isIncognitoEnabled) {
2536 ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_INCOGNITO_TAB,
2537 templateData.elementopeninincognitotab
2541 [ContextMenuItemIds.RECENTLY_CLOSED_REMOVE, templateData.removeall]);
2542 } else if (section == SectionType.FOREIGN_SESSION_HEADER) {
2545 ContextMenuItemIds.FOREIGN_SESSIONS_REMOVE,
2546 templateData.elementremove
2549 } else if (section == SectionType.PROMO_VC_SESSION_HEADER) {
2552 ContextMenuItemIds.PROMO_VC_SESSION_REMOVE,
2553 templateData.elementremove
2559 chrome.send('showContextMenu', menuOptions);
2564 // Return an object with all the exports
2566 bookmarks: bookmarks,
2567 bookmarkChanged: bookmarkChanged,
2568 clearPromotions: clearPromotions,
2570 setIncognitoEnabled: setIncognitoEnabled,
2571 onCustomMenuSelected: onCustomMenuSelected,
2572 openSection: openSection,
2573 setFaviconDominantColor: setFaviconDominantColor,
2574 setForeignSessions: setForeignSessions,
2575 setIncognitoMode: setIncognitoMode,
2576 setMostVisitedPages: setMostVisitedPages,
2577 setPromotions: setPromotions,
2578 setRecentlyClosedTabs: setRecentlyClosedTabs,
2579 setSyncEnabled: setSyncEnabled,
2580 snapshots: snapshots
2584 /////////////////////////////////////////////////////////////////////////////
2585 //Utility Functions.
2586 /////////////////////////////////////////////////////////////////////////////
2589 * A best effort approach for checking simple data object equality.
2590 * @param {?} val1 The first value to check equality for.
2591 * @param {?} val2 The second value to check equality for.
2592 * @return {boolean} Whether the two objects are equal(ish).
2594 function equals(val1, val2) {
2595 if (typeof val1 != 'object' || typeof val2 != 'object')
2596 return val1 === val2;
2598 // Object and array equality checks.
2599 var keyCountVal1 = 0;
2600 for (var key in val1) {
2601 if (!(key in val2) || !equals(val1[key], val2[key]))
2605 var keyCountVal2 = 0;
2606 for (var key in val2)
2608 if (keyCountVal1 != keyCountVal2)
2614 * Alias for document.getElementById.
2615 * @param {string} id The ID of the element to find.
2616 * @return {HTMLElement} The found element or null if not found.
2619 return document.getElementById(id);
2623 * @return {boolean} Whether the device is currently in portrait mode.
2625 function isPortrait() {
2626 return document.documentElement.offsetWidth <
2627 document.documentElement.offsetHeight;
2631 * Determine if the page should be formatted for tablets.
2632 * @return {boolean} true if the device is a tablet, false otherwise.
2634 function isTablet() {
2635 return document.body.getAttribute('device') == 'tablet';
2639 * Determine if the page should be formatted for phones.
2640 * @return {boolean} true if the device is a phone, false otherwise.
2642 function isPhone() {
2643 return document.body.getAttribute('device') == 'phone';
2647 * Get the page X coordinate of a touch event.
2648 * @param {TouchEvent} evt The touch event triggered by the browser.
2649 * @return {number} The page X coordinate of the touch event.
2651 function getTouchEventX(evt) {
2652 return (evt.touches[0] || e.changedTouches[0]).pageX;
2656 * Get the page Y coordinate of a touch event.
2657 * @param {TouchEvent} evt The touch event triggered by the browser.
2658 * @return {number} The page Y coordinate of the touch event.
2660 function getTouchEventY(evt) {
2661 return (evt.touches[0] || e.changedTouches[0]).pageY;
2665 * @param {Element} el The item to get the width of.
2666 * @param {boolean} excludeMargin If true, exclude the width of the margin.
2667 * @return {number} The total width of a given item.
2669 function getItemWidth(el, excludeMargin) {
2670 var elStyle = window.getComputedStyle(el);
2671 var width = el.offsetWidth;
2672 if (!width || width == 0) {
2673 width = parseInt(elStyle.getPropertyValue('width'));
2675 parseInt(elStyle.getPropertyValue('border-left-width')) +
2676 parseInt(elStyle.getPropertyValue('border-right-width'));
2678 parseInt(elStyle.getPropertyValue('padding-left')) +
2679 parseInt(elStyle.getPropertyValue('padding-right'));
2681 if (!excludeMargin) {
2682 width += parseInt(elStyle.getPropertyValue('margin-left')) +
2683 parseInt(elStyle.getPropertyValue('margin-right'));
2689 * @return {number} The padding height of the body due to the button bar
2691 function getButtonBarPadding() {
2692 var body = document.getElementsByTagName('body')[0];
2693 var style = window.getComputedStyle(body);
2694 return parseInt(style.getPropertyValue('padding-bottom'));
2699 * @param {string} selector The selector for the rule (passed to findCssRule())
2700 * @param {string} property The property to update
2701 * @param {string} value The value to update the property to
2702 * @return {boolean} true if the rule was updated, false otherwise.
2704 function modifyCssRule(selector, property, value) {
2705 var rule = findCssRule(selector);
2708 rule.style[property] = value;
2713 * Find a particular CSS rule. The stylesheets attached to the document
2714 * are traversed in reverse order. The rules in each stylesheet are also
2715 * traversed in reverse order. The first rule found to match the selector
2717 * @param {string} selector The selector for the rule.
2718 * @return {Object} The rule if one was found, null otherwise
2720 function findCssRule(selector) {
2721 var styleSheets = document.styleSheets;
2722 for (i = styleSheets.length - 1; i >= 0; i--) {
2723 var styleSheet = styleSheets[i];
2724 var rules = styleSheet.cssRules;
2727 for (j = rules.length - 1; j >= 0; j--) {
2728 if (rules[j].selectorText == selector)
2734 /////////////////////////////////////////////////////////////////////////////
2736 /////////////////////////////////////////////////////////////////////////////
2739 * Handles initializing the UI when the page has finished loading.
2741 window.addEventListener('DOMContentLoaded', function(evt) {
2743 $('content-area').style.display = 'block';