OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 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 /** | |
6 * @fileoverview New tab page | |
7 * This is the main code for the new tab page. NewTabView manages page list, | |
8 * dot list and handles apps pages callbacks from backend. It also handles | |
9 * the layout of the Bottom Panel and the global UI states of the New Tab Page. | |
10 */ | |
11 | |
12 // Use an anonymous function to enable strict mode just for this file (which | |
13 // will be concatenated with other files when embedded in Chrome | |
14 cr.define('ntp', function() { | |
15 'use strict'; | |
16 | |
17 var APP_LAUNCH = { | |
18 // The histogram buckets (keep in sync with extension_constants.h). | |
19 NTP_APPS_MAXIMIZED: 0, | |
20 NTP_APPS_COLLAPSED: 1, | |
21 NTP_APPS_MENU: 2, | |
22 NTP_MOST_VISITED: 3, | |
23 NTP_RECENTLY_CLOSED: 4, | |
24 NTP_APP_RE_ENABLE: 16, | |
25 NTP_WEBSTORE_FOOTER: 18, | |
26 NTP_WEBSTORE_PLUS_ICON: 19, | |
27 }; | |
28 | |
29 /** | |
30 * @type {number} | |
31 * @const | |
32 */ | |
33 var BOTTOM_PANEL_HORIZONTAL_MARGIN = 100; | |
34 | |
35 /** | |
36 * The height required to show the Bottom Panel. | |
37 * @type {number} | |
38 * @const | |
39 */ | |
40 var HEIGHT_FOR_BOTTOM_PANEL = 531; | |
41 | |
42 /** | |
43 * The Bottom Panel width required to show 6 cols of Tiles, which is used | |
44 * in the width computation. | |
45 * @type {number} | |
46 * @const | |
47 */ | |
48 var MAX_BOTTOM_PANEL_WIDTH = 920; | |
49 | |
50 /** | |
51 * The minimum width of the Bottom Panel's content. | |
52 * @type {number} | |
53 * @const | |
54 */ | |
55 var MIN_BOTTOM_PANEL_CONTENT_WIDTH = 200; | |
56 | |
57 /** | |
58 * The minimum Bottom Panel width. If the available width is smaller than | |
59 * this value, then the width of the Bottom Panel's content will be fixed to | |
60 * MIN_BOTTOM_PANEL_CONTENT_WIDTH. | |
61 * @type {number} | |
62 * @const | |
63 */ | |
64 var MIN_BOTTOM_PANEL_WIDTH = 300; | |
65 | |
66 /** | |
67 * The normal Bottom Panel width. If the window width is greater than or | |
68 * equal to this value, then the width of the Bottom Panel's content will be | |
69 * the available width minus side margin. If the available width is smaller | |
70 * than this value, then the width of the Bottom Panel's content will be an | |
71 * interpolation between the normal width, and the minimum width defined by | |
72 * the constant MIN_BOTTOM_PANEL_CONTENT_WIDTH. | |
73 * @type {number} | |
74 * @const | |
75 */ | |
76 var NORMAL_BOTTOM_PANEL_WIDTH = 500; | |
77 | |
78 /** | |
79 * @type {number} | |
80 * @const | |
81 */ | |
82 var TILE_ROW_HEIGHT = 100; | |
83 | |
84 //---------------------------------------------------------------------------- | |
85 | |
86 /** | |
87 * NewTabView instance. | |
88 * @type {!Object|undefined} | |
89 */ | |
90 var newTabView; | |
91 | |
92 /** | |
93 * The 'notification-container' element. | |
94 * @type {!Element|undefined} | |
95 */ | |
96 var notificationContainer; | |
97 | |
98 /** | |
99 * If non-null, an info bubble for showing messages to the user. It points at | |
100 * the Most Visited label, and is used to draw more attention to the | |
101 * navigation dot UI. | |
102 * @type {!Element|undefined} | |
103 */ | |
104 var promoBubble; | |
105 | |
106 /** | |
107 * The total number of thumbnails that were hovered over. | |
108 * @type {number} | |
109 * @private | |
110 */ | |
111 var hoveredThumbnailCount = 0; | |
112 | |
113 /** | |
114 * The time when all sections are ready. | |
115 * @type {number|undefined} | |
116 * @private | |
117 */ | |
118 var startTime; | |
119 | |
120 /** | |
121 * The top position of the Bottom Panel. | |
122 * @type {number|undefined} | |
123 * @private | |
124 */ | |
125 var bottomPanelOffsetTop; | |
126 | |
127 /** | |
128 * The height of the Bottom Panel Header, in pixels. | |
129 * @type {number|undefined} | |
130 * @private | |
131 */ | |
132 var headerHeight; | |
133 | |
134 /** | |
135 * The time in milliseconds for most transitions. This should match what's | |
136 * in new_tab.css. Unfortunately there's no better way to try to time | |
137 * something to occur until after a transition has completed. | |
138 * @type {number} | |
139 * @const | |
140 */ | |
141 var DEFAULT_TRANSITION_TIME = 500; | |
142 | |
143 /** | |
144 * See description for these values in ntp_stats.h. | |
145 * @enum {number} | |
146 */ | |
147 var NtpFollowAction = { | |
148 CLICKED_TILE: 11, | |
149 CLICKED_OTHER_NTP_PANE: 12, | |
150 OTHER: 13, | |
151 }; | |
152 | |
153 /** | |
154 * Creates a NewTabView object. | |
155 * @constructor | |
156 */ | |
157 function NewTabView() { | |
158 this.initialize(getRequiredElement('page-list'), | |
159 getRequiredElement('dot-list'), | |
160 getRequiredElement('card-slider-frame')); | |
161 } | |
162 | |
163 NewTabView.prototype = { | |
164 /** | |
165 * The CardSlider object to use for changing app pages. | |
166 * @type {CardSlider|undefined} | |
167 */ | |
168 cardSlider: undefined, | |
169 | |
170 /** | |
171 * The frame div for this.cardSlider. | |
172 * @type {!Element|undefined} | |
173 */ | |
174 sliderFrame: undefined, | |
175 | |
176 /** | |
177 * The 'page-list' element. | |
178 * @type {!Element|undefined} | |
179 */ | |
180 pageList: undefined, | |
181 | |
182 /** | |
183 * A list of all 'tile-page' elements. | |
184 * @type {!NodeList|undefined} | |
185 */ | |
186 tilePages: undefined, | |
187 | |
188 /** | |
189 * The Apps page. | |
190 * @type {!Element|undefined} | |
191 */ | |
192 appsPage: undefined, | |
193 | |
194 /** | |
195 * The Most Visited page. | |
196 * @type {!Element|undefined} | |
197 */ | |
198 mostVisitedPage: undefined, | |
199 | |
200 /** | |
201 * The Recently Closed page. | |
202 * @type {!Element|undefined} | |
203 */ | |
204 recentlyClosedPage: undefined, | |
205 | |
206 /** | |
207 * The Devices page. | |
208 * @type {!Element|undefined} | |
209 */ | |
210 otherDevicesPage: undefined, | |
211 | |
212 /** | |
213 * The 'dots-list' element. | |
214 * @type {!Element|undefined} | |
215 */ | |
216 dotList: undefined, | |
217 | |
218 /** | |
219 * The type of page that is currently shown. The value is a numerical ID. | |
220 * @type {number} | |
221 */ | |
222 shownPage: 0, | |
223 | |
224 /** | |
225 * The index of the page that is currently shown, within the page type. | |
226 * For example if the third Apps page is showing, this will be 2. | |
227 * @type {number} | |
228 */ | |
229 shownPageIndex: 0, | |
230 | |
231 /** | |
232 * If non-null, this is the ID of the app to highlight to the user the next | |
233 * time getAppsCallback runs. "Highlight" in this case means to switch to | |
234 * the page and run the new tile animation. | |
235 * @type {?string} | |
236 */ | |
237 highlightAppId: null, | |
238 | |
239 /** | |
240 * Initializes new tab view. | |
241 * @param {!Element} pageList A DIV element to host all pages. | |
242 * @param {!Element} dotList An UL element to host nav dots. Each dot | |
243 * represents a page. | |
244 * @param {!Element} cardSliderFrame The card slider frame that hosts | |
245 * pageList. | |
246 */ | |
247 initialize: function(pageList, dotList, cardSliderFrame) { | |
248 this.pageList = pageList; | |
249 | |
250 this.dotList = dotList; | |
251 cr.ui.decorate(this.dotList, ntp.DotList); | |
252 | |
253 this.shownPage = loadTimeData.getInteger('shown_page_type'); | |
254 this.shownPageIndex = loadTimeData.getInteger('shown_page_index'); | |
255 | |
256 if (loadTimeData.getBoolean('showApps')) { | |
257 // When the Apps Page is available, then the dot list should be visible. | |
258 this.dotList.removeAttribute('hidden'); | |
259 // Request data on the apps so we can fill them in. | |
260 // Note that this is kicked off asynchronously. 'getAppsCallback' will | |
261 // be invoked at some point after this function returns. | |
262 chrome.send('getApps'); | |
263 } else if (this.shownPage == loadTimeData.getInteger('apps_page_id')) { | |
264 // No apps page. | |
265 this.setShownPage_( | |
266 loadTimeData.getInteger('most_visited_page_id'), 0); | |
267 } | |
268 | |
269 this.tilePages = this.pageList.getElementsByClassName('tile-page'); | |
270 | |
271 // Initialize the cardSlider without any cards at the moment. | |
272 this.sliderFrame = cardSliderFrame; | |
273 this.cardSlider = new cr.ui.CardSlider(this.sliderFrame, this.pageList, | |
274 this.sliderFrame.offsetWidth); | |
275 | |
276 var cardSlider = this.cardSlider; | |
277 this.cardSlider.initialize( | |
278 loadTimeData.getBoolean('isSwipeTrackingFromScrollEventsEnabled')); | |
279 | |
280 // Prevent touch events from triggering any sort of native scrolling. | |
281 document.addEventListener('touchmove', function(e) { | |
282 e.preventDefault(); | |
283 }, true); | |
284 | |
285 // Handle events from the card slider. | |
286 this.pageList.addEventListener('cardSlider:card_changed', | |
287 this.onCardChanged_.bind(this)); | |
288 this.pageList.addEventListener('cardSlider:card_added', | |
289 this.onCardAdded_.bind(this)); | |
290 this.pageList.addEventListener('cardSlider:card_removed', | |
291 this.onCardRemoved_.bind(this)); | |
292 | |
293 $('bottom-panel').addEventListener('webkitTransitionEnd', | |
294 this.onBottomPanelTransitionEnd_.bind(this)); | |
295 | |
296 // Update apps when online state changes. | |
297 window.addEventListener('online', | |
298 this.updateOfflineEnabledApps_.bind(this)); | |
299 window.addEventListener('offline', | |
300 this.updateOfflineEnabledApps_.bind(this)); | |
301 }, | |
302 | |
303 /** | |
304 * Starts listening to user input events. The resize and keydown events | |
305 * must be added only when all NTP have finished loading because they | |
306 * will act in the current selected page. | |
307 */ | |
308 onReady: function() { | |
309 window.addEventListener('resize', this.onWindowResize_.bind(this)); | |
310 document.addEventListener('keydown', this.onDocKeyDown_.bind(this)); | |
311 }, | |
312 | |
313 /** | |
314 * Appends a tile page. | |
315 * | |
316 * @param {TilePage} page The page element. | |
317 * @param {string} title The title of the tile page. | |
318 * @param {TilePage=} opt_refNode Optional reference node to insert in front | |
319 * of. | |
320 * When opt_refNode is falsey, |page| will just be appended to the end of | |
321 * the page list. | |
322 */ | |
323 appendTilePage: function(page, title, opt_refNode) { | |
324 if (opt_refNode) { | |
325 var refIndex = this.getTilePageIndex(opt_refNode); | |
326 this.cardSlider.addCardAtIndex(page, refIndex); | |
327 } else { | |
328 this.cardSlider.appendCard(page); | |
329 } | |
330 | |
331 // Remember special MostVisitedPage. | |
332 if (typeof ntp.MostVisitedPage != 'undefined' && | |
333 page instanceof ntp.MostVisitedPage) { | |
334 assert(this.tilePages.length == 1, | |
335 'MostVisitedPage should be added as first tile page'); | |
336 this.mostVisitedPage = page; | |
337 } | |
338 | |
339 if (typeof ntp.AppsPage != 'undefined' && | |
340 page instanceof ntp.AppsPage) { | |
341 this.appsPage = page; | |
342 } | |
343 | |
344 if (typeof ntp.RecentlyClosedPage != 'undefined' && | |
345 page instanceof ntp.RecentlyClosedPage) { | |
346 this.recentlyClosedPage = page; | |
347 } | |
348 | |
349 // Remember special OtherDevicesPage. | |
350 if (typeof ntp.OtherDevicesPage != 'undefined' && | |
351 page instanceof ntp.OtherDevicesPage) { | |
352 this.otherDevicesPage = page; | |
353 } | |
354 | |
355 // Make a deep copy of the dot template to add a new one. | |
356 var newDot = new ntp.NavDot(page, title); | |
357 page.navigationDot = newDot; | |
358 this.dotList.insertBefore(newDot, | |
359 opt_refNode ? opt_refNode.navigationDot : null); | |
360 // Set a tab index on the first dot. | |
361 if (this.dotList.dots.length == 1) | |
362 newDot.tabIndex = 3; | |
363 }, | |
364 | |
365 /** | |
366 * Called by chrome when an app has changed positions. | |
367 * @param {Object} data The data for the app. This contains page and | |
368 * position indices. | |
369 */ | |
370 appMoved: function(data) { | |
371 assert(loadTimeData.getBoolean('showApps')); | |
372 | |
373 var app = $(data.id); | |
374 assert(app, 'trying to move an app that doesn\'t exist'); | |
375 app.remove(false); | |
376 | |
377 this.appsPage.insertApp(data, false); | |
378 }, | |
379 | |
380 /** | |
381 * Called by chrome when an existing app has been disabled or | |
382 * removed/uninstalled from chrome. | |
383 * @param {Object} data A data structure full of relevant information for | |
384 * the app. | |
385 * @param {boolean} isUninstall True if the app is being uninstalled; | |
386 * false if the app is being disabled. | |
387 * @param {boolean} fromPage True if the removal was from the current page. | |
388 */ | |
389 appRemoved: function(data, isUninstall, fromPage) { | |
390 assert(loadTimeData.getBoolean('showApps')); | |
391 | |
392 var app = $(data.id); | |
393 assert(app, 'trying to remove an app that doesn\'t exist'); | |
394 | |
395 if (!isUninstall) | |
396 app.replaceAppData(data); | |
397 else | |
398 app.remove(!!fromPage); | |
399 }, | |
400 | |
401 /** | |
402 * @return {boolean} If the page is still starting up. | |
403 * @private | |
404 */ | |
405 isStartingUp_: function() { | |
406 return document.documentElement.classList.contains('starting-up'); | |
407 }, | |
408 | |
409 /** | |
410 * Tracks whether apps have been loaded at least once. | |
411 * @type {boolean} | |
412 * @private | |
413 */ | |
414 appsLoaded_: false, | |
415 | |
416 /** | |
417 * Callback invoked by chrome with the apps available. | |
418 * | |
419 * Note that calls to this function can occur at any time, not just in | |
420 * response to a getApps request. For example, when a user | |
421 * installs/uninstalls an app on another synchronized devices. | |
422 * @param {Object} data An object with all the data on available | |
423 * applications. | |
424 */ | |
425 getAppsCallback: function(data) { | |
426 assert(loadTimeData.getBoolean('showApps')); | |
427 | |
428 var startTime = Date.now(); | |
429 | |
430 // Get the array of apps and add any special synthesized entries. | |
431 var apps = data.apps; | |
432 | |
433 // Sort alphabetically. | |
434 apps.sort(function(a, b) { | |
435 return a.title.toLocaleLowerCase() > b.title.toLocaleLowerCase() ? 1 : | |
436 a.title.toLocaleLowerCase() < b.title.toLocaleLowerCase() ? -1 : 0; | |
437 }); | |
438 | |
439 // An app to animate (in case it was just installed). | |
440 var highlightApp; | |
441 | |
442 if (this.appsPage) { | |
443 this.appsPage.removeAllTiles(); | |
444 } else { | |
445 var page = new ntp.AppsPage(); | |
446 page.setDataList(apps); | |
447 this.appendTilePage(page, loadTimeData.getString('appDefaultPageName')); | |
448 } | |
449 | |
450 for (var i = 0; i < apps.length; i++) { | |
451 var app = apps[i]; | |
452 if (app.id == this.highlightAppId) | |
453 highlightApp = app; | |
454 else | |
455 this.appsPage.insertApp(app, false); | |
456 } | |
457 | |
458 if (highlightApp) | |
459 this.appAdded(highlightApp, true); | |
460 | |
461 logEvent('apps.layout: ' + (Date.now() - startTime)); | |
462 | |
463 // Tell the slider about the pages and mark the current page. | |
464 this.updateSliderCards(); | |
465 | |
466 if (!this.appsLoaded_) { | |
467 this.appsLoaded_ = true; | |
468 cr.dispatchSimpleEvent(document, 'sectionready', true, true); | |
469 } | |
470 }, | |
471 | |
472 /** | |
473 * Called by chrome when a new app has been added to chrome or has been | |
474 * enabled if previously disabled. | |
475 * @param {Object} data A data structure full of relevant information for | |
476 * the app. | |
477 * @param {boolean=} opt_highlight Whether the app about to be added should | |
478 * be highlighted. | |
479 */ | |
480 appAdded: function(data, opt_highlight) { | |
481 assert(loadTimeData.getBoolean('showApps')); | |
482 | |
483 if (data.id == this.highlightAppId) { | |
484 opt_highlight = true; | |
485 this.highlightAppId = null; | |
486 } | |
487 | |
488 if (!this.appsLoaded_) | |
489 opt_highlight = false; | |
490 | |
491 var app = $(data.id); | |
492 if (app) { | |
493 app.replaceAppData(data); | |
494 } else if (opt_highlight) { | |
495 this.appsPage.insertAndHighlightApp(data); | |
496 this.setShownPage_(loadTimeData.getInteger('apps_page_id'), | |
497 data.page_index); | |
498 } else { | |
499 this.appsPage.insertApp(data, false); | |
500 } | |
501 }, | |
502 | |
503 /** | |
504 * Callback invoked by chrome whenever an app preference changes. | |
505 * @param {Object} data An object with all the data on available | |
506 * applications. | |
507 */ | |
508 appsPrefChangedCallback: function(data) { | |
509 assert(loadTimeData.getBoolean('showApps')); | |
510 | |
511 for (var i = 0; i < data.apps.length; ++i) { | |
512 var element = $(data.apps[i].id); | |
513 if (element) | |
514 element.data = data.apps[i]; | |
515 } | |
516 }, | |
517 | |
518 /** | |
519 * Invoked whenever the pages in page-list have changed so that the | |
520 * CardSlider knows about the new elements. | |
521 */ | |
522 updateSliderCards: function() { | |
523 var pageNo = Math.max(0, Math.min(this.cardSlider.currentCard, | |
524 this.tilePages.length - 1)); | |
525 this.cardSlider.setCards(Array.prototype.slice.call(this.tilePages), | |
526 pageNo); | |
527 switch (this.shownPage) { | |
528 case loadTimeData.getInteger('apps_page_id'): | |
529 this.cardSlider.selectCardByValue(this.appsPage); | |
530 break; | |
531 case loadTimeData.getInteger('most_visited_page_id'): | |
532 if (this.mostVisitedPage) | |
533 this.cardSlider.selectCardByValue(this.mostVisitedPage); | |
534 break; | |
535 } | |
536 }, | |
537 | |
538 /** | |
539 * Handler for cardSlider:card_changed events from this.cardSlider. | |
540 * @param {Event} e The cardSlider:card_changed event. | |
541 * @private | |
542 */ | |
543 onCardChanged_: function(e) { | |
544 var page = e.cardSlider.currentCardValue; | |
545 | |
546 // Don't change shownPage until startup is done (and page changes actually | |
547 // reflect user actions). | |
548 if (!this.isStartingUp_()) { | |
549 if (page.classList.contains('apps-page')) { | |
550 this.setShownPage_(loadTimeData.getInteger('apps_page_id'), 0); | |
551 } else if (page.classList.contains('most-visited-page')) { | |
552 this.setShownPage_( | |
553 loadTimeData.getInteger('most_visited_page_id'), 0); | |
554 } else if (page.classList.contains('recently-closed-page')) { | |
555 this.setShownPage_( | |
556 loadTimeData.getInteger('recently_closed_page_id'), 0); | |
557 } else if (page.classList.contains('other-devices-page')) { | |
558 this.setShownPage_( | |
559 loadTimeData.getInteger('other_devices_page_id'), 0); | |
560 } else { | |
561 console.error('unknown page selected'); | |
562 } | |
563 } | |
564 | |
565 // Update the active dot | |
566 var curDot = this.dotList.getElementsByClassName('selected')[0]; | |
567 if (curDot) | |
568 curDot.classList.remove('selected'); | |
569 page.navigationDot.classList.add('selected'); | |
570 }, | |
571 | |
572 /** | |
573 * Saves/updates the newly selected page to open when first loading the NTP. | |
574 * @type {number} shownPage The new shown page type. | |
575 * @type {number} shownPageIndex The new shown page index. | |
576 * @private | |
577 */ | |
578 setShownPage_: function(shownPage, shownPageIndex) { | |
579 assert(shownPageIndex >= 0); | |
580 this.shownPage = shownPage; | |
581 this.shownPageIndex = shownPageIndex; | |
582 chrome.send('pageSelected', [this.shownPage, this.shownPageIndex]); | |
583 }, | |
584 | |
585 /** | |
586 * Listen for card additions to update the current card accordingly. | |
587 * @param {Event} e A card removed or added event. | |
588 */ | |
589 onCardAdded_: function(e) { | |
590 var page = e.addedCard; | |
591 // When the second arg passed to insertBefore is falsey, it acts just like | |
592 // appendChild. | |
593 this.pageList.insertBefore(page, this.tilePages[e.addedIndex]); | |
594 this.layout(false, page); | |
595 this.onCardAddedOrRemoved_(); | |
596 }, | |
597 | |
598 /** | |
599 * Listen for card removals to update the current card accordingly. | |
600 * @param {Event} e A card removed or added event. | |
601 */ | |
602 onCardRemoved_: function(e) { | |
603 e.removedCard.remove(); | |
604 this.onCardAddedOrRemoved_(); | |
605 }, | |
606 | |
607 /** | |
608 * Called when a card is removed or added. | |
609 * @private | |
610 */ | |
611 onCardAddedOrRemoved_: function() { | |
612 if (this.isStartingUp_()) | |
613 return; | |
614 | |
615 // Without repositioning there were issues - http://crbug.com/133457. | |
616 this.cardSlider.repositionFrame(); | |
617 }, | |
618 | |
619 /** | |
620 * Window resize handler. | |
621 * @private | |
622 */ | |
623 onWindowResize_: function(e) { | |
624 this.cardSlider.resize(this.sliderFrame.offsetWidth); | |
625 this.layout(true); | |
626 }, | |
627 | |
628 /** | |
629 * Handler for key events on the page. Ctrl-Arrow will switch the visible | |
630 * page. | |
631 * @param {Event} e The KeyboardEvent. | |
632 * @private | |
633 */ | |
634 onDocKeyDown_: function(e) { | |
635 if (!e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) | |
636 return; | |
637 | |
638 var direction = 0; | |
639 if (e.keyIdentifier == 'Left') | |
640 direction = -1; | |
641 else if (e.keyIdentifier == 'Right') | |
642 direction = 1; | |
643 else | |
644 return; | |
645 | |
646 var cardIndex = | |
647 (this.cardSlider.currentCard + direction + | |
648 this.cardSlider.cardCount) % this.cardSlider.cardCount; | |
649 this.cardSlider.selectCard(cardIndex, true); | |
650 | |
651 e.stopPropagation(); | |
652 }, | |
653 | |
654 /** | |
655 * Listener for offline status change events. Updates apps that are | |
656 * not offline-enabled to be grayscale if the browser is offline. | |
657 * @private | |
658 */ | |
659 updateOfflineEnabledApps_: function() { | |
660 var apps = document.querySelectorAll('.app'); | |
661 for (var i = 0; i < apps.length; ++i) { | |
662 if (apps[i].data.enabled && !apps[i].data.offline_enabled) { | |
663 apps[i].setIcon(); | |
664 apps[i].loadIcon(); | |
665 } | |
666 } | |
667 }, | |
668 | |
669 /** | |
670 * Returns the index of a given tile page. | |
671 * @param {TilePage} page The TilePage we wish to find. | |
672 * @return {number} The index of |page| or -1 if it is not in the | |
673 * collection. | |
674 */ | |
675 getTilePageIndex: function(page) { | |
676 return Array.prototype.indexOf.call(this.tilePages, page); | |
677 }, | |
678 | |
679 /** | |
680 * Removes a page and navigation dot (if the navdot exists). | |
681 * @param {TilePage} page The page to be removed. | |
682 */ | |
683 removeTilePageAndDot_: function(page) { | |
684 if (page.navigationDot) | |
685 page.navigationDot.remove(); | |
686 this.cardSlider.removeCard(page); | |
687 }, | |
688 | |
689 /** | |
690 * The width of the Bottom Panel's content. | |
691 * @type {number} | |
692 */ | |
693 contentWidth_: 0, | |
694 | |
695 /** | |
696 * Calculates the layout of the NTP's Bottom Panel. This method will resize | |
697 * and position all container elements in the Bottom Panel. At the end of | |
698 * the layout process it will dispatch the layout method to the current | |
699 * selected TilePage. Alternatively, you can pass a specific TilePage in | |
700 * the |opt_page| parameter, which is useful for initializing the layout | |
701 * of a recently created TilePage. | |
702 * | |
703 * The |NewTabView.layout| deals with the global layout state while the | |
704 * |TilePage.layout| deals with the per-page layout state. A general rule | |
705 * would be: if you need to resize any element which is outside the | |
706 * card-slider-frame, it should be handled here in NewTabView. Otherwise, | |
707 * it should be handled in TilePage. | |
708 * | |
709 * @param {boolean=} opt_animate Whether the layout should be animated. | |
710 * @param {ntp.TilePage=} opt_page Alternative TilePage to calculate layout. | |
711 */ | |
712 layout: function(opt_animate, opt_page) { | |
713 opt_animate = typeof opt_animate == 'undefined' ? false : opt_animate; | |
714 | |
715 var viewHeight = cr.doc.documentElement.clientHeight; | |
716 var isBottomPanelVisible = viewHeight >= HEIGHT_FOR_BOTTOM_PANEL; | |
717 // Toggles the visibility of the Bottom Panel when there is (or there | |
718 // is not) space to show the entire panel. | |
719 this.showBottomPanel_(isBottomPanelVisible); | |
720 | |
721 // The layout calculation can be skipped if Bottom Panel is not visible. | |
722 if (!isBottomPanelVisible && !opt_page) | |
723 return; | |
724 | |
725 // Calculates the width of the Bottom Panel's Content. | |
726 var width = this.calculateContentWidth_(); | |
727 if (width != this.contentWidth_) { | |
728 this.contentWidth_ = width; | |
729 $('bottom-panel-footer').style.width = width + 'px'; | |
730 } | |
731 | |
732 // Finally, dispatch the layout method to the current page. | |
733 var currentPage = opt_page || this.cardSlider.currentCardValue; | |
734 | |
735 var contentHeight = TILE_ROW_HEIGHT; | |
736 if (!opt_page && currentPage.config.scrollable) { | |
737 contentHeight = viewHeight - bottomPanelOffsetTop - | |
738 headerHeight - $('bottom-panel-footer').offsetHeight; | |
739 contentHeight = Math.max(TILE_ROW_HEIGHT, contentHeight); | |
740 } | |
741 this.contentHeight_ = contentHeight; | |
742 | |
743 $('card-slider-frame').style.height = contentHeight + 'px'; | |
744 | |
745 currentPage.layout(opt_animate); | |
746 }, | |
747 | |
748 /** | |
749 * @return {number} The height of the Bottom Panel's content. | |
750 */ | |
751 get contentHeight() { | |
752 return this.contentHeight_; | |
753 }, | |
754 | |
755 /** | |
756 * @return {number} The width of the Bottom Panel's content. | |
757 */ | |
758 get contentWidth() { | |
759 return this.contentWidth_; | |
760 }, | |
761 | |
762 /** | |
763 * @return {number} The width of the Bottom Panel's content. | |
764 * @private | |
765 */ | |
766 calculateContentWidth_: function() { | |
767 var windowWidth = cr.doc.documentElement.clientWidth; | |
768 var margin = 2 * BOTTOM_PANEL_HORIZONTAL_MARGIN; | |
769 | |
770 var width; | |
771 if (windowWidth >= MAX_BOTTOM_PANEL_WIDTH) { | |
772 width = MAX_BOTTOM_PANEL_WIDTH - margin; | |
773 } else if (windowWidth >= NORMAL_BOTTOM_PANEL_WIDTH) { | |
774 width = windowWidth - margin; | |
775 } else if (windowWidth >= MIN_BOTTOM_PANEL_WIDTH) { | |
776 // Interpolation between the previous and next states. | |
777 var minMargin = MIN_BOTTOM_PANEL_WIDTH - MIN_BOTTOM_PANEL_CONTENT_WIDTH; | |
778 var factor = (windowWidth - MIN_BOTTOM_PANEL_WIDTH) / | |
779 (NORMAL_BOTTOM_PANEL_WIDTH - MIN_BOTTOM_PANEL_WIDTH); | |
780 var interpolatedMargin = minMargin + factor * (margin - minMargin); | |
781 width = windowWidth - interpolatedMargin; | |
782 } else { | |
783 width = MIN_BOTTOM_PANEL_CONTENT_WIDTH; | |
784 } | |
785 | |
786 return width; | |
787 }, | |
788 | |
789 /** | |
790 * Animates the display of the Bottom Panel. | |
791 * @param {boolean} show Whether or not to show the Bottom Panel. | |
792 */ | |
793 showBottomPanel_: function(show) { | |
794 var bottomPanel = $('bottom-panel'); | |
795 | |
796 if (show) { | |
797 bottomPanel.hidden = false; | |
798 // Forces the reflow. | |
799 bottomPanel.offsetHeight; | |
800 } | |
801 | |
802 bottomPanel.classList.toggle('hide-bottom-panel', !show); | |
803 }, | |
804 | |
805 /** | |
806 * Handles the end of the bottom panel transition. | |
807 * @param {Event} e The bottom panel webkitTransitionEnd event. | |
808 * @private | |
809 */ | |
810 onBottomPanelTransitionEnd_: function(e) { | |
811 var bottomPanel = $('bottom-panel'); | |
812 if (e.target == bottomPanel && e.propertyName == 'opacity' && | |
813 bottomPanel.classList.contains('hide-bottom-panel')) { | |
814 bottomPanel.hidden = true; | |
815 } | |
816 }, | |
817 }; | |
818 | |
819 /** | |
820 * Invoked at startup once the DOM is available to initialize the app. | |
821 */ | |
822 function onLoad() { | |
823 | |
824 if (!loadTimeData.getBoolean('showApps')) | |
825 cr.dispatchSimpleEvent(document, 'sectionready', true, true); | |
826 | |
827 // Load the current theme colors. | |
828 themeChanged(); | |
829 | |
830 newTabView = new NewTabView(); | |
831 | |
832 bottomPanelOffsetTop = $('bottom-panel').offsetTop; | |
833 headerHeight = $('bottom-panel-header').offsetHeight; | |
834 | |
835 notificationContainer = getRequiredElement('notification-container'); | |
836 | |
837 var mostVisited = new ntp.MostVisitedPage(); | |
838 newTabView.appendTilePage(mostVisited, | |
839 loadTimeData.getString('mostvisited')); | |
840 chrome.send('getMostVisited'); | |
841 | |
842 if (loadTimeData.valueExists('bubblePromoText')) { | |
843 promoBubble = new cr.ui.Bubble; | |
844 promoBubble.anchorNode = getRequiredElement('promo-bubble-anchor'); | |
845 promoBubble.arrowLocation = cr.ui.ArrowLocation.BOTTOM_START; | |
846 promoBubble.bubbleAlignment = cr.ui.BubbleAlignment.ENTIRELY_VISIBLE; | |
847 promoBubble.deactivateToDismissDelay = 2000; | |
848 promoBubble.content = parseHtmlSubset( | |
849 loadTimeData.getString('bubblePromoText'), ['BR']); | |
850 | |
851 var bubbleLink = promoBubble.querySelector('a'); | |
852 if (bubbleLink) { | |
853 bubbleLink.addEventListener('click', function(e) { | |
854 chrome.send('bubblePromoLinkClicked'); | |
855 }); | |
856 } | |
857 | |
858 promoBubble.handleCloseEvent = function() { | |
859 promoBubble.hide(); | |
860 chrome.send('bubblePromoClosed'); | |
861 }; | |
862 promoBubble.show(); | |
863 chrome.send('bubblePromoViewed'); | |
864 } | |
865 | |
866 doWhenAllSectionsReady(function() { | |
867 // Tell the slider about the pages. | |
868 newTabView.updateSliderCards(); | |
869 newTabView.onReady(); | |
870 | |
871 // Restore the visibility only after calling updateSliderCards to avoid | |
872 // flickering, otherwise for a small fraction of a second the Page List is | |
873 // partially rendered. | |
874 $('bottom-panel').style.visibility = 'visible'; | |
875 | |
876 if (loadTimeData.valueExists('notificationPromoText')) { | |
877 var promoText = loadTimeData.getString('notificationPromoText'); | |
878 var tags = ['IMG']; | |
879 var attrs = { | |
880 src: function(node, value) { | |
881 return node.tagName == 'IMG' && | |
882 /^data\:image\/(?:png|gif|jpe?g)/.test(value); | |
883 }, | |
884 }; | |
885 | |
886 var promo = parseHtmlSubset(promoText, tags, attrs); | |
887 var promoLink = promo.querySelector('a'); | |
888 if (promoLink) { | |
889 promoLink.addEventListener('click', function(e) { | |
890 chrome.send('notificationPromoLinkClicked'); | |
891 }); | |
892 } | |
893 | |
894 showNotification(promo, [], function() { | |
895 chrome.send('notificationPromoClosed'); | |
896 }, 60000); | |
897 chrome.send('notificationPromoViewed'); | |
898 } | |
899 | |
900 cr.dispatchSimpleEvent(document, 'ntpLoaded', true, true); | |
901 document.documentElement.classList.remove('starting-up'); | |
902 | |
903 startTime = Date.now(); | |
904 }); | |
905 } | |
906 | |
907 /* | |
908 * The number of sections to wait on. | |
909 * @type {number} | |
910 */ | |
911 var sectionsToWaitFor = 2; | |
912 | |
913 /** | |
914 * Queued callbacks which lie in wait for all sections to be ready. | |
915 * @type {!Array} | |
916 */ | |
917 var readyCallbacks = []; | |
918 | |
919 /** | |
920 * Fired as each section of pages becomes ready. | |
921 * @param {Event} e Each page's synthetic DOM event. | |
922 */ | |
923 document.addEventListener('sectionready', function(e) { | |
924 if (--sectionsToWaitFor <= 0) { | |
925 while (readyCallbacks.length) { | |
926 readyCallbacks.shift()(); | |
927 } | |
928 } | |
929 }); | |
930 | |
931 /** | |
932 * This is used to simulate a fire-once event (i.e. $(document).ready() in | |
933 * jQuery or Y.on('domready') in YUI. If all sections are ready, the callback | |
934 * is fired right away. If all pages are not ready yet, the function is queued | |
935 * for later execution. | |
936 * @param {function} callback The work to be done when ready. | |
937 */ | |
938 function doWhenAllSectionsReady(callback) { | |
939 assert(typeof callback == 'function'); | |
940 if (sectionsToWaitFor > 0) | |
941 readyCallbacks.push(callback); | |
942 else | |
943 window.setTimeout(callback, 0); // Do soon after, but asynchronously. | |
944 } | |
945 | |
946 function themeChanged(opt_hasAttribution) { | |
947 $('themecss').href = 'chrome://theme/css/new_tab_theme.css?' + Date.now(); | |
948 | |
949 if (typeof opt_hasAttribution != 'undefined') { | |
950 document.documentElement.setAttribute('hasattribution', | |
951 opt_hasAttribution); | |
952 } | |
953 | |
954 updateAttribution(); | |
955 } | |
956 | |
957 function setBookmarkBarAttached(attached) { | |
958 document.documentElement.setAttribute('bookmarkbarattached', attached); | |
959 } | |
960 | |
961 /** | |
962 * Attributes the attribution image at the bottom left. | |
963 */ | |
964 function updateAttribution() { | |
965 var attribution = $('attribution'); | |
966 if (document.documentElement.getAttribute('hasattribution') == 'true') { | |
967 $('attribution-img').src = | |
968 'chrome://theme/IDR_THEME_NTP_ATTRIBUTION?' + Date.now(); | |
969 attribution.hidden = false; | |
970 } else { | |
971 attribution.hidden = true; | |
972 } | |
973 } | |
974 | |
975 /** | |
976 * Timeout ID. | |
977 * @type {number} | |
978 */ | |
979 var notificationTimeout = 0; | |
980 | |
981 /** | |
982 * Shows the notification bubble. | |
983 * @param {string|Node} message The notification message or node to use as | |
984 * message. | |
985 * @param {Array.<{text: string, action: function()}>} links An array of | |
986 * records describing the links in the notification. Each record should | |
987 * have a 'text' attribute (the display string) and an 'action' attribute | |
988 * (a function to run when the link is activated). | |
989 * @param {Function=} opt_closeHandler The callback invoked if the user | |
990 * manually dismisses the notification. | |
991 */ | |
992 function showNotification(message, links, opt_closeHandler, opt_timeout) { | |
993 window.clearTimeout(notificationTimeout); | |
994 | |
995 var span = document.querySelector('#notification > span'); | |
996 if (typeof message == 'string') { | |
997 span.textContent = message; | |
998 } else { | |
999 span.textContent = ''; // Remove all children. | |
1000 span.appendChild(message); | |
1001 } | |
1002 | |
1003 var linksBin = $('notificationLinks'); | |
1004 linksBin.textContent = ''; | |
1005 for (var i = 0; i < links.length; i++) { | |
1006 var link = linksBin.ownerDocument.createElement('div'); | |
1007 link.textContent = links[i].text; | |
1008 link.action = links[i].action; | |
1009 link.onclick = function() { | |
1010 this.action(); | |
1011 hideNotification(); | |
1012 }; | |
1013 link.setAttribute('role', 'button'); | |
1014 link.setAttribute('tabindex', 0); | |
1015 link.className = 'link-button'; | |
1016 linksBin.appendChild(link); | |
1017 } | |
1018 | |
1019 function closeFunc(e) { | |
1020 if (opt_closeHandler) | |
1021 opt_closeHandler(); | |
1022 hideNotification(); | |
1023 } | |
1024 | |
1025 document.querySelector('#notification button').onclick = closeFunc; | |
1026 document.addEventListener('dragstart', closeFunc); | |
1027 | |
1028 notificationContainer.hidden = false; | |
1029 | |
1030 var timeout = opt_timeout || 10000; | |
1031 notificationTimeout = window.setTimeout(hideNotification, timeout); | |
1032 | |
1033 layout(); | |
1034 } | |
1035 | |
1036 /** | |
1037 * Hide the notification bubble. | |
1038 */ | |
1039 function hideNotification() { | |
1040 notificationContainer.hidden = true; | |
1041 | |
1042 layout(); | |
1043 } | |
1044 | |
1045 function setMostVisitedPages(dataList, hasBlacklistedUrls) { | |
1046 var page = newTabView.mostVisitedPage; | |
1047 var state = page.getTileRepositioningState(); | |
1048 if (state) { | |
1049 if (state.isRemoving) | |
1050 page.animateTileRemoval(state.index, dataList); | |
1051 else | |
1052 page.animateTileRestoration(state.index, dataList); | |
1053 | |
1054 page.resetTileRepositioningState(); | |
1055 } else { | |
1056 page.setDataList(dataList); | |
1057 cr.dispatchSimpleEvent(document, 'sectionready', true, true); | |
1058 } | |
1059 } | |
1060 | |
1061 /** | |
1062 * Set the dominant color for a node. This will be called in response to | |
1063 * getFaviconDominantColor. The node represented by |id| better have a setter | |
1064 * for stripeColor. | |
1065 * @param {string} id The ID of a node. | |
1066 * @param {string} color The color represented as a CSS string. | |
1067 */ | |
1068 function setFaviconDominantColor(id, color) { | |
1069 var node = $(id); | |
1070 var prop = Object.getOwnPropertyDescriptor(node.__proto__, 'stripeColor'); | |
1071 assert(prop && prop.set, 'Node doesn\'t have a stripeColor setter'); | |
1072 if (node) | |
1073 node.stripeColor = color; | |
1074 } | |
1075 | |
1076 function getThumbnailUrl(url) { | |
1077 return 'chrome://thumb/' + url; | |
1078 } | |
1079 | |
1080 /** | |
1081 * Increments the parameter used to log the total number of thumbnail hovered | |
1082 * over. | |
1083 */ | |
1084 function incrementHoveredThumbnailCount() { | |
1085 hoveredThumbnailCount++; | |
1086 } | |
1087 | |
1088 /** | |
1089 * Logs the time to click for the specified item and the total number of | |
1090 * thumbnails hovered over. | |
1091 * @param {string} item The item to log the time-to-click. | |
1092 */ | |
1093 function logTimeToClickAndHoverCount(item) { | |
1094 var timeToClick = Date.now() - startTime; | |
1095 chrome.send('logTimeToClick', | |
1096 ['ExtendedNewTabPage.TimeToClick' + item, timeToClick]); | |
1097 chrome.send('metricsHandler:recordInHistogram', | |
1098 ['ExtendedNewTabPage.hoveredThumbnailCount', | |
1099 hoveredThumbnailCount, 40]); | |
1100 } | |
1101 | |
1102 /** | |
1103 * Wrappers to forward the callback to corresponding NewTabView member. | |
1104 */ | |
1105 function appAdded() { | |
1106 return newTabView.appAdded.apply(newTabView, arguments); | |
1107 } | |
1108 | |
1109 function appMoved() { | |
1110 return newTabView.appMoved.apply(newTabView, arguments); | |
1111 } | |
1112 | |
1113 function appRemoved() { | |
1114 return newTabView.appRemoved.apply(newTabView, arguments); | |
1115 } | |
1116 | |
1117 function appsPrefChangeCallback() { | |
1118 return newTabView.appsPrefChangedCallback.apply(newTabView, arguments); | |
1119 } | |
1120 | |
1121 function getAppsCallback() { | |
1122 return newTabView.getAppsCallback.apply(newTabView, arguments); | |
1123 } | |
1124 | |
1125 function getCardSlider() { | |
1126 return newTabView.cardSlider; | |
1127 } | |
1128 | |
1129 function setAppToBeHighlighted(appId) { | |
1130 newTabView.highlightAppId = appId; | |
1131 } | |
1132 | |
1133 function layout() { | |
1134 newTabView.layout.apply(newTabView, arguments); | |
1135 } | |
1136 | |
1137 function getContentHeight() { | |
1138 return newTabView.contentHeight; | |
1139 } | |
1140 | |
1141 function getContentWidth() { | |
1142 return newTabView.contentWidth; | |
1143 } | |
1144 | |
1145 function noop() { | |
1146 // Ignore some NTP4 callbacks for backwards compatibility purposes. | |
1147 } | |
1148 | |
1149 // Return an object with all the exports | |
1150 return { | |
1151 APP_LAUNCH: APP_LAUNCH, | |
1152 TILE_ROW_HEIGHT: TILE_ROW_HEIGHT, | |
1153 appAdded: appAdded, | |
1154 appMoved: appMoved, | |
1155 appRemoved: appRemoved, | |
1156 appsPrefChangeCallback: appsPrefChangeCallback, | |
1157 getAppsCallback: getAppsCallback, | |
1158 getCardSlider: getCardSlider, | |
1159 getContentHeight: getContentHeight, | |
1160 getContentWidth: getContentWidth, | |
1161 getThumbnailUrl: getThumbnailUrl, | |
1162 incrementHoveredThumbnailCount: incrementHoveredThumbnailCount, | |
1163 layout: layout, | |
1164 logTimeToClickAndHoverCount: logTimeToClickAndHoverCount, | |
1165 NtpFollowAction: NtpFollowAction, | |
1166 onLoad: onLoad, | |
1167 setAppToBeHighlighted: setAppToBeHighlighted, | |
1168 setBookmarkBarAttached: setBookmarkBarAttached, | |
1169 setFaviconDominantColor: setFaviconDominantColor, | |
1170 setForeignSessions: noop, | |
1171 setMostVisitedPages: setMostVisitedPages, | |
1172 setRecentlyClosedTabs: noop, | |
1173 showNotification: showNotification, | |
1174 themeChanged: themeChanged, | |
1175 updateLogin: noop, | |
1176 }; | |
1177 }); | |
1178 | |
1179 document.addEventListener('DOMContentLoaded', ntp.onLoad); | |
1180 | |
1181 var toCssPx = cr.ui.toCssPx; | |
OLD | NEW |