Home | History | Annotate | Download | only in ntp4
      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 cr.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