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.
6 * @fileoverview PageListView implementation.
7 * PageListView manages page list, dot list, switcher buttons and handles apps
8 * pages callbacks from backend.
10 * Note that you need to have AppLauncherHandler in your WebUI to use this code.
14 * @typedef {{app_launch_ordinal: string,
15 * description: string,
20 * full_name_direction: string,
21 * homepageUrl: string,
23 * icon_big_exists: boolean,
25 * icon_small_exists: boolean,
27 * is_component: boolean,
28 * is_webstore: boolean,
29 * kioskEnabled: boolean,
32 * launch_container: number,
33 * launch_type: number,
34 * mayDisable: boolean,
36 * offlineEnabled: boolean,
38 * packagedApp: boolean,
43 * @see chrome/browser/ui/webui/ntp/app_launcher_handler.cc
47 cr
.define('ntp', function() {
51 * Creates a PageListView object.
55 function PageListView() {
58 PageListView
.prototype = {
60 * The CardSlider object to use for changing app pages.
61 * @type {cr.ui.CardSlider|undefined}
63 cardSlider
: undefined,
66 * The frame div for this.cardSlider.
67 * @type {!Element|undefined}
69 sliderFrame
: undefined,
72 * The 'page-list' element.
73 * @type {!Element|undefined}
78 * A list of all 'tile-page' elements.
79 * @type {!NodeList|undefined}
84 * A list of all 'apps-page' elements.
85 * @type {!NodeList|undefined}
90 * The 'dots-list' element.
91 * @type {!Element|undefined}
96 * The left and right paging buttons.
97 * @type {!ntp.PageSwitcher|undefined}
99 pageSwitcherStart
: undefined,
100 pageSwitcherEnd
: undefined,
103 * The 'trash' element. Note that technically this is unnecessary,
104 * JavaScript creates the object for us based on the id. But I don't want
105 * to rely on the ID being the same, and JSCompiler doesn't know about it.
106 * @type {!Element|undefined}
111 * The type of page that is currently shown. The value is a numerical ID.
117 * The index of the page that is currently shown, within the page type.
118 * For example if the third Apps page is showing, this will be 2.
124 * EventTracker for managing event listeners for page events.
125 * @type {!EventTracker}
127 eventTracker
: new EventTracker
,
130 * If non-null, this is the ID of the app to highlight to the user the next
131 * time getAppsCallback runs. "Highlight" in this case means to switch to
132 * the page and run the new tile animation.
135 highlightAppId
: null,
138 * Initializes page list view.
139 * @param {!Element} pageList A DIV element to host all pages.
140 * @param {!Element} dotList An UL element to host nav dots. Each dot
142 * @param {!Element} cardSliderFrame The card slider frame that hosts
143 * pageList and switcher buttons.
144 * @param {!Element|undefined} opt_trash Optional trash element.
145 * @param {!ntp.PageSwitcher|undefined} opt_pageSwitcherStart Optional start
146 * page switcher button.
147 * @param {!ntp.PageSwitcher|undefined} opt_pageSwitcherEnd Optional end
148 * page switcher button.
150 initialize: function(pageList
, dotList
, cardSliderFrame
, opt_trash
,
151 opt_pageSwitcherStart
, opt_pageSwitcherEnd
) {
152 this.pageList
= pageList
;
154 this.dotList
= dotList
;
155 cr
.ui
.decorate(this.dotList
, ntp
.DotList
);
157 this.trash
= opt_trash
;
159 new ntp
.Trash(this.trash
);
161 this.pageSwitcherStart
= opt_pageSwitcherStart
;
162 if (this.pageSwitcherStart
)
163 ntp
.initializePageSwitcher(this.pageSwitcherStart
);
165 this.pageSwitcherEnd
= opt_pageSwitcherEnd
;
166 if (this.pageSwitcherEnd
)
167 ntp
.initializePageSwitcher(this.pageSwitcherEnd
);
169 this.shownPage
= loadTimeData
.getInteger('shown_page_type');
170 this.shownPageIndex
= loadTimeData
.getInteger('shown_page_index');
172 // TODO(dbeam): remove showApps and everything that says if (apps).
173 assert(loadTimeData
.getBoolean('showApps'));
175 // Request data on the apps so we can fill them in.
176 // Note that this is kicked off asynchronously. 'getAppsCallback' will
177 // be invoked at some point after this function returns.
178 chrome
.send('getApps');
180 document
.addEventListener('keydown', this.onDocKeyDown_
.bind(this));
182 this.tilePages
= this.pageList
.getElementsByClassName('tile-page');
183 this.appsPages
= this.pageList
.getElementsByClassName('apps-page');
185 // Initialize the cardSlider without any cards at the moment.
186 this.sliderFrame
= cardSliderFrame
;
187 this.cardSlider
= new cr
.ui
.CardSlider(this.sliderFrame
, this.pageList
,
188 this.sliderFrame
.offsetWidth
);
190 // Prevent touch events from triggering any sort of native scrolling if
191 // there are multiple cards in the slider frame.
192 var cardSlider
= this.cardSlider
;
193 cardSliderFrame
.addEventListener('touchmove', function(e
) {
194 if (cardSlider
.cardCount
<= 1)
199 // Handle mousewheel events anywhere in the card slider, so that wheel
200 // events on the page switchers will still scroll the page.
201 // This listener must be added before the card slider is initialized,
202 // because it needs to be called before the card slider's handler.
203 cardSliderFrame
.addEventListener('mousewheel', function(e
) {
204 if (cardSlider
.currentCardValue
.handleMouseWheel(e
)) {
205 e
.preventDefault(); // Prevent default scroll behavior.
206 e
.stopImmediatePropagation(); // Prevent horizontal card flipping.
210 this.cardSlider
.initialize(
211 loadTimeData
.getBoolean('isSwipeTrackingFromScrollEventsEnabled'));
213 // Handle events from the card slider.
214 this.pageList
.addEventListener('cardSlider:card_changed',
215 this.onCardChanged_
.bind(this));
216 this.pageList
.addEventListener('cardSlider:card_added',
217 this.onCardAdded_
.bind(this));
218 this.pageList
.addEventListener('cardSlider:card_removed',
219 this.onCardRemoved_
.bind(this));
221 // Ensure the slider is resized appropriately with the window.
222 window
.addEventListener('resize', this.onWindowResize_
.bind(this));
224 // Update apps when online state changes.
225 window
.addEventListener('online',
226 this.updateOfflineEnabledApps_
.bind(this));
227 window
.addEventListener('offline',
228 this.updateOfflineEnabledApps_
.bind(this));
232 * Appends a tile page.
234 * @param {!ntp.TilePage} page The page element.
235 * @param {string} title The title of the tile page.
236 * @param {boolean} titleIsEditable If true, the title can be changed.
237 * @param {ntp.TilePage=} opt_refNode Optional reference node to insert in
239 * When opt_refNode is falsey, |page| will just be appended to the end of
242 appendTilePage: function(page
, title
, titleIsEditable
, opt_refNode
) {
244 var refIndex
= this.getTilePageIndex(opt_refNode
);
245 this.cardSlider
.addCardAtIndex(page
, refIndex
);
247 this.cardSlider
.appendCard(page
);
250 // If we're appending an AppsPage and it's a temporary page, animate it.
251 var animate
= page
instanceof ntp
.AppsPage
&&
252 page
.classList
.contains('temporary');
253 // Make a deep copy of the dot template to add a new one.
254 var newDot
= new ntp
.NavDot(page
, title
, titleIsEditable
, animate
);
255 page
.navigationDot
= newDot
;
256 this.dotList
.insertBefore(newDot
,
257 opt_refNode
? opt_refNode
.navigationDot
: null);
258 // Set a tab index on the first dot.
259 if (this.dotList
.dots
.length
== 1)
262 this.eventTracker
.add(page
, 'pagelayout', this.onPageLayout_
.bind(this));
266 * Called by chrome when an app has changed positions.
267 * @param {AppInfo} appData The data for the app. This contains page and
270 appMoved: function(appData
) {
271 assert(loadTimeData
.getBoolean('showApps'));
273 var app
= $(appData
.id
);
274 assert(app
, 'trying to move an app that doesn\'t exist');
277 this.appsPages
[appData
.page_index
].insertApp(appData
, false);
281 * Called by chrome when an existing app has been disabled or
282 * removed/uninstalled from chrome.
283 * @param {AppInfo} appData A data structure full of relevant information
285 * @param {boolean} isUninstall True if the app is being uninstalled;
286 * false if the app is being disabled.
287 * @param {boolean} fromPage True if the removal was from the current page.
289 appRemoved: function(appData
, isUninstall
, fromPage
) {
290 assert(loadTimeData
.getBoolean('showApps'));
292 var app
= $(appData
.id
);
293 assert(app
, 'trying to remove an app that doesn\'t exist');
296 app
.replaceAppData(appData
);
298 app
.remove(!!fromPage
);
302 * @return {boolean} If the page is still starting up.
305 isStartingUp_: function() {
306 return document
.documentElement
.classList
.contains('starting-up');
310 * Tracks whether apps have been loaded at least once.
317 * Callback invoked by chrome with the apps available.
319 * Note that calls to this function can occur at any time, not just in
320 * response to a getApps request. For example, when a user
321 * installs/uninstalls an app on another synchronized devices.
322 * @param {{apps: Array<AppInfo>, appPageNames: Array<string>}} data
323 * An object with all the data on available applications.
325 getAppsCallback: function(data
) {
326 assert(loadTimeData
.getBoolean('showApps'));
328 var startTime
= Date
.now();
330 // Remember this to select the correct card when done rebuilding.
331 var prevCurrentCard
= this.cardSlider
.currentCard
;
333 // Make removal of pages and dots as quick as possible with less DOM
334 // operations, reflows, or repaints. We set currentCard = 0 and remove
335 // from the end to not encounter any auto-magic card selections in the
336 // process and we hide the card slider throughout.
337 this.cardSlider
.currentCard
= 0;
339 // Clear any existing apps pages and dots.
340 // TODO(rbyers): It might be nice to preserve animation of dots after an
341 // uninstall. Could we re-use the existing page and dot elements? It
342 // seems unfortunate to have Chrome send us the entire apps list after an
344 while (this.appsPages
.length
> 0)
345 this.removeTilePageAndDot_(this.appsPages
[this.appsPages
.length
- 1]);
347 // Get the array of apps and add any special synthesized entries
348 var apps
= data
.apps
;
350 // Get a list of page names
351 var pageNames
= data
.appPageNames
;
353 function stringListIsEmpty(list
) {
354 for (var i
= 0; i
< list
.length
; i
++) {
361 // Sort by launch ordinal
362 apps
.sort(function(a
, b
) {
363 return a
.app_launch_ordinal
> b
.app_launch_ordinal
? 1 :
364 a
.app_launch_ordinal
< b
.app_launch_ordinal
? -1 : 0;
367 // An app to animate (in case it was just installed).
370 // If there are any pages after the apps, add new pages before them.
371 var lastAppsPage
= (this.appsPages
.length
> 0) ?
372 this.appsPages
[this.appsPages
.length
- 1] : null;
373 var lastAppsPageIndex
= (lastAppsPage
!= null) ?
374 Array
.prototype.indexOf
.call(this.tilePages
, lastAppsPage
) : -1;
375 var nextPageAfterApps
= lastAppsPageIndex
!= -1 ?
376 this.tilePages
[lastAppsPageIndex
+ 1] : null;
378 // Add the apps, creating pages as necessary
379 for (var i
= 0; i
< apps
.length
; i
++) {
381 var pageIndex
= app
.page_index
|| 0;
382 while (pageIndex
>= this.appsPages
.length
) {
383 var pageName
= loadTimeData
.getString('appDefaultPageName');
384 if (this.appsPages
.length
< pageNames
.length
)
385 pageName
= pageNames
[this.appsPages
.length
];
387 var origPageCount
= this.appsPages
.length
;
388 this.appendTilePage(new ntp
.AppsPage(), pageName
, true,
390 // Confirm that appsPages is a live object, updated when a new page is
391 // added (otherwise we'd have an infinite loop)
392 assert(this.appsPages
.length
== origPageCount
+ 1,
393 'expected new page');
396 if (app
.id
== this.highlightAppId
)
399 this.appsPages
[pageIndex
].insertApp(app
, false);
402 this.cardSlider
.currentCard
= prevCurrentCard
;
405 this.appAdded(highlightApp
, true);
407 logEvent('apps.layout: ' + (Date
.now() - startTime
));
409 // Tell the slider about the pages and mark the current page.
410 this.updateSliderCards();
411 this.cardSlider
.currentCardValue
.navigationDot
.classList
.add('selected');
413 if (!this.appsLoaded_
) {
414 this.appsLoaded_
= true;
415 cr
.dispatchSimpleEvent(document
, 'sectionready', true, true);
417 this.updateAppLauncherPromoHiddenState_();
421 * Called by chrome when a new app has been added to chrome or has been
422 * enabled if previously disabled.
423 * @param {AppInfo} appData A data structure full of relevant information
425 * @param {boolean=} opt_highlight Whether the app about to be added should
428 appAdded: function(appData
, opt_highlight
) {
429 assert(loadTimeData
.getBoolean('showApps'));
431 if (appData
.id
== this.highlightAppId
) {
432 opt_highlight
= true;
433 this.highlightAppId
= null;
436 var pageIndex
= appData
.page_index
|| 0;
438 if (pageIndex
>= this.appsPages
.length
) {
439 while (pageIndex
>= this.appsPages
.length
) {
440 this.appendTilePage(new ntp
.AppsPage(),
441 loadTimeData
.getString('appDefaultPageName'),
444 this.updateSliderCards();
447 var page
= this.appsPages
[pageIndex
];
448 var app
= $(appData
.id
);
450 app
.replaceAppData(appData
);
451 } else if (opt_highlight
) {
452 page
.insertAndHighlightApp(appData
);
453 this.setShownPage_(loadTimeData
.getInteger('apps_page_id'),
456 page
.insertApp(appData
, false);
461 * Callback invoked by chrome whenever an app preference changes.
462 * @param {Object} data An object with all the data on available
465 appsPrefChangedCallback: function(data
) {
466 assert(loadTimeData
.getBoolean('showApps'));
468 for (var i
= 0; i
< data
.apps
.length
; ++i
) {
469 $(data
.apps
[i
].id
).appData
= data
.apps
[i
];
472 // Set the App dot names.
473 var dots
= this.dotList
.getElementsByClassName('dot');
474 for (var i
= 0; i
< dots
.length
; ++i
) {
475 dots
[i
].displayTitle
= data
.appPageNames
[i
] || '';
480 * Callback invoked by chrome whenever the app launcher promo pref changes.
481 * @param {boolean} show Identifies if we should show or hide the promo.
483 appLauncherPromoPrefChangeCallback: function(show
) {
484 loadTimeData
.overrideValues({showAppLauncherPromo
: show
});
485 this.updateAppLauncherPromoHiddenState_();
489 * Updates the hidden state of the app launcher promo based on the page
490 * shown and load data content.
492 updateAppLauncherPromoHiddenState_: function() {
493 $('app-launcher-promo').hidden
=
494 !loadTimeData
.getBoolean('showAppLauncherPromo') ||
495 this.shownPage
!= loadTimeData
.getInteger('apps_page_id');
499 * Invoked whenever the pages in apps-page-list have changed so that
500 * the Slider knows about the new elements.
502 updateSliderCards: function() {
503 var pageNo
= Math
.max(0, Math
.min(this.cardSlider
.currentCard
,
504 this.tilePages
.length
- 1));
505 this.cardSlider
.setCards(Array
.prototype.slice
.call(this.tilePages
),
507 if (this.shownPage
== loadTimeData
.getInteger('apps_page_id') &&
508 loadTimeData
.getBoolean('showApps')) {
509 this.cardSlider
.selectCardByValue(
510 this.appsPages
[Math
.min(this.shownPageIndex
,
511 this.appsPages
.length
- 1)]);
516 * Called whenever tiles should be re-arranging themselves out of the way
517 * of a moving or insert tile.
519 enterRearrangeMode: function() {
520 if (loadTimeData
.getBoolean('showApps')) {
521 var tempPage
= new ntp
.AppsPage();
522 tempPage
.classList
.add('temporary');
523 var pageName
= loadTimeData
.getString('appDefaultPageName');
524 this.appendTilePage(tempPage
, pageName
, true);
527 if (ntp
.getCurrentlyDraggingTile().firstChild
.canBeRemoved()) {
528 $('footer').classList
.add('showing-trash-mode');
529 $('footer-menu-container').style
.minWidth
= $('trash').offsetWidth
-
530 $('chrome-web-store-link').offsetWidth
+ 'px';
533 document
.documentElement
.classList
.add('dragging-mode');
537 * Invoked whenever some app is released
539 leaveRearrangeMode: function() {
540 var tempPage
= /** @type {ntp.AppsPage} */(
541 document
.querySelector('.tile-page.temporary'));
543 var dot
= tempPage
.navigationDot
;
544 if (!tempPage
.tileCount
&&
545 tempPage
!= this.cardSlider
.currentCardValue
) {
546 this.removeTilePageAndDot_(tempPage
, true);
548 tempPage
.classList
.remove('temporary');
549 this.saveAppPageName(tempPage
,
550 loadTimeData
.getString('appDefaultPageName'));
554 $('footer').classList
.remove('showing-trash-mode');
555 $('footer-menu-container').style
.minWidth
= '';
556 document
.documentElement
.classList
.remove('dragging-mode');
560 * Callback for the 'pagelayout' event.
561 * @param {Event} e The event.
563 onPageLayout_: function(e
) {
564 if (Array
.prototype.indexOf
.call(this.tilePages
, e
.currentTarget
) !=
565 this.cardSlider
.currentCard
) {
569 this.updatePageSwitchers();
573 * Adjusts the size and position of the page switchers according to the
574 * layout of the current card, and updates the aria-label attributes of
575 * the page switchers.
577 updatePageSwitchers: function() {
578 if (!this.pageSwitcherStart
|| !this.pageSwitcherEnd
)
581 var page
= this.cardSlider
.currentCardValue
;
583 this.pageSwitcherStart
.hidden
= !page
||
584 (this.cardSlider
.currentCard
== 0);
585 this.pageSwitcherEnd
.hidden
= !page
||
586 (this.cardSlider
.currentCard
== this.cardSlider
.cardCount
- 1);
591 var pageSwitcherLeft
= isRTL() ? this.pageSwitcherEnd
:
592 this.pageSwitcherStart
;
593 var pageSwitcherRight
= isRTL() ? this.pageSwitcherStart
:
594 this.pageSwitcherEnd
;
595 var scrollbarWidth
= page
.scrollbarWidth
;
596 pageSwitcherLeft
.style
.width
=
597 (page
.sideMargin
+ 13) + 'px';
598 pageSwitcherLeft
.style
.left
= '0';
599 pageSwitcherRight
.style
.width
=
600 (page
.sideMargin
- scrollbarWidth
+ 13) + 'px';
601 pageSwitcherRight
.style
.right
= scrollbarWidth
+ 'px';
603 var offsetTop
= page
.querySelector('.tile-page-content').offsetTop
+ 'px';
604 pageSwitcherLeft
.style
.top
= offsetTop
;
605 pageSwitcherRight
.style
.top
= offsetTop
;
606 pageSwitcherLeft
.style
.paddingBottom
= offsetTop
;
607 pageSwitcherRight
.style
.paddingBottom
= offsetTop
;
609 // Update the aria-label attributes of the two page switchers.
610 this.pageSwitcherStart
.updateButtonAccessibleLabel(this.dotList
.dots
);
611 this.pageSwitcherEnd
.updateButtonAccessibleLabel(this.dotList
.dots
);
615 * Returns the index of the given apps page.
616 * @param {ntp.AppsPage} page The AppsPage we wish to find.
617 * @return {number} The index of |page| or -1 if it is not in the
620 getAppsPageIndex: function(page
) {
621 return Array
.prototype.indexOf
.call(this.appsPages
, page
);
625 * Handler for cardSlider:card_changed events from this.cardSlider.
626 * @param {Event} e The cardSlider:card_changed event.
629 onCardChanged_: function(e
) {
630 var page
= e
.cardSlider
.currentCardValue
;
632 // Don't change shownPage until startup is done (and page changes actually
633 // reflect user actions).
634 if (!this.isStartingUp_()) {
635 if (page
.classList
.contains('apps-page')) {
636 this.setShownPage_(loadTimeData
.getInteger('apps_page_id'),
637 this.getAppsPageIndex(page
));
639 console
.error('unknown page selected');
643 // Update the active dot
644 var curDot
= this.dotList
.getElementsByClassName('selected')[0];
646 curDot
.classList
.remove('selected');
647 page
.navigationDot
.classList
.add('selected');
648 this.updatePageSwitchers();
652 * Saves/updates the newly selected page to open when first loading the NTP.
653 * @param {number} shownPage The new shown page type.
654 * @param {number} shownPageIndex The new shown page index.
657 setShownPage_: function(shownPage
, shownPageIndex
) {
658 assert(shownPageIndex
>= 0);
659 this.shownPage
= shownPage
;
660 this.shownPageIndex
= shownPageIndex
;
661 chrome
.send('pageSelected', [this.shownPage
, this.shownPageIndex
]);
662 this.updateAppLauncherPromoHiddenState_();
666 * Listen for card additions to update the page switchers or the current
668 * @param {Event} e A card removed or added event.
670 onCardAdded_: function(e
) {
671 // When the second arg passed to insertBefore is falsey, it acts just like
673 this.pageList
.insertBefore(e
.addedCard
, this.tilePages
[e
.addedIndex
]);
674 this.onCardAddedOrRemoved_();
678 * Listen for card removals to update the page switchers or the current card
680 * @param {Event} e A card removed or added event.
682 onCardRemoved_: function(e
) {
683 e
.removedCard
.parentNode
.removeChild(e
.removedCard
);
684 this.onCardAddedOrRemoved_();
688 * Called when a card is removed or added.
691 onCardAddedOrRemoved_: function() {
692 if (this.isStartingUp_())
695 // Without repositioning there were issues - http://crbug.com/133457.
696 this.cardSlider
.repositionFrame();
697 this.updatePageSwitchers();
701 * Save the name of an apps page.
702 * Store the apps page name into the preferences store.
703 * @param {ntp.AppsPage} appPage The app page for which we wish to save.
704 * @param {string} name The name of the page.
706 saveAppPageName: function(appPage
, name
) {
707 var index
= this.getAppsPageIndex(appPage
);
709 chrome
.send('saveAppPageName', [name
, index
]);
713 * Window resize handler.
716 onWindowResize_: function(e
) {
717 this.cardSlider
.resize(this.sliderFrame
.offsetWidth
);
718 this.updatePageSwitchers();
722 * Listener for offline status change events. Updates apps that are
723 * not offline-enabled to be grayscale if the browser is offline.
726 updateOfflineEnabledApps_: function() {
727 var apps
= document
.querySelectorAll('.app');
728 for (var i
= 0; i
< apps
.length
; ++i
) {
729 if (apps
[i
].appData
.enabled
&& !apps
[i
].appData
.offlineEnabled
) {
737 * Handler for key events on the page. Ctrl-Arrow will switch the visible
739 * @param {Event} e The KeyboardEvent.
742 onDocKeyDown_: function(e
) {
743 if (!e
.ctrlKey
|| e
.altKey
|| e
.metaKey
|| e
.shiftKey
)
747 if (e
.keyIdentifier
== 'Left')
749 else if (e
.keyIdentifier
== 'Right')
755 (this.cardSlider
.currentCard
+ direction
+
756 this.cardSlider
.cardCount
) % this.cardSlider
.cardCount
;
757 this.cardSlider
.selectCard(cardIndex
, true);
763 * Returns the index of a given tile page.
764 * @param {ntp.TilePage} page The TilePage we wish to find.
765 * @return {number} The index of |page| or -1 if it is not in the
768 getTilePageIndex: function(page
) {
769 return Array
.prototype.indexOf
.call(this.tilePages
, page
);
773 * Removes a page and navigation dot (if the navdot exists).
774 * @param {ntp.TilePage} page The page to be removed.
775 * @param {boolean=} opt_animate If the removal should be animated.
777 removeTilePageAndDot_: function(page
, opt_animate
) {
778 if (page
.navigationDot
)
779 page
.navigationDot
.remove(opt_animate
);
780 this.cardSlider
.removeCard(page
);
785 PageListView
: PageListView