Index: chrome/browser/resources/ntp/apps.js |
diff --git a/chrome/browser/resources/ntp/apps.js b/chrome/browser/resources/ntp/apps.js |
index e61ebe0ea34a85798781e3f0628bddc5e0675a27..db15e58c75b188582dd499538d6ffac6b26b64a2 100644 |
--- a/chrome/browser/resources/ntp/apps.js |
+++ b/chrome/browser/resources/ntp/apps.js |
@@ -45,6 +45,15 @@ function getAppsCallback(data) { |
return a.app_launch_index - b.app_launch_index; |
}); |
+ // Determines if the web store link should be detached and place in the |
+ // top right of the screen. |
+ apps.detachWebstoreEntry = |
+ !apps.showPromo && data.apps.length >= MAX_APPS_PER_ROW[layoutMode]; |
+ |
+ apps.data = data.apps; |
+ if (!apps.detachWebstoreEntry) |
+ apps.data.push('web-store-entry'); |
+ |
clearClosedMenu(apps.menu); |
data.apps.forEach(function(app) { |
appsSectionContent.appendChild(apps.createElement(app)); |
@@ -84,12 +93,15 @@ function getAppsCallback(data) { |
appsPromoLink.setAttribute('ping', appsPromoPing); |
maybeDoneLoading(); |
- if (isDoneLoading()) { |
- if (!apps.showPromo && data.apps.length >= MAX_APPS_PER_ROW[layoutMode]) |
- webStoreEntry.classList.add('loner'); |
- else |
- webStoreEntry.classList.remove('loner'); |
+ // Disable the animations when the app launcher is being (re)initailized. |
+ apps.layout({disableAnimations:true}); |
+ |
+ if (apps.detachWebstoreEntry) |
+ webStoreEntry.classList.add('loner'); |
+ else |
+ webStoreEntry.classList.remove('loner'); |
+ if (isDoneLoading()) { |
updateMiniviewClipping(appsMiniview); |
layoutSections(); |
} |
@@ -261,6 +273,12 @@ var apps = (function() { |
} |
}); |
+ // Moves the element at position |from| in array |arr| to position |to|. |
+ function arrayMove(arr, from, to) { |
+ var element = arr.splice(from, 1); |
+ arr.splice(to, 0, element[0]); |
+ } |
+ |
return { |
loaded: false, |
@@ -268,6 +286,230 @@ var apps = (function() { |
showPromo: false, |
+ detachWebstoreEntry: false, |
+ |
+ // The list of app ids, in order, of each app in the launcher. |
+ data_: null, |
+ get data() { return this.data_; }, |
+ set data(data) { |
+ this.data_ = data.map(function(app) { |
+ return app.id; |
+ }); |
+ this.invalidate_(); |
+ }, |
+ |
+ dirty_: true, |
+ invalidate_: function() { |
+ this.dirty_ = true; |
+ }, |
+ |
+ visible_: true, |
+ get visible() { |
+ return this.visible_; |
+ }, |
+ set visible(visible) { |
+ this.visible_ = visible; |
+ this.invalidate_(); |
+ }, |
+ |
+ // DragAndDropDelegate |
+ |
+ dragContainer: $('apps-content'), |
+ transitionsDuration: 200, |
+ |
+ get dragItem() { return this.dragItem_; }, |
+ set dragItem(dragItem) { |
+ if (this.dragItem_ != dragItem) { |
+ this.dragItem_ = dragItem; |
+ this.invalidate_(); |
+ } |
+ }, |
+ |
+ // The dimensions of each item in the app launcher. This calculates the |
+ // dimensions dynamically, so it should be called after creating the DOM. |
+ dimensions_: null, |
+ get dimensions() { |
+ if (this.dimensions_) |
+ return this.dimensions_; |
+ |
+ var app = this.dragContainer.firstChild; |
+ |
+ var width = app.offsetWidth; |
+ var height = app.offsetHeight; |
+ |
+ // If the apps haven't properly loaded yet, don't cache the result. |
+ if (app.offsetWidth == 0 || app.offsetHeight == 0) |
+ return {width:0, height:0}; |
+ |
+ var style = getComputedStyle(app); |
+ |
+ var marginWidth = |
+ parseInt(style.marginLeft) + parseInt(style.marginRight); |
+ var marginHeight = |
+ parseInt(style.marginTop) + parseInt(style.marginBottom); |
+ |
+ var borderWidth = parseInt(style.borderLeftWidth) + |
+ parseInt(style.borderRightWidth); |
+ var borderHeight = parseInt(style.borderTopWidth) + |
+ parseInt(style.borderBottomWidth); |
+ |
+ this.dimensions_ = { |
+ width: width + marginWidth + borderWidth, |
+ height: height + marginHeight + borderHeight |
+ }; |
+ |
+ return this.dimensions_; |
+ }, |
+ |
+ // Gets the item under the mouse event |e|. Returns null if there is no |
+ // item or if the item is not draggable. |
+ getItem: function(e) { |
+ var item = findAncestorByClass(e.target, 'app'); |
+ |
+ // You can't drag the web store launcher. |
+ if (item.classList.contains('web-store-entry')) |
+ return null; |
+ |
+ return item; |
+ }, |
+ |
+ // Returns true if |coordinates| point to a valid drop location. The |
+ // coordinates are relative to the drag container and the object should |
+ // have the 'x' and 'y' properties set. |
+ canDropOn: function(coordinates) { |
+ var cols = MAX_APPS_PER_ROW[layoutMode]; |
+ var rows = Math.ceil(this.data.length / cols); |
+ |
+ var bottom = rows * this.dimensions.height; |
+ var right = cols * this.dimensions.width; |
+ |
+ if (coordinates.x > right || coordinates.x < 0 || |
+ coordinates.y > bottom || coordinates.y < 0) |
+ return false; |
+ |
+ var position = this.getIndexAt_(coordinates); |
+ var appCount = this.data.length; |
+ |
+ if (!this.detachWebstoreEntry) |
+ appCount--; |
+ |
+ return position >= 0 && position < appCount; |
+ }, |
+ |
+ setDragPlaceholder: function(coordinates) { |
+ var position = this.getIndexAt_(coordinates); |
+ var appId = this.dragItem.querySelector('a').getAttribute('app-id'); |
+ var current = this.data.indexOf(appId); |
+ |
+ if (current == position || current < 0) |
+ return; |
+ |
+ arrayMove(this.data, current, position); |
+ this.invalidate_(); |
+ this.layout(); |
+ }, |
+ |
+ getIndexAt_: function(coordinates) { |
+ var x = coordinates.x; |
+ var y = coordinates.y; |
+ |
+ var w = this.dimensions.width; |
+ var h = this.dimensions.height; |
+ |
+ var availableWidth = this.dragContainer.offsetWidth; |
+ |
+ var row = Math.floor(y / h); |
+ var col = Math.floor(x / w); |
+ var index = Math.floor(availableWidth / w) * row + col; |
+ |
+ return index; |
+ }, |
+ |
+ saveDrag: function() { |
+ this.invalidate_(); |
+ this.layout(); |
+ |
+ var appIds = this.data.filter(function(id) { |
+ return id != 'web-store-entry'; |
+ }); |
+ |
+ // Wait until the transitions are complete before notifying the browser. |
+ // Otherwise, the apps will be re-rendered while still transitioning. |
+ setTimeout(function() { |
+ chrome.send('reorderApps', appIds); |
+ }, this.transitionsDuration + 10); |
+ }, |
+ |
+ layout: function(options) { |
+ options = options || {}; |
+ if (!this.dirty_ && options.force != true) |
+ return; |
+ |
+ try { |
+ var container = this.dragContainer; |
+ if (options.disableAnimations) |
+ container.setAttribute('launcher-animations', false); |
+ var d0 = Date.now(); |
+ this.layoutImpl_(); |
+ this.dirty_ = false; |
+ logEvent('apps.layout: ' + (Date.now() - d0)); |
+ |
+ } finally { |
+ if (options.disableAnimations) { |
+ // We need to re-enable animations asynchronously, so that the |
+ // animations are still disabled for this layout update. |
+ setTimeout(function() { |
+ container.setAttribute('launcher-animations', true); |
+ }, 0); |
+ } |
+ } |
+ }, |
+ |
+ layoutImpl_: function() { |
+ var apps = this.data; |
+ var rects = this.getLayoutRects_(apps.length); |
+ var appsContent = this.dragContainer; |
+ |
+ if (!this.visible) |
+ return; |
+ |
+ for (var i = 0; i < apps.length; i++) { |
+ var app = appsContent.querySelector('[app-id='+apps[i]+']').parentNode; |
+ |
+ // If the node is being dragged, don't try to place it in the grid. |
+ if (app == this.dragItem) |
+ continue; |
+ |
+ app.style.left = rects[i].left + 'px'; |
+ app.style.top = rects[i].top + 'px'; |
+ } |
+ |
+ // We need to set the container's height manually because the apps use |
+ // absolute positioning. |
+ var rows = Math.ceil(apps.length / MAX_APPS_PER_ROW[layoutMode]); |
+ appsContent.style.height = (rows * this.dimensions.height) + 'px'; |
+ }, |
+ |
+ getLayoutRects_: function(appCount) { |
+ var availableWidth = this.dragContainer.offsetWidth; |
+ var rtl = isRtl(); |
+ var rects = []; |
+ var w = this.dimensions.width; |
+ var h = this.dimensions.height; |
+ |
+ for (var i = 0; i < appCount; i++) { |
+ var row = Math.floor((w * i) / availableWidth); |
+ var top = row * h; |
+ var left = (w * i) % availableWidth; |
+ |
+ // Reflect the X axis if an RTL language is active. |
+ if (rtl) |
+ left = availableWidth - left - w; |
+ rects[i] = {left: left, top: top, row: row}; |
+ } |
+ return rects; |
+ }, |
+ |
createElement: function(app) { |
var div = createElement(app); |
var a = div.firstChild; |
@@ -344,7 +586,7 @@ var apps = (function() { |
'name': localStrings.getString('web_store_title'), |
'launch_url': localStrings.getString('web_store_url') |
}); |
- elm.setAttribute('app-id', 'web-store-entry'); |
+ elm.classList.add('web-store-entry'); |
return elm; |
}, |
@@ -364,3 +606,6 @@ var apps = (function() { |
} |
}; |
})(); |
+ |
+// Enable drag and drop reordering of the app launcher. |
+var appDragAndDrop = new DragAndDropController(apps); |