Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(555)

Side by Side Diff: chrome/browser/resources/ntp_android/ntp_android.js

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

Powered by Google App Engine
This is Rietveld 408576698