Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(3809)

Unified Diff: chrome/browser/resources/ntp4/page_list_view.js

Issue 8637001: [NTP4] Auto-deletion of empty apps panes. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: removing bits for refactor Created 8 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « chrome/browser/resources/ntp4/new_tab.js ('k') | chrome/browser/resources/ntp4/tile_page.js » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
index f6da56bd1f1ac8747143513b259219f72dc04fe6..c36ff8b83ab0b50d273d806258b03c93795574ee 100644
--- a/chrome/browser/resources/ntp4/page_list_view.js
+++ b/chrome/browser/resources/ntp4/page_list_view.js
@@ -172,6 +172,26 @@ cr.define('ntp4', function() {
cr.ui.CardSlider.EventType.CARD_CHANGED,
this.cardChangedHandler_.bind(this));
+ // Handle the end of cards being changed (useful if animated).
+ this.pageList.addEventListener(
+ cr.ui.CardSlider.EventType.CARD_CHANGE_ENDED,
+ this.cardChangeEndedHandler_.bind(this));
+
+ // Handle cards being added to the card slider.
+ this.pageList.addEventListener(
+ cr.ui.CardSlider.EventType.CARD_ADDED,
+ this.cardAddedHandler_.bind(this));
+
+ // Handle cards being removed from the card slider.
+ this.pageList.addEventListener(
+ cr.ui.CardSlider.EventType.CARD_REMOVED,
+ this.cardRemovedHandler_.bind(this));
+
+ // Handle tiles being removed from tile pages.
+ this.pageList.addEventListener(
+ ntp4.TilePage.EventType.TILE_REMOVED,
+ this.tileRemovedHandler_.bind(this));
+
// Ensure the slider is resized appropriately with the window
window.addEventListener('resize', this.onWindowResize_.bind(this));
@@ -194,8 +214,12 @@ cr.define('ntp4', function() {
* the page list.
*/
appendTilePage: function(page, title, titleIsEditable, opt_refNode) {
- // When opt_refNode is falsey, insertBefore acts just like appendChild.
- this.pageList.insertBefore(page, opt_refNode);
+ if (opt_refNode) {
+ var refIndex = this.getTilePageIndex(opt_refNode);
+ this.cardSlider.insertCardAtIndex(page, refIndex);
+ } else {
+ this.cardSlider.appendCard(page);
+ }
// Remember special MostVisitedPage.
if (typeof ntp4.MostVisitedPage != 'undefined' &&
@@ -227,15 +251,101 @@ cr.define('ntp4', function() {
* the app.
* @param {boolean} isUninstall True if the app is being uninstalled;
* false if the app is being disabled.
+ * @param {boolean} fromPage If the removal was from the current page.
*/
- appRemoved: function(appData, isUninstall) {
+ appRemoved: function(appData, isUninstall, fromPage) {
var app = $(appData.id);
assert(app, 'trying to remove an app that doesn\'t exist');
- if (!isUninstall)
+ if (!isUninstall) {
app.replaceAppData(appData);
- else
- app.remove();
+ } else {
+ // If the uninstall was from this page, run the blipout animation and
+ // the tile will be removed in TilePage#onContentsAnimationEnd_.
+ // Otherwise delete the tile without auto-deleting the page to avoid
+ // re-deleting the same page (or the page that slid in to take its
+ // place).
+ if (fromPage) {
+ // Unset the ID immediately, because the app is already gone. But
+ // leave the tile on the page as it animates out.
+ app.id = '';
+ app.classList.add('removing-tile-contents');
+ } else {
+ var tilePage = app.tile.tilePage;
+ tilePage.removeTile(app.tile, false, true);
+ this.removeAppsPageIfEmpty_(tilePage, false, true);
+ }
+ }
+ },
+
+ /**
+ * @return {boolean} If the page is still starting up.
+ * @private
+ */
+ isStartingUp_: function() {
+ return document.documentElement.classList.contains('starting-up');
+ },
+
+ /**
+ * Returns a hashmap of apps pages keyed by page ordinal.
+ * @return {Object.<string, AppsPage>} Map of apps pages.
+ * @private
+ */
+ getAppsPageOrdinalMap_: function() {
+ var map = {};
+ for (var i = 0; i < this.appsPages.length; ++i)
+ map[this.appsPages[i].ordinal] = this.appsPages[i];
+ return map;
+ },
+
+ /**
+ * Get an unsorted list of apps page ordinals.
+ */
+ getAppsPageOrdinals_: function() {
+ return Array.prototype.map.call(this.appsPages, function(page) {
+ return page.ordinal;
+ });
+ },
+
+ /**
+ * Gets an apps page by string ordinal.
+ * @param {string} ordinal The page ordinal we're searching for.
+ * @return {?AppsPage} The apps page with corresponding ordinal or null.
+ * @private
+ */
+ getAppsPageByOrdinal_: function(ordinal) {
+ assert(typeof ordinal == 'string' && ordinal);
+ return this.getAppsPageOrdinalMap_()[ordinal] || null;
+ },
+
+ /**
+ * Find an apps page index via ordinal.
+ * @return {number} The index of the ordinal or -1 if it doesn't exist.
+ */
+ getPageIndexFromOrdinal_: function(ordinal) {
+ return this.getAppsPageOrdinals_().indexOf(ordinal);
+ },
+
+ /**
+ * Finds a position for a specific ordinal in an ordinal map.
+ * @param {Array} ordinalList An array of ordinals.
+ * @param {string} ordinal A specific ordinal to search for.
+ * @return {number} The position where the ordinal should be positioned.
+ */
+ getPositionForNewOrdinal_: function(ordinalList, ordinal) {
+ assert(Array.isArray(ordinal) && ordinalList.indexOf(ordinal) == -1);
+ var sorted = Array.prototype.concat.call(ordinalList).sort();
+ function split(num) {
+ return Math.floor(num << 1);
+ }
+ var size = split(sorted.length);
+ var pos = size;
+ while (size > 0) {
+ size = split(size);
+ // This just no-ops when size = 0.
+ pos = ordinal > sorted[pos] ? pos + size : pos - size;
+ }
+ return pos;
},
/**
@@ -247,132 +357,74 @@ cr.define('ntp4', function() {
* @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 ordinal
- apps.sort(function(a, b) {
- return a.app_launch_ordinal > b.app_launch_ordinal ? 1 :
- a.app_launch_ordinal < b.app_launch_ordinal ? -1 : 0;
- });
-
- // 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);
- }
+ getAppsCallback: function(data, measureTime) {
+ var startTime;
+ if (measureTime)
+ startTime = Date.now();
+ this.syncAppsPages(data);
ntp4.AppsPage.setPromo(data.showPromo ? data : null);
- // Tell the slider about the pages.
+ // Tell the slider/switchers about the pages.
this.updateSliderCards();
+ this.updatePageSwitchers();
if (highlightApp)
this.appAdded(highlightApp, true);
// Mark the current page.
this.cardSlider.currentCardValue.navigationDot.classList.add('selected');
- logEvent('apps.layout: ' + (Date.now() - startTime));
+
+ if (measureTime)
+ 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.
+ * @param {Object<>}
*/
- 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);
+ syncAppsPages: function() {
+ // Remove pages that aren't in the data we were given.
+ var existingPages = this.getAppsPageOrdinalMap_();
+ for (var i in existingPages) {
+ if (existingPages.hasOwnProperty(i) &&
+ !(existingPages[i] in data.appsPages)) {
+ this.removeTilePageAndDot_(existingPages[i]);
+ delete existingPages[i];
}
- 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 dot (Most Visited).
- var dots = this.dotList.getElementsByClassName('dot');
- var start = this.mostVisitedPage ? 1 : 0;
- for (var i = start; i < dots.length; ++i) {
- dots[i].displayTitle = data.appPageNames[i - start] || '';
+ // Create or re-use pages or apps and ensure we're synced.
+ var pageOrdinals = Object.keys(data.appsPages).sort();
+ for (var i = 0; i < pageOrdinals.length; ++i) {
+ // Re-use previous pages or create new pages as necessary.
+ var appsPage = this.getOrCreateAppsPageAtOrdinal_(pageOrdinals[i]);
+ existingPages[pageOrdinals[i]] = appsPage;
+ var page = data.appsPages[pageOrdinals[i]];
+ appsPage.navigationDot.displayTitle = page.name;
+ var appOrdinals = Object.keys(page.apps).sort();
+ for (var j = 0; j < appOrdinals.length; ++j) {
+ var appData = page.apps[appOrdinals[j]];
+ var app = $(appData.id);
+ if (app) {
+ if (appData.page_ordinal != app.data.page_ordinal ||
+ appData.app_launch_ordinal != app.data.app_launch_ordinal) {
+ var originalPage = app.tile.tilePage;
+ var detached = originalPage.removeTile(app);
+ var index = this.getIndexForNewOrdinal_(
+ appOrdinals, appData.app_launch_ordinal);
+ existingPages[appData.page_ordinal].addTileAt(detached, index);
+ } else {
+ app.replaceAppData(appData);
+ }
+ } else {
+ var index = this.getIndexForNewOrdinal_(app.app_launch_ordinal);
+ appsPage.addTileAt(app, app.id == data.highlightId);
+ }
+ }
}
+ for (var i = 0; i < this.appsPages.length; ++i)
+ this.appsPages[i].cleanupDrag();
},
/**
@@ -380,21 +432,30 @@ cr.define('ntp4', function() {
* 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);
+ var index = -1;
+ var tiles = Array.prototype.slice.call(this.tilePages);
+ // Clamping this value each time helps self-heal unexpected input.
+ this.shownPageIndex = Math.max(0, Math.min(this.shownPageIndex,
+ this.appsPages.length - 1));
switch (this.shownPage) {
- case templateData['apps_page_id']:
- this.cardSlider.selectCardByValue(
- this.appsPages[Math.min(this.shownPageIndex,
- this.appsPages.length - 1)]);
+ case templateData.apps_page_id:
+ index = tiles.indexOf(this.appsPages[this.shownPageIndex]);
break;
- case templateData['most_visited_page_id']:
- if (this.mostVisitedPage)
- this.cardSlider.selectCardByValue(this.mostVisitedPage);
+ case templateData.most_visited_page_id:
+ index = tiles.indexOf(this.mostVisitedPage);
break;
}
+ // If shownPage was saved as a page that's now disabled or the shownPage
+ // doesn't exist any more, index will be -1. Change to the preferred
+ // default page (first apps pane) in this case.
+ if (index < 0) {
+ this.shownPage = templateData.apps_page_id;
+ index = tiles.indexOf(this.appsPages[0]);
+ }
+ // Set the new cards and index.
+ this.cardSlider.setCards(tiles, index);
+
+ chrome.send('pageSelected', [this.shownPage, this.shownPageIndex]);
},
/**
@@ -404,13 +465,8 @@ cr.define('ntp4', function() {
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();
+ var pageName = localStrings.getString('appDefaultPageName');
+ this.appendTilePage(tempPage, pageName, true);
if (ntp4.getCurrentlyDraggingTile().firstChild.canBeRemoved())
$('footer').classList.add('showing-trash-mode');
@@ -421,18 +477,14 @@ cr.define('ntp4', function() {
*/
leaveRearrangeMode: function() {
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 {
+ // Either remove a temp page if it's empty or save the page name (as an
+ // app has just been dropped on it or created somehow).
+ // TODO(dbeam): Animated removal if currently on temp page.
+ if (tempPage && !this.removeAppsPageIfEmpty_(tempPage, true, true)) {
+ this.saveAppsPageName(tempPage,
+ tempPage.navigationDot.displayTitle,
+ true);
tempPage.classList.remove('temporary');
- this.saveAppPageName(tempPage,
- localStrings.getString('appDefaultPageName'));
}
$('footer').classList.remove('showing-trash-mode');
@@ -507,7 +559,7 @@ cr.define('ntp4', function() {
// Don't change shownPage until startup is done (and page changes actually
// reflect user actions).
- if (!document.documentElement.classList.contains('starting-up')) {
+ if (!this.isStartingUp_()) {
if (page.classList.contains('apps-page')) {
this.shownPage = templateData['apps_page_id'];
this.shownPageIndex = this.getAppsPageIndex(page);
@@ -528,16 +580,129 @@ cr.define('ntp4', function() {
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.
+ /**
+ * Listen for card additions to update the page switchers or the current
+ * card accordingly.
+ * @param {Event} e A card removed or added event.
+ */
+ cardAddedHandler_: function(e) {
+ // When the second arg passed to insertBefore is falsey, it acts just like
+ // appendChild.
+ this.pageList.insertBefore(e.addedCard, this.tilePages[e.addedIndex]);
+ if (!this.isStartingUp_())
+ this.updatePageSwitchers();
+ },
+
+ /**
+ * Listen for card removals to update the page switchers or the current card
+ * accordingly.
+ * @param {Event} e A card removed or added event.
+ */
+ cardRemovedHandler_: function(e) {
+ if (!this.isStartingUp_())
+ this.updatePageSwitchers();
+ assert(!e.removedCard.classList.contains('selected-card'));
+ e.removedCard.parentNode.removeChild(e.removedCard);
+ },
+
+ /**
+ * Save the name of an apps page.
+ * Store the apps page name into the preferences store.
+ * @param {AppsPage} appsPage The app page for which we wish to save.
* @param {string} name The name of the page.
+ * @param {boolean} notify If we should notify of when saving the pref.
*/
- saveAppPageName: function(appPage, name) {
- var index = this.getAppsPageIndex(appPage);
+ saveAppsPageName: function(appsPage, name, notify) {
+ var index = this.getAppsPageIndex(appsPage);
assert(index != -1);
- chrome.send('saveAppPageName', [name, index]);
+ chrome.send('saveAppsPageName', [name, index, notify]);
+ },
+
+ /**
+ * An Array of callbacks to be called on the next CARD_CHANGE_ENDED event
+ * handled from the cardSlider.
+ * @private
+ */
+ cardChangeEndedCallbacks_: [],
+
+ /**
+ * Handler for CARD_CHANGE_ENDED on cardSlider.
+ * @param {Event} e The CARD_CHANGE_ENDED event.
+ * @private
+ */
+ cardChangeEndedHandler_: function(e) {
+ if (!this.isStartingUp_()) {
+ for (var i = 0; i < this.cardChangeEndedCallbacks_.length; ++i) {
+ if (this.cardChangeEndedCallbacks_[i].call(this, e) !== false)
+ this.cardChangeEndedCallbacks_.splice(i--, 1);
+ }
+ }
+ },
+
+ /**
+ * Happens when a tile is removed from a tile page.
+ * @param {Event} e An event dispatched from a tile when it is removed.
+ */
+ tileRemovedHandler_: function(e) {
+ if (e.tilePage instanceof ntp4.AppsPage)
+ this.removeAppsPageIfEmpty_(e.tilePage, e.wasAnimated);
+ },
+
+ /**
+ * Remove an apps page if it now has no tiles (is empty).
+ * @param {AppsPage} appsPage A page to check for emptiness.
+ * @param {boolean=} opt_animate Whether the prospective removal should be
+ * animated.
+ * @param {boolean=} opt_dontNotify Whether this NTP's AppLauncherHandler
+ * shouldn't be notified of this pages' prospective removal (default is
+ * to notify).
+ * @return {boolean} If |appsPage| was removed or not.
+ */
+ removeAppsPageIfEmpty_: function(appsPage, opt_animate, opt_dontNotify) {
+ assert(appsPage instanceof ntp4.AppsPage,
+ '|appsPage| is not really an AppsPage');
+
+ if (appsPage.tileCount !== 0)
+ return false;
+
+ // If the user is currently on the empty apps page, avoid visual issues
+ // by selecting a different page before deleting. If the user isn't on
+ // the empty/deleting page, just delete it right away.
+ var whenOnCorrectPage = function() {
+ if (!opt_dontNotify)
+ chrome.send('deleteEmptyAppsPage', [appsPage.ordinal]);
+ this.removeTilePageAndDot_(appsPage, opt_animate);
+ };
+
+ if (appsPage == this.cardSlider.currentCardValue) {
+ var index = this.getAppsPageIndex(appsPage);
+ assert(index != -1);
+ var tempPage = document.querySelector('.apps-page.temporary');
+ var tempOffset = tempPage.tileCount == 0 ? 1 : 0;
+ // If the apps page being deleted is the last apps page (excluding soon
+ // to be deleted temp pages), move backward one in the card slider.
+ // Otherwise, move forward one.
+ var change = index == this.appsPages.length - 1 - tempOffset ? -1 : 1;
+ var newIndex = this.cardSlider.currentCard + change;
+ this.cardSlider.selectCard(newIndex, opt_animate);
+ // In the case where |opt_animate| is truthy, the card selection is
+ // animated and asynchronous, so we simply append this callback to
+ // this.cardChangeEndedCallbacks_ to be done on the next animated
+ // CARD_CHANGE_ENDED event that switches to the proposed index.
+ if (opt_animate) {
+ this.cardChangeEndedCallbacks_.push(function(e) {
+ if (!e.wasAnimated || e.changedTo != newIndex)
+ return false;
+ whenOnCorrectPage.call(this);
+ });
+ } else {
+ whenOnCorrectPage.call(this);
+ }
+ } else {
+ whenOnCorrectPage.call(this);
+ }
+
+ return true;
},
/**
@@ -588,7 +753,65 @@ cr.define('ntp4', function() {
this.cardSlider.selectCard(cardIndex, true);
e.stopPropagation();
- }
+ },
+
+ /**
+ * Re-order apps on this inactive page when an active NTP gets re-ordered.
+ * @param {Object} data Position hashmap ordered by app id with indices:
+ * {id => {page_index: ##, app_launch_index: ##}}
+ */
+ appsReordered: function(data) {
+ },
+
+ /**
+ * Ensure there is an apps page at the given |ordinal| by checking for an
+ * existing page or creating a new once.
+ * @param {string} ordinal A string ordinal for which to check.
+ * @private
+ */
+ getOrCreateAppsPageAtOrdinal_: function(ordinal) {
+ var map = this.getAppsPageOrdinalMap_();
+ if (ordinal in map)
+ return map[ordinal];
+
+ var appsPage = new ntp4.AppsPage(ordinal);
+
+ var index = this.getIndexForNewOrdinal_(Object.keys(map));
+ assert(index >= 0 && index <= this.appsPage.length);
+
+ var origPageCount = this.appsPages.length;
+ this.appendTilePage(appsPage,
+ localStrings.getString('appDefaultPageName'),
+ true,
+ this.appsPages[index]);
+ // 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');
+
+ return appsPage;
+ },
+
+ /**
+ * Returns the index of a given tile page.
+ * @param {TilePage} page The TilePage we wish to find.
+ * @return {number} The index of |page| or -1 if it is not in the
+ * collection.
+ */
+ getTilePageIndex: function(page) {
+ return Array.prototype.indexOf.call(this.tilePages, page);
+ },
+
+ /**
+ * Removes a page and navigation dot (if the navdot exists).
+ * @param {TilePage} page The page to be removed.
+ * @param {boolean=} opt_animate If the removal should be animated.
+ */
+ removeTilePageAndDot_: function(page, opt_animate) {
+ if (page.navigationDot)
+ page.navigationDot.remove(opt_animate);
+ this.cardSlider.removeCard(page);
+ },
};
return {
« no previous file with comments | « chrome/browser/resources/ntp4/new_tab.js ('k') | chrome/browser/resources/ntp4/tile_page.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698