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