Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / ntp4 / apps_page.js
blob8136080e00827767e1f94f2f4a2b825a27229f52
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.
5 cr.define('ntp', function() {
6   'use strict';
8   var APP_LAUNCH = {
9     // The histogram buckets (keep in sync with extension_constants.h).
10     NTP_APPS_MAXIMIZED: 0,
11     NTP_APPS_COLLAPSED: 1,
12     NTP_APPS_MENU: 2,
13     NTP_MOST_VISITED: 3,
14     NTP_APP_RE_ENABLE: 16,
15     NTP_WEBSTORE_FOOTER: 18,
16     NTP_WEBSTORE_PLUS_ICON: 19,
17   };
19   // Histogram buckets for UMA tracking of where a DnD drop came from.
20   var DRAG_SOURCE = {
21     SAME_APPS_PANE: 0,
22     OTHER_APPS_PANE: 1,
23     MOST_VISITED_PANE: 2,  // Deprecated.
24     BOOKMARKS_PANE: 3,  // Deprecated.
25     OUTSIDE_NTP: 4
26   };
27   var DRAG_SOURCE_LIMIT = DRAG_SOURCE.OUTSIDE_NTP + 1;
29   /**
30    * App context menu. The class is designed to be used as a singleton with
31    * the app that is currently showing a context menu stored in this.app_.
32    * @constructor
33    */
34   function AppContextMenu() {
35     this.__proto__ = AppContextMenu.prototype;
36     this.initialize();
37   }
38   cr.addSingletonGetter(AppContextMenu);
40   AppContextMenu.prototype = {
41     initialize: function() {
42       var menu = new cr.ui.Menu;
43       cr.ui.decorate(menu, cr.ui.Menu);
44       menu.classList.add('app-context-menu');
45       this.menu = menu;
47       this.launch_ = this.appendMenuItem_();
48       this.launch_.addEventListener('activate', this.onLaunch_.bind(this));
50       menu.appendChild(cr.ui.MenuItem.createSeparator());
51       this.launchRegularTab_ = this.appendMenuItem_('applaunchtyperegular');
52       this.launchPinnedTab_ = this.appendMenuItem_('applaunchtypepinned');
53       if (loadTimeData.getBoolean('canHostedAppsOpenInWindows'))
54         this.launchNewWindow_ = this.appendMenuItem_('applaunchtypewindow');
55       this.launchFullscreen_ = this.appendMenuItem_('applaunchtypefullscreen');
57       var self = this;
58       this.forAllLaunchTypes_(function(launchTypeButton, id) {
59         launchTypeButton.addEventListener('activate',
60             self.onLaunchTypeChanged_.bind(self));
61       });
63       this.launchTypeMenuSeparator_ = cr.ui.MenuItem.createSeparator();
64       menu.appendChild(this.launchTypeMenuSeparator_);
65       this.options_ = this.appendMenuItem_('appoptions');
66       this.uninstall_ = this.appendMenuItem_('appuninstall');
68       if (loadTimeData.getBoolean('canShowAppInfoDialog')) {
69         this.appinfo_ = this.appendMenuItem_('appinfodialog');
70         this.appinfo_.addEventListener('activate',
71                                        this.onShowAppInfo_.bind(this));
72       } else {
73         this.details_ = this.appendMenuItem_('appdetails');
74         this.details_.addEventListener('activate',
75                                        this.onShowDetails_.bind(this));
76       }
78       this.options_.addEventListener('activate',
79                                      this.onShowOptions_.bind(this));
80       this.uninstall_.addEventListener('activate',
81                                        this.onUninstall_.bind(this));
83       if (!cr.isChromeOS) {
84         this.createShortcutSeparator_ =
85             menu.appendChild(cr.ui.MenuItem.createSeparator());
86         this.createShortcut_ = this.appendMenuItem_('appcreateshortcut');
87         this.createShortcut_.addEventListener(
88             'activate', this.onCreateShortcut_.bind(this));
89       }
91       document.body.appendChild(menu);
92     },
94     /**
95      * Appends a menu item to |this.menu|.
96      * @param {string=} opt_textId If defined, the ID for the localized string
97      *     that acts as the item's label.
98      */
99     appendMenuItem_: function(opt_textId) {
100       var button = cr.doc.createElement('button');
101       this.menu.appendChild(button);
102       cr.ui.decorate(button, cr.ui.MenuItem);
103       if (opt_textId)
104         button.textContent = loadTimeData.getString(opt_textId);
105       return button;
106     },
108     /**
109      * Iterates over all the launch type menu items.
110      * @param {function(cr.ui.MenuItem, number)} f The function to call for each
111      *     menu item. The parameters to the function include the menu item and
112      *     the associated launch ID.
113      */
114     forAllLaunchTypes_: function(f) {
115       // Order matters: index matches launchType id.
116       var launchTypes = [this.launchPinnedTab_,
117                          this.launchRegularTab_,
118                          this.launchFullscreen_,
119                          this.launchNewWindow_];
121       for (var i = 0; i < launchTypes.length; ++i) {
122         if (!launchTypes[i])
123           continue;
125         f(launchTypes[i], i);
126       }
127     },
129     /**
130      * Does all the necessary setup to show the menu for the given app.
131      * @param {App} app The App object that will be showing a context menu.
132      */
133     setupForApp: function(app) {
134       this.app_ = app;
136       this.launch_.textContent = app.appData.title;
138       var launchTypeWindow = this.launchNewWindow_;
139       var hasLaunchType = false;
140       this.forAllLaunchTypes_(function(launchTypeButton, id) {
141         launchTypeButton.disabled = false;
142         launchTypeButton.checked = app.appData.launch_type == id;
143         // There are three cases when a launch type is hidden:
144         //  1. packaged apps hide all launch types
145         //  2. canHostedAppsOpenInWindows is false and type is launchTypeWindow
146         //  3. enableNewBookmarkApps is true and type is anything except
147         //     launchTypeWindow
148         launchTypeButton.hidden = app.appData.packagedApp ||
149             (!loadTimeData.getBoolean('canHostedAppsOpenInWindows') &&
150              launchTypeButton == launchTypeWindow) ||
151             (loadTimeData.getBoolean('enableNewBookmarkApps') &&
152              launchTypeButton != launchTypeWindow);
153         if (!launchTypeButton.hidden) hasLaunchType = true;
154       });
156       this.launchTypeMenuSeparator_.hidden =
157           app.appData.packagedApp || !hasLaunchType;
159       this.options_.disabled = !app.appData.optionsUrl || !app.appData.enabled;
160       if (this.details_)
161         this.details_.disabled = !app.appData.detailsUrl;
162       this.uninstall_.disabled = !app.appData.mayDisable;
164       if (cr.isMac) {
165         // On Windows and Linux, these should always be visible. On ChromeOS,
166         // they are never created. On Mac, shortcuts can only be created for
167         // new-style packaged apps, so hide the menu item.
168         this.createShortcutSeparator_.hidden = this.createShortcut_.hidden =
169             !app.appData.packagedApp;
170       }
171     },
173     /**
174      * Handlers for menu item activation.
175      * @param {Event} e The activation event.
176      * @private
177      */
178     onLaunch_: function(e) {
179       chrome.send('launchApp', [this.app_.appId, APP_LAUNCH.NTP_APPS_MENU]);
180     },
181     onLaunchTypeChanged_: function(e) {
182       var pressed = e.currentTarget;
183       var app = this.app_;
184       var targetLaunchType = pressed;
185       // When bookmark apps are enabled, hosted apps can only toggle between
186       // open as window and open as tab.
187       if (loadTimeData.getBoolean('enableNewBookmarkApps')) {
188         targetLaunchType = this.launchNewWindow_.checked ?
189             this.launchRegularTab_ : this.launchNewWindow_;
190       }
191       this.forAllLaunchTypes_(function(launchTypeButton, id) {
192         if (launchTypeButton == targetLaunchType) {
193           chrome.send('setLaunchType', [app.appId, id]);
194           // Manually update the launch type. We will only get
195           // appsPrefChangeCallback calls after changes to other NTP instances.
196           app.appData.launch_type = id;
197         }
198       });
199     },
200     onShowOptions_: function(e) {
201       window.location = this.app_.appData.optionsUrl;
202     },
203     onShowDetails_: function(e) {
204       var url = this.app_.appData.detailsUrl;
205       url = appendParam(url, 'utm_source', 'chrome-ntp-launcher');
206       window.location = url;
207     },
208     onUninstall_: function(e) {
209       chrome.send('uninstallApp', [this.app_.appData.id]);
210     },
211     onCreateShortcut_: function(e) {
212       chrome.send('createAppShortcut', [this.app_.appData.id]);
213     },
214     onShowAppInfo_: function(e) {
215       chrome.send('showAppInfo', [this.app_.appData.id]);
216     }
217   };
219   /**
220    * Creates a new App object.
221    * @param {Object} appData The data object that describes the app.
222    * @constructor
223    * @extends {HTMLDivElement}
224    */
225   function App(appData) {
226     var el = cr.doc.createElement('div');
227     el.__proto__ = App.prototype;
228     el.initialize(appData);
230     return el;
231   }
233   App.prototype = {
234     __proto__: HTMLDivElement.prototype,
236     /**
237      * Initialize the app object.
238      * @param {Object} appData The data object that describes the app.
239      */
240     initialize: function(appData) {
241       this.appData = appData;
242       assert(this.appData_.id, 'Got an app without an ID');
243       this.id = this.appData_.id;
244       this.setAttribute('role', 'menuitem');
246       this.className = 'app focusable';
248       if (!this.appData_.icon_big_exists && this.appData_.icon_small_exists)
249         this.useSmallIcon_ = true;
251       this.appContents_ = this.useSmallIcon_ ?
252           $('app-small-icon-template').cloneNode(true) :
253           $('app-large-icon-template').cloneNode(true);
254       this.appContents_.id = '';
255       this.appendChild(this.appContents_);
257       this.appImgContainer_ = /** @type {HTMLElement} */(
258           this.querySelector('.app-img-container'));
259       this.appImg_ = this.appImgContainer_.querySelector('img');
260       this.setIcon();
262       if (this.useSmallIcon_) {
263         this.imgDiv_ = /** @type {HTMLElement} */(
264             this.querySelector('.app-icon-div'));
265         this.addLaunchClickTarget_(this.imgDiv_);
266         this.imgDiv_.title = this.appData_.full_name;
267         chrome.send('getAppIconDominantColor', [this.id]);
268       } else {
269         this.addLaunchClickTarget_(this.appImgContainer_);
270         this.appImgContainer_.title = this.appData_.full_name;
271       }
273       // The app's full name is shown in the tooltip, whereas the short name
274       // is used for the label.
275       var appSpan = /** @type {HTMLElement} */(
276           this.appContents_.querySelector('.title'));
277       appSpan.textContent = this.appData_.title;
278       appSpan.title = this.appData_.full_name;
279       this.addLaunchClickTarget_(appSpan);
281       this.addEventListener('keydown', cr.ui.contextMenuHandler);
282       this.addEventListener('keyup', cr.ui.contextMenuHandler);
284       // This hack is here so that appContents.contextMenu will be the same as
285       // this.contextMenu.
286       var self = this;
287       this.appContents_.__defineGetter__('contextMenu', function() {
288         return self.contextMenu;
289       });
291       if (!this.appData_.kioskMode) {
292         this.appContents_.addEventListener('contextmenu',
293                                            cr.ui.contextMenuHandler);
294       }
296       this.addEventListener('mousedown', this.onMousedown_, true);
297       this.addEventListener('keydown', this.onKeydown_);
298       this.addEventListener('keyup', this.onKeyup_);
299     },
301     /**
302      * Sets the color of the favicon dominant color bar.
303      * @param {string} color The css-parsable value for the color.
304      */
305     set stripeColor(color) {
306       this.querySelector('.color-stripe').style.backgroundColor = color;
307     },
309     /**
310      * Removes the app tile from the page. Should be called after the app has
311      * been uninstalled.
312      */
313     remove: function(opt_animate) {
314       // Unset the ID immediately, because the app is already gone. But leave
315       // the tile on the page as it animates out.
316       this.id = '';
317       this.tile.doRemove(opt_animate);
318     },
320     /**
321      * Set the URL of the icon from |appData_|. This won't actually show the
322      * icon until loadIcon() is called (for performance reasons; we don't want
323      * to load icons until we have to).
324      */
325     setIcon: function() {
326       var src = this.useSmallIcon_ ? this.appData_.icon_small :
327                                      this.appData_.icon_big;
328       if (!this.appData_.enabled ||
329           (!this.appData_.offlineEnabled && !navigator.onLine)) {
330         src += '?grayscale=true';
331       }
333       this.appImgSrc_ = src;
334       this.classList.add('icon-loading');
335     },
337     /**
338      * Shows the icon for the app. That is, it causes chrome to load the app
339      * icon resource.
340      */
341     loadIcon: function() {
342       if (this.appImgSrc_) {
343         this.appImg_.src = this.appImgSrc_;
344         this.appImg_.classList.remove('invisible');
345         this.appImgSrc_ = null;
346       }
348       this.classList.remove('icon-loading');
349     },
351     /**
352      * Set the size and position of the app tile.
353      * @param {number} size The total size of |this|.
354      * @param {number} x The x-position.
355      * @param {number} y The y-position.
356      *     animate.
357      */
358     setBounds: function(size, x, y) {
359       var imgSize = size * APP_IMG_SIZE_FRACTION;
360       this.appImgContainer_.style.width = this.appImgContainer_.style.height =
361           toCssPx(this.useSmallIcon_ ? 16 : imgSize);
362       if (this.useSmallIcon_) {
363         // 3/4 is the ratio of 96px to 128px (the used height and full height
364         // of icons in apps).
365         var iconSize = imgSize * 3 / 4;
366         // The -2 is for the div border to improve the visual alignment for the
367         // icon div.
368         this.imgDiv_.style.width = this.imgDiv_.style.height =
369             toCssPx(iconSize - 2);
370         // Margins set to get the icon placement right and the text to line up.
371         this.imgDiv_.style.marginTop = this.imgDiv_.style.marginBottom =
372             toCssPx((imgSize - iconSize) / 2);
373       }
375       this.style.width = this.style.height = toCssPx(size);
376       this.style.left = toCssPx(x);
377       this.style.right = toCssPx(x);
378       this.style.top = toCssPx(y);
379     },
381     /**
382      * Invoked when an app is clicked.
383      * @param {Event} e The click event.
384      * @private
385      */
386     onClick_: function(e) {
387       var url = !this.appData_.is_webstore ? '' :
388           appendParam(this.appData_.url,
389                       'utm_source',
390                       'chrome-ntp-icon');
392       chrome.send('launchApp',
393                   [this.appId, APP_LAUNCH.NTP_APPS_MAXIMIZED, url,
394                    e.button, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey]);
396       // Don't allow the click to trigger a link or anything
397       e.preventDefault();
398     },
400     /**
401      * Invoked when the user presses a key while the app is focused.
402      * @param {Event} e The key event.
403      * @private
404      */
405     onKeydown_: function(e) {
406       if (e.keyIdentifier == 'Enter') {
407         chrome.send('launchApp',
408                     [this.appId, APP_LAUNCH.NTP_APPS_MAXIMIZED, '',
409                      0, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey]);
410         e.preventDefault();
411         e.stopPropagation();
412       }
413       this.onKeyboardUsed_(e.keyCode);
414     },
416     /**
417      * Invoked when the user releases a key while the app is focused.
418      * @param {Event} e The key event.
419      * @private
420      */
421     onKeyup_: function(e) {
422       this.onKeyboardUsed_(e.keyCode);
423     },
425     /**
426      * Called when the keyboard has been used (key down or up). The .click-focus
427      * hack is removed if the user presses a key that can change focus.
428      * @param {number} keyCode The key code of the keyboard event.
429      * @private
430      */
431     onKeyboardUsed_: function(keyCode) {
432       switch (keyCode) {
433         case 9:  // Tab.
434         case 37:  // Left arrow.
435         case 38:  // Up arrow.
436         case 39:  // Right arrow.
437         case 40:  // Down arrow.
438           this.classList.remove('click-focus');
439       }
440     },
442     /**
443      * Adds a node to the list of targets that will launch the app. This list
444      * is also used in onMousedown to determine whether the app contents should
445      * be shown as active (if we don't do this, then clicking anywhere in
446      * appContents, even a part that is outside the ideally clickable region,
447      * will cause the app icon to look active).
448      * @param {HTMLElement} node The node that should be clickable.
449      */
450     addLaunchClickTarget_: function(node) {
451       node.classList.add('launch-click-target');
452       node.addEventListener('click', this.onClick_.bind(this));
453     },
455     /**
456      * Handler for mousedown on the App. Adds a class that allows us to
457      * not display as :active for right clicks (specifically, don't pulse on
458      * these occasions). Also, we don't pulse for clicks that aren't within the
459      * clickable regions.
460      * @param {Event} e The mousedown event.
461      */
462     onMousedown_: function(e) {
463       // If the current platform uses middle click to autoscroll and this
464       // mousedown isn't handled, onClick_() will never fire. crbug.com/142939
465       if (e.button == 1)
466         e.preventDefault();
468       if (e.button == 2 ||
469           !findAncestorByClass(/** @type {Element} */(e.target),
470                                'launch-click-target')) {
471         this.appContents_.classList.add('suppress-active');
472       } else {
473         this.appContents_.classList.remove('suppress-active');
474       }
476       // This class is here so we don't show the focus state for apps that
477       // gain keyboard focus via mouse clicking.
478       this.classList.add('click-focus');
479     },
481     /**
482      * Change the appData and update the appearance of the app.
483      * @param {AppInfo} appData The new data object that describes the app.
484      */
485     replaceAppData: function(appData) {
486       this.appData_ = appData;
487       this.setIcon();
488       this.loadIcon();
489     },
491     /**
492      * The data and preferences for this app.
493      * @type {Object}
494      */
495     set appData(data) {
496       this.appData_ = data;
497     },
498     get appData() {
499       return this.appData_;
500     },
502     get appId() {
503       return this.appData_.id;
504     },
506     /**
507      * Returns a pointer to the context menu for this app. All apps share the
508      * singleton AppContextMenu. This function is called by the
509      * ContextMenuHandler in response to the 'contextmenu' event.
510      * @type {cr.ui.Menu}
511      */
512     get contextMenu() {
513       var menu = AppContextMenu.getInstance();
514       menu.setupForApp(this);
515       return menu.menu;
516     },
518     /**
519      * Returns whether this element can be 'removed' from chrome (i.e. whether
520      * the user can drag it onto the trash and expect something to happen).
521      * @return {boolean} True if the app can be uninstalled.
522      */
523     canBeRemoved: function() {
524       return this.appData_.mayDisable;
525     },
527     /**
528      * Uninstalls the app after it's been dropped on the trash.
529      */
530     removeFromChrome: function() {
531       chrome.send('uninstallApp', [this.appData_.id, true]);
532       this.tile.tilePage.removeTile(this.tile, true);
533     },
535     /**
536      * Called when a drag is starting on the tile. Updates dataTransfer with
537      * data for this tile.
538      */
539     setDragData: function(dataTransfer) {
540       dataTransfer.setData('Text', this.appData_.title);
541       dataTransfer.setData('URL', this.appData_.url);
542     },
543   };
545   var TilePage = ntp.TilePage;
547   // The fraction of the app tile size that the icon uses.
548   var APP_IMG_SIZE_FRACTION = 4 / 5;
550   var appsPageGridValues = {
551     // The fewest tiles we will show in a row.
552     minColCount: 3,
553     // The most tiles we will show in a row.
554     maxColCount: 6,
556     // The smallest a tile can be.
557     minTileWidth: 64 / APP_IMG_SIZE_FRACTION,
558     // The biggest a tile can be.
559     maxTileWidth: 128 / APP_IMG_SIZE_FRACTION,
561     // The padding between tiles, as a fraction of the tile width.
562     tileSpacingFraction: 1 / 8,
563   };
564   TilePage.initGridValues(appsPageGridValues);
566   /**
567    * Creates a new AppsPage object.
568    * @constructor
569    * @extends {TilePage}
570    */
571   function AppsPage() {
572     var el = new TilePage(appsPageGridValues);
573     el.__proto__ = AppsPage.prototype;
574     el.initialize();
576     return el;
577   }
579   AppsPage.prototype = {
580     __proto__: TilePage.prototype,
582     initialize: function() {
583       this.classList.add('apps-page');
585       this.addEventListener('cardselected', this.onCardSelected_);
587       this.addEventListener('tilePage:tile_added', this.onTileAdded_);
589       this.content_.addEventListener('scroll', this.onScroll_.bind(this));
590     },
592     /**
593      * Highlight a newly installed app as it's added to the NTP.
594      * @param {AppInfo} appData The data object that describes the app.
595      */
596     insertAndHighlightApp: function(appData) {
597       ntp.getCardSlider().selectCardByValue(this);
598       this.content_.scrollTop = this.content_.scrollHeight;
599       this.insertApp(appData, true);
600     },
602     /**
603      * Similar to appendApp, but it respects the app_launch_ordinal field of
604      * |appData|.
605      * @param {Object} appData The data that describes the app.
606      * @param {boolean} animate Whether to animate the insertion.
607      */
608     insertApp: function(appData, animate) {
609       var index = this.tileElements_.length;
610       for (var i = 0; i < this.tileElements_.length; i++) {
611         if (appData.app_launch_ordinal <
612             this.tileElements_[i].firstChild.appData.app_launch_ordinal) {
613           index = i;
614           break;
615         }
616       }
618       this.addTileAt(new App(appData), index, animate);
619     },
621     /**
622      * Handler for 'cardselected' event, fired when |this| is selected. The
623      * first time this is called, we load all the app icons.
624      * @private
625      */
626     onCardSelected_: function(e) {
627       var apps = this.querySelectorAll('.app.icon-loading');
628       for (var i = 0; i < apps.length; i++) {
629         apps[i].loadIcon();
630       }
631     },
633     /**
634      * Handler for tile additions to this page.
635      * @param {Event} e The tilePage:tile_added event.
636      */
637     onTileAdded_: function(e) {
638       assert(e.currentTarget == this);
639       assert(e.addedTile.firstChild instanceof App);
640       if (this.classList.contains('selected-card'))
641         e.addedTile.firstChild.loadIcon();
642     },
644     /**
645      * A handler for when the apps page is scrolled (then we need to reposition
646      * the bubbles.
647      * @private
648      */
649     onScroll_: function(e) {
650       if (!this.selected)
651         return;
652       for (var i = 0; i < this.tileElements_.length; i++) {
653         var app = this.tileElements_[i].firstChild;
654         assert(app instanceof App);
655       }
656     },
658     /** @override */
659     doDragOver: function(e) {
660       // Only animatedly re-arrange if the user is currently dragging an app.
661       var tile = ntp.getCurrentlyDraggingTile();
662       if (tile && tile.querySelector('.app')) {
663         TilePage.prototype.doDragOver.call(this, e);
664       } else {
665         e.preventDefault();
666         this.setDropEffect(e.dataTransfer);
667       }
668     },
670     /** @override */
671     shouldAcceptDrag: function(e) {
672       if (ntp.getCurrentlyDraggingTile())
673         return true;
674       if (!e.dataTransfer || !e.dataTransfer.types)
675         return false;
676       return Array.prototype.indexOf.call(e.dataTransfer.types,
677                                           'text/uri-list') != -1;
678     },
680     /** @override */
681     addDragData: function(dataTransfer, index) {
682       var sourceId = -1;
683       var currentlyDraggingTile = ntp.getCurrentlyDraggingTile();
684       if (currentlyDraggingTile) {
685         var tileContents = currentlyDraggingTile.firstChild;
686         if (tileContents.classList.contains('app')) {
687           var originalPage = currentlyDraggingTile.tilePage;
688           var samePageDrag = originalPage == this;
689           sourceId = samePageDrag ? DRAG_SOURCE.SAME_APPS_PANE :
690                                     DRAG_SOURCE.OTHER_APPS_PANE;
691           this.tileGrid_.insertBefore(currentlyDraggingTile,
692                                       this.tileElements_[index]);
693           this.tileMoved(currentlyDraggingTile);
694           if (!samePageDrag) {
695             originalPage.fireRemovedEvent(currentlyDraggingTile, index, true);
696             this.fireAddedEvent(currentlyDraggingTile, index, true);
697           }
698         }
699       } else {
700         this.addOutsideData_(dataTransfer);
701         sourceId = DRAG_SOURCE.OUTSIDE_NTP;
702       }
704       assert(sourceId != -1);
705       chrome.send('metricsHandler:recordInHistogram',
706           ['NewTabPage.AppsPageDragSource', sourceId, DRAG_SOURCE_LIMIT]);
707     },
709     /**
710      * Adds drag data that has been dropped from a source that is not a tile.
711      * @param {Object} dataTransfer The data transfer object that holds drop
712      *     data.
713      * @private
714      */
715     addOutsideData_: function(dataTransfer) {
716       var url = dataTransfer.getData('url');
717       assert(url);
719       // If the dataTransfer has html data, use that html's text contents as the
720       // title of the new link.
721       var html = dataTransfer.getData('text/html');
722       var title;
723       if (html) {
724         // It's important that we don't attach this node to the document
725         // because it might contain scripts.
726         var node = this.ownerDocument.createElement('div');
727         node.innerHTML = html;
728         title = node.textContent;
729       }
731       // Make sure title is >=1 and <=45 characters for Chrome app limits.
732       if (!title)
733         title = url;
734       if (title.length > 45)
735         title = title.substring(0, 45);
736       var data = {url: url, title: title};
738       // Synthesize an app.
739       this.generateAppForLink(data);
740     },
742     /**
743      * Creates a new crx-less app manifest and installs it.
744      * @param {Object} data The data object describing the link. Must have |url|
745      *     and |title| members.
746      */
747     generateAppForLink: function(data) {
748       assert(data.url != undefined);
749       assert(data.title != undefined);
750       var pageIndex = ntp.getAppsPageIndex(this);
751       chrome.send('generateAppForLink', [data.url, data.title, pageIndex]);
752     },
754     /** @override */
755     tileMoved: function(draggedTile) {
756       if (!(draggedTile.firstChild instanceof App))
757         return;
759       var pageIndex = ntp.getAppsPageIndex(this);
760       chrome.send('setPageIndex', [draggedTile.firstChild.appId, pageIndex]);
762       var appIds = [];
763       for (var i = 0; i < this.tileElements_.length; i++) {
764         var tileContents = this.tileElements_[i].firstChild;
765         if (tileContents instanceof App)
766           appIds.push(tileContents.appId);
767       }
769       chrome.send('reorderApps', [draggedTile.firstChild.appId, appIds]);
770     },
772     /** @override */
773     setDropEffect: function(dataTransfer) {
774       var tile = ntp.getCurrentlyDraggingTile();
775       if (tile && tile.querySelector('.app'))
776         ntp.setCurrentDropEffect(dataTransfer, 'move');
777       else
778         ntp.setCurrentDropEffect(dataTransfer, 'copy');
779     },
780   };
782   /**
783    * Launches the specified app using the APP_LAUNCH_NTP_APP_RE_ENABLE
784    * histogram. This should only be invoked from the AppLauncherHandler.
785    * @param {string} appId The ID of the app.
786    */
787   function launchAppAfterEnable(appId) {
788     chrome.send('launchApp', [appId, APP_LAUNCH.NTP_APP_RE_ENABLE]);
789   }
791   return {
792     APP_LAUNCH: APP_LAUNCH,
793     AppsPage: AppsPage,
794     launchAppAfterEnable: launchAppAfterEnable,
795   };