1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5/** 6 * @fileoverview PageListView implementation. 7 * PageListView manages page list, dot list, switcher buttons and handles apps 8 * pages callbacks from backend. 9 * 10 * Note that you need to have AppLauncherHandler in your WebUI to use this code. 11 */ 12 13cr.define('ntp', function() { 14 'use strict'; 15 16 /** 17 * Creates a PageListView object. 18 * @constructor 19 * @extends {Object} 20 */ 21 function PageListView() { 22 } 23 24 PageListView.prototype = { 25 /** 26 * The CardSlider object to use for changing app pages. 27 * @type {CardSlider|undefined} 28 */ 29 cardSlider: undefined, 30 31 /** 32 * The frame div for this.cardSlider. 33 * @type {!Element|undefined} 34 */ 35 sliderFrame: undefined, 36 37 /** 38 * The 'page-list' element. 39 * @type {!Element|undefined} 40 */ 41 pageList: undefined, 42 43 /** 44 * A list of all 'tile-page' elements. 45 * @type {!NodeList|undefined} 46 */ 47 tilePages: undefined, 48 49 /** 50 * A list of all 'apps-page' elements. 51 * @type {!NodeList|undefined} 52 */ 53 appsPages: undefined, 54 55 /** 56 * The Suggestions page. 57 * @type {!Element|undefined} 58 */ 59 suggestionsPage: undefined, 60 61 /** 62 * The Most Visited page. 63 * @type {!Element|undefined} 64 */ 65 mostVisitedPage: undefined, 66 67 /** 68 * The 'dots-list' element. 69 * @type {!Element|undefined} 70 */ 71 dotList: undefined, 72 73 /** 74 * The left and right paging buttons. 75 * @type {!Element|undefined} 76 */ 77 pageSwitcherStart: undefined, 78 pageSwitcherEnd: undefined, 79 80 /** 81 * The 'trash' element. Note that technically this is unnecessary, 82 * JavaScript creates the object for us based on the id. But I don't want 83 * to rely on the ID being the same, and JSCompiler doesn't know about it. 84 * @type {!Element|undefined} 85 */ 86 trash: undefined, 87 88 /** 89 * The type of page that is currently shown. The value is a numerical ID. 90 * @type {number} 91 */ 92 shownPage: 0, 93 94 /** 95 * The index of the page that is currently shown, within the page type. 96 * For example if the third Apps page is showing, this will be 2. 97 * @type {number} 98 */ 99 shownPageIndex: 0, 100 101 /** 102 * EventTracker for managing event listeners for page events. 103 * @type {!EventTracker} 104 */ 105 eventTracker: new EventTracker, 106 107 /** 108 * If non-null, this is the ID of the app to highlight to the user the next 109 * time getAppsCallback runs. "Highlight" in this case means to switch to 110 * the page and run the new tile animation. 111 * @type {?string} 112 */ 113 highlightAppId: null, 114 115 /** 116 * Initializes page list view. 117 * @param {!Element} pageList A DIV element to host all pages. 118 * @param {!Element} dotList An UL element to host nav dots. Each dot 119 * represents a page. 120 * @param {!Element} cardSliderFrame The card slider frame that hosts 121 * pageList and switcher buttons. 122 * @param {!Element|undefined} opt_trash Optional trash element. 123 * @param {!Element|undefined} opt_pageSwitcherStart Optional start page 124 * switcher button. 125 * @param {!Element|undefined} opt_pageSwitcherEnd Optional end page 126 * switcher button. 127 */ 128 initialize: function(pageList, dotList, cardSliderFrame, opt_trash, 129 opt_pageSwitcherStart, opt_pageSwitcherEnd) { 130 this.pageList = pageList; 131 132 this.dotList = dotList; 133 cr.ui.decorate(this.dotList, ntp.DotList); 134 135 this.trash = opt_trash; 136 if (this.trash) 137 new ntp.Trash(this.trash); 138 139 this.pageSwitcherStart = opt_pageSwitcherStart; 140 if (this.pageSwitcherStart) 141 ntp.initializePageSwitcher(this.pageSwitcherStart); 142 143 this.pageSwitcherEnd = opt_pageSwitcherEnd; 144 if (this.pageSwitcherEnd) 145 ntp.initializePageSwitcher(this.pageSwitcherEnd); 146 147 this.shownPage = loadTimeData.getInteger('shown_page_type'); 148 this.shownPageIndex = loadTimeData.getInteger('shown_page_index'); 149 150 if (loadTimeData.getBoolean('showApps')) { 151 // Request data on the apps so we can fill them in. 152 // Note that this is kicked off asynchronously. 'getAppsCallback' will 153 // be invoked at some point after this function returns. 154 chrome.send('getApps'); 155 } else { 156 // No apps page. 157 if (this.shownPage == loadTimeData.getInteger('apps_page_id')) { 158 this.setShownPage_( 159 loadTimeData.getInteger('most_visited_page_id'), 0); 160 } 161 162 document.body.classList.add('bare-minimum'); 163 } 164 165 document.addEventListener('keydown', this.onDocKeyDown_.bind(this)); 166 167 this.tilePages = this.pageList.getElementsByClassName('tile-page'); 168 this.appsPages = this.pageList.getElementsByClassName('apps-page'); 169 170 // Initialize the cardSlider without any cards at the moment. 171 this.sliderFrame = cardSliderFrame; 172 this.cardSlider = new cr.ui.CardSlider(this.sliderFrame, this.pageList, 173 this.sliderFrame.offsetWidth); 174 175 // Prevent touch events from triggering any sort of native scrolling if 176 // there are multiple cards in the slider frame. 177 var cardSlider = this.cardSlider; 178 cardSliderFrame.addEventListener('touchmove', function(e) { 179 if (cardSlider.cardCount <= 1) 180 return; 181 e.preventDefault(); 182 }, true); 183 184 // Handle mousewheel events anywhere in the card slider, so that wheel 185 // events on the page switchers will still scroll the page. 186 // This listener must be added before the card slider is initialized, 187 // because it needs to be called before the card slider's handler. 188 cardSliderFrame.addEventListener('mousewheel', function(e) { 189 if (cardSlider.currentCardValue.handleMouseWheel(e)) { 190 e.preventDefault(); // Prevent default scroll behavior. 191 e.stopImmediatePropagation(); // Prevent horizontal card flipping. 192 } 193 }); 194 195 this.cardSlider.initialize( 196 loadTimeData.getBoolean('isSwipeTrackingFromScrollEventsEnabled')); 197 198 // Handle events from the card slider. 199 this.pageList.addEventListener('cardSlider:card_changed', 200 this.onCardChanged_.bind(this)); 201 this.pageList.addEventListener('cardSlider:card_added', 202 this.onCardAdded_.bind(this)); 203 this.pageList.addEventListener('cardSlider:card_removed', 204 this.onCardRemoved_.bind(this)); 205 206 // Ensure the slider is resized appropriately with the window. 207 window.addEventListener('resize', this.onWindowResize_.bind(this)); 208 209 // Update apps when online state changes. 210 window.addEventListener('online', 211 this.updateOfflineEnabledApps_.bind(this)); 212 window.addEventListener('offline', 213 this.updateOfflineEnabledApps_.bind(this)); 214 }, 215 216 /** 217 * Appends a tile page. 218 * 219 * @param {TilePage} page The page element. 220 * @param {string} title The title of the tile page. 221 * @param {boolean} titleIsEditable If true, the title can be changed. 222 * @param {TilePage} opt_refNode Optional reference node to insert in front 223 * of. 224 * When opt_refNode is falsey, |page| will just be appended to the end of 225 * the page list. 226 */ 227 appendTilePage: function(page, title, titleIsEditable, opt_refNode) { 228 if (opt_refNode) { 229 var refIndex = this.getTilePageIndex(opt_refNode); 230 this.cardSlider.addCardAtIndex(page, refIndex); 231 } else { 232 this.cardSlider.appendCard(page); 233 } 234 235 // Remember special MostVisitedPage. 236 if (typeof ntp.MostVisitedPage != 'undefined' && 237 page instanceof ntp.MostVisitedPage) { 238 assert(this.tilePages.length == 1, 239 'MostVisitedPage should be added as first tile page'); 240 this.mostVisitedPage = page; 241 } 242 243 if (typeof ntp.SuggestionsPage != 'undefined' && 244 page instanceof ntp.SuggestionsPage) { 245 this.suggestionsPage = page; 246 } 247 248 // If we're appending an AppsPage and it's a temporary page, animate it. 249 var animate = page instanceof ntp.AppsPage && 250 page.classList.contains('temporary'); 251 // Make a deep copy of the dot template to add a new one. 252 var newDot = new ntp.NavDot(page, title, titleIsEditable, animate); 253 page.navigationDot = newDot; 254 this.dotList.insertBefore(newDot, 255 opt_refNode ? opt_refNode.navigationDot : null); 256 // Set a tab index on the first dot. 257 if (this.dotList.dots.length == 1) 258 newDot.tabIndex = 3; 259 260 this.eventTracker.add(page, 'pagelayout', this.onPageLayout_.bind(this)); 261 }, 262 263 /** 264 * Called by chrome when an app has changed positions. 265 * @param {Object} appData The data for the app. This contains page and 266 * position indices. 267 */ 268 appMoved: function(appData) { 269 assert(loadTimeData.getBoolean('showApps')); 270 271 var app = $(appData.id); 272 assert(app, 'trying to move an app that doesn\'t exist'); 273 app.remove(false); 274 275 this.appsPages[appData.page_index].insertApp(appData, false); 276 }, 277 278 /** 279 * Called by chrome when an existing app has been disabled or 280 * removed/uninstalled from chrome. 281 * @param {Object} appData A data structure full of relevant information for 282 * the app. 283 * @param {boolean} isUninstall True if the app is being uninstalled; 284 * false if the app is being disabled. 285 * @param {boolean} fromPage True if the removal was from the current page. 286 */ 287 appRemoved: function(appData, isUninstall, fromPage) { 288 assert(loadTimeData.getBoolean('showApps')); 289 290 var app = $(appData.id); 291 assert(app, 'trying to remove an app that doesn\'t exist'); 292 293 if (!isUninstall) 294 app.replaceAppData(appData); 295 else 296 app.remove(!!fromPage); 297 }, 298 299 /** 300 * @return {boolean} If the page is still starting up. 301 * @private 302 */ 303 isStartingUp_: function() { 304 return document.documentElement.classList.contains('starting-up'); 305 }, 306 307 /** 308 * Tracks whether apps have been loaded at least once. 309 * @type {boolean} 310 * @private 311 */ 312 appsLoaded_: false, 313 314 /** 315 * Callback invoked by chrome with the apps available. 316 * 317 * Note that calls to this function can occur at any time, not just in 318 * response to a getApps request. For example, when a user 319 * installs/uninstalls an app on another synchronized devices. 320 * @param {Object} data An object with all the data on available 321 * applications. 322 */ 323 getAppsCallback: function(data) { 324 assert(loadTimeData.getBoolean('showApps')); 325 326 var startTime = Date.now(); 327 328 // Remember this to select the correct card when done rebuilding. 329 var prevCurrentCard = this.cardSlider.currentCard; 330 331 // Make removal of pages and dots as quick as possible with less DOM 332 // operations, reflows, or repaints. We set currentCard = 0 and remove 333 // from the end to not encounter any auto-magic card selections in the 334 // process and we hide the card slider throughout. 335 this.cardSlider.currentCard = 0; 336 337 // Clear any existing apps pages and dots. 338 // TODO(rbyers): It might be nice to preserve animation of dots after an 339 // uninstall. Could we re-use the existing page and dot elements? It 340 // seems unfortunate to have Chrome send us the entire apps list after an 341 // uninstall. 342 while (this.appsPages.length > 0) 343 this.removeTilePageAndDot_(this.appsPages[this.appsPages.length - 1]); 344 345 // Get the array of apps and add any special synthesized entries 346 var apps = data.apps; 347 348 // Get a list of page names 349 var pageNames = data.appPageNames; 350 351 function stringListIsEmpty(list) { 352 for (var i = 0; i < list.length; i++) { 353 if (list[i]) 354 return false; 355 } 356 return true; 357 } 358 359 // Sort by launch ordinal 360 apps.sort(function(a, b) { 361 return a.app_launch_ordinal > b.app_launch_ordinal ? 1 : 362 a.app_launch_ordinal < b.app_launch_ordinal ? -1 : 0; 363 }); 364 365 // An app to animate (in case it was just installed). 366 var highlightApp; 367 368 // If there are any pages after the apps, add new pages before them. 369 var lastAppsPage = (this.appsPages.length > 0) ? 370 this.appsPages[this.appsPages.length - 1] : null; 371 var lastAppsPageIndex = (lastAppsPage != null) ? 372 Array.prototype.indexOf.call(this.tilePages, lastAppsPage) : -1; 373 var nextPageAfterApps = lastAppsPageIndex != -1 ? 374 this.tilePages[lastAppsPageIndex + 1] : null; 375 376 // Add the apps, creating pages as necessary 377 for (var i = 0; i < apps.length; i++) { 378 var app = apps[i]; 379 var pageIndex = app.page_index || 0; 380 while (pageIndex >= this.appsPages.length) { 381 var pageName = loadTimeData.getString('appDefaultPageName'); 382 if (this.appsPages.length < pageNames.length) 383 pageName = pageNames[this.appsPages.length]; 384 385 var origPageCount = this.appsPages.length; 386 this.appendTilePage(new ntp.AppsPage(), pageName, true, 387 nextPageAfterApps); 388 // Confirm that appsPages is a live object, updated when a new page is 389 // added (otherwise we'd have an infinite loop) 390 assert(this.appsPages.length == origPageCount + 1, 391 'expected new page'); 392 } 393 394 if (app.id == this.highlightAppId) 395 highlightApp = app; 396 else 397 this.appsPages[pageIndex].insertApp(app, false); 398 } 399 400 this.cardSlider.currentCard = prevCurrentCard; 401 402 if (highlightApp) 403 this.appAdded(highlightApp, true); 404 405 logEvent('apps.layout: ' + (Date.now() - startTime)); 406 407 // Tell the slider about the pages and mark the current page. 408 this.updateSliderCards(); 409 this.cardSlider.currentCardValue.navigationDot.classList.add('selected'); 410 411 if (!this.appsLoaded_) { 412 this.appsLoaded_ = true; 413 cr.dispatchSimpleEvent(document, 'sectionready', true, true); 414 } 415 this.updateAppLauncherPromoHiddenState_(); 416 }, 417 418 /** 419 * Called by chrome when a new app has been added to chrome or has been 420 * enabled if previously disabled. 421 * @param {Object} appData A data structure full of relevant information for 422 * the app. 423 * @param {boolean=} opt_highlight Whether the app about to be added should 424 * be highlighted. 425 */ 426 appAdded: function(appData, opt_highlight) { 427 assert(loadTimeData.getBoolean('showApps')); 428 429 if (appData.id == this.highlightAppId) { 430 opt_highlight = true; 431 this.highlightAppId = null; 432 } 433 434 var pageIndex = appData.page_index || 0; 435 436 if (pageIndex >= this.appsPages.length) { 437 while (pageIndex >= this.appsPages.length) { 438 this.appendTilePage(new ntp.AppsPage(), 439 loadTimeData.getString('appDefaultPageName'), 440 true); 441 } 442 this.updateSliderCards(); 443 } 444 445 var page = this.appsPages[pageIndex]; 446 var app = $(appData.id); 447 if (app) { 448 app.replaceAppData(appData); 449 } else if (opt_highlight) { 450 page.insertAndHighlightApp(appData); 451 this.setShownPage_(loadTimeData.getInteger('apps_page_id'), 452 appData.page_index); 453 } else { 454 page.insertApp(appData, false); 455 } 456 }, 457 458 /** 459 * Callback invoked by chrome whenever an app preference changes. 460 * @param {Object} data An object with all the data on available 461 * applications. 462 */ 463 appsPrefChangedCallback: function(data) { 464 assert(loadTimeData.getBoolean('showApps')); 465 466 for (var i = 0; i < data.apps.length; ++i) { 467 $(data.apps[i].id).appData = data.apps[i]; 468 } 469 470 // Set the App dot names. Skip the first dot (Most Visited). 471 var dots = this.dotList.getElementsByClassName('dot'); 472 var start = this.mostVisitedPage ? 1 : 0; 473 for (var i = start; i < dots.length; ++i) { 474 dots[i].displayTitle = data.appPageNames[i - start] || ''; 475 } 476 }, 477 478 /** 479 * Callback invoked by chrome whenever the app launcher promo pref changes. 480 * @param {boolean} show Identifies if we should show or hide the promo. 481 */ 482 appLauncherPromoPrefChangeCallback: function(show) { 483 loadTimeData.overrideValues({showAppLauncherPromo: show}); 484 this.updateAppLauncherPromoHiddenState_(); 485 }, 486 487 /** 488 * Updates the hidden state of the app launcher promo based on the page 489 * shown and load data content. 490 */ 491 updateAppLauncherPromoHiddenState_: function() { 492 $('app-launcher-promo').hidden = 493 !loadTimeData.getBoolean('showAppLauncherPromo') || 494 this.shownPage != loadTimeData.getInteger('apps_page_id'); 495 }, 496 497 /** 498 * Invoked whenever the pages in apps-page-list have changed so that 499 * the Slider knows about the new elements. 500 */ 501 updateSliderCards: function() { 502 var pageNo = Math.max(0, Math.min(this.cardSlider.currentCard, 503 this.tilePages.length - 1)); 504 this.cardSlider.setCards(Array.prototype.slice.call(this.tilePages), 505 pageNo); 506 // The shownPage property was potentially saved from a previous webui that 507 // didn't have the same set of pages as the current one. So we cascade 508 // from suggestions, to most visited and then to apps because we can have 509 // an page with apps only (e.g., chrome://apps) or one with only the most 510 // visited, but not one with only suggestions. And we alwayd default to 511 // most visited first when previously shown page is not availabel anymore. 512 // If most visited isn't there either, we go to apps. 513 if (this.shownPage == loadTimeData.getInteger('suggestions_page_id')) { 514 if (this.suggestionsPage) 515 this.cardSlider.selectCardByValue(this.suggestionsPage); 516 else 517 this.shownPage = loadTimeData.getInteger('most_visited_page_id'); 518 } 519 if (this.shownPage == loadTimeData.getInteger('most_visited_page_id')) { 520 if (this.mostVisitedPage) 521 this.cardSlider.selectCardByValue(this.mostVisitedPage); 522 else 523 this.shownPage = loadTimeData.getInteger('apps_page_id'); 524 } 525 if (this.shownPage == loadTimeData.getInteger('apps_page_id') && 526 loadTimeData.getBoolean('showApps')) { 527 this.cardSlider.selectCardByValue( 528 this.appsPages[Math.min(this.shownPageIndex, 529 this.appsPages.length - 1)]); 530 } else if (this.mostVisitedPage) { 531 this.shownPage = loadTimeData.getInteger('most_visited_page_id'); 532 this.cardSlider.selectCardByValue(this.mostVisitedPage); 533 } 534 }, 535 536 /** 537 * Called whenever tiles should be re-arranging themselves out of the way 538 * of a moving or insert tile. 539 */ 540 enterRearrangeMode: function() { 541 if (loadTimeData.getBoolean('showApps')) { 542 var tempPage = new ntp.AppsPage(); 543 tempPage.classList.add('temporary'); 544 var pageName = loadTimeData.getString('appDefaultPageName'); 545 this.appendTilePage(tempPage, pageName, true); 546 } 547 548 if (ntp.getCurrentlyDraggingTile().firstChild.canBeRemoved()) { 549 $('footer').classList.add('showing-trash-mode'); 550 $('footer-menu-container').style.minWidth = $('trash').offsetWidth - 551 $('chrome-web-store-link').offsetWidth + 'px'; 552 } 553 554 document.documentElement.classList.add('dragging-mode'); 555 }, 556 557 /** 558 * Invoked whenever some app is released 559 */ 560 leaveRearrangeMode: function() { 561 var tempPage = document.querySelector('.tile-page.temporary'); 562 if (tempPage) { 563 var dot = tempPage.navigationDot; 564 if (!tempPage.tileCount && 565 tempPage != this.cardSlider.currentCardValue) { 566 this.removeTilePageAndDot_(tempPage, true); 567 } else { 568 tempPage.classList.remove('temporary'); 569 this.saveAppPageName(tempPage, 570 loadTimeData.getString('appDefaultPageName')); 571 } 572 } 573 574 $('footer').classList.remove('showing-trash-mode'); 575 $('footer-menu-container').style.minWidth = ''; 576 document.documentElement.classList.remove('dragging-mode'); 577 }, 578 579 /** 580 * Callback for the 'pagelayout' event. 581 * @param {Event} e The event. 582 */ 583 onPageLayout_: function(e) { 584 if (Array.prototype.indexOf.call(this.tilePages, e.currentTarget) != 585 this.cardSlider.currentCard) { 586 return; 587 } 588 589 this.updatePageSwitchers(); 590 }, 591 592 /** 593 * Adjusts the size and position of the page switchers according to the 594 * layout of the current card, and updates the aria-label attributes of 595 * the page switchers. 596 */ 597 updatePageSwitchers: function() { 598 if (!this.pageSwitcherStart || !this.pageSwitcherEnd) 599 return; 600 601 var page = this.cardSlider.currentCardValue; 602 603 this.pageSwitcherStart.hidden = !page || 604 (this.cardSlider.currentCard == 0); 605 this.pageSwitcherEnd.hidden = !page || 606 (this.cardSlider.currentCard == this.cardSlider.cardCount - 1); 607 608 if (!page) 609 return; 610 611 var pageSwitcherLeft = isRTL() ? this.pageSwitcherEnd : 612 this.pageSwitcherStart; 613 var pageSwitcherRight = isRTL() ? this.pageSwitcherStart : 614 this.pageSwitcherEnd; 615 var scrollbarWidth = page.scrollbarWidth; 616 pageSwitcherLeft.style.width = 617 (page.sideMargin + 13) + 'px'; 618 pageSwitcherLeft.style.left = '0'; 619 pageSwitcherRight.style.width = 620 (page.sideMargin - scrollbarWidth + 13) + 'px'; 621 pageSwitcherRight.style.right = scrollbarWidth + 'px'; 622 623 var offsetTop = page.querySelector('.tile-page-content').offsetTop + 'px'; 624 pageSwitcherLeft.style.top = offsetTop; 625 pageSwitcherRight.style.top = offsetTop; 626 pageSwitcherLeft.style.paddingBottom = offsetTop; 627 pageSwitcherRight.style.paddingBottom = offsetTop; 628 629 // Update the aria-label attributes of the two page switchers. 630 this.pageSwitcherStart.updateButtonAccessibleLabel(this.dotList.dots); 631 this.pageSwitcherEnd.updateButtonAccessibleLabel(this.dotList.dots); 632 }, 633 634 /** 635 * Returns the index of the given apps page. 636 * @param {AppsPage} page The AppsPage we wish to find. 637 * @return {number} The index of |page| or -1 if it is not in the 638 * collection. 639 */ 640 getAppsPageIndex: function(page) { 641 return Array.prototype.indexOf.call(this.appsPages, page); 642 }, 643 644 /** 645 * Handler for cardSlider:card_changed events from this.cardSlider. 646 * @param {Event} e The cardSlider:card_changed event. 647 * @private 648 */ 649 onCardChanged_: function(e) { 650 var page = e.cardSlider.currentCardValue; 651 652 // Don't change shownPage until startup is done (and page changes actually 653 // reflect user actions). 654 if (!this.isStartingUp_()) { 655 if (page.classList.contains('apps-page')) { 656 this.setShownPage_(loadTimeData.getInteger('apps_page_id'), 657 this.getAppsPageIndex(page)); 658 } else if (page.classList.contains('most-visited-page')) { 659 this.setShownPage_( 660 loadTimeData.getInteger('most_visited_page_id'), 0); 661 } else if (page.classList.contains('suggestions-page')) { 662 this.setShownPage_(loadTimeData.getInteger('suggestions_page_id'), 0); 663 } else { 664 console.error('unknown page selected'); 665 } 666 } 667 668 // Update the active dot 669 var curDot = this.dotList.getElementsByClassName('selected')[0]; 670 if (curDot) 671 curDot.classList.remove('selected'); 672 page.navigationDot.classList.add('selected'); 673 this.updatePageSwitchers(); 674 }, 675 676 /** 677 * Saves/updates the newly selected page to open when first loading the NTP. 678 * @type {number} shownPage The new shown page type. 679 * @type {number} shownPageIndex The new shown page index. 680 * @private 681 */ 682 setShownPage_: function(shownPage, shownPageIndex) { 683 assert(shownPageIndex >= 0); 684 this.shownPage = shownPage; 685 this.shownPageIndex = shownPageIndex; 686 chrome.send('pageSelected', [this.shownPage, this.shownPageIndex]); 687 this.updateAppLauncherPromoHiddenState_(); 688 }, 689 690 /** 691 * Listen for card additions to update the page switchers or the current 692 * card accordingly. 693 * @param {Event} e A card removed or added event. 694 */ 695 onCardAdded_: function(e) { 696 // When the second arg passed to insertBefore is falsey, it acts just like 697 // appendChild. 698 this.pageList.insertBefore(e.addedCard, this.tilePages[e.addedIndex]); 699 this.onCardAddedOrRemoved_(); 700 }, 701 702 /** 703 * Listen for card removals to update the page switchers or the current card 704 * accordingly. 705 * @param {Event} e A card removed or added event. 706 */ 707 onCardRemoved_: function(e) { 708 e.removedCard.parentNode.removeChild(e.removedCard); 709 this.onCardAddedOrRemoved_(); 710 }, 711 712 /** 713 * Called when a card is removed or added. 714 * @private 715 */ 716 onCardAddedOrRemoved_: function() { 717 if (this.isStartingUp_()) 718 return; 719 720 // Without repositioning there were issues - http://crbug.com/133457. 721 this.cardSlider.repositionFrame(); 722 this.updatePageSwitchers(); 723 }, 724 725 /** 726 * Save the name of an apps page. 727 * Store the apps page name into the preferences store. 728 * @param {AppsPage} appsPage The app page for which we wish to save. 729 * @param {string} name The name of the page. 730 */ 731 saveAppPageName: function(appPage, name) { 732 var index = this.getAppsPageIndex(appPage); 733 assert(index != -1); 734 chrome.send('saveAppPageName', [name, index]); 735 }, 736 737 /** 738 * Window resize handler. 739 * @private 740 */ 741 onWindowResize_: function(e) { 742 this.cardSlider.resize(this.sliderFrame.offsetWidth); 743 this.updatePageSwitchers(); 744 }, 745 746 /** 747 * Listener for offline status change events. Updates apps that are 748 * not offline-enabled to be grayscale if the browser is offline. 749 * @private 750 */ 751 updateOfflineEnabledApps_: function() { 752 var apps = document.querySelectorAll('.app'); 753 for (var i = 0; i < apps.length; ++i) { 754 if (apps[i].appData.enabled && !apps[i].appData.offline_enabled) { 755 apps[i].setIcon(); 756 apps[i].loadIcon(); 757 } 758 } 759 }, 760 761 /** 762 * Handler for key events on the page. Ctrl-Arrow will switch the visible 763 * page. 764 * @param {Event} e The KeyboardEvent. 765 * @private 766 */ 767 onDocKeyDown_: function(e) { 768 if (!e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) 769 return; 770 771 var direction = 0; 772 if (e.keyIdentifier == 'Left') 773 direction = -1; 774 else if (e.keyIdentifier == 'Right') 775 direction = 1; 776 else 777 return; 778 779 var cardIndex = 780 (this.cardSlider.currentCard + direction + 781 this.cardSlider.cardCount) % this.cardSlider.cardCount; 782 this.cardSlider.selectCard(cardIndex, true); 783 784 e.stopPropagation(); 785 }, 786 787 /** 788 * Returns the index of a given tile page. 789 * @param {TilePage} page The TilePage we wish to find. 790 * @return {number} The index of |page| or -1 if it is not in the 791 * collection. 792 */ 793 getTilePageIndex: function(page) { 794 return Array.prototype.indexOf.call(this.tilePages, page); 795 }, 796 797 /** 798 * Removes a page and navigation dot (if the navdot exists). 799 * @param {TilePage} page The page to be removed. 800 * @param {boolean=} opt_animate If the removal should be animated. 801 */ 802 removeTilePageAndDot_: function(page, opt_animate) { 803 if (page.navigationDot) 804 page.navigationDot.remove(opt_animate); 805 this.cardSlider.removeCard(page); 806 }, 807 }; 808 809 return { 810 PageListView: PageListView 811 }; 812}); 813