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