| Index: chrome/browser/resources/ntp4/page_list_view.js
|
| diff --git a/chrome/browser/resources/ntp4/page_list_view.js b/chrome/browser/resources/ntp4/page_list_view.js
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..7db35c0ba4dfa096f5d0e4480b76a0c36247543d
|
| --- /dev/null
|
| +++ b/chrome/browser/resources/ntp4/page_list_view.js
|
| @@ -0,0 +1,622 @@
|
| +// Copyright (c) 2011 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +/**
|
| + * @fileoverview PageListView implementation.
|
| + * PageListView manages page list, dot list, switcher buttons and handles apps
|
| + * pages callbacks from backend.
|
| + *
|
| + * Note that you need to have AppLauncherHandler in your WebUI to use this code.
|
| + */
|
| +
|
| +cr.define('ntp4', function() {
|
| + 'use strict';
|
| +
|
| + /**
|
| + * Object for accessing localized strings.
|
| + * @type {!LocalStrings}
|
| + */
|
| + var localStrings = new LocalStrings;
|
| +
|
| + /**
|
| + * Creates a PageListView object.
|
| + * @constructor
|
| + * @extends {Object}
|
| + */
|
| + function PageListView() {
|
| + }
|
| +
|
| + PageListView.prototype = {
|
| + /**
|
| + * The CardSlider object to use for changing app pages.
|
| + * @type {CardSlider|undefined}
|
| + */
|
| + cardSlider: undefined,
|
| +
|
| + /**
|
| + * The frame div for cardSlider.
|
| + * @type {!Element|undefined}
|
| + */
|
| + sliderFrame: undefined,
|
| +
|
| + /**
|
| + * The 'page-list' element.
|
| + * @type {!Element|undefined}
|
| + */
|
| + pageList: undefined,
|
| +
|
| + /**
|
| + * A list of all 'tile-page' elements.
|
| + * @type {!NodeList|undefined}
|
| + */
|
| + tilePages: undefined,
|
| +
|
| + /**
|
| + * A list of all 'apps-page' elements.
|
| + * @type {!NodeList|undefined}
|
| + */
|
| + appsPages: undefined,
|
| +
|
| + /**
|
| + * The Most Visited page.
|
| + * @type {!Element|undefined}
|
| + */
|
| + mostVisitedPage: undefined,
|
| +
|
| + /**
|
| + * The Bookmarks page.
|
| + * @type {!Element|undefined}
|
| + */
|
| + bookmarksPage: undefined,
|
| +
|
| + /**
|
| + * The 'dots-list' element.
|
| + * @type {!Element|undefined}
|
| + */
|
| + dotList: undefined,
|
| +
|
| + /**
|
| + * The left and right paging buttons.
|
| + * @type {!Element|undefined}
|
| + */
|
| + pageSwitcherStart: undefined,
|
| + pageSwitcherEnd: undefined,
|
| +
|
| + /**
|
| + * The 'trash' element. Note that technically this is unnecessary,
|
| + * JavaScript creates the object for us based on the id. But I don't want
|
| + * to rely on the ID being the same, and JSCompiler doesn't know about it.
|
| + * @type {!Element|undefined}
|
| + */
|
| + trash: undefined,
|
| +
|
| + /**
|
| + * The type of page that is currently shown. The value is a numerical ID.
|
| + * @type {number}
|
| + */
|
| + shownPage: 0,
|
| +
|
| + /**
|
| + * The index of the page that is currently shown, within the page type.
|
| + * For example if the third Apps page is showing, this will be 2.
|
| + * @type {number}
|
| + */
|
| + shownPageIndex: 0,
|
| +
|
| + /**
|
| + * EventTracker for managing event listeners for page events.
|
| + * @type {!EventTracker}
|
| + */
|
| + eventTracker: new EventTracker,
|
| +
|
| + /**
|
| + * If non-null, this is the ID of the app to highlight to the user the next
|
| + * time getAppsCallback runs. "Highlight" in this case means to switch to
|
| + * the page and run the new tile animation.
|
| + * @type {String}
|
| + */
|
| + highlightAppId: null,
|
| +
|
| + /**
|
| + * Initializes page list view.
|
| + * @param {!Element} pageList A DIV element to host all pages.
|
| + * @param {!Element} dotList An UL element to host nav dots. Each dot
|
| + * represents a page.
|
| + * @param {!Element} cardSliderFrame The card slider frame that hosts
|
| + * pageList and switcher buttons.
|
| + * @param {!Element|undefined} opt_trash Optional trash element.
|
| + * @param {!Element|undefined} opt_pageSwitcherStart Optional start page
|
| + * switcher button.
|
| + * @param {!Element|undefined} opt_pageSwitcherEnd Optional end page
|
| + * switcher button.
|
| + */
|
| + initialize: function(pageList, dotList, cardSliderFrame, opt_trash,
|
| + opt_pageSwitcherStart, opt_pageSwitcherEnd) {
|
| + this.pageList = pageList;
|
| +
|
| + this.dotList = dotList;
|
| + cr.ui.decorate(this.dotList, ntp4.DotList);
|
| +
|
| + this.trash = opt_trash;
|
| + if (this.trash)
|
| + new ntp4.Trash(this.trash);
|
| +
|
| + this.pageSwitcherStart = opt_pageSwitcherStart;
|
| + if (this.pageSwitcherStart)
|
| + ntp4.initializePageSwitcher(this.pageSwitcherStart);
|
| +
|
| + this.pageSwitcherEnd = opt_pageSwitcherEnd;
|
| + if (this.pageSwitcherEnd)
|
| + ntp4.initializePageSwitcher(this.pageSwitcherEnd);
|
| +
|
| + this.shownPage = templateData['shown_page_type'];
|
| + this.shownPageIndex = templateData['shown_page_index'];
|
| +
|
| + // Request data on the apps so we can fill them in.
|
| + // Note that this is kicked off asynchronously. 'getAppsCallback' will be
|
| + // invoked at some point after this function returns.
|
| + chrome.send('getApps');
|
| +
|
| + document.addEventListener('keydown', this.onDocKeyDown_.bind(this));
|
| + // Prevent touch events from triggering any sort of native scrolling
|
| + document.addEventListener('touchmove', function(e) {
|
| + e.preventDefault();
|
| + }, true);
|
| +
|
| + this.tilePages = this.pageList.getElementsByClassName('tile-page');
|
| + this.appsPages = this.pageList.getElementsByClassName('apps-page');
|
| +
|
| + // Initialize the cardSlider without any cards at the moment
|
| + this.sliderFrame = cardSliderFrame;
|
| + this.cardSlider = new cr.ui.CardSlider(this.sliderFrame, this.pageList,
|
| + this.sliderFrame.offsetWidth);
|
| + this.cardSlider.initialize();
|
| +
|
| + // Handle the page being changed
|
| + this.pageList.addEventListener(
|
| + cr.ui.CardSlider.EventType.CARD_CHANGED,
|
| + this.cardChangedHandler_.bind(this));
|
| +
|
| + // Ensure the slider is resized appropriately with the window
|
| + window.addEventListener('resize', this.onWindowResize_.bind(this));
|
| +
|
| + // Update apps when online state changes.
|
| + window.addEventListener('online',
|
| + this.updateOfflineEnabledApps_.bind(this));
|
| + window.addEventListener('offline',
|
| + this.updateOfflineEnabledApps_.bind(this));
|
| + },
|
| +
|
| + /**
|
| + * Appends a tile page (for bookmarks or most visited).
|
| + *
|
| + * @param {TilePage} page The page element.
|
| + * @param {string} title The title of the tile page.
|
| + * @param {bool} titleIsEditable If true, the title can be changed.
|
| + * @param {TilePage} opt_refNode Optional reference node to insert in front
|
| + * of.
|
| + * When opt_refNode is falsey, |page| will just be appended to the end of
|
| + * the page list.
|
| + */
|
| + appendTilePage: function(page, title, titleIsEditable, opt_refNode) {
|
| + // If no opt_refNode given, use bookmarksPage (if any).
|
| + if (!opt_refNode)
|
| + opt_refNode = this.bookmarksPage;
|
| +
|
| + // When opt_refNode is falsey, insertBefore acts just like appendChild.
|
| + this.pageList.insertBefore(page, opt_refNode);
|
| +
|
| + // Remember special MostVisitedPage and BookmarksPage.
|
| + if (typeof ntp4.MostVisitedPage != 'undefined' &&
|
| + page instanceof ntp4.MostVisitedPage) {
|
| + assert(this.tilePages.length == 1,
|
| + 'MostVisitedPage should be added as first tile page');
|
| + this.mostVisitedPage = page;
|
| + }
|
| + if (typeof ntp4.BookmarksPage != 'undefined' &&
|
| + page instanceof ntp4.BookmarksPage) {
|
| + this.bookmarksPage = page;
|
| + }
|
| +
|
| + // If we're appending an AppsPage and it's a temporary page, animate it.
|
| + var animate = page instanceof ntp4.AppsPage &&
|
| + page.classList.contains('temporary');
|
| + // Make a deep copy of the dot template to add a new one.
|
| + var newDot = new ntp4.NavDot(page, title, titleIsEditable, animate);
|
| + page.navigationDot = newDot;
|
| + this.dotList.insertBefore(newDot, opt_refNode ? opt_refNode.navigationDot
|
| + : null);
|
| + // Set a tab index on the first dot.
|
| + if (this.dotList.dots.length == 1)
|
| + newDot.tabIndex = 3;
|
| +
|
| + this.eventTracker.add(page, 'pagelayout', this.onPageLayout_.bind(this));
|
| + },
|
| +
|
| + /**
|
| + * Called by chrome when an existing app has been disabled or
|
| + * removed/uninstalled from chrome.
|
| + * @param {Object} appData A data structure full of relevant information for
|
| + * the app.
|
| + * @param {boolean} isUninstall True if the app is being uninstalled;
|
| + * false if the app is being disabled.
|
| + */
|
| + appRemoved: function(appData, isUninstall) {
|
| + var app = $(appData.id);
|
| + assert(app, 'trying to remove an app that doesn\'t exist');
|
| +
|
| + if (!isUninstall)
|
| + app.replaceAppData(appData);
|
| + else
|
| + app.remove();
|
| + },
|
| +
|
| + /**
|
| + * Callback invoked by chrome with the apps available.
|
| + *
|
| + * Note that calls to this function can occur at any time, not just in
|
| + * response to a getApps request. For example, when a user
|
| + * installs/uninstalls an app on another synchronized devices.
|
| + * @param {Object} data An object with all the data on available
|
| + * applications.
|
| + */
|
| + getAppsCallback: function(data) {
|
| + var startTime = Date.now();
|
| +
|
| + // Clear any existing apps pages and dots.
|
| + // TODO(rbyers): It might be nice to preserve animation of dots after an
|
| + // uninstall. Could we re-use the existing page and dot elements? It
|
| + // seems unfortunate to have Chrome send us the entire apps list after an
|
| + // uninstall.
|
| + while (this.appsPages.length > 0) {
|
| + var page = this.appsPages[0];
|
| + var dot = page.navigationDot;
|
| +
|
| + this.eventTracker.remove(page);
|
| + page.tearDown();
|
| + page.parentNode.removeChild(page);
|
| + dot.parentNode.removeChild(dot);
|
| + }
|
| +
|
| + // Get the array of apps and add any special synthesized entries
|
| + var apps = data.apps;
|
| +
|
| + // Get a list of page names
|
| + var pageNames = data.appPageNames;
|
| +
|
| + function stringListIsEmpty(list) {
|
| + for (var i = 0; i < list.length; i++) {
|
| + if (list[i])
|
| + return false;
|
| + }
|
| + return true;
|
| + }
|
| +
|
| + // Sort by launch index
|
| + apps.sort(function(a, b) {
|
| + return a.app_launch_index - b.app_launch_index;
|
| + });
|
| +
|
| + // An app to animate (in case it was just installed).
|
| + var highlightApp;
|
| +
|
| + // Add the apps, creating pages as necessary
|
| + for (var i = 0; i < apps.length; i++) {
|
| + var app = apps[i];
|
| + var pageIndex = app.page_index || 0;
|
| + while (pageIndex >= this.appsPages.length) {
|
| + var pageName = localStrings.getString('appDefaultPageName');
|
| + if (this.appsPages.length < pageNames.length)
|
| + pageName = pageNames[this.appsPages.length];
|
| +
|
| + var origPageCount = this.appsPages.length;
|
| + this.appendTilePage(new ntp4.AppsPage(), pageName, true);
|
| + // Confirm that appsPages is a live object, updated when a new page is
|
| + // added (otherwise we'd have an infinite loop)
|
| + assert(this.appsPages.length == origPageCount + 1,
|
| + 'expected new page');
|
| + }
|
| +
|
| + if (app.id == this.highlightAppId)
|
| + highlightApp = app;
|
| + else
|
| + this.appsPages[pageIndex].appendApp(app);
|
| + }
|
| +
|
| + ntp4.AppsPage.setPromo(data.showPromo ? data : null);
|
| +
|
| + // Tell the slider about the pages.
|
| + this.updateSliderCards();
|
| +
|
| + if (highlightApp)
|
| + this.appAdded(highlightApp, true);
|
| +
|
| + // Mark the current page.
|
| + this.cardSlider.currentCardValue.navigationDot.classList.add('selected');
|
| + logEvent('apps.layout: ' + (Date.now() - startTime));
|
| +
|
| + document.documentElement.classList.remove('starting-up');
|
| + },
|
| +
|
| + /**
|
| + * Called by chrome when a new app has been added to chrome or has been
|
| + * enabled if previously disabled.
|
| + * @param {Object} appData A data structure full of relevant information for
|
| + * the app.
|
| + */
|
| + appAdded: function(appData, opt_highlight) {
|
| + if (appData.id == this.highlightAppId) {
|
| + opt_highlight = true;
|
| + this.highlightAppId = null;
|
| + }
|
| +
|
| + var pageIndex = appData.page_index || 0;
|
| +
|
| + if (pageIndex >= this.appsPages.length) {
|
| + while (pageIndex >= this.appsPages.length) {
|
| + this.appendTilePage(new ntp4.AppsPage(),
|
| + localStrings.getString('appDefaultPageName'),
|
| + true);
|
| + }
|
| + this.updateSliderCards();
|
| + }
|
| +
|
| + var page = this.appsPages[pageIndex];
|
| + var app = $(appData.id);
|
| + if (app)
|
| + app.replaceAppData(appData);
|
| + else
|
| + page.appendApp(appData, opt_highlight);
|
| + },
|
| +
|
| + /**
|
| + * Callback invoked by chrome whenever an app preference changes.
|
| + * @param {Object} data An object with all the data on available
|
| + * applications.
|
| + */
|
| + appsPrefChangedCallback: function(data) {
|
| + for (var i = 0; i < data.apps.length; ++i) {
|
| + $(data.apps[i].id).appData = data.apps[i];
|
| + }
|
| +
|
| + // Set the App dot names. Skip the first and last dots (Most Visited and
|
| + // Bookmarks).
|
| + var dots = this.dotList.getElementsByClassName('dot');
|
| + // TODO(csilv): Remove this calcluation if/when we remove the flag for
|
| + // for the bookmarks page.
|
| + var start = this.mostVisitedPage ? 1 : 0;
|
| + var length = this.bookmarksPage ? dots.length - 1 : dots.length;
|
| + for (var i = start; i < length; ++i) {
|
| + dots[i].displayTitle = data.appPageNames[i - start] || '';
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Invoked whenever the pages in apps-page-list have changed so that
|
| + * the Slider knows about the new elements.
|
| + */
|
| + updateSliderCards: function() {
|
| + var pageNo = Math.min(this.cardSlider.currentCard,
|
| + this.tilePages.length - 1);
|
| + this.cardSlider.setCards(Array.prototype.slice.call(this.tilePages),
|
| + pageNo);
|
| + switch (this.shownPage) {
|
| + case templateData['apps_page_id']:
|
| + this.cardSlider.selectCardByValue(
|
| + this.appsPages[Math.min(this.shownPageIndex,
|
| + this.appsPages.length - 1)]);
|
| + break;
|
| + case templateData['bookmarks_page_id']:
|
| + if (this.bookmarksPage)
|
| + this.cardSlider.selectCardByValue(this.bookmarksPage);
|
| + break;
|
| + case templateData['most_visited_page_id']:
|
| + if (this.mostVisitedPage)
|
| + this.cardSlider.selectCardByValue(this.mostVisitedPage);
|
| + break;
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Called whenever tiles should be re-arranging themselves out of the way
|
| + * of a moving or insert tile.
|
| + */
|
| + enterRearrangeMode: function() {
|
| + var tempPage = new ntp4.AppsPage();
|
| + tempPage.classList.add('temporary');
|
| + this.appendTilePage(tempPage,
|
| + localStrings.getString('appDefaultPageName'),
|
| + true);
|
| + var tempIndex = Array.prototype.indexOf.call(this.tilePages, tempPage);
|
| + if (this.cardSlider.currentCard >= tempIndex)
|
| + this.cardSlider.currentCard += 1;
|
| + this.updateSliderCards();
|
| +
|
| + if (ntp4.getCurrentlyDraggingTile().firstChild.canBeRemoved())
|
| + $('footer').classList.add('showing-trash-mode');
|
| + },
|
| +
|
| + /**
|
| + * Invoked whenever some app is released
|
| + * @param {cr.ui.Grabber.Event} e The Grabber RELEASE event.
|
| + */
|
| + leaveRearrangeMode: function(e) {
|
| + var tempPage = document.querySelector('.tile-page.temporary');
|
| + var dot = tempPage.navigationDot;
|
| + if (!tempPage.tileCount && tempPage != this.cardSlider.currentCardValue) {
|
| + dot.animateRemove();
|
| + var tempIndex = Array.prototype.indexOf.call(this.tilePages, tempPage);
|
| + if (this.cardSlider.currentCard > tempIndex)
|
| + this.cardSlider.currentCard -= 1;
|
| + tempPage.parentNode.removeChild(tempPage);
|
| + this.updateSliderCards();
|
| + } else {
|
| + tempPage.classList.remove('temporary');
|
| + this.saveAppPageName(tempPage,
|
| + localStrings.getString('appDefaultPageName'));
|
| + }
|
| +
|
| + $('footer').classList.remove('showing-trash-mode');
|
| + },
|
| +
|
| + /**
|
| + * Callback for the 'pagelayout' event.
|
| + * @param {Event} e The event.
|
| + */
|
| + onPageLayout_: function(e) {
|
| + if (Array.prototype.indexOf.call(this.tilePages, e.currentTarget) !=
|
| + this.cardSlider.currentCard) {
|
| + return;
|
| + }
|
| +
|
| + this.updatePageSwitchers();
|
| + },
|
| +
|
| + /**
|
| + * Adjusts the size and position of the page switchers according to the
|
| + * layout of the current card.
|
| + */
|
| + updatePageSwitchers: function() {
|
| + if (!this.pageSwitcherStart || !this.pageSwitcherEnd)
|
| + return;
|
| +
|
| + var page = this.cardSlider.currentCardValue;
|
| +
|
| + this.pageSwitcherStart.hidden = !page ||
|
| + (this.cardSlider.currentCard == 0);
|
| + this.pageSwitcherEnd.hidden = !page ||
|
| + (this.cardSlider.currentCard == this.cardSlider.cardCount - 1);
|
| +
|
| + if (!page)
|
| + return;
|
| +
|
| + var pageSwitcherLeft = isRTL() ? this.pageSwitcherEnd
|
| + : this.pageSwitcherStart;
|
| + var pageSwitcherRight = isRTL() ? this.pageSwitcherStart
|
| + : this.pageSwitcherEnd;
|
| + var scrollbarWidth = page.scrollbarWidth;
|
| + pageSwitcherLeft.style.width =
|
| + (page.sideMargin + 13) + 'px';
|
| + pageSwitcherLeft.style.left = '0';
|
| + pageSwitcherRight.style.width =
|
| + (page.sideMargin - scrollbarWidth + 13) + 'px';
|
| + pageSwitcherRight.style.right = scrollbarWidth + 'px';
|
| +
|
| + var offsetTop = page.querySelector('.tile-page-content').offsetTop + 'px';
|
| + pageSwitcherLeft.style.top = offsetTop;
|
| + pageSwitcherRight.style.top = offsetTop;
|
| + pageSwitcherLeft.style.paddingBottom = offsetTop;
|
| + pageSwitcherRight.style.paddingBottom = offsetTop;
|
| + },
|
| +
|
| + /**
|
| + * Returns the index of the given page.
|
| + * @param {AppsPage} page The AppsPage for we wish to find.
|
| + * @return {number} The index of |page|, or -1 if it is not here.
|
| + */
|
| + getAppsPageIndex: function(page) {
|
| + return Array.prototype.indexOf.call(this.appsPages, page);
|
| + },
|
| +
|
| + /**
|
| + * Handler for CARD_CHANGED on cardSlider.
|
| + * @param {Event} e The CARD_CHANGED event.
|
| + * @private
|
| + */
|
| + cardChangedHandler_: function(e) {
|
| + var page = e.cardSlider.currentCardValue;
|
| +
|
| + // Don't change shownPage until startup is done (and page changes actually
|
| + // reflect user actions).
|
| + if (!document.documentElement.classList.contains('starting-up')) {
|
| + if (page.classList.contains('apps-page')) {
|
| + this.shownPage = templateData['apps_page_id'];
|
| + this.shownPageIndex = this.getAppsPageIndex(page);
|
| + } else if (page.classList.contains('most-visited-page')) {
|
| + this.shownPage = templateData['most_visited_page_id'];
|
| + this.shownPageIndex = 0;
|
| + } else if (page.classList.contains('bookmarks-page')) {
|
| + this.shownPage = templateData['bookmarks_page_id'];
|
| + this.shownPageIndex = 0;
|
| + } else {
|
| + console.error('unknown page selected');
|
| + }
|
| + chrome.send('pageSelected', [this.shownPage, this.shownPageIndex]);
|
| + }
|
| +
|
| + // Update the active dot
|
| + var curDot = this.dotList.getElementsByClassName('selected')[0];
|
| + if (curDot)
|
| + curDot.classList.remove('selected');
|
| + page.navigationDot.classList.add('selected');
|
| + this.updatePageSwitchers();
|
| + },
|
| +
|
| + /*
|
| + * Save the name of an app page.
|
| + * Store the app page name into the preferences store.
|
| + * @param {AppsPage} appPage The app page for which we wish to save.
|
| + * @param {string} name The name of the page.
|
| + */
|
| + saveAppPageName: function(appPage, name) {
|
| + var index = this.getAppsPageIndex(appPage);
|
| + assert(index != -1);
|
| + chrome.send('saveAppPageName', [name, index]);
|
| + },
|
| +
|
| + /**
|
| + * Window resize handler.
|
| + * @private
|
| + */
|
| + onWindowResize_: function(e) {
|
| + this.cardSlider.resize(this.sliderFrame.offsetWidth);
|
| + this.updatePageSwitchers();
|
| + },
|
| +
|
| + /**
|
| + * Listener for offline status change events. Updates apps that are
|
| + * not offline-enabled to be grayscale if the browser is offline.
|
| + * @private
|
| + */
|
| + updateOfflineEnabledApps_: function() {
|
| + var apps = document.querySelectorAll('.app');
|
| + for (var i = 0; i < apps.length; ++i) {
|
| + if (apps[i].appData.enabled && !apps[i].appData.offline_enabled) {
|
| + apps[i].setIcon();
|
| + apps[i].loadIcon();
|
| + }
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Handler for key events on the page. Ctrl-Arrow will switch the visible
|
| + * page.
|
| + * @param {Event} e The KeyboardEvent.
|
| + * @private
|
| + */
|
| + onDocKeyDown_: function(e) {
|
| + if (!e.ctrlKey || e.altKey || e.metaKey || e.shiftKey)
|
| + return;
|
| +
|
| + var direction = 0;
|
| + if (e.keyIdentifier == 'Left')
|
| + direction = -1;
|
| + else if (e.keyIdentifier == 'Right')
|
| + direction = 1;
|
| + else
|
| + return;
|
| +
|
| + var cardIndex =
|
| + (this.cardSlider.currentCard + direction +
|
| + this.cardSlider.cardCount) % this.cardSlider.cardCount;
|
| + this.cardSlider.selectCard(cardIndex, true);
|
| +
|
| + e.stopPropagation();
|
| + }
|
| + };
|
| +
|
| + return {
|
| + PageListView: PageListView
|
| + };
|
| +});
|
|
|