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.
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.
14 * @typedef {{app_launch_ordinal: string,
15 * description: string,
20 * full_name_direction: string,
21 * homepageUrl: string,
23 * icon_big_exists: boolean,
25 * icon_small_exists: boolean,
27 * is_component: boolean,
28 * is_webstore: boolean,
29 * kioskEnabled: boolean,
31 * launch_container: number,
32 * launch_type: number,
33 * mayDisable: boolean,
35 * offlineEnabled: boolean,
37 * packagedApp: boolean,
42 * @see chrome/browser/ui/webui/ntp/app_launcher_handler.cc
46 cr.define('ntp', function() {
50 * Creates a PageListView object.
54 function PageListView() {
57 PageListView.prototype = {
59 * The CardSlider object to use for changing app pages.
60 * @type {cr.ui.CardSlider|undefined}
62 cardSlider: undefined,
65 * The frame div for this.cardSlider.
66 * @type {!Element|undefined}
68 sliderFrame: undefined,
71 * The 'page-list' element.
72 * @type {!Element|undefined}
77 * A list of all 'tile-page' elements.
78 * @type {!NodeList|undefined}
83 * A list of all 'apps-page' elements.
84 * @type {!NodeList|undefined}
89 * The Suggestions page.
90 * @type {!Element|undefined}
92 suggestionsPage: undefined,
95 * The Most Visited page.
96 * @type {!Element|undefined}
98 mostVisitedPage: undefined,
101 * The 'dots-list' element.
102 * @type {!Element|undefined}
107 * The left and right paging buttons.
108 * @type {!ntp.PageSwitcher|undefined}
110 pageSwitcherStart: undefined,
111 pageSwitcherEnd: undefined,
114 * The 'trash' element. Note that technically this is unnecessary,
115 * JavaScript creates the object for us based on the id. But I don't want
116 * to rely on the ID being the same, and JSCompiler doesn't know about it.
117 * @type {!Element|undefined}
122 * The type of page that is currently shown. The value is a numerical ID.
128 * The index of the page that is currently shown, within the page type.
129 * For example if the third Apps page is showing, this will be 2.
135 * EventTracker for managing event listeners for page events.
136 * @type {!EventTracker}
138 eventTracker: new EventTracker,
141 * If non-null, this is the ID of the app to highlight to the user the next
142 * time getAppsCallback runs. "Highlight" in this case means to switch to
143 * the page and run the new tile animation.
146 highlightAppId: null,
149 * Initializes page list view.
150 * @param {!Element} pageList A DIV element to host all pages.
151 * @param {!Element} dotList An UL element to host nav dots. Each dot
153 * @param {!Element} cardSliderFrame The card slider frame that hosts
154 * pageList and switcher buttons.
155 * @param {!Element|undefined} opt_trash Optional trash element.
156 * @param {!ntp.PageSwitcher|undefined} opt_pageSwitcherStart Optional start
157 * page switcher button.
158 * @param {!ntp.PageSwitcher|undefined} opt_pageSwitcherEnd Optional end
159 * page switcher button.
161 initialize: function(pageList, dotList, cardSliderFrame, opt_trash,
162 opt_pageSwitcherStart, opt_pageSwitcherEnd) {
163 this.pageList = pageList;
165 this.dotList = dotList;
166 cr.ui.decorate(this.dotList, ntp.DotList);
168 this.trash = opt_trash;
170 new ntp.Trash(this.trash);
172 this.pageSwitcherStart = opt_pageSwitcherStart;
173 if (this.pageSwitcherStart)
174 ntp.initializePageSwitcher(this.pageSwitcherStart);
176 this.pageSwitcherEnd = opt_pageSwitcherEnd;
177 if (this.pageSwitcherEnd)
178 ntp.initializePageSwitcher(this.pageSwitcherEnd);
180 this.shownPage = loadTimeData.getInteger('shown_page_type');
181 this.shownPageIndex = loadTimeData.getInteger('shown_page_index');
183 if (loadTimeData.getBoolean('showApps')) {
184 // Request data on the apps so we can fill them in.
185 // Note that this is kicked off asynchronously. 'getAppsCallback' will
186 // be invoked at some point after this function returns.
187 chrome.send('getApps');
190 if (this.shownPage == loadTimeData.getInteger('apps_page_id')) {
192 loadTimeData.getInteger('most_visited_page_id'), 0);
195 document.body.classList.add('bare-minimum');
198 document.addEventListener('keydown', this.onDocKeyDown_.bind(this));
200 this.tilePages = this.pageList.getElementsByClassName('tile-page');
201 this.appsPages = this.pageList.getElementsByClassName('apps-page');
203 // Initialize the cardSlider without any cards at the moment.
204 this.sliderFrame = cardSliderFrame;
205 this.cardSlider = new cr.ui.CardSlider(this.sliderFrame, this.pageList,
206 this.sliderFrame.offsetWidth);
208 // Prevent touch events from triggering any sort of native scrolling if
209 // there are multiple cards in the slider frame.
210 var cardSlider = this.cardSlider;
211 cardSliderFrame.addEventListener('touchmove', function(e) {
212 if (cardSlider.cardCount <= 1)
217 // Handle mousewheel events anywhere in the card slider, so that wheel
218 // events on the page switchers will still scroll the page.
219 // This listener must be added before the card slider is initialized,
220 // because it needs to be called before the card slider's handler.
221 cardSliderFrame.addEventListener('mousewheel', function(e) {
222 if (cardSlider.currentCardValue.handleMouseWheel(e)) {
223 e.preventDefault(); // Prevent default scroll behavior.
224 e.stopImmediatePropagation(); // Prevent horizontal card flipping.
228 this.cardSlider.initialize(
229 loadTimeData.getBoolean('isSwipeTrackingFromScrollEventsEnabled'));
231 // Handle events from the card slider.
232 this.pageList.addEventListener('cardSlider:card_changed',
233 this.onCardChanged_.bind(this));
234 this.pageList.addEventListener('cardSlider:card_added',
235 this.onCardAdded_.bind(this));
236 this.pageList.addEventListener('cardSlider:card_removed',
237 this.onCardRemoved_.bind(this));
239 // Ensure the slider is resized appropriately with the window.
240 window.addEventListener('resize', this.onWindowResize_.bind(this));
242 // Update apps when online state changes.
243 window.addEventListener('online',
244 this.updateOfflineEnabledApps_.bind(this));
245 window.addEventListener('offline',
246 this.updateOfflineEnabledApps_.bind(this));
250 * Appends a tile page.
252 * @param {!ntp.TilePage} page The page element.
253 * @param {string} title The title of the tile page.
254 * @param {boolean} titleIsEditable If true, the title can be changed.
255 * @param {ntp.TilePage=} opt_refNode Optional reference node to insert in
257 * When opt_refNode is falsey, |page| will just be appended to the end of
260 appendTilePage: function(page, title, titleIsEditable, opt_refNode) {
262 var refIndex = this.getTilePageIndex(opt_refNode);
263 this.cardSlider.addCardAtIndex(page, refIndex);
265 this.cardSlider.appendCard(page);
268 // Remember special MostVisitedPage.
269 if (typeof ntp.MostVisitedPage != 'undefined' &&
270 page instanceof ntp.MostVisitedPage) {
271 assert(this.tilePages.length == 1,
272 'MostVisitedPage should be added as first tile page');
273 this.mostVisitedPage = page;
276 if (typeof ntp.SuggestionsPage != 'undefined' &&
277 page instanceof ntp.SuggestionsPage) {
278 this.suggestionsPage = page;
281 // If we're appending an AppsPage and it's a temporary page, animate it.
282 var animate = page instanceof ntp.AppsPage &&
283 page.classList.contains('temporary');
284 // Make a deep copy of the dot template to add a new one.
285 var newDot = new ntp.NavDot(page, title, titleIsEditable, animate);
286 page.navigationDot = newDot;
287 this.dotList.insertBefore(newDot,
288 opt_refNode ? opt_refNode.navigationDot : null);
289 // Set a tab index on the first dot.
290 if (this.dotList.dots.length == 1)
293 this.eventTracker.add(page, 'pagelayout', this.onPageLayout_.bind(this));
297 * Called by chrome when an app has changed positions.
298 * @param {AppInfo} appData The data for the app. This contains page and
301 appMoved: function(appData) {
302 assert(loadTimeData.getBoolean('showApps'));
304 var app = $(appData.id);
305 assert(app, 'trying to move an app that doesn\'t exist');
308 this.appsPages[appData.page_index].insertApp(appData, false);
312 * Called by chrome when an existing app has been disabled or
313 * removed/uninstalled from chrome.
314 * @param {AppInfo} appData A data structure full of relevant information
316 * @param {boolean} isUninstall True if the app is being uninstalled;
317 * false if the app is being disabled.
318 * @param {boolean} fromPage True if the removal was from the current page.
320 appRemoved: function(appData, isUninstall, fromPage) {
321 assert(loadTimeData.getBoolean('showApps'));
323 var app = $(appData.id);
324 assert(app, 'trying to remove an app that doesn\'t exist');
327 app.replaceAppData(appData);
329 app.remove(!!fromPage);
333 * @return {boolean} If the page is still starting up.
336 isStartingUp_: function() {
337 return document.documentElement.classList.contains('starting-up');
341 * Tracks whether apps have been loaded at least once.
348 * Callback invoked by chrome with the apps available.
350 * Note that calls to this function can occur at any time, not just in
351 * response to a getApps request. For example, when a user
352 * installs/uninstalls an app on another synchronized devices.
353 * @param {{apps: Array.<AppInfo>, appPageNames: Array.<string>}} data
354 * An object with all the data on available applications.
356 getAppsCallback: function(data) {
357 assert(loadTimeData.getBoolean('showApps'));
359 var startTime = Date.now();
361 // Remember this to select the correct card when done rebuilding.
362 var prevCurrentCard = this.cardSlider.currentCard;
364 // Make removal of pages and dots as quick as possible with less DOM
365 // operations, reflows, or repaints. We set currentCard = 0 and remove
366 // from the end to not encounter any auto-magic card selections in the
367 // process and we hide the card slider throughout.
368 this.cardSlider.currentCard = 0;
370 // Clear any existing apps pages and dots.
371 // TODO(rbyers): It might be nice to preserve animation of dots after an
372 // uninstall. Could we re-use the existing page and dot elements? It
373 // seems unfortunate to have Chrome send us the entire apps list after an
375 while (this.appsPages.length > 0)
376 this.removeTilePageAndDot_(this.appsPages[this.appsPages.length - 1]);
378 // Get the array of apps and add any special synthesized entries
379 var apps = data.apps;
381 // Get a list of page names
382 var pageNames = data.appPageNames;
384 function stringListIsEmpty(list) {
385 for (var i = 0; i < list.length; i++) {
392 // Sort by launch ordinal
393 apps.sort(function(a, b) {
394 return a.app_launch_ordinal > b.app_launch_ordinal ? 1 :
395 a.app_launch_ordinal < b.app_launch_ordinal ? -1 : 0;
398 // An app to animate (in case it was just installed).
401 // If there are any pages after the apps, add new pages before them.
402 var lastAppsPage = (this.appsPages.length > 0) ?
403 this.appsPages[this.appsPages.length - 1] : null;
404 var lastAppsPageIndex = (lastAppsPage != null) ?
405 Array.prototype.indexOf.call(this.tilePages, lastAppsPage) : -1;
406 var nextPageAfterApps = lastAppsPageIndex != -1 ?
407 this.tilePages[lastAppsPageIndex + 1] : null;
409 // Add the apps, creating pages as necessary
410 for (var i = 0; i < apps.length; i++) {
412 var pageIndex = app.page_index || 0;
413 while (pageIndex >= this.appsPages.length) {
414 var pageName = loadTimeData.getString('appDefaultPageName');
415 if (this.appsPages.length < pageNames.length)
416 pageName = pageNames[this.appsPages.length];
418 var origPageCount = this.appsPages.length;
419 this.appendTilePage(new ntp.AppsPage(), pageName, true,
421 // Confirm that appsPages is a live object, updated when a new page is
422 // added (otherwise we'd have an infinite loop)
423 assert(this.appsPages.length == origPageCount + 1,
424 'expected new page');
427 if (app.id == this.highlightAppId)
430 this.appsPages[pageIndex].insertApp(app, false);
433 this.cardSlider.currentCard = prevCurrentCard;
436 this.appAdded(highlightApp, true);
438 logEvent('apps.layout: ' + (Date.now() - startTime));
440 // Tell the slider about the pages and mark the current page.
441 this.updateSliderCards();
442 this.cardSlider.currentCardValue.navigationDot.classList.add('selected');
444 if (!this.appsLoaded_) {
445 this.appsLoaded_ = true;
446 cr.dispatchSimpleEvent(document, 'sectionready', true, true);
448 this.updateAppLauncherPromoHiddenState_();
452 * Called by chrome when a new app has been added to chrome or has been
453 * enabled if previously disabled.
454 * @param {AppInfo} appData A data structure full of relevant information
456 * @param {boolean=} opt_highlight Whether the app about to be added should
459 appAdded: function(appData, opt_highlight) {
460 assert(loadTimeData.getBoolean('showApps'));
462 if (appData.id == this.highlightAppId) {
463 opt_highlight = true;
464 this.highlightAppId = null;
467 var pageIndex = appData.page_index || 0;
469 if (pageIndex >= this.appsPages.length) {
470 while (pageIndex >= this.appsPages.length) {
471 this.appendTilePage(new ntp.AppsPage(),
472 loadTimeData.getString('appDefaultPageName'),
475 this.updateSliderCards();
478 var page = this.appsPages[pageIndex];
479 var app = $(appData.id);
481 app.replaceAppData(appData);
482 } else if (opt_highlight) {
483 page.insertAndHighlightApp(appData);
484 this.setShownPage_(loadTimeData.getInteger('apps_page_id'),
487 page.insertApp(appData, false);
492 * Callback invoked by chrome whenever an app preference changes.
493 * @param {Object} data An object with all the data on available
496 appsPrefChangedCallback: function(data) {
497 assert(loadTimeData.getBoolean('showApps'));
499 for (var i = 0; i < data.apps.length; ++i) {
500 $(data.apps[i].id).appData = data.apps[i];
503 // Set the App dot names. Skip the first dot (Most Visited).
504 var dots = this.dotList.getElementsByClassName('dot');
505 var start = this.mostVisitedPage ? 1 : 0;
506 for (var i = start; i < dots.length; ++i) {
507 dots[i].displayTitle = data.appPageNames[i - start] || '';
512 * Callback invoked by chrome whenever the app launcher promo pref changes.
513 * @param {boolean} show Identifies if we should show or hide the promo.
515 appLauncherPromoPrefChangeCallback: function(show) {
516 loadTimeData.overrideValues({showAppLauncherPromo: show});
517 this.updateAppLauncherPromoHiddenState_();
521 * Updates the hidden state of the app launcher promo based on the page
522 * shown and load data content.
524 updateAppLauncherPromoHiddenState_: function() {
525 $('app-launcher-promo').hidden =
526 !loadTimeData.getBoolean('showAppLauncherPromo') ||
527 this.shownPage != loadTimeData.getInteger('apps_page_id');
531 * Invoked whenever the pages in apps-page-list have changed so that
532 * the Slider knows about the new elements.
534 updateSliderCards: function() {
535 var pageNo = Math.max(0, Math.min(this.cardSlider.currentCard,
536 this.tilePages.length - 1));
537 this.cardSlider.setCards(Array.prototype.slice.call(this.tilePages),
539 // The shownPage property was potentially saved from a previous webui that
540 // didn't have the same set of pages as the current one. So we cascade
541 // from suggestions, to most visited and then to apps because we can have
542 // an page with apps only (e.g., chrome://apps) or one with only the most
543 // visited, but not one with only suggestions. And we alwayd default to
544 // most visited first when previously shown page is not availabel anymore.
545 // If most visited isn't there either, we go to apps.
546 if (this.shownPage == loadTimeData.getInteger('suggestions_page_id')) {
547 if (this.suggestionsPage)
548 this.cardSlider.selectCardByValue(this.suggestionsPage);
550 this.shownPage = loadTimeData.getInteger('most_visited_page_id');
552 if (this.shownPage == loadTimeData.getInteger('most_visited_page_id')) {
553 if (this.mostVisitedPage)
554 this.cardSlider.selectCardByValue(this.mostVisitedPage);
556 this.shownPage = loadTimeData.getInteger('apps_page_id');
558 if (this.shownPage == loadTimeData.getInteger('apps_page_id') &&
559 loadTimeData.getBoolean('showApps')) {
560 this.cardSlider.selectCardByValue(
561 this.appsPages[Math.min(this.shownPageIndex,
562 this.appsPages.length - 1)]);
563 } else if (this.mostVisitedPage) {
564 this.shownPage = loadTimeData.getInteger('most_visited_page_id');
565 this.cardSlider.selectCardByValue(this.mostVisitedPage);
570 * Called whenever tiles should be re-arranging themselves out of the way
571 * of a moving or insert tile.
573 enterRearrangeMode: function() {
574 if (loadTimeData.getBoolean('showApps')) {
575 var tempPage = new ntp.AppsPage();
576 tempPage.classList.add('temporary');
577 var pageName = loadTimeData.getString('appDefaultPageName');
578 this.appendTilePage(tempPage, pageName, true);
581 if (ntp.getCurrentlyDraggingTile().firstChild.canBeRemoved()) {
582 $('footer').classList.add('showing-trash-mode');
583 $('footer-menu-container').style.minWidth = $('trash').offsetWidth -
584 $('chrome-web-store-link').offsetWidth + 'px';
587 document.documentElement.classList.add('dragging-mode');
591 * Invoked whenever some app is released
593 leaveRearrangeMode: function() {
594 var tempPage = /** @type {ntp.AppsPage} */(
595 document.querySelector('.tile-page.temporary'));
597 var dot = tempPage.navigationDot;
598 if (!tempPage.tileCount &&
599 tempPage != this.cardSlider.currentCardValue) {
600 this.removeTilePageAndDot_(tempPage, true);
602 tempPage.classList.remove('temporary');
603 this.saveAppPageName(tempPage,
604 loadTimeData.getString('appDefaultPageName'));
608 $('footer').classList.remove('showing-trash-mode');
609 $('footer-menu-container').style.minWidth = '';
610 document.documentElement.classList.remove('dragging-mode');
614 * Callback for the 'pagelayout' event.
615 * @param {Event} e The event.
617 onPageLayout_: function(e) {
618 if (Array.prototype.indexOf.call(this.tilePages, e.currentTarget) !=
619 this.cardSlider.currentCard) {
623 this.updatePageSwitchers();
627 * Adjusts the size and position of the page switchers according to the
628 * layout of the current card, and updates the aria-label attributes of
629 * the page switchers.
631 updatePageSwitchers: function() {
632 if (!this.pageSwitcherStart || !this.pageSwitcherEnd)
635 var page = this.cardSlider.currentCardValue;
637 this.pageSwitcherStart.hidden = !page ||
638 (this.cardSlider.currentCard == 0);
639 this.pageSwitcherEnd.hidden = !page ||
640 (this.cardSlider.currentCard == this.cardSlider.cardCount - 1);
645 var pageSwitcherLeft = isRTL() ? this.pageSwitcherEnd :
646 this.pageSwitcherStart;
647 var pageSwitcherRight = isRTL() ? this.pageSwitcherStart :
648 this.pageSwitcherEnd;
649 var scrollbarWidth = page.scrollbarWidth;
650 pageSwitcherLeft.style.width =
651 (page.sideMargin + 13) + 'px';
652 pageSwitcherLeft.style.left = '0';
653 pageSwitcherRight.style.width =
654 (page.sideMargin - scrollbarWidth + 13) + 'px';
655 pageSwitcherRight.style.right = scrollbarWidth + 'px';
657 var offsetTop = page.querySelector('.tile-page-content').offsetTop + 'px';
658 pageSwitcherLeft.style.top = offsetTop;
659 pageSwitcherRight.style.top = offsetTop;
660 pageSwitcherLeft.style.paddingBottom = offsetTop;
661 pageSwitcherRight.style.paddingBottom = offsetTop;
663 // Update the aria-label attributes of the two page switchers.
664 this.pageSwitcherStart.updateButtonAccessibleLabel(this.dotList.dots);
665 this.pageSwitcherEnd.updateButtonAccessibleLabel(this.dotList.dots);
669 * Returns the index of the given apps page.
670 * @param {ntp.AppsPage} page The AppsPage we wish to find.
671 * @return {number} The index of |page| or -1 if it is not in the
674 getAppsPageIndex: function(page) {
675 return Array.prototype.indexOf.call(this.appsPages, page);
679 * Handler for cardSlider:card_changed events from this.cardSlider.
680 * @param {Event} e The cardSlider:card_changed event.
683 onCardChanged_: function(e) {
684 var page = e.cardSlider.currentCardValue;
686 // Don't change shownPage until startup is done (and page changes actually
687 // reflect user actions).
688 if (!this.isStartingUp_()) {
689 if (page.classList.contains('apps-page')) {
690 this.setShownPage_(loadTimeData.getInteger('apps_page_id'),
691 this.getAppsPageIndex(page));
692 } else if (page.classList.contains('most-visited-page')) {
694 loadTimeData.getInteger('most_visited_page_id'), 0);
695 } else if (page.classList.contains('suggestions-page')) {
696 this.setShownPage_(loadTimeData.getInteger('suggestions_page_id'), 0);
698 console.error('unknown page selected');
702 // Update the active dot
703 var curDot = this.dotList.getElementsByClassName('selected')[0];
705 curDot.classList.remove('selected');
706 page.navigationDot.classList.add('selected');
707 this.updatePageSwitchers();
711 * Saves/updates the newly selected page to open when first loading the NTP.
712 * @param {number} shownPage The new shown page type.
713 * @param {number} shownPageIndex The new shown page index.
716 setShownPage_: function(shownPage, shownPageIndex) {
717 assert(shownPageIndex >= 0);
718 this.shownPage = shownPage;
719 this.shownPageIndex = shownPageIndex;
720 chrome.send('pageSelected', [this.shownPage, this.shownPageIndex]);
721 this.updateAppLauncherPromoHiddenState_();
725 * Listen for card additions to update the page switchers or the current
727 * @param {Event} e A card removed or added event.
729 onCardAdded_: function(e) {
730 // When the second arg passed to insertBefore is falsey, it acts just like
732 this.pageList.insertBefore(e.addedCard, this.tilePages[e.addedIndex]);
733 this.onCardAddedOrRemoved_();
737 * Listen for card removals to update the page switchers or the current card
739 * @param {Event} e A card removed or added event.
741 onCardRemoved_: function(e) {
742 e.removedCard.parentNode.removeChild(e.removedCard);
743 this.onCardAddedOrRemoved_();
747 * Called when a card is removed or added.
750 onCardAddedOrRemoved_: function() {
751 if (this.isStartingUp_())
754 // Without repositioning there were issues - http://crbug.com/133457.
755 this.cardSlider.repositionFrame();
756 this.updatePageSwitchers();
760 * Save the name of an apps page.
761 * Store the apps page name into the preferences store.
762 * @param {ntp.AppsPage} appPage The app page for which we wish to save.
763 * @param {string} name The name of the page.
765 saveAppPageName: function(appPage, name) {
766 var index = this.getAppsPageIndex(appPage);
768 chrome.send('saveAppPageName', [name, index]);
772 * Window resize handler.
775 onWindowResize_: function(e) {
776 this.cardSlider.resize(this.sliderFrame.offsetWidth);
777 this.updatePageSwitchers();
781 * Listener for offline status change events. Updates apps that are
782 * not offline-enabled to be grayscale if the browser is offline.
785 updateOfflineEnabledApps_: function() {
786 var apps = document.querySelectorAll('.app');
787 for (var i = 0; i < apps.length; ++i) {
788 if (apps[i].appData.enabled && !apps[i].appData.offlineEnabled) {
796 * Handler for key events on the page. Ctrl-Arrow will switch the visible
798 * @param {Event} e The KeyboardEvent.
801 onDocKeyDown_: function(e) {
802 if (!e.ctrlKey || e.altKey || e.metaKey || e.shiftKey)
806 if (e.keyIdentifier == 'Left')
808 else if (e.keyIdentifier == 'Right')
814 (this.cardSlider.currentCard + direction +
815 this.cardSlider.cardCount) % this.cardSlider.cardCount;
816 this.cardSlider.selectCard(cardIndex, true);
822 * Returns the index of a given tile page.
823 * @param {ntp.TilePage} page The TilePage we wish to find.
824 * @return {number} The index of |page| or -1 if it is not in the
827 getTilePageIndex: function(page) {
828 return Array.prototype.indexOf.call(this.tilePages, page);
832 * Removes a page and navigation dot (if the navdot exists).
833 * @param {ntp.TilePage} page The page to be removed.
834 * @param {boolean=} opt_animate If the removal should be animated.
836 removeTilePageAndDot_: function(page, opt_animate) {
837 if (page.navigationDot)
838 page.navigationDot.remove(opt_animate);
839 this.cardSlider.removeCard(page);
844 PageListView: PageListView