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