Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / ntp4 / page_list_view.js
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 PageListView implementation.
7  * PageListView manages page list, dot list, switcher buttons and handles apps
8  * pages callbacks from backend.
9  *
10  * Note that you need to have AppLauncherHandler in your WebUI to use this code.
11  */
12
13 /**
14  * @typedef {{app_launch_ordinal: string,
15  *            description: string,
16  *            detailsUrl: string,
17  *            direction: string,
18  *            enabled: boolean,
19  *            full_name: string,
20  *            full_name_direction: string,
21  *            homepageUrl: string,
22  *            icon_big: string,
23  *            icon_big_exists: boolean,
24  *            icon_small: string,
25  *            icon_small_exists: boolean,
26  *            id: string,
27  *            is_component: boolean,
28  *            is_webstore: boolean,
29  *            kioskEnabled: boolean,
30  *            kioskOnly: boolean,
31  *            launch_container: number,
32  *            launch_type: number,
33  *            mayDisable: boolean,
34  *            name: string,
35  *            offlineEnabled: boolean,
36  *            optionsUrl: string,
37  *            packagedApp: boolean,
38  *            page_index: number,
39  *            title: string,
40  *            url: string,
41  *            version: string}}
42  * @see chrome/browser/ui/webui/ntp/app_launcher_handler.cc
43  */
44 var AppInfo;
45
46 cr.define('ntp', function() {
47   'use strict';
48
49   /**
50    * Creates a PageListView object.
51    * @constructor
52    * @extends {Object}
53    */
54   function PageListView() {
55   }
56
57   PageListView.prototype = {
58     /**
59      * The CardSlider object to use for changing app pages.
60      * @type {cr.ui.CardSlider|undefined}
61      */
62     cardSlider: undefined,
63
64     /**
65      * The frame div for this.cardSlider.
66      * @type {!Element|undefined}
67      */
68     sliderFrame: undefined,
69
70     /**
71      * The 'page-list' element.
72      * @type {!Element|undefined}
73      */
74     pageList: undefined,
75
76     /**
77      * A list of all 'tile-page' elements.
78      * @type {!NodeList|undefined}
79      */
80     tilePages: undefined,
81
82     /**
83      * A list of all 'apps-page' elements.
84      * @type {!NodeList|undefined}
85      */
86     appsPages: undefined,
87
88     /**
89      * The Suggestions page.
90      * @type {!Element|undefined}
91      */
92     suggestionsPage: undefined,
93
94     /**
95      * The Most Visited page.
96      * @type {!Element|undefined}
97      */
98     mostVisitedPage: undefined,
99
100     /**
101      * The 'dots-list' element.
102      * @type {!Element|undefined}
103      */
104     dotList: undefined,
105
106     /**
107      * The left and right paging buttons.
108      * @type {!ntp.PageSwitcher|undefined}
109      */
110     pageSwitcherStart: undefined,
111     pageSwitcherEnd: undefined,
112
113     /**
114      * The 'trash' element.  Note that technically this is unnecessary,
115      * JavaScript creates the object for us based on the id.  But I don't want
116      * to rely on the ID being the same, and JSCompiler doesn't know about it.
117      * @type {!Element|undefined}
118      */
119     trash: undefined,
120
121     /**
122      * The type of page that is currently shown. The value is a numerical ID.
123      * @type {number}
124      */
125     shownPage: 0,
126
127     /**
128      * The index of the page that is currently shown, within the page type.
129      * For example if the third Apps page is showing, this will be 2.
130      * @type {number}
131      */
132     shownPageIndex: 0,
133
134     /**
135      * EventTracker for managing event listeners for page events.
136      * @type {!EventTracker}
137      */
138     eventTracker: new EventTracker,
139
140     /**
141      * If non-null, this is the ID of the app to highlight to the user the next
142      * time getAppsCallback runs. "Highlight" in this case means to switch to
143      * the page and run the new tile animation.
144      * @type {?string}
145      */
146     highlightAppId: null,
147
148     /**
149      * Initializes page list view.
150      * @param {!Element} pageList A DIV element to host all pages.
151      * @param {!Element} dotList An UL element to host nav dots. Each dot
152      *     represents a page.
153      * @param {!Element} cardSliderFrame The card slider frame that hosts
154      *     pageList and switcher buttons.
155      * @param {!Element|undefined} opt_trash Optional trash element.
156      * @param {!ntp.PageSwitcher|undefined} opt_pageSwitcherStart Optional start
157      *     page switcher button.
158      * @param {!ntp.PageSwitcher|undefined} opt_pageSwitcherEnd Optional end
159      *     page switcher button.
160      */
161     initialize: function(pageList, dotList, cardSliderFrame, opt_trash,
162                          opt_pageSwitcherStart, opt_pageSwitcherEnd) {
163       this.pageList = pageList;
164
165       this.dotList = dotList;
166       cr.ui.decorate(this.dotList, ntp.DotList);
167
168       this.trash = opt_trash;
169       if (this.trash)
170         new ntp.Trash(this.trash);
171
172       this.pageSwitcherStart = opt_pageSwitcherStart;
173       if (this.pageSwitcherStart)
174         ntp.initializePageSwitcher(this.pageSwitcherStart);
175
176       this.pageSwitcherEnd = opt_pageSwitcherEnd;
177       if (this.pageSwitcherEnd)
178         ntp.initializePageSwitcher(this.pageSwitcherEnd);
179
180       this.shownPage = loadTimeData.getInteger('shown_page_type');
181       this.shownPageIndex = loadTimeData.getInteger('shown_page_index');
182
183       if (loadTimeData.getBoolean('showApps')) {
184         // Request data on the apps so we can fill them in.
185         // Note that this is kicked off asynchronously.  'getAppsCallback' will
186         // be invoked at some point after this function returns.
187         chrome.send('getApps');
188       } else {
189         // No apps page.
190         if (this.shownPage == loadTimeData.getInteger('apps_page_id')) {
191           this.setShownPage_(
192               loadTimeData.getInteger('most_visited_page_id'), 0);
193         }
194
195         document.body.classList.add('bare-minimum');
196       }
197
198       document.addEventListener('keydown', this.onDocKeyDown_.bind(this));
199
200       this.tilePages = this.pageList.getElementsByClassName('tile-page');
201       this.appsPages = this.pageList.getElementsByClassName('apps-page');
202
203       // Initialize the cardSlider without any cards at the moment.
204       this.sliderFrame = cardSliderFrame;
205       this.cardSlider = new cr.ui.CardSlider(this.sliderFrame, this.pageList,
206           this.sliderFrame.offsetWidth);
207
208       // Prevent touch events from triggering any sort of native scrolling if
209       // there are multiple cards in the slider frame.
210       var cardSlider = this.cardSlider;
211       cardSliderFrame.addEventListener('touchmove', function(e) {
212         if (cardSlider.cardCount <= 1)
213           return;
214         e.preventDefault();
215       }, true);
216
217       // Handle mousewheel events anywhere in the card slider, so that wheel
218       // events on the page switchers will still scroll the page.
219       // This listener must be added before the card slider is initialized,
220       // because it needs to be called before the card slider's handler.
221       cardSliderFrame.addEventListener('mousewheel', function(e) {
222         if (cardSlider.currentCardValue.handleMouseWheel(e)) {
223           e.preventDefault();  // Prevent default scroll behavior.
224           e.stopImmediatePropagation();  // Prevent horizontal card flipping.
225         }
226       });
227
228       this.cardSlider.initialize(
229           loadTimeData.getBoolean('isSwipeTrackingFromScrollEventsEnabled'));
230
231       // Handle events from the card slider.
232       this.pageList.addEventListener('cardSlider:card_changed',
233                                      this.onCardChanged_.bind(this));
234       this.pageList.addEventListener('cardSlider:card_added',
235                                      this.onCardAdded_.bind(this));
236       this.pageList.addEventListener('cardSlider:card_removed',
237                                      this.onCardRemoved_.bind(this));
238
239       // Ensure the slider is resized appropriately with the window.
240       window.addEventListener('resize', this.onWindowResize_.bind(this));
241
242       // Update apps when online state changes.
243       window.addEventListener('online',
244           this.updateOfflineEnabledApps_.bind(this));
245       window.addEventListener('offline',
246           this.updateOfflineEnabledApps_.bind(this));
247     },
248
249     /**
250      * Appends a tile page.
251      *
252      * @param {!ntp.TilePage} page The page element.
253      * @param {string} title The title of the tile page.
254      * @param {boolean} titleIsEditable If true, the title can be changed.
255      * @param {ntp.TilePage=} opt_refNode Optional reference node to insert in
256      *     front of.
257      * When opt_refNode is falsey, |page| will just be appended to the end of
258      * the page list.
259      */
260     appendTilePage: function(page, title, titleIsEditable, opt_refNode) {
261       if (opt_refNode) {
262         var refIndex = this.getTilePageIndex(opt_refNode);
263         this.cardSlider.addCardAtIndex(page, refIndex);
264       } else {
265         this.cardSlider.appendCard(page);
266       }
267
268       // Remember special MostVisitedPage.
269       if (typeof ntp.MostVisitedPage != 'undefined' &&
270           page instanceof ntp.MostVisitedPage) {
271         assert(this.tilePages.length == 1,
272                'MostVisitedPage should be added as first tile page');
273         this.mostVisitedPage = page;
274       }
275
276       if (typeof ntp.SuggestionsPage != 'undefined' &&
277           page instanceof ntp.SuggestionsPage) {
278         this.suggestionsPage = page;
279       }
280
281       // If we're appending an AppsPage and it's a temporary page, animate it.
282       var animate = page instanceof ntp.AppsPage &&
283                     page.classList.contains('temporary');
284       // Make a deep copy of the dot template to add a new one.
285       var newDot = new ntp.NavDot(page, title, titleIsEditable, animate);
286       page.navigationDot = newDot;
287       this.dotList.insertBefore(newDot,
288                                 opt_refNode ? opt_refNode.navigationDot : null);
289       // Set a tab index on the first dot.
290       if (this.dotList.dots.length == 1)
291         newDot.tabIndex = 3;
292
293       this.eventTracker.add(page, 'pagelayout', this.onPageLayout_.bind(this));
294     },
295
296     /**
297      * Called by chrome when an app has changed positions.
298      * @param {AppInfo} appData The data for the app. This contains page and
299      *     position indices.
300      */
301     appMoved: function(appData) {
302       assert(loadTimeData.getBoolean('showApps'));
303
304       var app = $(appData.id);
305       assert(app, 'trying to move an app that doesn\'t exist');
306       app.remove(false);
307
308       this.appsPages[appData.page_index].insertApp(appData, false);
309     },
310
311     /**
312      * Called by chrome when an existing app has been disabled or
313      * removed/uninstalled from chrome.
314      * @param {AppInfo} appData A data structure full of relevant information
315      *     for the app.
316      * @param {boolean} isUninstall True if the app is being uninstalled;
317      *     false if the app is being disabled.
318      * @param {boolean} fromPage True if the removal was from the current page.
319      */
320     appRemoved: function(appData, isUninstall, fromPage) {
321       assert(loadTimeData.getBoolean('showApps'));
322
323       var app = $(appData.id);
324       assert(app, 'trying to remove an app that doesn\'t exist');
325
326       if (!isUninstall)
327         app.replaceAppData(appData);
328       else
329         app.remove(!!fromPage);
330     },
331
332     /**
333      * @return {boolean} If the page is still starting up.
334      * @private
335      */
336     isStartingUp_: function() {
337       return document.documentElement.classList.contains('starting-up');
338     },
339
340     /**
341      * Tracks whether apps have been loaded at least once.
342      * @type {boolean}
343      * @private
344      */
345     appsLoaded_: false,
346
347     /**
348      * Callback invoked by chrome with the apps available.
349      *
350      * Note that calls to this function can occur at any time, not just in
351      * response to a getApps request. For example, when a user
352      * installs/uninstalls an app on another synchronized devices.
353      * @param {{apps: Array.<AppInfo>, appPageNames: Array.<string>}} data
354      *     An object with all the data on available applications.
355      */
356     getAppsCallback: function(data) {
357       assert(loadTimeData.getBoolean('showApps'));
358
359       var startTime = Date.now();
360
361       // Remember this to select the correct card when done rebuilding.
362       var prevCurrentCard = this.cardSlider.currentCard;
363
364       // Make removal of pages and dots as quick as possible with less DOM
365       // operations, reflows, or repaints. We set currentCard = 0 and remove
366       // from the end to not encounter any auto-magic card selections in the
367       // process and we hide the card slider throughout.
368       this.cardSlider.currentCard = 0;
369
370       // Clear any existing apps pages and dots.
371       // TODO(rbyers): It might be nice to preserve animation of dots after an
372       // uninstall. Could we re-use the existing page and dot elements?  It
373       // seems unfortunate to have Chrome send us the entire apps list after an
374       // uninstall.
375       while (this.appsPages.length > 0)
376         this.removeTilePageAndDot_(this.appsPages[this.appsPages.length - 1]);
377
378       // Get the array of apps and add any special synthesized entries
379       var apps = data.apps;
380
381       // Get a list of page names
382       var pageNames = data.appPageNames;
383
384       function stringListIsEmpty(list) {
385         for (var i = 0; i < list.length; i++) {
386           if (list[i])
387             return false;
388         }
389         return true;
390       }
391
392       // Sort by launch ordinal
393       apps.sort(function(a, b) {
394         return a.app_launch_ordinal > b.app_launch_ordinal ? 1 :
395           a.app_launch_ordinal < b.app_launch_ordinal ? -1 : 0;
396       });
397
398       // An app to animate (in case it was just installed).
399       var highlightApp;
400
401       // If there are any pages after the apps, add new pages before them.
402       var lastAppsPage = (this.appsPages.length > 0) ?
403           this.appsPages[this.appsPages.length - 1] : null;
404       var lastAppsPageIndex = (lastAppsPage != null) ?
405           Array.prototype.indexOf.call(this.tilePages, lastAppsPage) : -1;
406       var nextPageAfterApps = lastAppsPageIndex != -1 ?
407           this.tilePages[lastAppsPageIndex + 1] : null;
408
409       // Add the apps, creating pages as necessary
410       for (var i = 0; i < apps.length; i++) {
411         var app = apps[i];
412         var pageIndex = app.page_index || 0;
413         while (pageIndex >= this.appsPages.length) {
414           var pageName = loadTimeData.getString('appDefaultPageName');
415           if (this.appsPages.length < pageNames.length)
416             pageName = pageNames[this.appsPages.length];
417
418           var origPageCount = this.appsPages.length;
419           this.appendTilePage(new ntp.AppsPage(), pageName, true,
420                               nextPageAfterApps);
421           // Confirm that appsPages is a live object, updated when a new page is
422           // added (otherwise we'd have an infinite loop)
423           assert(this.appsPages.length == origPageCount + 1,
424                  'expected new page');
425         }
426
427         if (app.id == this.highlightAppId)
428           highlightApp = app;
429         else
430           this.appsPages[pageIndex].insertApp(app, false);
431       }
432
433       this.cardSlider.currentCard = prevCurrentCard;
434
435       if (highlightApp)
436         this.appAdded(highlightApp, true);
437
438       logEvent('apps.layout: ' + (Date.now() - startTime));
439
440       // Tell the slider about the pages and mark the current page.
441       this.updateSliderCards();
442       this.cardSlider.currentCardValue.navigationDot.classList.add('selected');
443
444       if (!this.appsLoaded_) {
445         this.appsLoaded_ = true;
446         cr.dispatchSimpleEvent(document, 'sectionready', true, true);
447       }
448       this.updateAppLauncherPromoHiddenState_();
449     },
450
451     /**
452      * Called by chrome when a new app has been added to chrome or has been
453      * enabled if previously disabled.
454      * @param {AppInfo} appData A data structure full of relevant information
455      *     for the app.
456      * @param {boolean=} opt_highlight Whether the app about to be added should
457      *     be highlighted.
458      */
459     appAdded: function(appData, opt_highlight) {
460       assert(loadTimeData.getBoolean('showApps'));
461
462       if (appData.id == this.highlightAppId) {
463         opt_highlight = true;
464         this.highlightAppId = null;
465       }
466
467       var pageIndex = appData.page_index || 0;
468
469       if (pageIndex >= this.appsPages.length) {
470         while (pageIndex >= this.appsPages.length) {
471           this.appendTilePage(new ntp.AppsPage(),
472                               loadTimeData.getString('appDefaultPageName'),
473                               true);
474         }
475         this.updateSliderCards();
476       }
477
478       var page = this.appsPages[pageIndex];
479       var app = $(appData.id);
480       if (app) {
481         app.replaceAppData(appData);
482       } else if (opt_highlight) {
483         page.insertAndHighlightApp(appData);
484         this.setShownPage_(loadTimeData.getInteger('apps_page_id'),
485                            appData.page_index);
486       } else {
487         page.insertApp(appData, false);
488       }
489     },
490
491     /**
492      * Callback invoked by chrome whenever an app preference changes.
493      * @param {Object} data An object with all the data on available
494      *     applications.
495      */
496     appsPrefChangedCallback: function(data) {
497       assert(loadTimeData.getBoolean('showApps'));
498
499       for (var i = 0; i < data.apps.length; ++i) {
500         $(data.apps[i].id).appData = data.apps[i];
501       }
502
503       // Set the App dot names. Skip the first dot (Most Visited).
504       var dots = this.dotList.getElementsByClassName('dot');
505       var start = this.mostVisitedPage ? 1 : 0;
506       for (var i = start; i < dots.length; ++i) {
507         dots[i].displayTitle = data.appPageNames[i - start] || '';
508       }
509     },
510
511     /**
512      * Callback invoked by chrome whenever the app launcher promo pref changes.
513      * @param {boolean} show Identifies if we should show or hide the promo.
514      */
515     appLauncherPromoPrefChangeCallback: function(show) {
516       loadTimeData.overrideValues({showAppLauncherPromo: show});
517       this.updateAppLauncherPromoHiddenState_();
518     },
519
520     /**
521      * Updates the hidden state of the app launcher promo based on the page
522      * shown and load data content.
523      */
524     updateAppLauncherPromoHiddenState_: function() {
525       $('app-launcher-promo').hidden =
526           !loadTimeData.getBoolean('showAppLauncherPromo') ||
527           this.shownPage != loadTimeData.getInteger('apps_page_id');
528     },
529
530     /**
531      * Invoked whenever the pages in apps-page-list have changed so that
532      * the Slider knows about the new elements.
533      */
534     updateSliderCards: function() {
535       var pageNo = Math.max(0, Math.min(this.cardSlider.currentCard,
536                                         this.tilePages.length - 1));
537       this.cardSlider.setCards(Array.prototype.slice.call(this.tilePages),
538                                pageNo);
539       // The shownPage property was potentially saved from a previous webui that
540       // didn't have the same set of pages as the current one. So we cascade
541       // from suggestions, to most visited and then to apps because we can have
542       // an page with apps only (e.g., chrome://apps) or one with only the most
543       // visited, but not one with only suggestions. And we alwayd default to
544       // most visited first when previously shown page is not availabel anymore.
545       // If most visited isn't there either, we go to apps.
546       if (this.shownPage == loadTimeData.getInteger('suggestions_page_id')) {
547         if (this.suggestionsPage)
548           this.cardSlider.selectCardByValue(this.suggestionsPage);
549         else
550           this.shownPage = loadTimeData.getInteger('most_visited_page_id');
551       }
552       if (this.shownPage == loadTimeData.getInteger('most_visited_page_id')) {
553         if (this.mostVisitedPage)
554           this.cardSlider.selectCardByValue(this.mostVisitedPage);
555         else
556           this.shownPage = loadTimeData.getInteger('apps_page_id');
557       }
558       if (this.shownPage == loadTimeData.getInteger('apps_page_id') &&
559           loadTimeData.getBoolean('showApps')) {
560         this.cardSlider.selectCardByValue(
561             this.appsPages[Math.min(this.shownPageIndex,
562                                     this.appsPages.length - 1)]);
563       } else if (this.mostVisitedPage) {
564         this.shownPage = loadTimeData.getInteger('most_visited_page_id');
565         this.cardSlider.selectCardByValue(this.mostVisitedPage);
566       }
567     },
568
569     /**
570      * Called whenever tiles should be re-arranging themselves out of the way
571      * of a moving or insert tile.
572      */
573     enterRearrangeMode: function() {
574       if (loadTimeData.getBoolean('showApps')) {
575         var tempPage = new ntp.AppsPage();
576         tempPage.classList.add('temporary');
577         var pageName = loadTimeData.getString('appDefaultPageName');
578         this.appendTilePage(tempPage, pageName, true);
579       }
580
581       if (ntp.getCurrentlyDraggingTile().firstChild.canBeRemoved()) {
582         $('footer').classList.add('showing-trash-mode');
583         $('footer-menu-container').style.minWidth = $('trash').offsetWidth -
584             $('chrome-web-store-link').offsetWidth + 'px';
585       }
586
587       document.documentElement.classList.add('dragging-mode');
588     },
589
590     /**
591      * Invoked whenever some app is released
592      */
593     leaveRearrangeMode: function() {
594       var tempPage = /** @type {ntp.AppsPage} */(
595           document.querySelector('.tile-page.temporary'));
596       if (tempPage) {
597         var dot = tempPage.navigationDot;
598         if (!tempPage.tileCount &&
599             tempPage != this.cardSlider.currentCardValue) {
600           this.removeTilePageAndDot_(tempPage, true);
601         } else {
602           tempPage.classList.remove('temporary');
603           this.saveAppPageName(tempPage,
604                                loadTimeData.getString('appDefaultPageName'));
605         }
606       }
607
608       $('footer').classList.remove('showing-trash-mode');
609       $('footer-menu-container').style.minWidth = '';
610       document.documentElement.classList.remove('dragging-mode');
611     },
612
613     /**
614      * Callback for the 'pagelayout' event.
615      * @param {Event} e The event.
616      */
617     onPageLayout_: function(e) {
618       if (Array.prototype.indexOf.call(this.tilePages, e.currentTarget) !=
619           this.cardSlider.currentCard) {
620         return;
621       }
622
623       this.updatePageSwitchers();
624     },
625
626     /**
627      * Adjusts the size and position of the page switchers according to the
628      * layout of the current card, and updates the aria-label attributes of
629      * the page switchers.
630      */
631     updatePageSwitchers: function() {
632       if (!this.pageSwitcherStart || !this.pageSwitcherEnd)
633         return;
634
635       var page = this.cardSlider.currentCardValue;
636
637       this.pageSwitcherStart.hidden = !page ||
638           (this.cardSlider.currentCard == 0);
639       this.pageSwitcherEnd.hidden = !page ||
640           (this.cardSlider.currentCard == this.cardSlider.cardCount - 1);
641
642       if (!page)
643         return;
644
645       var pageSwitcherLeft = isRTL() ? this.pageSwitcherEnd :
646                                        this.pageSwitcherStart;
647       var pageSwitcherRight = isRTL() ? this.pageSwitcherStart :
648                                         this.pageSwitcherEnd;
649       var scrollbarWidth = page.scrollbarWidth;
650       pageSwitcherLeft.style.width =
651           (page.sideMargin + 13) + 'px';
652       pageSwitcherLeft.style.left = '0';
653       pageSwitcherRight.style.width =
654           (page.sideMargin - scrollbarWidth + 13) + 'px';
655       pageSwitcherRight.style.right = scrollbarWidth + 'px';
656
657       var offsetTop = page.querySelector('.tile-page-content').offsetTop + 'px';
658       pageSwitcherLeft.style.top = offsetTop;
659       pageSwitcherRight.style.top = offsetTop;
660       pageSwitcherLeft.style.paddingBottom = offsetTop;
661       pageSwitcherRight.style.paddingBottom = offsetTop;
662
663       // Update the aria-label attributes of the two page switchers.
664       this.pageSwitcherStart.updateButtonAccessibleLabel(this.dotList.dots);
665       this.pageSwitcherEnd.updateButtonAccessibleLabel(this.dotList.dots);
666     },
667
668     /**
669      * Returns the index of the given apps page.
670      * @param {ntp.AppsPage} page The AppsPage we wish to find.
671      * @return {number} The index of |page| or -1 if it is not in the
672      *    collection.
673      */
674     getAppsPageIndex: function(page) {
675       return Array.prototype.indexOf.call(this.appsPages, page);
676     },
677
678     /**
679      * Handler for cardSlider:card_changed events from this.cardSlider.
680      * @param {Event} e The cardSlider:card_changed event.
681      * @private
682      */
683     onCardChanged_: function(e) {
684       var page = e.cardSlider.currentCardValue;
685
686       // Don't change shownPage until startup is done (and page changes actually
687       // reflect user actions).
688       if (!this.isStartingUp_()) {
689         if (page.classList.contains('apps-page')) {
690           this.setShownPage_(loadTimeData.getInteger('apps_page_id'),
691                              this.getAppsPageIndex(page));
692         } else if (page.classList.contains('most-visited-page')) {
693           this.setShownPage_(
694               loadTimeData.getInteger('most_visited_page_id'), 0);
695         } else if (page.classList.contains('suggestions-page')) {
696           this.setShownPage_(loadTimeData.getInteger('suggestions_page_id'), 0);
697         } else {
698           console.error('unknown page selected');
699         }
700       }
701
702       // Update the active dot
703       var curDot = this.dotList.getElementsByClassName('selected')[0];
704       if (curDot)
705         curDot.classList.remove('selected');
706       page.navigationDot.classList.add('selected');
707       this.updatePageSwitchers();
708     },
709
710     /**
711      * Saves/updates the newly selected page to open when first loading the NTP.
712      * @param {number} shownPage The new shown page type.
713      * @param {number} shownPageIndex The new shown page index.
714      * @private
715      */
716     setShownPage_: function(shownPage, shownPageIndex) {
717       assert(shownPageIndex >= 0);
718       this.shownPage = shownPage;
719       this.shownPageIndex = shownPageIndex;
720       chrome.send('pageSelected', [this.shownPage, this.shownPageIndex]);
721       this.updateAppLauncherPromoHiddenState_();
722     },
723
724     /**
725      * Listen for card additions to update the page switchers or the current
726      * card accordingly.
727      * @param {Event} e A card removed or added event.
728      */
729     onCardAdded_: function(e) {
730       // When the second arg passed to insertBefore is falsey, it acts just like
731       // appendChild.
732       this.pageList.insertBefore(e.addedCard, this.tilePages[e.addedIndex]);
733       this.onCardAddedOrRemoved_();
734     },
735
736     /**
737      * Listen for card removals to update the page switchers or the current card
738      * accordingly.
739      * @param {Event} e A card removed or added event.
740      */
741     onCardRemoved_: function(e) {
742       e.removedCard.parentNode.removeChild(e.removedCard);
743       this.onCardAddedOrRemoved_();
744     },
745
746     /**
747      * Called when a card is removed or added.
748      * @private
749      */
750     onCardAddedOrRemoved_: function() {
751       if (this.isStartingUp_())
752         return;
753
754       // Without repositioning there were issues - http://crbug.com/133457.
755       this.cardSlider.repositionFrame();
756       this.updatePageSwitchers();
757     },
758
759     /**
760      * Save the name of an apps page.
761      * Store the apps page name into the preferences store.
762      * @param {ntp.AppsPage} appPage The app page for which we wish to save.
763      * @param {string} name The name of the page.
764      */
765     saveAppPageName: function(appPage, name) {
766       var index = this.getAppsPageIndex(appPage);
767       assert(index != -1);
768       chrome.send('saveAppPageName', [name, index]);
769     },
770
771     /**
772      * Window resize handler.
773      * @private
774      */
775     onWindowResize_: function(e) {
776       this.cardSlider.resize(this.sliderFrame.offsetWidth);
777       this.updatePageSwitchers();
778     },
779
780     /**
781      * Listener for offline status change events. Updates apps that are
782      * not offline-enabled to be grayscale if the browser is offline.
783      * @private
784      */
785     updateOfflineEnabledApps_: function() {
786       var apps = document.querySelectorAll('.app');
787       for (var i = 0; i < apps.length; ++i) {
788         if (apps[i].appData.enabled && !apps[i].appData.offlineEnabled) {
789           apps[i].setIcon();
790           apps[i].loadIcon();
791         }
792       }
793     },
794
795     /**
796      * Handler for key events on the page. Ctrl-Arrow will switch the visible
797      * page.
798      * @param {Event} e The KeyboardEvent.
799      * @private
800      */
801     onDocKeyDown_: function(e) {
802       if (!e.ctrlKey || e.altKey || e.metaKey || e.shiftKey)
803         return;
804
805       var direction = 0;
806       if (e.keyIdentifier == 'Left')
807         direction = -1;
808       else if (e.keyIdentifier == 'Right')
809         direction = 1;
810       else
811         return;
812
813       var cardIndex =
814           (this.cardSlider.currentCard + direction +
815            this.cardSlider.cardCount) % this.cardSlider.cardCount;
816       this.cardSlider.selectCard(cardIndex, true);
817
818       e.stopPropagation();
819     },
820
821     /**
822      * Returns the index of a given tile page.
823      * @param {ntp.TilePage} page The TilePage we wish to find.
824      * @return {number} The index of |page| or -1 if it is not in the
825      *    collection.
826      */
827     getTilePageIndex: function(page) {
828       return Array.prototype.indexOf.call(this.tilePages, page);
829     },
830
831     /**
832      * Removes a page and navigation dot (if the navdot exists).
833      * @param {ntp.TilePage} page The page to be removed.
834      * @param {boolean=} opt_animate If the removal should be animated.
835      */
836     removeTilePageAndDot_: function(page, opt_animate) {
837       if (page.navigationDot)
838         page.navigationDot.remove(opt_animate);
839       this.cardSlider.removeCard(page);
840     },
841   };
842
843   return {
844     PageListView: PageListView
845   };
846 });