cros: Remove default pinned apps trial.
[chromium-blink-merge.git] / chrome / browser / resources / ntp4 / apps_page.js
blob210e30586298c3e0b075aa131fb51620620abd88
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_RECENTLY_CLOSED: 4,
15     NTP_APP_RE_ENABLE: 16,
16     NTP_WEBSTORE_FOOTER: 18,
17     NTP_WEBSTORE_PLUS_ICON: 19,
18   };
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;
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);
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;
48       this.launch_ = this.appendMenuItem_();
49       this.launch_.addEventListener('activate', this.onLaunch_.bind(this));
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');
61       var self = this;
62       this.forAllLaunchTypes_(function(launchTypeButton, id) {
63         launchTypeButton.addEventListener('activate',
64             self.onLaunchTypeChanged_.bind(self));
65       });
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));
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       }
87       document.body.appendChild(menu);
88     },
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     },
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_];
117       for (var i = 0; i < launchTypes.length; ++i) {
118         if (!launchTypes[i])
119           continue;
121         f(launchTypes[i], i);
122       }
123     },
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;
132       this.launch_.textContent = app.appData.title;
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       });
144       this.launchTypeMenuSeparator_.hidden = app.appData.packagedApp;
146       this.options_.disabled = !app.appData.optionsUrl || !app.appData.enabled;
147       this.details_.disabled = !app.appData.detailsUrl;
148       this.uninstall_.disabled = !app.appData.mayDisable;
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     },
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   };
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);
216     return el;
217   }
219   App.prototype = {
220     __proto__: HTMLDivElement.prototype,
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');
232       this.className = 'app focusable';
234       if (!this.appData_.icon_big_exists && this.appData_.icon_small_exists)
235         this.useSmallIcon_ = true;
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_);
243       this.appImgContainer_ = this.querySelector('.app-img-container');
244       this.appImg_ = this.appImgContainer_.querySelector('img');
245       this.setIcon();
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       }
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);
264       this.addEventListener('keydown', cr.ui.contextMenuHandler);
265       this.addEventListener('keyup', cr.ui.contextMenuHandler);
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);
276       this.addEventListener('mousedown', this.onMousedown_, true);
277       this.addEventListener('keydown', this.onKeydown_);
278       this.addEventListener('keyup', this.onKeyup_);
279     },
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     },
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     },
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       }
313       this.appImgSrc_ = src;
314       this.classList.add('icon-loading');
315     },
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       }
328       this.classList.remove('icon-loading');
329     },
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       }
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     },
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');
372       chrome.send('launchApp',
373                   [this.appId, APP_LAUNCH.NTP_APPS_MAXIMIZED, url,
374                    e.button, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey]);
376       // Don't allow the click to trigger a link or anything
377       e.preventDefault();
378     },
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     },
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     },
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     },
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     },
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();
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       }
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     },
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     },
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     },
481     get appId() {
482       return this.appData_.id;
483     },
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     },
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     },
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     },
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   };
524   var TilePage = ntp.TilePage;
526   // The fraction of the app tile size that the icon uses.
527   var APP_IMG_SIZE_FRACTION = 4 / 5;
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,
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,
540     // The padding between tiles, as a fraction of the tile width.
541     tileSpacingFraction: 1 / 8,
542   };
543   TilePage.initGridValues(appsPageGridValues);
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();
555     return el;
556   }
558   AppsPage.prototype = {
559     __proto__: TilePage.prototype,
561     initialize: function() {
562       this.classList.add('apps-page');
564       this.addEventListener('cardselected', this.onCardSelected_);
566       this.addEventListener('tilePage:tile_added', this.onTileAdded_);
568       this.content_.addEventListener('scroll', this.onScroll_.bind(this));
569     },
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     },
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       }
597       this.addTileAt(new App(appData), index, animate);
598     },
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     },
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     },
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     },
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     },
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     },
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       }
686       assert(sourceId != -1);
687       chrome.send('metricsHandler:recordInHistogram',
688           ['NewTabPage.AppsPageDragSource', sourceId, DRAG_SOURCE_LIMIT]);
689     },
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);
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       }
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};
720       // Synthesize an app.
721       this.generateAppForLink(data);
722     },
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     },
736     /** @override */
737     tileMoved: function(draggedTile) {
738       if (!(draggedTile.firstChild instanceof App))
739         return;
741       var pageIndex = ntp.getAppsPageIndex(this);
742       chrome.send('setPageIndex', [draggedTile.firstChild.appId, pageIndex]);
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       }
751       chrome.send('reorderApps', [draggedTile.firstChild.appId, appIds]);
752     },
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   };
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   }
773   return {
774     APP_LAUNCH: APP_LAUNCH,
775     AppsPage: AppsPage,
776     launchAppAfterEnable: launchAppAfterEnable,
777   };