[Aura] Initial app list webui.
[chromium-blink-merge.git] / chrome / browser / resources / ntp4 / page_list_view.js
blob7db35c0ba4dfa096f5d0e4480b76a0c36247543d
1 // Copyright (c) 2011 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('ntp4', function() {
14   'use strict';
16   /**
17    * Object for accessing localized strings.
18    * @type {!LocalStrings}
19    */
20   var localStrings = new LocalStrings;
22   /**
23    * Creates a PageListView object.
24    * @constructor
25    * @extends {Object}
26    */
27   function PageListView() {
28   }
30   PageListView.prototype = {
31     /**
32      * The CardSlider object to use for changing app pages.
33      * @type {CardSlider|undefined}
34      */
35     cardSlider: undefined,
37     /**
38      * The frame div for cardSlider.
39      * @type {!Element|undefined}
40      */
41     sliderFrame: undefined,
43     /**
44      * The 'page-list' element.
45      * @type {!Element|undefined}
46      */
47     pageList: undefined,
49     /**
50      * A list of all 'tile-page' elements.
51      * @type {!NodeList|undefined}
52      */
53     tilePages: undefined,
55     /**
56      * A list of all 'apps-page' elements.
57      * @type {!NodeList|undefined}
58      */
59     appsPages: undefined,
61     /**
62      * The Most Visited page.
63      * @type {!Element|undefined}
64      */
65     mostVisitedPage: undefined,
67     /**
68      * The Bookmarks page.
69      * @type {!Element|undefined}
70      */
71     bookmarksPage: undefined,
73     /**
74      * The 'dots-list' element.
75      * @type {!Element|undefined}
76      */
77     dotList: undefined,
79     /**
80      * The left and right paging buttons.
81      * @type {!Element|undefined}
82      */
83     pageSwitcherStart: undefined,
84     pageSwitcherEnd: undefined,
86     /**
87      * The 'trash' element.  Note that technically this is unnecessary,
88      * JavaScript creates the object for us based on the id.  But I don't want
89      * to rely on the ID being the same, and JSCompiler doesn't know about it.
90      * @type {!Element|undefined}
91      */
92     trash: undefined,
94     /**
95      * The type of page that is currently shown. The value is a numerical ID.
96      * @type {number}
97      */
98     shownPage: 0,
100     /**
101      * The index of the page that is currently shown, within the page type.
102      * For example if the third Apps page is showing, this will be 2.
103      * @type {number}
104      */
105     shownPageIndex: 0,
107     /**
108      * EventTracker for managing event listeners for page events.
109      * @type {!EventTracker}
110      */
111     eventTracker: new EventTracker,
113     /**
114      * If non-null, this is the ID of the app to highlight to the user the next
115      * time getAppsCallback runs. "Highlight" in this case means to switch to
116      * the page and run the new tile animation.
117      * @type {String}
118      */
119     highlightAppId: null,
121     /**
122      * Initializes page list view.
123      * @param {!Element} pageList A DIV element to host all pages.
124      * @param {!Element} dotList An UL element to host nav dots. Each dot
125      *     represents a page.
126      * @param {!Element} cardSliderFrame The card slider frame that hosts
127      *     pageList and switcher buttons.
128      * @param {!Element|undefined} opt_trash Optional trash element.
129      * @param {!Element|undefined} opt_pageSwitcherStart Optional start page
130      *     switcher button.
131      * @param {!Element|undefined} opt_pageSwitcherEnd Optional end page
132      *     switcher button.
133      */
134     initialize: function(pageList, dotList, cardSliderFrame, opt_trash,
135                          opt_pageSwitcherStart, opt_pageSwitcherEnd) {
136       this.pageList = pageList;
138       this.dotList = dotList;
139       cr.ui.decorate(this.dotList, ntp4.DotList);
141       this.trash = opt_trash;
142       if (this.trash)
143         new ntp4.Trash(this.trash);
145       this.pageSwitcherStart = opt_pageSwitcherStart;
146       if (this.pageSwitcherStart)
147         ntp4.initializePageSwitcher(this.pageSwitcherStart);
149       this.pageSwitcherEnd = opt_pageSwitcherEnd;
150       if (this.pageSwitcherEnd)
151         ntp4.initializePageSwitcher(this.pageSwitcherEnd);
153       this.shownPage = templateData['shown_page_type'];
154       this.shownPageIndex = templateData['shown_page_index'];
156       // Request data on the apps so we can fill them in.
157       // Note that this is kicked off asynchronously.  'getAppsCallback' will be
158       // invoked at some point after this function returns.
159       chrome.send('getApps');
161       document.addEventListener('keydown', this.onDocKeyDown_.bind(this));
162       // Prevent touch events from triggering any sort of native scrolling
163       document.addEventListener('touchmove', function(e) {
164         e.preventDefault();
165       }, true);
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);
174       this.cardSlider.initialize();
176       // Handle the page being changed
177       this.pageList.addEventListener(
178           cr.ui.CardSlider.EventType.CARD_CHANGED,
179           this.cardChangedHandler_.bind(this));
181       // Ensure the slider is resized appropriately with the window
182       window.addEventListener('resize', this.onWindowResize_.bind(this));
184       // Update apps when online state changes.
185       window.addEventListener('online',
186           this.updateOfflineEnabledApps_.bind(this));
187       window.addEventListener('offline',
188           this.updateOfflineEnabledApps_.bind(this));
189     },
191     /**
192      * Appends a tile page (for bookmarks or most visited).
193      *
194      * @param {TilePage} page The page element.
195      * @param {string} title The title of the tile page.
196      * @param {bool} titleIsEditable If true, the title can be changed.
197      * @param {TilePage} opt_refNode Optional reference node to insert in front
198      * of.
199      * When opt_refNode is falsey, |page| will just be appended to the end of
200      * the page list.
201      */
202     appendTilePage: function(page, title, titleIsEditable, opt_refNode) {
203       // If no opt_refNode given, use bookmarksPage (if any).
204       if (!opt_refNode)
205         opt_refNode = this.bookmarksPage;
207       // When opt_refNode is falsey, insertBefore acts just like appendChild.
208       this.pageList.insertBefore(page, opt_refNode);
210       // Remember special MostVisitedPage and BookmarksPage.
211       if (typeof ntp4.MostVisitedPage != 'undefined' &&
212           page instanceof ntp4.MostVisitedPage) {
213         assert(this.tilePages.length == 1,
214                'MostVisitedPage should be added as first tile page');
215         this.mostVisitedPage = page;
216       }
217       if (typeof ntp4.BookmarksPage != 'undefined' &&
218           page instanceof ntp4.BookmarksPage) {
219         this.bookmarksPage = page;
220       }
222       // If we're appending an AppsPage and it's a temporary page, animate it.
223       var animate = page instanceof ntp4.AppsPage &&
224                     page.classList.contains('temporary');
225       // Make a deep copy of the dot template to add a new one.
226       var newDot = new ntp4.NavDot(page, title, titleIsEditable, animate);
227       page.navigationDot = newDot;
228       this.dotList.insertBefore(newDot, opt_refNode ? opt_refNode.navigationDot
229                                                     : null);
230       // Set a tab index on the first dot.
231       if (this.dotList.dots.length == 1)
232         newDot.tabIndex = 3;
234       this.eventTracker.add(page, 'pagelayout', this.onPageLayout_.bind(this));
235     },
237     /**
238      * Called by chrome when an existing app has been disabled or
239      * removed/uninstalled from chrome.
240      * @param {Object} appData A data structure full of relevant information for
241      *     the app.
242      * @param {boolean} isUninstall True if the app is being uninstalled;
243      *     false if the app is being disabled.
244      */
245     appRemoved: function(appData, isUninstall) {
246       var app = $(appData.id);
247       assert(app, 'trying to remove an app that doesn\'t exist');
249       if (!isUninstall)
250         app.replaceAppData(appData);
251       else
252         app.remove();
253     },
255     /**
256      * Callback invoked by chrome with the apps available.
257      *
258      * Note that calls to this function can occur at any time, not just in
259      * response to a getApps request. For example, when a user
260      * installs/uninstalls an app on another synchronized devices.
261      * @param {Object} data An object with all the data on available
262      *        applications.
263      */
264     getAppsCallback: function(data) {
265       var startTime = Date.now();
267       // Clear any existing apps pages and dots.
268       // TODO(rbyers): It might be nice to preserve animation of dots after an
269       // uninstall. Could we re-use the existing page and dot elements?  It
270       // seems unfortunate to have Chrome send us the entire apps list after an
271       // uninstall.
272       while (this.appsPages.length > 0) {
273         var page = this.appsPages[0];
274         var dot = page.navigationDot;
276         this.eventTracker.remove(page);
277         page.tearDown();
278         page.parentNode.removeChild(page);
279         dot.parentNode.removeChild(dot);
280       }
282       // Get the array of apps and add any special synthesized entries
283       var apps = data.apps;
285       // Get a list of page names
286       var pageNames = data.appPageNames;
288       function stringListIsEmpty(list) {
289         for (var i = 0; i < list.length; i++) {
290           if (list[i])
291             return false;
292         }
293         return true;
294       }
296       // Sort by launch index
297       apps.sort(function(a, b) {
298         return a.app_launch_index - b.app_launch_index;
299       });
301       // An app to animate (in case it was just installed).
302       var highlightApp;
304       // Add the apps, creating pages as necessary
305       for (var i = 0; i < apps.length; i++) {
306         var app = apps[i];
307         var pageIndex = app.page_index || 0;
308         while (pageIndex >= this.appsPages.length) {
309           var pageName = localStrings.getString('appDefaultPageName');
310           if (this.appsPages.length < pageNames.length)
311             pageName = pageNames[this.appsPages.length];
313           var origPageCount = this.appsPages.length;
314           this.appendTilePage(new ntp4.AppsPage(), pageName, true);
315           // Confirm that appsPages is a live object, updated when a new page is
316           // added (otherwise we'd have an infinite loop)
317           assert(this.appsPages.length == origPageCount + 1,
318                  'expected new page');
319         }
321         if (app.id == this.highlightAppId)
322           highlightApp = app;
323         else
324           this.appsPages[pageIndex].appendApp(app);
325       }
327       ntp4.AppsPage.setPromo(data.showPromo ? data : null);
329       // Tell the slider about the pages.
330       this.updateSliderCards();
332       if (highlightApp)
333         this.appAdded(highlightApp, true);
335       // Mark the current page.
336       this.cardSlider.currentCardValue.navigationDot.classList.add('selected');
337       logEvent('apps.layout: ' + (Date.now() - startTime));
339       document.documentElement.classList.remove('starting-up');
340     },
342     /**
343      * Called by chrome when a new app has been added to chrome or has been
344      * enabled if previously disabled.
345      * @param {Object} appData A data structure full of relevant information for
346      *     the app.
347      */
348     appAdded: function(appData, opt_highlight) {
349       if (appData.id == this.highlightAppId) {
350         opt_highlight = true;
351         this.highlightAppId = null;
352       }
354       var pageIndex = appData.page_index || 0;
356       if (pageIndex >= this.appsPages.length) {
357         while (pageIndex >= this.appsPages.length) {
358           this.appendTilePage(new ntp4.AppsPage(),
359                               localStrings.getString('appDefaultPageName'),
360                               true);
361         }
362         this.updateSliderCards();
363       }
365       var page = this.appsPages[pageIndex];
366       var app = $(appData.id);
367       if (app)
368         app.replaceAppData(appData);
369       else
370         page.appendApp(appData, opt_highlight);
371     },
373     /**
374      * Callback invoked by chrome whenever an app preference changes.
375      * @param {Object} data An object with all the data on available
376      *     applications.
377      */
378     appsPrefChangedCallback: function(data) {
379       for (var i = 0; i < data.apps.length; ++i) {
380         $(data.apps[i].id).appData = data.apps[i];
381       }
383       // Set the App dot names. Skip the first and last dots (Most Visited and
384       // Bookmarks).
385       var dots = this.dotList.getElementsByClassName('dot');
386       // TODO(csilv): Remove this calcluation if/when we remove the flag for
387       // for the bookmarks page.
388       var start = this.mostVisitedPage ? 1 : 0;
389       var length = this.bookmarksPage ? dots.length - 1 : dots.length;
390       for (var i = start; i < length; ++i) {
391         dots[i].displayTitle = data.appPageNames[i - start] || '';
392       }
393     },
395     /**
396      * Invoked whenever the pages in apps-page-list have changed so that
397      * the Slider knows about the new elements.
398      */
399     updateSliderCards: function() {
400       var pageNo = Math.min(this.cardSlider.currentCard,
401                             this.tilePages.length - 1);
402       this.cardSlider.setCards(Array.prototype.slice.call(this.tilePages),
403                                                           pageNo);
404       switch (this.shownPage) {
405         case templateData['apps_page_id']:
406           this.cardSlider.selectCardByValue(
407               this.appsPages[Math.min(this.shownPageIndex,
408                                       this.appsPages.length - 1)]);
409           break;
410         case templateData['bookmarks_page_id']:
411           if (this.bookmarksPage)
412             this.cardSlider.selectCardByValue(this.bookmarksPage);
413           break;
414         case templateData['most_visited_page_id']:
415           if (this.mostVisitedPage)
416             this.cardSlider.selectCardByValue(this.mostVisitedPage);
417           break;
418       }
419     },
421     /**
422      * Called whenever tiles should be re-arranging themselves out of the way
423      * of a moving or insert tile.
424      */
425     enterRearrangeMode: function() {
426       var tempPage = new ntp4.AppsPage();
427       tempPage.classList.add('temporary');
428       this.appendTilePage(tempPage,
429                           localStrings.getString('appDefaultPageName'),
430                           true);
431       var tempIndex = Array.prototype.indexOf.call(this.tilePages, tempPage);
432       if (this.cardSlider.currentCard >= tempIndex)
433         this.cardSlider.currentCard += 1;
434       this.updateSliderCards();
436       if (ntp4.getCurrentlyDraggingTile().firstChild.canBeRemoved())
437         $('footer').classList.add('showing-trash-mode');
438     },
440     /**
441      * Invoked whenever some app is released
442      * @param {cr.ui.Grabber.Event} e The Grabber RELEASE event.
443      */
444     leaveRearrangeMode: function(e) {
445       var tempPage = document.querySelector('.tile-page.temporary');
446       var dot = tempPage.navigationDot;
447       if (!tempPage.tileCount && tempPage != this.cardSlider.currentCardValue) {
448         dot.animateRemove();
449         var tempIndex = Array.prototype.indexOf.call(this.tilePages, tempPage);
450         if (this.cardSlider.currentCard > tempIndex)
451           this.cardSlider.currentCard -= 1;
452         tempPage.parentNode.removeChild(tempPage);
453         this.updateSliderCards();
454       } else {
455         tempPage.classList.remove('temporary');
456         this.saveAppPageName(tempPage,
457                              localStrings.getString('appDefaultPageName'));
458       }
460       $('footer').classList.remove('showing-trash-mode');
461     },
463     /**
464      * Callback for the 'pagelayout' event.
465      * @param {Event} e The event.
466      */
467     onPageLayout_: function(e) {
468       if (Array.prototype.indexOf.call(this.tilePages, e.currentTarget) !=
469           this.cardSlider.currentCard) {
470         return;
471       }
473       this.updatePageSwitchers();
474     },
476     /**
477      * Adjusts the size and position of the page switchers according to the
478      * layout of the current card.
479      */
480     updatePageSwitchers: function() {
481       if (!this.pageSwitcherStart || !this.pageSwitcherEnd)
482         return;
484       var page = this.cardSlider.currentCardValue;
486       this.pageSwitcherStart.hidden = !page ||
487           (this.cardSlider.currentCard == 0);
488       this.pageSwitcherEnd.hidden = !page ||
489           (this.cardSlider.currentCard == this.cardSlider.cardCount - 1);
491       if (!page)
492         return;
494       var pageSwitcherLeft = isRTL() ? this.pageSwitcherEnd
495                                      : this.pageSwitcherStart;
496       var pageSwitcherRight = isRTL() ? this.pageSwitcherStart
497                                       : this.pageSwitcherEnd;
498       var scrollbarWidth = page.scrollbarWidth;
499       pageSwitcherLeft.style.width =
500           (page.sideMargin + 13) + 'px';
501       pageSwitcherLeft.style.left = '0';
502       pageSwitcherRight.style.width =
503           (page.sideMargin - scrollbarWidth + 13) + 'px';
504       pageSwitcherRight.style.right = scrollbarWidth + 'px';
506       var offsetTop = page.querySelector('.tile-page-content').offsetTop + 'px';
507       pageSwitcherLeft.style.top = offsetTop;
508       pageSwitcherRight.style.top = offsetTop;
509       pageSwitcherLeft.style.paddingBottom = offsetTop;
510       pageSwitcherRight.style.paddingBottom = offsetTop;
511     },
513     /**
514      * Returns the index of the given page.
515      * @param {AppsPage} page The AppsPage for we wish to find.
516      * @return {number} The index of |page|, or -1 if it is not here.
517      */
518     getAppsPageIndex: function(page) {
519       return Array.prototype.indexOf.call(this.appsPages, page);
520     },
522     /**
523      * Handler for CARD_CHANGED on cardSlider.
524      * @param {Event} e The CARD_CHANGED event.
525      * @private
526      */
527     cardChangedHandler_: function(e) {
528       var page = e.cardSlider.currentCardValue;
530       // Don't change shownPage until startup is done (and page changes actually
531       // reflect user actions).
532       if (!document.documentElement.classList.contains('starting-up')) {
533         if (page.classList.contains('apps-page')) {
534           this.shownPage = templateData['apps_page_id'];
535           this.shownPageIndex = this.getAppsPageIndex(page);
536         } else if (page.classList.contains('most-visited-page')) {
537           this.shownPage = templateData['most_visited_page_id'];
538           this.shownPageIndex = 0;
539         } else if (page.classList.contains('bookmarks-page')) {
540           this.shownPage = templateData['bookmarks_page_id'];
541           this.shownPageIndex = 0;
542         } else {
543           console.error('unknown page selected');
544         }
545         chrome.send('pageSelected', [this.shownPage, this.shownPageIndex]);
546       }
548       // Update the active dot
549       var curDot = this.dotList.getElementsByClassName('selected')[0];
550       if (curDot)
551         curDot.classList.remove('selected');
552       page.navigationDot.classList.add('selected');
553       this.updatePageSwitchers();
554     },
556     /*
557      * Save the name of an app page.
558      * Store the app page name into the preferences store.
559      * @param {AppsPage} appPage The app page for which we wish to save.
560      * @param {string} name The name of the page.
561      */
562     saveAppPageName: function(appPage, name) {
563       var index = this.getAppsPageIndex(appPage);
564       assert(index != -1);
565       chrome.send('saveAppPageName', [name, index]);
566     },
568     /**
569      * Window resize handler.
570      * @private
571      */
572     onWindowResize_: function(e) {
573       this.cardSlider.resize(this.sliderFrame.offsetWidth);
574       this.updatePageSwitchers();
575     },
577     /**
578      * Listener for offline status change events. Updates apps that are
579      * not offline-enabled to be grayscale if the browser is offline.
580      * @private
581      */
582     updateOfflineEnabledApps_: function() {
583       var apps = document.querySelectorAll('.app');
584       for (var i = 0; i < apps.length; ++i) {
585         if (apps[i].appData.enabled && !apps[i].appData.offline_enabled) {
586           apps[i].setIcon();
587           apps[i].loadIcon();
588         }
589       }
590     },
592     /**
593      * Handler for key events on the page. Ctrl-Arrow will switch the visible
594      * page.
595      * @param {Event} e The KeyboardEvent.
596      * @private
597      */
598     onDocKeyDown_: function(e) {
599       if (!e.ctrlKey || e.altKey || e.metaKey || e.shiftKey)
600         return;
602       var direction = 0;
603       if (e.keyIdentifier == 'Left')
604         direction = -1;
605       else if (e.keyIdentifier == 'Right')
606         direction = 1;
607       else
608         return;
610       var cardIndex =
611           (this.cardSlider.currentCard + direction +
612            this.cardSlider.cardCount) % this.cardSlider.cardCount;
613       this.cardSlider.selectCard(cardIndex, true);
615       e.stopPropagation();
616     }
617   };
619   return {
620     PageListView: PageListView
621   };