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