Allow only one bookmark to be added for multiple fast starring
[chromium-blink-merge.git] / chrome / browser / resources / ntp4 / apps_page.js
blob751d9cea2eb6ea8bf3da40ac9e91a8173e6a0616
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('enableNewBookmarkApps') || !cr.isMac)
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       this.forAllLaunchTypes_(function(launchTypeButton, id) {
140         launchTypeButton.disabled = false;
141         launchTypeButton.checked = app.appData.launch_type == id;
142         // If bookmark apps are enabled, only show the "Open as window" button.
143         launchTypeButton.hidden = app.appData.packagedApp ||
144             (loadTimeData.getBoolean('enableNewBookmarkApps') &&
145              launchTypeButton != launchTypeWindow);
146       });
148       this.launchTypeMenuSeparator_.hidden = app.appData.packagedApp;
150       this.options_.disabled = !app.appData.optionsUrl || !app.appData.enabled;
151       if (this.details_)
152         this.details_.disabled = !app.appData.detailsUrl;
153       this.uninstall_.disabled = !app.appData.mayDisable;
155       if (cr.isMac) {
156         // On Windows and Linux, these should always be visible. On ChromeOS,
157         // they are never created. On Mac, shortcuts can only be created for
158         // new-style packaged apps, so hide the menu item.
159         this.createShortcutSeparator_.hidden = this.createShortcut_.hidden =
160             !app.appData.packagedApp;
161       }
162     },
164     /**
165      * Handlers for menu item activation.
166      * @param {Event} e The activation event.
167      * @private
168      */
169     onLaunch_: function(e) {
170       chrome.send('launchApp', [this.app_.appId, APP_LAUNCH.NTP_APPS_MENU]);
171     },
172     onLaunchTypeChanged_: function(e) {
173       var pressed = e.currentTarget;
174       var app = this.app_;
175       var targetLaunchType = pressed;
176       // When bookmark apps are enabled, hosted apps can only toggle between
177       // open as window and open as tab.
178       if (loadTimeData.getBoolean('enableNewBookmarkApps')) {
179         targetLaunchType = this.launchNewWindow_.checked ?
180             this.launchRegularTab_ : this.launchNewWindow_;
181       }
182       this.forAllLaunchTypes_(function(launchTypeButton, id) {
183         if (launchTypeButton == targetLaunchType) {
184           chrome.send('setLaunchType', [app.appId, id]);
185           // Manually update the launch type. We will only get
186           // appsPrefChangeCallback calls after changes to other NTP instances.
187           app.appData.launch_type = id;
188         }
189       });
190     },
191     onShowOptions_: function(e) {
192       window.location = this.app_.appData.optionsUrl;
193     },
194     onShowDetails_: function(e) {
195       var url = this.app_.appData.detailsUrl;
196       url = appendParam(url, 'utm_source', 'chrome-ntp-launcher');
197       window.location = url;
198     },
199     onUninstall_: function(e) {
200       chrome.send('uninstallApp', [this.app_.appData.id]);
201     },
202     onCreateShortcut_: function(e) {
203       chrome.send('createAppShortcut', [this.app_.appData.id]);
204     },
205     onShowAppInfo_: function(e) {
206       chrome.send('showAppInfo', [this.app_.appData.id]);
207     }
208   };
210   /**
211    * Creates a new App object.
212    * @param {Object} appData The data object that describes the app.
213    * @constructor
214    * @extends {HTMLDivElement}
215    */
216   function App(appData) {
217     var el = cr.doc.createElement('div');
218     el.__proto__ = App.prototype;
219     el.initialize(appData);
221     return el;
222   }
224   App.prototype = {
225     __proto__: HTMLDivElement.prototype,
227     /**
228      * Initialize the app object.
229      * @param {Object} appData The data object that describes the app.
230      */
231     initialize: function(appData) {
232       this.appData = appData;
233       assert(this.appData_.id, 'Got an app without an ID');
234       this.id = this.appData_.id;
235       this.setAttribute('role', 'menuitem');
237       this.className = 'app focusable';
239       if (!this.appData_.icon_big_exists && this.appData_.icon_small_exists)
240         this.useSmallIcon_ = true;
242       this.appContents_ = this.useSmallIcon_ ?
243           $('app-small-icon-template').cloneNode(true) :
244           $('app-large-icon-template').cloneNode(true);
245       this.appContents_.id = '';
246       this.appendChild(this.appContents_);
248       this.appImgContainer_ = /** @type {HTMLElement} */(
249           this.querySelector('.app-img-container'));
250       this.appImg_ = this.appImgContainer_.querySelector('img');
251       this.setIcon();
253       if (this.useSmallIcon_) {
254         this.imgDiv_ = /** @type {HTMLElement} */(
255             this.querySelector('.app-icon-div'));
256         this.addLaunchClickTarget_(this.imgDiv_);
257         this.imgDiv_.title = this.appData_.full_name;
258         chrome.send('getAppIconDominantColor', [this.id]);
259       } else {
260         this.addLaunchClickTarget_(this.appImgContainer_);
261         this.appImgContainer_.title = this.appData_.full_name;
262       }
264       // The app's full name is shown in the tooltip, whereas the short name
265       // is used for the label.
266       var appSpan = /** @type {HTMLElement} */(
267           this.appContents_.querySelector('.title'));
268       appSpan.textContent = this.appData_.title;
269       appSpan.title = this.appData_.full_name;
270       this.addLaunchClickTarget_(appSpan);
272       this.addEventListener('keydown', cr.ui.contextMenuHandler);
273       this.addEventListener('keyup', cr.ui.contextMenuHandler);
275       // This hack is here so that appContents.contextMenu will be the same as
276       // this.contextMenu.
277       var self = this;
278       this.appContents_.__defineGetter__('contextMenu', function() {
279         return self.contextMenu;
280       });
282       if (!this.appData_.kioskMode) {
283         this.appContents_.addEventListener('contextmenu',
284                                            cr.ui.contextMenuHandler);
285       }
287       this.addEventListener('mousedown', this.onMousedown_, true);
288       this.addEventListener('keydown', this.onKeydown_);
289       this.addEventListener('keyup', this.onKeyup_);
290     },
292     /**
293      * Sets the color of the favicon dominant color bar.
294      * @param {string} color The css-parsable value for the color.
295      */
296     set stripeColor(color) {
297       this.querySelector('.color-stripe').style.backgroundColor = color;
298     },
300     /**
301      * Removes the app tile from the page. Should be called after the app has
302      * been uninstalled.
303      */
304     remove: function(opt_animate) {
305       // Unset the ID immediately, because the app is already gone. But leave
306       // the tile on the page as it animates out.
307       this.id = '';
308       this.tile.doRemove(opt_animate);
309     },
311     /**
312      * Set the URL of the icon from |appData_|. This won't actually show the
313      * icon until loadIcon() is called (for performance reasons; we don't want
314      * to load icons until we have to).
315      */
316     setIcon: function() {
317       var src = this.useSmallIcon_ ? this.appData_.icon_small :
318                                      this.appData_.icon_big;
319       if (!this.appData_.enabled ||
320           (!this.appData_.offlineEnabled && !navigator.onLine)) {
321         src += '?grayscale=true';
322       }
324       this.appImgSrc_ = src;
325       this.classList.add('icon-loading');
326     },
328     /**
329      * Shows the icon for the app. That is, it causes chrome to load the app
330      * icon resource.
331      */
332     loadIcon: function() {
333       if (this.appImgSrc_) {
334         this.appImg_.src = this.appImgSrc_;
335         this.appImg_.classList.remove('invisible');
336         this.appImgSrc_ = null;
337       }
339       this.classList.remove('icon-loading');
340     },
342     /**
343      * Set the size and position of the app tile.
344      * @param {number} size The total size of |this|.
345      * @param {number} x The x-position.
346      * @param {number} y The y-position.
347      *     animate.
348      */
349     setBounds: function(size, x, y) {
350       var imgSize = size * APP_IMG_SIZE_FRACTION;
351       this.appImgContainer_.style.width = this.appImgContainer_.style.height =
352           toCssPx(this.useSmallIcon_ ? 16 : imgSize);
353       if (this.useSmallIcon_) {
354         // 3/4 is the ratio of 96px to 128px (the used height and full height
355         // of icons in apps).
356         var iconSize = imgSize * 3 / 4;
357         // The -2 is for the div border to improve the visual alignment for the
358         // icon div.
359         this.imgDiv_.style.width = this.imgDiv_.style.height =
360             toCssPx(iconSize - 2);
361         // Margins set to get the icon placement right and the text to line up.
362         this.imgDiv_.style.marginTop = this.imgDiv_.style.marginBottom =
363             toCssPx((imgSize - iconSize) / 2);
364       }
366       this.style.width = this.style.height = toCssPx(size);
367       this.style.left = toCssPx(x);
368       this.style.right = toCssPx(x);
369       this.style.top = toCssPx(y);
370     },
372     /**
373      * Invoked when an app is clicked.
374      * @param {Event} e The click event.
375      * @private
376      */
377     onClick_: function(e) {
378       var url = !this.appData_.is_webstore ? '' :
379           appendParam(this.appData_.url,
380                       'utm_source',
381                       'chrome-ntp-icon');
383       chrome.send('launchApp',
384                   [this.appId, APP_LAUNCH.NTP_APPS_MAXIMIZED, url,
385                    e.button, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey]);
387       // Don't allow the click to trigger a link or anything
388       e.preventDefault();
389     },
391     /**
392      * Invoked when the user presses a key while the app is focused.
393      * @param {Event} e The key event.
394      * @private
395      */
396     onKeydown_: function(e) {
397       if (e.keyIdentifier == 'Enter') {
398         chrome.send('launchApp',
399                     [this.appId, APP_LAUNCH.NTP_APPS_MAXIMIZED, '',
400                      0, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey]);
401         e.preventDefault();
402         e.stopPropagation();
403       }
404       this.onKeyboardUsed_(e.keyCode);
405     },
407     /**
408      * Invoked when the user releases a key while the app is focused.
409      * @param {Event} e The key event.
410      * @private
411      */
412     onKeyup_: function(e) {
413       this.onKeyboardUsed_(e.keyCode);
414     },
416     /**
417      * Called when the keyboard has been used (key down or up). The .click-focus
418      * hack is removed if the user presses a key that can change focus.
419      * @param {number} keyCode The key code of the keyboard event.
420      * @private
421      */
422     onKeyboardUsed_: function(keyCode) {
423       switch (keyCode) {
424         case 9:  // Tab.
425         case 37:  // Left arrow.
426         case 38:  // Up arrow.
427         case 39:  // Right arrow.
428         case 40:  // Down arrow.
429           this.classList.remove('click-focus');
430       }
431     },
433     /**
434      * Adds a node to the list of targets that will launch the app. This list
435      * is also used in onMousedown to determine whether the app contents should
436      * be shown as active (if we don't do this, then clicking anywhere in
437      * appContents, even a part that is outside the ideally clickable region,
438      * will cause the app icon to look active).
439      * @param {HTMLElement} node The node that should be clickable.
440      */
441     addLaunchClickTarget_: function(node) {
442       node.classList.add('launch-click-target');
443       node.addEventListener('click', this.onClick_.bind(this));
444     },
446     /**
447      * Handler for mousedown on the App. Adds a class that allows us to
448      * not display as :active for right clicks (specifically, don't pulse on
449      * these occasions). Also, we don't pulse for clicks that aren't within the
450      * clickable regions.
451      * @param {Event} e The mousedown event.
452      */
453     onMousedown_: function(e) {
454       // If the current platform uses middle click to autoscroll and this
455       // mousedown isn't handled, onClick_() will never fire. crbug.com/142939
456       if (e.button == 1)
457         e.preventDefault();
459       if (e.button == 2 ||
460           !findAncestorByClass(/** @type {Element} */(e.target),
461                                'launch-click-target')) {
462         this.appContents_.classList.add('suppress-active');
463       } else {
464         this.appContents_.classList.remove('suppress-active');
465       }
467       // This class is here so we don't show the focus state for apps that
468       // gain keyboard focus via mouse clicking.
469       this.classList.add('click-focus');
470     },
472     /**
473      * Change the appData and update the appearance of the app.
474      * @param {AppInfo} appData The new data object that describes the app.
475      */
476     replaceAppData: function(appData) {
477       this.appData_ = appData;
478       this.setIcon();
479       this.loadIcon();
480     },
482     /**
483      * The data and preferences for this app.
484      * @type {Object}
485      */
486     set appData(data) {
487       this.appData_ = data;
488     },
489     get appData() {
490       return this.appData_;
491     },
493     get appId() {
494       return this.appData_.id;
495     },
497     /**
498      * Returns a pointer to the context menu for this app. All apps share the
499      * singleton AppContextMenu. This function is called by the
500      * ContextMenuHandler in response to the 'contextmenu' event.
501      * @type {cr.ui.Menu}
502      */
503     get contextMenu() {
504       var menu = AppContextMenu.getInstance();
505       menu.setupForApp(this);
506       return menu.menu;
507     },
509     /**
510      * Returns whether this element can be 'removed' from chrome (i.e. whether
511      * the user can drag it onto the trash and expect something to happen).
512      * @return {boolean} True if the app can be uninstalled.
513      */
514     canBeRemoved: function() {
515       return this.appData_.mayDisable;
516     },
518     /**
519      * Uninstalls the app after it's been dropped on the trash.
520      */
521     removeFromChrome: function() {
522       chrome.send('uninstallApp', [this.appData_.id, true]);
523       this.tile.tilePage.removeTile(this.tile, true);
524     },
526     /**
527      * Called when a drag is starting on the tile. Updates dataTransfer with
528      * data for this tile.
529      */
530     setDragData: function(dataTransfer) {
531       dataTransfer.setData('Text', this.appData_.title);
532       dataTransfer.setData('URL', this.appData_.url);
533     },
534   };
536   var TilePage = ntp.TilePage;
538   // The fraction of the app tile size that the icon uses.
539   var APP_IMG_SIZE_FRACTION = 4 / 5;
541   var appsPageGridValues = {
542     // The fewest tiles we will show in a row.
543     minColCount: 3,
544     // The most tiles we will show in a row.
545     maxColCount: 6,
547     // The smallest a tile can be.
548     minTileWidth: 64 / APP_IMG_SIZE_FRACTION,
549     // The biggest a tile can be.
550     maxTileWidth: 128 / APP_IMG_SIZE_FRACTION,
552     // The padding between tiles, as a fraction of the tile width.
553     tileSpacingFraction: 1 / 8,
554   };
555   TilePage.initGridValues(appsPageGridValues);
557   /**
558    * Creates a new AppsPage object.
559    * @constructor
560    * @extends {TilePage}
561    */
562   function AppsPage() {
563     var el = new TilePage(appsPageGridValues);
564     el.__proto__ = AppsPage.prototype;
565     el.initialize();
567     return el;
568   }
570   AppsPage.prototype = {
571     __proto__: TilePage.prototype,
573     initialize: function() {
574       this.classList.add('apps-page');
576       this.addEventListener('cardselected', this.onCardSelected_);
578       this.addEventListener('tilePage:tile_added', this.onTileAdded_);
580       this.content_.addEventListener('scroll', this.onScroll_.bind(this));
581     },
583     /**
584      * Highlight a newly installed app as it's added to the NTP.
585      * @param {AppInfo} appData The data object that describes the app.
586      */
587     insertAndHighlightApp: function(appData) {
588       ntp.getCardSlider().selectCardByValue(this);
589       this.content_.scrollTop = this.content_.scrollHeight;
590       this.insertApp(appData, true);
591     },
593     /**
594      * Similar to appendApp, but it respects the app_launch_ordinal field of
595      * |appData|.
596      * @param {Object} appData The data that describes the app.
597      * @param {boolean} animate Whether to animate the insertion.
598      */
599     insertApp: function(appData, animate) {
600       var index = this.tileElements_.length;
601       for (var i = 0; i < this.tileElements_.length; i++) {
602         if (appData.app_launch_ordinal <
603             this.tileElements_[i].firstChild.appData.app_launch_ordinal) {
604           index = i;
605           break;
606         }
607       }
609       this.addTileAt(new App(appData), index, animate);
610     },
612     /**
613      * Handler for 'cardselected' event, fired when |this| is selected. The
614      * first time this is called, we load all the app icons.
615      * @private
616      */
617     onCardSelected_: function(e) {
618       var apps = this.querySelectorAll('.app.icon-loading');
619       for (var i = 0; i < apps.length; i++) {
620         apps[i].loadIcon();
621       }
622     },
624     /**
625      * Handler for tile additions to this page.
626      * @param {Event} e The tilePage:tile_added event.
627      */
628     onTileAdded_: function(e) {
629       assert(e.currentTarget == this);
630       assert(e.addedTile.firstChild instanceof App);
631       if (this.classList.contains('selected-card'))
632         e.addedTile.firstChild.loadIcon();
633     },
635     /**
636      * A handler for when the apps page is scrolled (then we need to reposition
637      * the bubbles.
638      * @private
639      */
640     onScroll_: function(e) {
641       if (!this.selected)
642         return;
643       for (var i = 0; i < this.tileElements_.length; i++) {
644         var app = this.tileElements_[i].firstChild;
645         assert(app instanceof App);
646       }
647     },
649     /** @override */
650     doDragOver: function(e) {
651       // Only animatedly re-arrange if the user is currently dragging an app.
652       var tile = ntp.getCurrentlyDraggingTile();
653       if (tile && tile.querySelector('.app')) {
654         TilePage.prototype.doDragOver.call(this, e);
655       } else {
656         e.preventDefault();
657         this.setDropEffect(e.dataTransfer);
658       }
659     },
661     /** @override */
662     shouldAcceptDrag: function(e) {
663       if (ntp.getCurrentlyDraggingTile())
664         return true;
665       if (!e.dataTransfer || !e.dataTransfer.types)
666         return false;
667       return Array.prototype.indexOf.call(e.dataTransfer.types,
668                                           'text/uri-list') != -1;
669     },
671     /** @override */
672     addDragData: function(dataTransfer, index) {
673       var sourceId = -1;
674       var currentlyDraggingTile = ntp.getCurrentlyDraggingTile();
675       if (currentlyDraggingTile) {
676         var tileContents = currentlyDraggingTile.firstChild;
677         if (tileContents.classList.contains('app')) {
678           var originalPage = currentlyDraggingTile.tilePage;
679           var samePageDrag = originalPage == this;
680           sourceId = samePageDrag ? DRAG_SOURCE.SAME_APPS_PANE :
681                                     DRAG_SOURCE.OTHER_APPS_PANE;
682           this.tileGrid_.insertBefore(currentlyDraggingTile,
683                                       this.tileElements_[index]);
684           this.tileMoved(currentlyDraggingTile);
685           if (!samePageDrag) {
686             originalPage.fireRemovedEvent(currentlyDraggingTile, index, true);
687             this.fireAddedEvent(currentlyDraggingTile, index, true);
688           }
689         }
690       } else {
691         this.addOutsideData_(dataTransfer);
692         sourceId = DRAG_SOURCE.OUTSIDE_NTP;
693       }
695       assert(sourceId != -1);
696       chrome.send('metricsHandler:recordInHistogram',
697           ['NewTabPage.AppsPageDragSource', sourceId, DRAG_SOURCE_LIMIT]);
698     },
700     /**
701      * Adds drag data that has been dropped from a source that is not a tile.
702      * @param {Object} dataTransfer The data transfer object that holds drop
703      *     data.
704      * @private
705      */
706     addOutsideData_: function(dataTransfer) {
707       var url = dataTransfer.getData('url');
708       assert(url);
710       // If the dataTransfer has html data, use that html's text contents as the
711       // title of the new link.
712       var html = dataTransfer.getData('text/html');
713       var title;
714       if (html) {
715         // It's important that we don't attach this node to the document
716         // because it might contain scripts.
717         var node = this.ownerDocument.createElement('div');
718         node.innerHTML = html;
719         title = node.textContent;
720       }
722       // Make sure title is >=1 and <=45 characters for Chrome app limits.
723       if (!title)
724         title = url;
725       if (title.length > 45)
726         title = title.substring(0, 45);
727       var data = {url: url, title: title};
729       // Synthesize an app.
730       this.generateAppForLink(data);
731     },
733     /**
734      * Creates a new crx-less app manifest and installs it.
735      * @param {Object} data The data object describing the link. Must have |url|
736      *     and |title| members.
737      */
738     generateAppForLink: function(data) {
739       assert(data.url != undefined);
740       assert(data.title != undefined);
741       var pageIndex = ntp.getAppsPageIndex(this);
742       chrome.send('generateAppForLink', [data.url, data.title, pageIndex]);
743     },
745     /** @override */
746     tileMoved: function(draggedTile) {
747       if (!(draggedTile.firstChild instanceof App))
748         return;
750       var pageIndex = ntp.getAppsPageIndex(this);
751       chrome.send('setPageIndex', [draggedTile.firstChild.appId, pageIndex]);
753       var appIds = [];
754       for (var i = 0; i < this.tileElements_.length; i++) {
755         var tileContents = this.tileElements_[i].firstChild;
756         if (tileContents instanceof App)
757           appIds.push(tileContents.appId);
758       }
760       chrome.send('reorderApps', [draggedTile.firstChild.appId, appIds]);
761     },
763     /** @override */
764     setDropEffect: function(dataTransfer) {
765       var tile = ntp.getCurrentlyDraggingTile();
766       if (tile && tile.querySelector('.app'))
767         ntp.setCurrentDropEffect(dataTransfer, 'move');
768       else
769         ntp.setCurrentDropEffect(dataTransfer, 'copy');
770     },
771   };
773   /**
774    * Launches the specified app using the APP_LAUNCH_NTP_APP_RE_ENABLE
775    * histogram. This should only be invoked from the AppLauncherHandler.
776    * @param {string} appId The ID of the app.
777    */
778   function launchAppAfterEnable(appId) {
779     chrome.send('launchApp', [appId, APP_LAUNCH.NTP_APP_RE_ENABLE]);
780   }
782   return {
783     APP_LAUNCH: APP_LAUNCH,
784     AppsPage: AppsPage,
785     launchAppAfterEnable: launchAppAfterEnable,
786   };