Only grant permissions to new extensions from sync if they have the expected version
[chromium-blink-merge.git] / chrome / browser / resources / ntp4 / page_list_view.js
blob69027b966ae8823173ef19802ce4de8d87f52f37
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.
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  */
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  *            kioskMode: boolean,
31  *            kioskOnly: boolean,
32  *            launch_container: number,
33  *            launch_type: number,
34  *            mayDisable: boolean,
35  *            name: string,
36  *            offlineEnabled: boolean,
37  *            optionsUrl: string,
38  *            packagedApp: boolean,
39  *            page_index: number,
40  *            title: string,
41  *            url: string,
42  *            version: string}}
43  * @see chrome/browser/ui/webui/ntp/app_launcher_handler.cc
44  */
45 var AppInfo;
47 cr.define('ntp', function() {
48   'use strict';
50   /**
51    * Creates a PageListView object.
52    * @constructor
53    * @extends {Object}
54    */
55   function PageListView() {
56   }
58   PageListView.prototype = {
59     /**
60      * The CardSlider object to use for changing app pages.
61      * @type {cr.ui.CardSlider|undefined}
62      */
63     cardSlider: undefined,
65     /**
66      * The frame div for this.cardSlider.
67      * @type {!Element|undefined}
68      */
69     sliderFrame: undefined,
71     /**
72      * The 'page-list' element.
73      * @type {!Element|undefined}
74      */
75     pageList: undefined,
77     /**
78      * A list of all 'tile-page' elements.
79      * @type {!NodeList|undefined}
80      */
81     tilePages: undefined,
83     /**
84      * A list of all 'apps-page' elements.
85      * @type {!NodeList|undefined}
86      */
87     appsPages: undefined,
89     /**
90      * The 'dots-list' element.
91      * @type {!Element|undefined}
92      */
93     dotList: undefined,
95     /**
96      * The left and right paging buttons.
97      * @type {!ntp.PageSwitcher|undefined}
98      */
99     pageSwitcherStart: undefined,
100     pageSwitcherEnd: undefined,
102     /**
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}
107      */
108     trash: undefined,
110     /**
111      * The type of page that is currently shown. The value is a numerical ID.
112      * @type {number}
113      */
114     shownPage: 0,
116     /**
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.
119      * @type {number}
120      */
121     shownPageIndex: 0,
123     /**
124      * EventTracker for managing event listeners for page events.
125      * @type {!EventTracker}
126      */
127     eventTracker: new EventTracker,
129     /**
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.
133      * @type {?string}
134      */
135     highlightAppId: null,
137     /**
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
141      *     represents a page.
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.
149      */
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;
158       if (this.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)
195           return;
196         e.preventDefault();
197       }, true);
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.
207         }
208       });
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));
229     },
231     /**
232      * Appends a tile page.
233      *
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
238      *     front of.
239      * When opt_refNode is falsey, |page| will just be appended to the end of
240      * the page list.
241      */
242     appendTilePage: function(page, title, titleIsEditable, opt_refNode) {
243       if (opt_refNode) {
244         var refIndex = this.getTilePageIndex(opt_refNode);
245         this.cardSlider.addCardAtIndex(page, refIndex);
246       } else {
247         this.cardSlider.appendCard(page);
248       }
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)
260         newDot.tabIndex = 3;
262       this.eventTracker.add(page, 'pagelayout', this.onPageLayout_.bind(this));
263     },
265     /**
266      * Called by chrome when an app has changed positions.
267      * @param {AppInfo} appData The data for the app. This contains page and
268      *     position indices.
269      */
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');
275       app.remove(false);
277       this.appsPages[appData.page_index].insertApp(appData, false);
278     },
280     /**
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
284      *     for the app.
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.
288      */
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');
295       if (!isUninstall)
296         app.replaceAppData(appData);
297       else
298         app.remove(!!fromPage);
299     },
301     /**
302      * @return {boolean} If the page is still starting up.
303      * @private
304      */
305     isStartingUp_: function() {
306       return document.documentElement.classList.contains('starting-up');
307     },
309     /**
310      * Tracks whether apps have been loaded at least once.
311      * @type {boolean}
312      * @private
313      */
314     appsLoaded_: false,
316     /**
317      * Callback invoked by chrome with the apps available.
318      *
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.
324      */
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
343       // uninstall.
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++) {
355           if (list[i])
356             return false;
357         }
358         return true;
359       }
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;
365       });
367       // An app to animate (in case it was just installed).
368       var highlightApp;
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++) {
380         var app = apps[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,
389                               nextPageAfterApps);
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');
394         }
396         if (app.id == this.highlightAppId)
397           highlightApp = app;
398         else
399           this.appsPages[pageIndex].insertApp(app, false);
400       }
402       this.cardSlider.currentCard = prevCurrentCard;
404       if (highlightApp)
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);
416       }
417       this.updateAppLauncherPromoHiddenState_();
418     },
420     /**
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
424      *     for the app.
425      * @param {boolean=} opt_highlight Whether the app about to be added should
426      *     be highlighted.
427      */
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;
434       }
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'),
442                               true);
443         }
444         this.updateSliderCards();
445       }
447       var page = this.appsPages[pageIndex];
448       var app = $(appData.id);
449       if (app) {
450         app.replaceAppData(appData);
451       } else if (opt_highlight) {
452         page.insertAndHighlightApp(appData);
453         this.setShownPage_(loadTimeData.getInteger('apps_page_id'),
454                            appData.page_index);
455       } else {
456         page.insertApp(appData, false);
457       }
458     },
460     /**
461      * Callback invoked by chrome whenever an app preference changes.
462      * @param {Object} data An object with all the data on available
463      *     applications.
464      */
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];
470       }
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] || '';
476       }
477     },
479     /**
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.
482      */
483     appLauncherPromoPrefChangeCallback: function(show) {
484       loadTimeData.overrideValues({showAppLauncherPromo: show});
485       this.updateAppLauncherPromoHiddenState_();
486     },
488     /**
489      * Updates the hidden state of the app launcher promo based on the page
490      * shown and load data content.
491      */
492     updateAppLauncherPromoHiddenState_: function() {
493       $('app-launcher-promo').hidden =
494           !loadTimeData.getBoolean('showAppLauncherPromo') ||
495           this.shownPage != loadTimeData.getInteger('apps_page_id');
496     },
498     /**
499      * Invoked whenever the pages in apps-page-list have changed so that
500      * the Slider knows about the new elements.
501      */
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),
506                                pageNo);
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)]);
512       }
513     },
515     /**
516      * Called whenever tiles should be re-arranging themselves out of the way
517      * of a moving or insert tile.
518      */
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);
525       }
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';
531       }
533       document.documentElement.classList.add('dragging-mode');
534     },
536     /**
537      * Invoked whenever some app is released
538      */
539     leaveRearrangeMode: function() {
540       var tempPage = /** @type {ntp.AppsPage} */(
541           document.querySelector('.tile-page.temporary'));
542       if (tempPage) {
543         var dot = tempPage.navigationDot;
544         if (!tempPage.tileCount &&
545             tempPage != this.cardSlider.currentCardValue) {
546           this.removeTilePageAndDot_(tempPage, true);
547         } else {
548           tempPage.classList.remove('temporary');
549           this.saveAppPageName(tempPage,
550                                loadTimeData.getString('appDefaultPageName'));
551         }
552       }
554       $('footer').classList.remove('showing-trash-mode');
555       $('footer-menu-container').style.minWidth = '';
556       document.documentElement.classList.remove('dragging-mode');
557     },
559     /**
560      * Callback for the 'pagelayout' event.
561      * @param {Event} e The event.
562      */
563     onPageLayout_: function(e) {
564       if (Array.prototype.indexOf.call(this.tilePages, e.currentTarget) !=
565           this.cardSlider.currentCard) {
566         return;
567       }
569       this.updatePageSwitchers();
570     },
572     /**
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.
576      */
577     updatePageSwitchers: function() {
578       if (!this.pageSwitcherStart || !this.pageSwitcherEnd)
579         return;
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);
588       if (!page)
589         return;
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);
612     },
614     /**
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
618      *    collection.
619      */
620     getAppsPageIndex: function(page) {
621       return Array.prototype.indexOf.call(this.appsPages, page);
622     },
624     /**
625      * Handler for cardSlider:card_changed events from this.cardSlider.
626      * @param {Event} e The cardSlider:card_changed event.
627      * @private
628      */
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));
638         } else {
639           console.error('unknown page selected');
640         }
641       }
643       // Update the active dot
644       var curDot = this.dotList.getElementsByClassName('selected')[0];
645       if (curDot)
646         curDot.classList.remove('selected');
647       page.navigationDot.classList.add('selected');
648       this.updatePageSwitchers();
649     },
651     /**
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.
655      * @private
656      */
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_();
663     },
665     /**
666      * Listen for card additions to update the page switchers or the current
667      * card accordingly.
668      * @param {Event} e A card removed or added event.
669      */
670     onCardAdded_: function(e) {
671       // When the second arg passed to insertBefore is falsey, it acts just like
672       // appendChild.
673       this.pageList.insertBefore(e.addedCard, this.tilePages[e.addedIndex]);
674       this.onCardAddedOrRemoved_();
675     },
677     /**
678      * Listen for card removals to update the page switchers or the current card
679      * accordingly.
680      * @param {Event} e A card removed or added event.
681      */
682     onCardRemoved_: function(e) {
683       e.removedCard.parentNode.removeChild(e.removedCard);
684       this.onCardAddedOrRemoved_();
685     },
687     /**
688      * Called when a card is removed or added.
689      * @private
690      */
691     onCardAddedOrRemoved_: function() {
692       if (this.isStartingUp_())
693         return;
695       // Without repositioning there were issues - http://crbug.com/133457.
696       this.cardSlider.repositionFrame();
697       this.updatePageSwitchers();
698     },
700     /**
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.
705      */
706     saveAppPageName: function(appPage, name) {
707       var index = this.getAppsPageIndex(appPage);
708       assert(index != -1);
709       chrome.send('saveAppPageName', [name, index]);
710     },
712     /**
713      * Window resize handler.
714      * @private
715      */
716     onWindowResize_: function(e) {
717       this.cardSlider.resize(this.sliderFrame.offsetWidth);
718       this.updatePageSwitchers();
719     },
721     /**
722      * Listener for offline status change events. Updates apps that are
723      * not offline-enabled to be grayscale if the browser is offline.
724      * @private
725      */
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) {
730           apps[i].setIcon();
731           apps[i].loadIcon();
732         }
733       }
734     },
736     /**
737      * Handler for key events on the page. Ctrl-Arrow will switch the visible
738      * page.
739      * @param {Event} e The KeyboardEvent.
740      * @private
741      */
742     onDocKeyDown_: function(e) {
743       if (!e.ctrlKey || e.altKey || e.metaKey || e.shiftKey)
744         return;
746       var direction = 0;
747       if (e.keyIdentifier == 'Left')
748         direction = -1;
749       else if (e.keyIdentifier == 'Right')
750         direction = 1;
751       else
752         return;
754       var cardIndex =
755           (this.cardSlider.currentCard + direction +
756            this.cardSlider.cardCount) % this.cardSlider.cardCount;
757       this.cardSlider.selectCard(cardIndex, true);
759       e.stopPropagation();
760     },
762     /**
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
766      *    collection.
767      */
768     getTilePageIndex: function(page) {
769       return Array.prototype.indexOf.call(this.tilePages, page);
770     },
772     /**
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.
776      */
777     removeTilePageAndDot_: function(page, opt_animate) {
778       if (page.navigationDot)
779         page.navigationDot.remove(opt_animate);
780       this.cardSlider.removeCard(page);
781     },
782   };
784   return {
785     PageListView: PageListView
786   };