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.
6 * @fileoverview PageListView implementation.
7 * PageListView manages page list, dot list, switcher buttons and handles apps
8 * pages callbacks from backend.
10 * Note that you need to have AppLauncherHandler in your WebUI to use this code.
13 cr.define('ntp4', function() {
17 * Object for accessing localized strings.
18 * @type {!LocalStrings}
20 var localStrings = new LocalStrings;
23 * Creates a PageListView object.
27 function PageListView() {
30 PageListView.prototype = {
32 * The CardSlider object to use for changing app pages.
33 * @type {CardSlider|undefined}
35 cardSlider: undefined,
38 * The frame div for cardSlider.
39 * @type {!Element|undefined}
41 sliderFrame: undefined,
44 * The 'page-list' element.
45 * @type {!Element|undefined}
50 * A list of all 'tile-page' elements.
51 * @type {!NodeList|undefined}
56 * A list of all 'apps-page' elements.
57 * @type {!NodeList|undefined}
62 * The Most Visited page.
63 * @type {!Element|undefined}
65 mostVisitedPage: undefined,
69 * @type {!Element|undefined}
71 bookmarksPage: undefined,
74 * The 'dots-list' element.
75 * @type {!Element|undefined}
80 * The left and right paging buttons.
81 * @type {!Element|undefined}
83 pageSwitcherStart: undefined,
84 pageSwitcherEnd: undefined,
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}
95 * The type of page that is currently shown. The value is a numerical ID.
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.
108 * EventTracker for managing event listeners for page events.
109 * @type {!EventTracker}
111 eventTracker: new EventTracker,
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.
119 highlightAppId: null,
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
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
131 * @param {!Element|undefined} opt_pageSwitcherEnd Optional end page
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;
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) {
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));
192 * Appends a tile page (for bookmarks or most visited).
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
199 * When opt_refNode is falsey, |page| will just be appended to the end of
202 appendTilePage: function(page, title, titleIsEditable, opt_refNode) {
203 // If no opt_refNode given, use bookmarksPage (if any).
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;
217 if (typeof ntp4.BookmarksPage != 'undefined' &&
218 page instanceof ntp4.BookmarksPage) {
219 this.bookmarksPage = page;
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
230 // Set a tab index on the first dot.
231 if (this.dotList.dots.length == 1)
234 this.eventTracker.add(page, 'pagelayout', this.onPageLayout_.bind(this));
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
242 * @param {boolean} isUninstall True if the app is being uninstalled;
243 * false if the app is being disabled.
245 appRemoved: function(appData, isUninstall) {
246 var app = $(appData.id);
247 assert(app, 'trying to remove an app that doesn\'t exist');
250 app.replaceAppData(appData);
256 * Callback invoked by chrome with the apps available.
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
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
272 while (this.appsPages.length > 0) {
273 var page = this.appsPages[0];
274 var dot = page.navigationDot;
276 this.eventTracker.remove(page);
278 page.parentNode.removeChild(page);
279 dot.parentNode.removeChild(dot);
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++) {
296 // Sort by launch index
297 apps.sort(function(a, b) {
298 return a.app_launch_index - b.app_launch_index;
301 // An app to animate (in case it was just installed).
304 // Add the apps, creating pages as necessary
305 for (var i = 0; i < apps.length; 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');
321 if (app.id == this.highlightAppId)
324 this.appsPages[pageIndex].appendApp(app);
327 ntp4.AppsPage.setPromo(data.showPromo ? data : null);
329 // Tell the slider about the pages.
330 this.updateSliderCards();
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');
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
348 appAdded: function(appData, opt_highlight) {
349 if (appData.id == this.highlightAppId) {
350 opt_highlight = true;
351 this.highlightAppId = null;
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'),
362 this.updateSliderCards();
365 var page = this.appsPages[pageIndex];
366 var app = $(appData.id);
368 app.replaceAppData(appData);
370 page.appendApp(appData, opt_highlight);
374 * Callback invoked by chrome whenever an app preference changes.
375 * @param {Object} data An object with all the data on available
378 appsPrefChangedCallback: function(data) {
379 for (var i = 0; i < data.apps.length; ++i) {
380 $(data.apps[i].id).appData = data.apps[i];
383 // Set the App dot names. Skip the first and last dots (Most Visited and
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] || '';
396 * Invoked whenever the pages in apps-page-list have changed so that
397 * the Slider knows about the new elements.
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),
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)]);
410 case templateData['bookmarks_page_id']:
411 if (this.bookmarksPage)
412 this.cardSlider.selectCardByValue(this.bookmarksPage);
414 case templateData['most_visited_page_id']:
415 if (this.mostVisitedPage)
416 this.cardSlider.selectCardByValue(this.mostVisitedPage);
422 * Called whenever tiles should be re-arranging themselves out of the way
423 * of a moving or insert tile.
425 enterRearrangeMode: function() {
426 var tempPage = new ntp4.AppsPage();
427 tempPage.classList.add('temporary');
428 this.appendTilePage(tempPage,
429 localStrings.getString('appDefaultPageName'),
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');
441 * Invoked whenever some app is released
442 * @param {cr.ui.Grabber.Event} e The Grabber RELEASE event.
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) {
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();
455 tempPage.classList.remove('temporary');
456 this.saveAppPageName(tempPage,
457 localStrings.getString('appDefaultPageName'));
460 $('footer').classList.remove('showing-trash-mode');
464 * Callback for the 'pagelayout' event.
465 * @param {Event} e The event.
467 onPageLayout_: function(e) {
468 if (Array.prototype.indexOf.call(this.tilePages, e.currentTarget) !=
469 this.cardSlider.currentCard) {
473 this.updatePageSwitchers();
477 * Adjusts the size and position of the page switchers according to the
478 * layout of the current card.
480 updatePageSwitchers: function() {
481 if (!this.pageSwitcherStart || !this.pageSwitcherEnd)
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);
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;
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.
518 getAppsPageIndex: function(page) {
519 return Array.prototype.indexOf.call(this.appsPages, page);
523 * Handler for CARD_CHANGED on cardSlider.
524 * @param {Event} e The CARD_CHANGED event.
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;
543 console.error('unknown page selected');
545 chrome.send('pageSelected', [this.shownPage, this.shownPageIndex]);
548 // Update the active dot
549 var curDot = this.dotList.getElementsByClassName('selected')[0];
551 curDot.classList.remove('selected');
552 page.navigationDot.classList.add('selected');
553 this.updatePageSwitchers();
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.
562 saveAppPageName: function(appPage, name) {
563 var index = this.getAppsPageIndex(appPage);
565 chrome.send('saveAppPageName', [name, index]);
569 * Window resize handler.
572 onWindowResize_: function(e) {
573 this.cardSlider.resize(this.sliderFrame.offsetWidth);
574 this.updatePageSwitchers();
578 * Listener for offline status change events. Updates apps that are
579 * not offline-enabled to be grayscale if the browser is offline.
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) {
593 * Handler for key events on the page. Ctrl-Arrow will switch the visible
595 * @param {Event} e The KeyboardEvent.
598 onDocKeyDown_: function(e) {
599 if (!e.ctrlKey || e.altKey || e.metaKey || e.shiftKey)
603 if (e.keyIdentifier == 'Left')
605 else if (e.keyIdentifier == 'Right')
611 (this.cardSlider.currentCard + direction +
612 this.cardSlider.cardCount) % this.cardSlider.cardCount;
613 this.cardSlider.selectCard(cardIndex, true);
620 PageListView: PageListView