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
13cr.define('ntp', function() {
14  'use strict';
15
16  /**
17   * Creates a PageListView object.
18   * @constructor
19   * @extends {Object}
20   */
21  function PageListView() {
22  }
23
24  PageListView.prototype = {
25    /**
26     * The CardSlider object to use for changing app pages.
27     * @type {CardSlider|undefined}
28     */
29    cardSlider: undefined,
30
31    /**
32     * The frame div for this.cardSlider.
33     * @type {!Element|undefined}
34     */
35    sliderFrame: undefined,
36
37    /**
38     * The 'page-list' element.
39     * @type {!Element|undefined}
40     */
41    pageList: undefined,
42
43    /**
44     * A list of all 'tile-page' elements.
45     * @type {!NodeList|undefined}
46     */
47    tilePages: undefined,
48
49    /**
50     * A list of all 'apps-page' elements.
51     * @type {!NodeList|undefined}
52     */
53    appsPages: undefined,
54
55    /**
56     * The Suggestions page.
57     * @type {!Element|undefined}
58     */
59    suggestionsPage: undefined,
60
61    /**
62     * The Most Visited page.
63     * @type {!Element|undefined}
64     */
65    mostVisitedPage: undefined,
66
67    /**
68     * The 'dots-list' element.
69     * @type {!Element|undefined}
70     */
71    dotList: undefined,
72
73    /**
74     * The left and right paging buttons.
75     * @type {!Element|undefined}
76     */
77    pageSwitcherStart: undefined,
78    pageSwitcherEnd: undefined,
79
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,
87
88    /**
89     * The type of page that is currently shown. The value is a numerical ID.
90     * @type {number}
91     */
92    shownPage: 0,
93
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,
100
101    /**
102     * EventTracker for managing event listeners for page events.
103     * @type {!EventTracker}
104     */
105    eventTracker: new EventTracker,
106
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,
114
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;
131
132      this.dotList = dotList;
133      cr.ui.decorate(this.dotList, ntp.DotList);
134
135      this.trash = opt_trash;
136      if (this.trash)
137        new ntp.Trash(this.trash);
138
139      this.pageSwitcherStart = opt_pageSwitcherStart;
140      if (this.pageSwitcherStart)
141        ntp.initializePageSwitcher(this.pageSwitcherStart);
142
143      this.pageSwitcherEnd = opt_pageSwitcherEnd;
144      if (this.pageSwitcherEnd)
145        ntp.initializePageSwitcher(this.pageSwitcherEnd);
146
147      this.shownPage = loadTimeData.getInteger('shown_page_type');
148      this.shownPageIndex = loadTimeData.getInteger('shown_page_index');
149
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        }
161
162        document.body.classList.add('bare-minimum');
163      }
164
165      document.addEventListener('keydown', this.onDocKeyDown_.bind(this));
166
167      this.tilePages = this.pageList.getElementsByClassName('tile-page');
168      this.appsPages = this.pageList.getElementsByClassName('apps-page');
169
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);
174
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);
183
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      });
194
195      this.cardSlider.initialize(
196          loadTimeData.getBoolean('isSwipeTrackingFromScrollEventsEnabled'));
197
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));
205
206      // Ensure the slider is resized appropriately with the window.
207      window.addEventListener('resize', this.onWindowResize_.bind(this));
208
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    },
215
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      }
234
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      }
242
243      if (typeof ntp.SuggestionsPage != 'undefined' &&
244          page instanceof ntp.SuggestionsPage) {
245        this.suggestionsPage = page;
246      }
247
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;
259
260      this.eventTracker.add(page, 'pagelayout', this.onPageLayout_.bind(this));
261    },
262
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'));
270
271      var app = $(appData.id);
272      assert(app, 'trying to move an app that doesn\'t exist');
273      app.remove(false);
274
275      this.appsPages[appData.page_index].insertApp(appData, false);
276    },
277
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'));
289
290      var app = $(appData.id);
291      assert(app, 'trying to remove an app that doesn\'t exist');
292
293      if (!isUninstall)
294        app.replaceAppData(appData);
295      else
296        app.remove(!!fromPage);
297    },
298
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    },
306
307    /**
308     * Tracks whether apps have been loaded at least once.
309     * @type {boolean}
310     * @private
311     */
312    appsLoaded_: false,
313
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'));
325
326      var startTime = Date.now();
327
328      // Remember this to select the correct card when done rebuilding.
329      var prevCurrentCard = this.cardSlider.currentCard;
330
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;
336
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]);
344
345      // Get the array of apps and add any special synthesized entries
346      var apps = data.apps;
347
348      // Get a list of page names
349      var pageNames = data.appPageNames;
350
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      }
358
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      });
364
365      // An app to animate (in case it was just installed).
366      var highlightApp;
367
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;
375
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];
384
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        }
393
394        if (app.id == this.highlightAppId)
395          highlightApp = app;
396        else
397          this.appsPages[pageIndex].insertApp(app, false);
398      }
399
400      this.cardSlider.currentCard = prevCurrentCard;
401
402      if (highlightApp)
403        this.appAdded(highlightApp, true);
404
405      logEvent('apps.layout: ' + (Date.now() - startTime));
406
407      // Tell the slider about the pages and mark the current page.
408      this.updateSliderCards();
409      this.cardSlider.currentCardValue.navigationDot.classList.add('selected');
410
411      if (!this.appsLoaded_) {
412        this.appsLoaded_ = true;
413        cr.dispatchSimpleEvent(document, 'sectionready', true, true);
414      }
415      this.updateAppLauncherPromoHiddenState_();
416    },
417
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'));
428
429      if (appData.id == this.highlightAppId) {
430        opt_highlight = true;
431        this.highlightAppId = null;
432      }
433
434      var pageIndex = appData.page_index || 0;
435
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      }
444
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    },
457
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'));
465
466      for (var i = 0; i < data.apps.length; ++i) {
467        $(data.apps[i].id).appData = data.apps[i];
468      }
469
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    },
477
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    },
486
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    },
496
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    },
535
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      }
547
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      }
553
554      document.documentElement.classList.add('dragging-mode');
555    },
556
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      }
573
574      $('footer').classList.remove('showing-trash-mode');
575      $('footer-menu-container').style.minWidth = '';
576      document.documentElement.classList.remove('dragging-mode');
577    },
578
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      }
588
589      this.updatePageSwitchers();
590    },
591
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;
600
601      var page = this.cardSlider.currentCardValue;
602
603      this.pageSwitcherStart.hidden = !page ||
604          (this.cardSlider.currentCard == 0);
605      this.pageSwitcherEnd.hidden = !page ||
606          (this.cardSlider.currentCard == this.cardSlider.cardCount - 1);
607
608      if (!page)
609        return;
610
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';
622
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;
628
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    },
633
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    },
643
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;
651
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      }
667
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    },
675
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    },
689
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    },
701
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    },
711
712    /**
713     * Called when a card is removed or added.
714     * @private
715     */
716    onCardAddedOrRemoved_: function() {
717      if (this.isStartingUp_())
718        return;
719
720      // Without repositioning there were issues - http://crbug.com/133457.
721      this.cardSlider.repositionFrame();
722      this.updatePageSwitchers();
723    },
724
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    },
736
737    /**
738     * Window resize handler.
739     * @private
740     */
741    onWindowResize_: function(e) {
742      this.cardSlider.resize(this.sliderFrame.offsetWidth);
743      this.updatePageSwitchers();
744    },
745
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    },
760
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;
770
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;
778
779      var cardIndex =
780          (this.cardSlider.currentCard + direction +
781           this.cardSlider.cardCount) % this.cardSlider.cardCount;
782      this.cardSlider.selectCard(cardIndex, true);
783
784      e.stopPropagation();
785    },
786
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    },
796
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  };
808
809  return {
810    PageListView: PageListView
811  };
812});
813